mapillary-js
Version:
WebGL JavaScript library for displaying street level imagery from mapillary.com
405 lines (336 loc) • 13.2 kB
text/typescript
import * as geohash from "latlon-geohash";
import * as pako from "pako";
import {
empty as observableEmpty,
from as observableFrom,
of as observableOf,
zip as observableZip,
Observable,
Subscriber,
} from "rxjs";
import {
mergeMap,
catchError,
tap,
publish,
refCount,
finalize,
map,
filter,
} from "rxjs/operators";
import {
IGPano,
ILatLon,
} from "../../API";
import {
AbortMapillaryError,
} from "../../Error";
import {
GraphService,
Node,
} from "../../Graph";
import {
Urls,
} from "../../Utils";
import { CameraProjection } from "../../api/interfaces/CameraProjection";
import IClusterReconstruction from "./interfaces/IClusterReconstruction";
export type NodeData = {
alt: number;
cameraProjection: CameraProjection;
clusterKey: string;
focal: number;
gpano: IGPano;
height: number;
k1: number;
k2: number;
key: string,
lat: number;
lon: number;
mergeCC: number;
orientation: number;
originalLat: number;
originalLon: number;
rotation: number[];
scale: number;
sequenceKey: string;
width: number;
};
export class SpatialDataCache {
private _graphService: GraphService;
private _cacheRequests: { [hash: string]: XMLHttpRequest[] };
private _tiles: { [hash: string]: NodeData[] };
private _clusterReconstructions: { [key: string]: IClusterReconstruction };
private _clusterReconstructionTiles: { [key: string]: string[] };
private _tileClusters: { [hash: string]: string[] };
private _cachingClusterReconstructions$: { [hash: string]: Observable<IClusterReconstruction> };
private _cachingTiles$: { [hash: string]: Observable<NodeData[]> };
constructor(graphService: GraphService) {
this._graphService = graphService;
this._tiles = {};
this._cacheRequests = {};
this._clusterReconstructions = {};
this._clusterReconstructionTiles = {};
this._tileClusters = {};
this._cachingTiles$ = {};
this._cachingClusterReconstructions$ = {};
}
public cacheClusterReconstructions$(hash: string): Observable<IClusterReconstruction> {
if (!this.hasTile(hash)) {
throw new Error("Cannot cache reconstructions of a non-existing tile.");
}
if (this.hasClusterReconstructions(hash)) {
throw new Error("Cannot cache reconstructions that already exists.");
}
if (this.isCachingClusterReconstructions(hash)) {
return this._cachingClusterReconstructions$[hash];
}
const clusterKeys: string[] = this.getTile(hash)
.filter(
(nd: NodeData): boolean => {
return !!nd.clusterKey;
})
.map(
(nd: NodeData): string => {
return nd.clusterKey;
})
.filter(
(v: string, i: number, a: string[]) => {
return a.indexOf(v) === i;
});
this._tileClusters[hash] = clusterKeys;
this._cacheRequests[hash] = [];
this._cachingClusterReconstructions$[hash] = observableFrom(clusterKeys).pipe(
mergeMap(
(key: string): Observable<IClusterReconstruction> => {
if (this._hasClusterReconstruction(key)) {
return observableOf(this._getClusterReconstruction(key));
}
return this._getClusterReconstruction$(key, this._cacheRequests[hash])
.pipe(
catchError(
(error: Error): Observable<IClusterReconstruction> => {
if (error instanceof AbortMapillaryError) {
return observableEmpty();
}
console.error(error);
return observableEmpty();
}));
},
6),
filter(
(): boolean => {
return hash in this._tileClusters;
}),
tap(
(reconstruction: IClusterReconstruction): void => {
if (!this._hasClusterReconstruction(reconstruction.key)) {
this._clusterReconstructions[reconstruction.key] = reconstruction;
}
if (!(reconstruction.key in this._clusterReconstructionTiles)) {
this._clusterReconstructionTiles[reconstruction.key] = [];
}
if (this._clusterReconstructionTiles[reconstruction.key].indexOf(hash) === -1) {
this._clusterReconstructionTiles[reconstruction.key].push(hash);
}
}),
finalize(
(): void => {
if (hash in this._cachingClusterReconstructions$) {
delete this._cachingClusterReconstructions$[hash];
}
if (hash in this._cacheRequests) {
delete this._cacheRequests[hash];
}
}),
publish(),
refCount());
return this._cachingClusterReconstructions$[hash];
}
public cacheTile$(hash: string): Observable<NodeData[]> {
if (hash.length !== 8) {
throw new Error("Hash needs to be level 8.");
}
if (this.hasTile(hash)) {
throw new Error("Cannot cache tile that already exists.");
}
if (this.isCachingTile(hash)) {
return this._cachingTiles$[hash];
}
const bounds: geohash.Bounds = geohash.bounds(hash);
const sw: ILatLon = { lat: bounds.sw.lat, lon: bounds.sw.lon };
const ne: ILatLon = { lat: bounds.ne.lat, lon: bounds.ne.lon };
this._cachingTiles$[hash] = this._graphService.cacheBoundingBox$(sw, ne).pipe(
catchError(
(error: Error): Observable<Node[]> => {
console.error(error);
return observableEmpty();
}),
map(
(nodes: Node[]): NodeData[] => {
return nodes
.map(
(n: Node): NodeData => {
return this._createNodeData(n);
});
}),
filter(
(): boolean => {
return !(hash in this._tiles);
}),
tap(
(nodeData: NodeData[]): void => {
this._tiles[hash] = [];
this._tiles[hash].push(...nodeData);
delete this._cachingTiles$[hash];
}),
finalize(
(): void => {
if (hash in this._cachingTiles$) {
delete this._cachingTiles$[hash];
}
}),
publish(),
refCount());
return this._cachingTiles$[hash];
}
public isCachingClusterReconstructions(hash: string): boolean {
return hash in this._cachingClusterReconstructions$;
}
public isCachingTile(hash: string): boolean {
return hash in this._cachingTiles$;
}
public hasClusterReconstructions(hash: string): boolean {
if (hash in this._cachingClusterReconstructions$ ||
!(hash in this._tileClusters)) {
return false;
}
for (const key of this._tileClusters[hash]) {
if (!(key in this._clusterReconstructions)) {
return false;
}
}
return true;
}
public hasTile(hash: string): boolean {
return !(hash in this._cachingTiles$) && hash in this._tiles;
}
public getClusterReconstructions(hash: string): IClusterReconstruction[] {
return hash in this._tileClusters ?
this._tileClusters[hash]
.map(
(key: string): IClusterReconstruction => {
return this._clusterReconstructions[key];
})
.filter(
(reconstruction: IClusterReconstruction): boolean => {
return !!reconstruction;
}) :
[];
}
public getTile(hash: string): NodeData[] {
return hash in this._tiles ? this._tiles[hash] : [];
}
public uncache(keepHashes?: string[]): void {
for (let hash of Object.keys(this._cacheRequests)) {
if (!!keepHashes && keepHashes.indexOf(hash) !== -1) {
continue;
}
for (const request of this._cacheRequests[hash]) {
request.abort();
}
delete this._cacheRequests[hash];
}
for (let hash of Object.keys(this._tileClusters)) {
if (!!keepHashes && keepHashes.indexOf(hash) !== -1) {
continue;
}
for (const key of this._tileClusters[hash]) {
if (!(key in this._clusterReconstructionTiles)) {
continue;
}
const index: number = this._clusterReconstructionTiles[key].indexOf(hash);
if (index === -1) {
continue;
}
this._clusterReconstructionTiles[key].splice(index, 1);
if (this._clusterReconstructionTiles[key].length > 0) {
continue;
}
delete this._clusterReconstructionTiles[key];
delete this._clusterReconstructions[key];
}
delete this._tileClusters[hash];
}
for (let hash of Object.keys(this._tiles)) {
if (!!keepHashes && keepHashes.indexOf(hash) !== -1) {
continue;
}
delete this._tiles[hash];
}
}
private _createNodeData(node: Node): NodeData {
return {
alt: node.alt,
cameraProjection: node.cameraProjection,
clusterKey: node.clusterKey,
focal: node.focal,
gpano: node.gpano,
height: node.height,
k1: node.ck1,
k2: node.ck2,
key: node.key,
lat: node.latLon.lat,
lon: node.latLon.lon,
mergeCC: node.mergeCC,
orientation: node.orientation,
originalLat: node.originalLatLon.lat,
originalLon: node.originalLatLon.lon,
rotation: [node.rotation[0], node.rotation[1], node.rotation[2]],
scale: node.scale,
sequenceKey: node.sequenceKey,
width: node.width,
};
}
private _getClusterReconstruction(key: string): IClusterReconstruction {
return this._clusterReconstructions[key];
}
private _getClusterReconstruction$(key: string, requests: XMLHttpRequest[]): Observable<IClusterReconstruction> {
return Observable.create(
(subscriber: Subscriber<IClusterReconstruction>): void => {
const xhr: XMLHttpRequest = new XMLHttpRequest();
xhr.open("GET", Urls.clusterReconstruction(key), true);
xhr.responseType = "arraybuffer";
xhr.timeout = 15000;
xhr.onload = () => {
if (!xhr.response) {
subscriber.error(new Error(`Cluster reconstruction retreival failed (${key})`));
} else {
const inflated: string = pako.inflate(xhr.response, { to: "string" });
const reconstructions: IClusterReconstruction[] = JSON.parse(inflated);
if (reconstructions.length < 1) {
subscriber.error(new Error(`No cluster reconstruction exists (${key})`));
}
const reconstruction: IClusterReconstruction = reconstructions[0];
reconstruction.key = key;
subscriber.next(reconstruction);
subscriber.complete();
}
};
xhr.onerror = () => {
subscriber.error(new Error(`Failed to get cluster reconstruction (${key})`));
};
xhr.ontimeout = () => {
subscriber.error(new Error(`Cluster reconstruction request timed out (${key})`));
};
xhr.onabort = () => {
subscriber.error(new AbortMapillaryError(`Cluster reconstruction request was aborted (${key})`));
};
requests.push(xhr);
xhr.send(null);
});
}
private _hasClusterReconstruction(key: string): boolean {
return key in this._clusterReconstructions;
}
}
export default SpatialDataCache;