feat(api): get static resources through data provider

Use data provider in node caching.
This commit is contained in:
Oscar Lorentzon 2020-10-05 18:06:40 +02:00
parent 0469c966ab
commit 2b4617bd7a
15 changed files with 500 additions and 255 deletions

View File

@ -1,7 +1,4 @@
import {empty as observableEmpty, Observable} from "rxjs";
import {catchError, retry} from "rxjs/operators";
import * as pako from "pako";
import * as falcor from "falcor";
import {
@ -12,6 +9,8 @@ import {
ModelCreator,
} from "../../src/API";
import DataProvider from "../../src/api/DataProvider";
import { MapillaryError } from "../../src/Error";
import IClusterReconstruction from "../../src/component/spatialdata/interfaces/IClusterReconstruction";
describe("DataProvider.ctor", () => {
it("should create a data provider", () => {
@ -43,7 +42,7 @@ describe("DataProvider.getFillImages", () => {
provider.getFillImages([key])
.then(
(result: { [key: string]: IFillNode}): void => {
(result: { [key: string]: IFillNode }): void => {
expect(result).toBeDefined();
expect(modelSpy.calls.count()).toBe(1);
@ -186,7 +185,7 @@ describe("DataProvider.getFullImages", () => {
provider.getFullImages([key])
.then(
(result: { [key: string]: IFillNode}): void => {
(result: { [key: string]: IFillNode }): void => {
expect(result).toBeDefined();
expect(spy.calls.count()).toBe(1);
@ -252,7 +251,7 @@ describe("DataProvider.getFullImages", () => {
provider.getFullImages([key])
.then(
(result: { [key: string]: IFillNode}): void => { return; },
(result: { [key: string]: IFillNode }): void => { return; },
(error: Error): void => {
expect(invalidateSpy.calls.count()).toBe(1);
expect(invalidateSpy.calls.first().args.length).toBe(1);
@ -575,3 +574,175 @@ describe("DataProvider.setToken", () => {
expect(creatorSpy.calls.mostRecent().args[1]).toBe("token");
});
});
class XMLHTTPRequestMock {
public response: {};
public responseType: string;
public status: number;
public timeout: number;
public onload: (e: Event) => any;
public onerror: (e: Event) => any;
public ontimeout: (e: Event) => any;
public onabort: (e: Event) => any;
public abort(): void { this.onabort(new Event("abort")); }
public open(...args: any[]): void { return; }
public send(...args: any[]): void { return; }
};
describe("DataProvider.getImage", () => {
it("should return array buffer on successful load", (done: Function) => {
const requestMock: XMLHTTPRequestMock = new XMLHTTPRequestMock();
spyOn(window, <keyof Window>"XMLHttpRequest").and.returnValue(requestMock);
const abort: Promise<void> = new Promise((_, __): void => { /*noop*/ });
const provider: DataProvider = new DataProvider(
"clientId", undefined, undefined);
const response: ArrayBuffer = new ArrayBuffer(1024);
provider.getImage("key", 320, abort)
.then(
(buffer: ArrayBuffer): void => {
expect(buffer instanceof ArrayBuffer).toBeTrue();
expect(buffer).toEqual(response);
done();
});
requestMock.status = 200;
requestMock.response = response;
requestMock.onload(undefined);
});
it("should reject on abort", (done: Function) => {
const requestMock: XMLHTTPRequestMock = new XMLHTTPRequestMock();
spyOn(window, <keyof Window>"XMLHttpRequest").and.returnValue(requestMock);
let aborter: Function;
const abort: Promise<void> = new Promise(
(_, reject): void => {
aborter = reject;
});
const provider: DataProvider = new DataProvider(
"clientId", undefined, undefined);
provider.getImage("key", 320, abort)
.then(
undefined,
(reason: Error): void => {
expect(reason instanceof MapillaryError).toBeTrue();
expect(reason.message).toContain("abort");
done();
});
aborter();
});
it("should reject on unsuccessful load", (done: Function) => {
const requestMock: XMLHTTPRequestMock = new XMLHTTPRequestMock();
spyOn(window, <keyof Window>"XMLHttpRequest").and.returnValue(requestMock);
const abort: Promise<void> = new Promise((_, __): void => { /*noop*/ });
const provider: DataProvider = new DataProvider(
"clientId", undefined, undefined);
const response: ArrayBuffer = new ArrayBuffer(1024);
provider.getImage("key", 320, abort)
.then(
undefined,
(reason: Error): void => {
expect(reason instanceof MapillaryError).toBeTrue();
expect(reason.message).toContain("status");
done();
});
requestMock.status = 404;
requestMock.response = response;
requestMock.onload(undefined);
});
it("should reject for empty response on load", (done: Function) => {
const requestMock: XMLHTTPRequestMock = new XMLHTTPRequestMock();
spyOn(window, <keyof Window>"XMLHttpRequest").and.returnValue(requestMock);
const abort: Promise<void> = new Promise((_, __): void => { /*noop*/ });
const provider: DataProvider = new DataProvider(
"clientId", undefined, undefined);
const response: ArrayBuffer = new ArrayBuffer(1024);
provider.getImage("key", 320, abort)
.then(
undefined,
(reason: Error): void => {
expect(reason instanceof MapillaryError).toBeTrue();
expect(reason.message).toContain("empty");
done();
});
requestMock.status = 200;
requestMock.response = undefined;
requestMock.onload(undefined);
});
it("should reject on error", (done: Function) => {
const requestMock: XMLHTTPRequestMock = new XMLHTTPRequestMock();
spyOn(window, <keyof Window>"XMLHttpRequest").and.returnValue(requestMock);
const abort: Promise<void> = new Promise((_, __): void => { /*noop*/ });
const provider: DataProvider = new DataProvider(
"clientId", undefined, undefined);
const response: ArrayBuffer = new ArrayBuffer(1024);
provider.getImage("key", 320, abort)
.then(
undefined,
(reason: Error): void => {
expect(reason instanceof MapillaryError).toBeTrue();
expect(reason.message).toContain("error");
done();
});
requestMock.onerror(undefined);
});
});
describe("DataProvider.getClusterReconstruction", () => {
it("should return cluster reconstruction on successful load", (done: Function) => {
const requestMock: XMLHTTPRequestMock = new XMLHTTPRequestMock();
spyOn(window, <keyof Window>"XMLHttpRequest").and.returnValue(requestMock);
const provider: DataProvider = new DataProvider(
"clientId", undefined, undefined);
provider.getClusterReconstruction("clusterKey")
.then(
(r: IClusterReconstruction): void => {
expect(r.points).toEqual({});
expect(r.reference_lla.altitude).toBe(1);
expect(r.reference_lla.latitude).toBe(2);
expect(r.reference_lla.longitude).toBe(3);
done();
});
const response: string = pako.deflate(
JSON.stringify([{
points: {},
reference_lla: { altitude: 1, latitude: 2, longitude: 3 },
}]),
{ to: "string" });
requestMock.status = 200;
requestMock.response = response;
requestMock.onload(undefined);
});
});

View File

@ -60,7 +60,7 @@ describe("DirectionComponent.activate", () => {
directionComponent.activate();
const node: Node = new NodeHelper().createNode();
node.initializeCache(new NodeCache());
node.initializeCache(new NodeCache(undefined));
node.cacheSpatialEdges([]);
(<Subject<Node>>navigatorMock.stateService.currentNode$).next(node);
@ -88,7 +88,7 @@ describe("DirectionComponent.activate", () => {
(<jasmine.Spy>navigatorMock.graphService.cacheSequence$).and.returnValue(observableOf<Sequence>(sequence));
const node: Node = new NodeHelper().createNode();
node.initializeCache(new NodeCache());
node.initializeCache(new NodeCache(undefined));
node.cacheSpatialEdges([]);
(<Subject<Node>>navigatorMock.stateService.currentNode$).next(node);
@ -117,7 +117,7 @@ describe("DirectionComponent.activate", () => {
(<jasmine.Spy>navigatorMock.graphService.cacheSequence$).and.returnValue(cacheSequence$);
const node: Node = new NodeHelper().createNode();
node.initializeCache(new NodeCache());
node.initializeCache(new NodeCache(undefined));
node.cacheSpatialEdges([]);
(<Subject<Node>>navigatorMock.stateService.currentNode$).next(node);
@ -149,7 +149,7 @@ describe("DirectionComponent.activate", () => {
(<jasmine.Spy>navigatorMock.graphService.cacheSequence$).and.returnValue(observableThrowError(new Error("Failed to cache seq.")));
const node: Node = new NodeHelper().createNode();
node.initializeCache(new NodeCache());
node.initializeCache(new NodeCache(undefined));
node.cacheSpatialEdges([]);
(<Subject<Node>>navigatorMock.stateService.currentNode$).next(node);

View File

@ -1,6 +1,7 @@
import {NodeHelper} from "../helper/NodeHelper.spec";
import {ICoreNode, IFillNode} from "../../src/API";
import {IMesh, Node, NodeCache} from "../../src/Graph";
import {Node, NodeCache} from "../../src/Graph";
import IMesh from "../../src/api/interfaces/IMesh";
describe("Node", () => {
let helper: NodeHelper;
@ -68,7 +69,7 @@ describe("Node.dispose", () => {
it("should dipose cache", () => {
let coreNode: ICoreNode = helper.createCoreNode();
let node: Node = new Node(coreNode);
let nodeCache: NodeCache = new NodeCache();
let nodeCache: NodeCache = new NodeCache(undefined);
let disposeSpy: jasmine.Spy = spyOn(nodeCache, "dispose");
disposeSpy.and.stub();
@ -98,7 +99,7 @@ describe("Node.uncache", () => {
it("should dispose node cache", () => {
let coreNode: ICoreNode = helper.createCoreNode();
let node: Node = new Node(coreNode);
let nodeCache: NodeCache = new NodeCache();
let nodeCache: NodeCache = new NodeCache(undefined);
let disposeSpy: jasmine.Spy = spyOn(nodeCache, "dispose");
disposeSpy.and.stub();
@ -113,7 +114,7 @@ describe("Node.uncache", () => {
it("should be able to initialize cache again after uncache", () => {
let coreNode: ICoreNode = helper.createCoreNode();
let node: Node = new Node(coreNode);
let nodeCache: NodeCache = new NodeCache();
let nodeCache: NodeCache = new NodeCache(undefined);
let disposeSpy: jasmine.Spy = spyOn(nodeCache, "dispose");
disposeSpy.and.stub();
@ -337,6 +338,8 @@ describe("Node.assetsCached", () => {
protected _overridingImage: HTMLImageElement;
protected _overridingMesh: IMesh;
constructor() { super(undefined); }
public get image(): HTMLImageElement {
return this._overridingImage;
}

View File

@ -1,36 +1,38 @@
import {first, skip} from "rxjs/operators";
import {EdgeDirection, IEdge} from "../../src/Edge";
import { first, skip } from "rxjs/operators";
import { EdgeDirection, IEdge } from "../../src/Edge";
import {
IEdgeStatus,
NodeCache,
} from "../../src/Graph";
import { ImageSize } from "../../src/Viewer";
import { MockCreator } from "../helper/MockCreator.spec";
import { IDataProvider } from "../../src/API";
import DataProvider from "../../src/api/DataProvider";
describe("NodeCache.ctor", () => {
it("should create a node cache", () => {
let nodeCache: NodeCache = new NodeCache();
let nodeCache: NodeCache = new NodeCache(undefined);
expect(nodeCache).toBeDefined();
});
});
describe("NodeCache.mesh", () => {
it("should be null initially", () => {
let nodeCache: NodeCache = new NodeCache();
let nodeCache: NodeCache = new NodeCache(undefined);
expect(nodeCache.mesh).toBeNull();
});
});
describe("NodeCache.image", () => {
it("should be null initially", () => {
let nodeCache: NodeCache = new NodeCache();
let nodeCache: NodeCache = new NodeCache(undefined);
expect(nodeCache.image).toBeNull();
});
});
describe("NodeCache.sequenceEdges$", () => {
it("should emit uncached empty edge status initially", (done: Function) => {
let nodeCache: NodeCache = new NodeCache();
let nodeCache: NodeCache = new NodeCache(undefined);
nodeCache.sequenceEdges$.pipe(
first())
@ -44,7 +46,7 @@ describe("NodeCache.sequenceEdges$", () => {
});
it("should emit cached non empty edge status when sequence edges cached", (done: Function) => {
let nodeCache: NodeCache = new NodeCache();
let nodeCache: NodeCache = new NodeCache(undefined);
let sequenceEdge: IEdge = {
data: {
@ -76,7 +78,7 @@ describe("NodeCache.sequenceEdges$", () => {
describe("NodeCache.resetSequenceEdges", () => {
it("should reset the sequence edges", () => {
let nodeCache: NodeCache = new NodeCache();
let nodeCache: NodeCache = new NodeCache(undefined);
let sequenceEdge: IEdge = {
data: {
@ -102,7 +104,7 @@ describe("NodeCache.resetSequenceEdges", () => {
describe("NodeCache.spatialEdges$", () => {
it("should emit uncached empty edge status initially", (done: Function) => {
let nodeCache: NodeCache = new NodeCache();
let nodeCache: NodeCache = new NodeCache(undefined);
nodeCache.spatialEdges$.pipe(
first())
@ -116,7 +118,7 @@ describe("NodeCache.spatialEdges$", () => {
});
it("should emit cached non empty edge status when spatial edges cached", (done: Function) => {
let nodeCache: NodeCache = new NodeCache();
let nodeCache: NodeCache = new NodeCache(undefined);
let spatialEdge: IEdge = {
data: {
@ -148,7 +150,7 @@ describe("NodeCache.spatialEdges$", () => {
describe("NodeCache.resetSpatialEdges", () => {
it("should reset the spatial edges", () => {
let nodeCache: NodeCache = new NodeCache();
let nodeCache: NodeCache = new NodeCache(undefined);
let spatialEdge: IEdge = {
data: {
@ -174,7 +176,7 @@ describe("NodeCache.resetSpatialEdges", () => {
describe("NodeCache.dispose", () => {
it("should clear all properties", () => {
let nodeCache: NodeCache = new NodeCache();
let nodeCache: NodeCache = new NodeCache(undefined);
let sequencEdge: IEdge = {
data: {
@ -211,11 +213,14 @@ describe("NodeCache.dispose", () => {
describe("NodeCache.cacheImage$", () => {
it("should return the node cache with a cached image", (done: Function) => {
const requestMock: XMLHttpRequest = new XMLHttpRequest();
spyOn(requestMock, "send").and.stub();
spyOn(requestMock, "open").and.stub();
const promise: any = {
then: (resolve: (result: any) => void, reject: (error: Error) => void): void => {
resolve(undefined);
},
};
spyOn(window, <keyof Window>"XMLHttpRequest").and.returnValue(requestMock);
const dataProvider: IDataProvider = new DataProvider("cid");
spyOn(dataProvider, "getImage").and.returnValue(promise);
const imageMock: HTMLImageElement = new Image();
spyOn(window, <keyof Window>"Image").and.returnValue(imageMock);
@ -225,7 +230,7 @@ describe("NodeCache.cacheImage$", () => {
spyOn(window, "Blob").and.returnValue(<Blob>{});
spyOn(window.URL, "createObjectURL").and.returnValue("url");
const nodeCache: NodeCache = new NodeCache();
const nodeCache: NodeCache = new NodeCache(dataProvider);
expect(nodeCache.image).toBeNull();
@ -238,18 +243,18 @@ describe("NodeCache.cacheImage$", () => {
done();
});
new MockCreator().mockProperty(requestMock, "status", 200);
requestMock.dispatchEvent(new ProgressEvent("load", { total: 1, loaded: 1}));
imageMock.dispatchEvent(new CustomEvent("load"));
});
it("should cache an image", () => {
const requestMock: XMLHttpRequest = new XMLHttpRequest();
spyOn(requestMock, "send").and.stub();
spyOn(requestMock, "open").and.stub();
const promise: any = {
then: (resolve: (result: any) => void, reject: (error: Error) => void): void => {
resolve(undefined);
},
};
spyOn(window, <keyof Window>"XMLHttpRequest").and.returnValue(requestMock);
const dataProvider: IDataProvider = new DataProvider("cid");
spyOn(dataProvider, "getImage").and.returnValue(promise);
const imageMock: HTMLImageElement = new Image();
spyOn(window, <keyof Window>"Image").and.returnValue(imageMock);
@ -259,15 +264,12 @@ describe("NodeCache.cacheImage$", () => {
spyOn(window, "Blob").and.returnValue(<Blob>{});
spyOn(window.URL, "createObjectURL").and.returnValue("url");
const nodeCache: NodeCache = new NodeCache();
const nodeCache: NodeCache = new NodeCache(dataProvider);
expect(nodeCache.image).toBeNull();
nodeCache.cacheImage$("key", ImageSize.Size640).subscribe();
new MockCreator().mockProperty(requestMock, "status", 200);
requestMock.dispatchEvent(new ProgressEvent("load", { total: 1, loaded: 1}));
imageMock.dispatchEvent(new CustomEvent("load"));
expect(nodeCache.image).not.toBeNull();
@ -275,11 +277,14 @@ describe("NodeCache.cacheImage$", () => {
});
it("should emit the cached image", (done: Function) => {
const requestMock: XMLHttpRequest = new XMLHttpRequest();
spyOn(requestMock, "send").and.stub();
spyOn(requestMock, "open").and.stub();
const promise: any = {
then: (resolve: (result: any) => void, reject: (error: Error) => void): void => {
resolve(undefined);
},
};
spyOn(window, <keyof Window>"XMLHttpRequest").and.returnValue(requestMock);
const dataProvider: IDataProvider = new DataProvider("cid");
spyOn(dataProvider, "getImage").and.returnValue(promise);
const imageMock: HTMLImageElement = new Image();
spyOn(window, <keyof Window>"Image").and.returnValue(imageMock);
@ -289,7 +294,7 @@ describe("NodeCache.cacheImage$", () => {
spyOn(window, "Blob").and.returnValue(<Blob>{});
spyOn(window.URL, "createObjectURL").and.returnValue("url");
const nodeCache: NodeCache = new NodeCache();
const nodeCache: NodeCache = new NodeCache(dataProvider);
expect(nodeCache.image).toBeNull();
@ -304,10 +309,6 @@ describe("NodeCache.cacheImage$", () => {
});
nodeCache.cacheImage$("key", ImageSize.Size640).subscribe();
new MockCreator().mockProperty(requestMock, "status", 200);
requestMock.dispatchEvent(new ProgressEvent("load", { total: 1, loaded: 1}));
imageMock.dispatchEvent(new CustomEvent("load"));
});
});

View File

@ -285,7 +285,7 @@ describe("PlayService.play", () => {
playService.play();
const frame: IFrame = new FrameHelper().createFrame();
frame.state.currentNode.initializeCache(new NodeCache());
frame.state.currentNode.initializeCache(new NodeCache(undefined));
(<Subject<IFrame>>stateService.currentState$).next(frame);
frame.state.currentNode.cacheSequenceEdges([]);
@ -350,7 +350,7 @@ describe("PlayService.play", () => {
playService.play();
const frame: IFrame = new FrameHelper().createFrame();
frame.state.currentNode.initializeCache(new NodeCache());
frame.state.currentNode.initializeCache(new NodeCache(undefined));
(<Subject<IFrame>>stateService.currentState$).next(frame);
frame.state.currentNode.cacheSequenceEdges([]);
@ -370,7 +370,7 @@ describe("PlayService.play", () => {
const frame: IFrame = new FrameHelper().createFrame();
const node: Node = frame.state.currentNode;
node.initializeCache(new NodeCache());
node.initializeCache(new NodeCache(undefined));
const sequenceEdgesSubject: Subject<IEdgeStatus> = new Subject<IEdgeStatus>();
new MockCreator().mockProperty(node, "sequenceEdges$", sequenceEdgesSubject);
@ -400,7 +400,7 @@ describe("PlayService.play", () => {
const frame: IFrame = new FrameHelper().createFrame();
const node: Node = frame.state.currentNode;
node.initializeCache(new NodeCache());
node.initializeCache(new NodeCache(undefined));
const sequenceEdgesSubject: Subject<IEdgeStatus> = new Subject<IEdgeStatus>();
const prevFullNode: IFullNode = new NodeHelper().createFullNode();
@ -658,7 +658,7 @@ describe("PlayService.play", () => {
const frame: IFrame = new FrameHelper().createFrame();
const node: Node = frame.state.currentNode;
node.initializeCache(new NodeCache());
node.initializeCache(new NodeCache(undefined));
const sequenceEdgesSubject: Subject<IEdgeStatus> = new Subject<IEdgeStatus>();
new MockCreator().mockProperty(node, "sequenceEdges$", sequenceEdgesSubject);
@ -710,7 +710,7 @@ describe("PlayService.play", () => {
const frame: IFrame = new FrameHelper().createFrame();
const node: Node = frame.state.currentNode;
node.initializeCache(new NodeCache());
node.initializeCache(new NodeCache(undefined));
const sequenceEdgesSubject: Subject<IEdgeStatus> = new Subject<IEdgeStatus>();
new MockCreator().mockProperty(node, "sequenceEdges$", sequenceEdgesSubject);

View File

@ -13,7 +13,6 @@ export {GraphCalculator} from "./graph/GraphCalculator";
export {GraphMode} from "./graph/GraphMode";
export {GraphService} from "./graph/GraphService";
export {ImageLoadingService} from "./graph/ImageLoadingService";
export {MeshReader} from "./graph/MeshReader";
export {Node} from "./graph/Node";
export {NodeCache} from "./graph/NodeCache";
export {Sequence} from "./graph/Sequence";

View File

@ -1,13 +1,18 @@
import * as falcor from "falcor";
import * as pako from "pako";
import {
ICoreNode,
IDataProvider,
IFillNode,
IFullNode,
ISequence,
ModelCreator,
} from "../API";
import MapillaryError from "../error/MapillaryError";
import IMesh from "./interfaces/IMesh";
import MeshReader from "./MeshReader";
import Urls from "../utils/Urls";
import IClusterReconstruction from "../component/spatialdata/interfaces/IClusterReconstruction";
import IDataProvider from "./interfaces/IDataProvider";
import ModelCreator from "./ModelCreator";
import ICoreNode from "./interfaces/ICoreNode";
import IFillNode from "./interfaces/IFillNode";
import IFullNode from "./interfaces/IFullNode";
import ISequence from "./interfaces/ISequence";
interface IImageByKey<T> {
imageByKey: { [key: string]: T };
@ -125,29 +130,53 @@ export class DataProvider implements IDataProvider {
Promise<{ [geohash: string]: { [imageKey: string]: ICoreNode } }> {
return Promise.resolve(<PromiseLike<falcor.JSONEnvelope<IImagesByH<ICoreNode>>>>this._model
.get([
this._pathImagesByH,
geohashes,
{ from: 0, to: this._pageCount },
this._propertiesKey
.concat(this._propertiesCore)]))
this._pathImagesByH,
geohashes,
{ from: 0, to: this._pageCount },
this._propertiesKey
.concat(this._propertiesCore)]))
.then(
(value: falcor.JSONEnvelope<IImagesByH<ICoreNode>>): { [h: string]: { [index: string]: ICoreNode } } => {
if (!value) {
value = { json: { imagesByH: {} } };
for (let h of geohashes) {
value.json.imagesByH[h] = {};
for (let i: number = 0; i <= this._pageCount; i++) {
value.json.imagesByH[h][i] = null;
}
}
}
(value: falcor.JSONEnvelope<IImagesByH<ICoreNode>>): { [h: string]: { [index: string]: ICoreNode } } => {
if (!value) {
value = { json: { imagesByH: {} } };
for (const h of geohashes) {
value.json.imagesByH[h] = {};
for (let i: number = 0; i <= this._pageCount; i++) {
value.json.imagesByH[h][i] = null;
}
}
}
return value.json.imagesByH;
},
(error: Error) => {
this._invalidateGet(this._pathImagesByH, geohashes);
throw error;
});
return value.json.imagesByH;
},
(error: Error) => {
this._invalidateGet(this._pathImagesByH, geohashes);
throw error;
});
}
public getClusterReconstruction(clusterKey: string, abort?: Promise<void>): Promise<IClusterReconstruction> {
return this._getArrayBuffer(Urls.clusterReconstruction(clusterKey), abort)
.then(
(buffer: ArrayBuffer): IClusterReconstruction => {
const inflated: string =
pako.inflate(<pako.Data>buffer, { to: "string" });
const reconstructions: IClusterReconstruction[] =
JSON.parse(inflated);
if (reconstructions.length < 1) {
throw new MapillaryError("");
}
const reconstruction: IClusterReconstruction = reconstructions[0];
reconstruction.key = clusterKey;
return reconstruction;
},
(reason: Error): IClusterReconstruction => {
throw reason;
});
}
public getFillImages(keys: string[]): Promise<{ [key: string]: IFillNode }> {
@ -161,8 +190,7 @@ export class DataProvider implements IDataProvider {
this._propertiesKey
.concat(this._propertiesUser)]))
.then(
(value: falcor.JSONEnvelope<IImageByKey<IFillNode>>):
{ [key: string]: IFillNode } => {
(value: falcor.JSONEnvelope<IImageByKey<IFillNode>>): { [key: string]: IFillNode } => {
if (!value) {
this._invalidateGet(this._pathImageByKey, keys);
throw new Error(`Images (${keys.join(", ")}) could not be found.`);
@ -188,8 +216,7 @@ export class DataProvider implements IDataProvider {
this._propertiesKey
.concat(this._propertiesUser)]))
.then(
(value: falcor.JSONEnvelope<IImageByKey<IFullNode>>):
{ [key: string]: IFullNode } => {
(value: falcor.JSONEnvelope<IImageByKey<IFullNode>>): { [key: string]: IFullNode } => {
if (!value) {
this._invalidateGet(this._pathImageByKey, keys);
throw new Error(`Images (${keys.join(", ")}) could not be found.`);
@ -203,10 +230,36 @@ export class DataProvider implements IDataProvider {
});
}
public setToken(token?: string): void {
this._model.invalidate([]);
this._model = null;
this._model = this._modelCreator.createModel(this._clientId, token);
public getImage(imageKey: string, size: number, abort?: Promise<void>): Promise<ArrayBuffer> {
return this._getArrayBuffer(Urls.thumbnail(imageKey, size, Urls.origin), abort);
}
public getImageTile(
imageKey: string,
x: number,
y: number,
w: number,
h: number,
scaledW: number,
scaledH: number,
abort?: Promise<void>): Promise<ArrayBuffer> {
const coords: string = `${x},${y},${w},${h}`
const size: string = `${scaledW},${scaledH}`;
return this._getArrayBuffer(
Urls.imageTile(imageKey, coords, size),
abort);
}
public getMesh(imageKey: string, abort?: Promise<void>): Promise<IMesh> {
return this._getArrayBuffer(Urls.protoMesh(imageKey), abort)
.then(
(buffer: ArrayBuffer): IMesh => {
return MeshReader.read(new Buffer(buffer));
},
(reason: Error): IMesh => {
throw reason;
});
}
public getSequences(sequenceKeys: string[]):
@ -239,6 +292,53 @@ export class DataProvider implements IDataProvider {
});
}
public setToken(token?: string): void {
this._model.invalidate([]);
this._model = null;
this._model = this._modelCreator.createModel(this._clientId, token);
}
protected _getArrayBuffer(url: string, abort?: Promise<void>): Promise<ArrayBuffer> {
const xhr: XMLHttpRequest = new XMLHttpRequest();
const promise: Promise<ArrayBuffer> = new Promise(
(resolve, reject) => {
xhr.open("GET", url, true);
xhr.responseType = "arraybuffer";
xhr.timeout = 15000;
xhr.onload = () => {
if (xhr.status !== 200) {
reject(new MapillaryError(`Response status error: ${url}`));
}
if (!xhr.response) {
reject(new MapillaryError(`Response empty: ${url}`));
}
resolve(xhr.response);
};
xhr.onerror = () => {
reject(new MapillaryError(`Request error: ${url}`));
};
xhr.ontimeout = (e: Event) => {
reject(new MapillaryError(`Request timeout: ${url}`));
};
xhr.onabort = (e: Event) => {
reject(new MapillaryError(`Request aborted: ${url}`));
};
xhr.send(null);
});
if (!!abort) { abort.catch((): void => { xhr.abort(); }); }
return promise;
}
private _invalidateGet(path: APIPath, paths: string[]): void {
this._model.invalidate([path, paths]);
}

View File

@ -1,6 +1,6 @@
import * as Pbf from "pbf";
import {IMesh} from "../Graph";
import IMesh from "./interfaces/IMesh";
export class MeshReader {
public static read(buffer: Buffer): IMesh {
@ -17,3 +17,5 @@ export class MeshReader {
}
}
}
export default MeshReader;

View File

@ -2,6 +2,8 @@ import IFillNode from "./IFillNode";
import IFullNode from "./IFullNode";
import ICoreNode from "./ICoreNode";
import ISequence from "./ISequence";
import IClusterReconstruction from "../../component/spatialdata/interfaces/IClusterReconstruction";
import IMesh from "./IMesh";
/**
* Interface that describes the data provider functionality.
@ -11,10 +13,24 @@ import ISequence from "./ISequence";
export interface IDataProvider {
getCoreImages(geohashes: string[]):
Promise<{ [geohash: string]: { [imageKey: string]: ICoreNode } }>;
getClusterReconstruction(clusterKey: string, abort?: Promise<void>):
Promise<IClusterReconstruction>;
getFillImages(imageKeys: string[]):
Promise<{ [imageKey: string]: IFillNode }>;
getFullImages(imageKeys: string[]):
Promise<{ [imageKey: string]: IFullNode }>;
getImage(imageKey: string, size: number, abort?: Promise<void>):
Promise<ArrayBuffer>;
getImageTile(
imageKey: string,
x: number,
y: number,
w: number,
h: number,
scaledW: number,
scaledH: number,
abort?: Promise<void>): Promise<ArrayBuffer>;
getMesh(imageKey: string, abort?: Promise<void>): Promise<IMesh>;
getSequences(sequenceKeys: string[]):
Promise<{ [sequenceKey: string]: ISequence }>;
setToken(token?: string): void;

View File

@ -27,6 +27,7 @@ import {
} from "../Graph";
import { GeoRBush } from "../Geo";
import API from "../api/API";
import { IDataProvider } from "../api/interfaces/interfaces";
type NodeIndexItem = {
lat: number;
@ -894,10 +895,11 @@ export class Graph {
throw new GraphMapillaryError(`Node already in cache (${key}).`);
}
let node: Node = this.getNode(key);
node.initializeCache(new NodeCache());
const node: Node = this.getNode(key);
const provider: IDataProvider = this._api.dataProvider;
node.initializeCache(new NodeCache(provider));
let accessed: number = new Date().getTime();
const accessed: number = new Date().getTime();
this._cachedNodes[key] = { accessed: accessed, node: node };
this._updateCachedTileAccess(key, accessed);

View File

@ -10,11 +10,11 @@ import {
import {IEdge} from "../Edge";
import {
IEdgeStatus,
IMesh,
ILoadStatus,
NodeCache,
} from "../Graph";
import {ImageSize} from "../Viewer";
import IMesh from "../api/interfaces/IMesh";
/**
* @class Node

View File

@ -1,20 +1,22 @@
import {of as observableOf, combineLatest as observableCombineLatest, Subject, Observable, Subscriber, Subscription} from "rxjs";
import { of as observableOf, combineLatest as observableCombineLatest, Subject, Observable, Subscriber, Subscription } from "rxjs";
import {map, tap, startWith, publishReplay, refCount, finalize, first} from "rxjs/operators";
import { map, tap, startWith, publishReplay, refCount, finalize, first, throttleTime } from "rxjs/operators";
import {IEdge} from "../Edge";
import { IEdge } from "../Edge";
import {
IEdgeStatus,
IMesh,
ILoadStatus,
ILoadStatusObject,
MeshReader,
} from "../Graph";
import {
Settings,
Urls,
} from "../Utils";
import {ImageSize} from "../Viewer";
import { ImageSize } from "../Viewer";
import IMesh from "../api/interfaces/IMesh";
import MeshReader from "../api/MeshReader";
import DataProvider from "../api/DataProvider";
import { IDataProvider } from "../api/interfaces/interfaces";
/**
* @class NodeCache
@ -24,14 +26,16 @@ import {ImageSize} from "../Viewer";
export class NodeCache {
private _disposed: boolean;
private _provider: IDataProvider;
private _image: HTMLImageElement;
private _loadStatus: ILoadStatus;
private _mesh: IMesh;
private _sequenceEdges: IEdgeStatus;
private _spatialEdges: IEdgeStatus;
private _imageRequest: XMLHttpRequest;
private _meshRequest: XMLHttpRequest;
private _imageAborter: Function;
private _meshAborter: Function;
private _imageChanged$: Subject<HTMLImageElement>;
private _image$: Observable<HTMLImageElement>;
@ -49,9 +53,11 @@ export class NodeCache {
/**
* Create a new node cache instance.
*/
constructor() {
constructor(provider: IDataProvider) {
this._disposed = false;
this._provider = provider;
this._image = null;
this._loadStatus = { loaded: 0, total: 0 };
this._mesh = null;
@ -190,33 +196,33 @@ export class NodeCache {
Settings.baseImageSize;
this._cachingAssets$ = observableCombineLatest(
this._cacheImage$(key, imageSize),
this._cacheMesh$(key, merged)).pipe(
map(
([imageStatus, meshStatus]: [ILoadStatusObject<HTMLImageElement>, ILoadStatusObject<IMesh>]): NodeCache => {
this._loadStatus.loaded = 0;
this._loadStatus.total = 0;
this._cacheImage$(key, imageSize),
this._cacheMesh$(key, merged)).pipe(
map(
([imageStatus, meshStatus]: [ILoadStatusObject<HTMLImageElement>, ILoadStatusObject<IMesh>]): NodeCache => {
this._loadStatus.loaded = 0;
this._loadStatus.total = 0;
if (meshStatus) {
this._mesh = meshStatus.object;
this._loadStatus.loaded += meshStatus.loaded.loaded;
this._loadStatus.total += meshStatus.loaded.total;
}
if (meshStatus) {
this._mesh = meshStatus.object;
this._loadStatus.loaded += meshStatus.loaded.loaded;
this._loadStatus.total += meshStatus.loaded.total;
}
if (imageStatus) {
this._image = imageStatus.object;
this._loadStatus.loaded += imageStatus.loaded.loaded;
this._loadStatus.total += imageStatus.loaded.total;
}
if (imageStatus) {
this._image = imageStatus.object;
this._loadStatus.loaded += imageStatus.loaded.loaded;
this._loadStatus.total += imageStatus.loaded.total;
}
return this;
}),
finalize(
(): void => {
this._cachingAssets$ = null;
}),
publishReplay(1),
refCount());
return this;
}),
finalize(
(): void => {
this._cachingAssets$ = null;
}),
publishReplay(1),
refCount());
this._cachingAssets$.pipe(
first(
@ -319,13 +325,16 @@ export class NodeCache {
this._disposed = true;
if (this._imageRequest != null) {
this._imageRequest.abort();
if (this._imageAborter != null) {
this._imageAborter();
this._imageAborter = null;
}
if (this._meshRequest != null) {
this._meshRequest.abort();
if (this._meshAborter != null) {
this._meshAborter();
this._meshAborter = null;
}
}
/**
@ -356,77 +365,47 @@ export class NodeCache {
private _cacheImage$(key: string, imageSize: ImageSize): Observable<ILoadStatusObject<HTMLImageElement>> {
return Observable.create(
(subscriber: Subscriber<ILoadStatusObject<HTMLImageElement>>): void => {
let xmlHTTP: XMLHttpRequest = new XMLHttpRequest();
xmlHTTP.open("GET", Urls.thumbnail(key, imageSize, Urls.origin), true);
xmlHTTP.responseType = "arraybuffer";
xmlHTTP.timeout = 15000;
const abort: Promise<void> = new Promise(
(_, reject): void => {
this._imageAborter = reject;
});
xmlHTTP.onload = (pe: ProgressEvent) => {
if (xmlHTTP.status !== 200) {
this._imageRequest = null;
this._provider.getImage(key, imageSize, abort)
.then(
(buffer: ArrayBuffer): void => {
this._imageAborter = null;
subscriber.error(
new Error(`Failed to fetch image (${key}). Status: ${xmlHTTP.status}, ${xmlHTTP.statusText}`));
const image: HTMLImageElement = new Image();
image.crossOrigin = "Anonymous";
return;
}
image.onload = (e: Event) => {
if (this._disposed) {
window.URL.revokeObjectURL(image.src);
subscriber.error(new Error(`Image load was aborted (${key})`));
let image: HTMLImageElement = new Image();
image.crossOrigin = "Anonymous";
return;
}
image.onload = (e: Event) => {
this._imageRequest = null;
subscriber.next({
loaded: { loaded: 1, total: 1 },
object: image
});
subscriber.complete();
};
if (this._disposed) {
window.URL.revokeObjectURL(image.src);
subscriber.error(new Error(`Image load was aborted (${key})`));
image.onerror = () => {
this._imageAborter = null;
return;
}
subscriber.error(new Error(`Failed to load image (${key})`));
};
subscriber.next({ loaded: { loaded: pe.loaded, total: pe.total }, object: image });
subscriber.complete();
};
image.onerror = (error: ErrorEvent) => {
this._imageRequest = null;
subscriber.error(new Error(`Failed to load image (${key})`));
};
let blob: Blob = new Blob([xmlHTTP.response]);
image.src = window.URL.createObjectURL(blob);
};
xmlHTTP.onprogress = (pe: ProgressEvent) => {
if (this._disposed) {
return;
}
subscriber.next({loaded: { loaded: pe.loaded, total: pe.total }, object: null });
};
xmlHTTP.onerror = (error: Event) => {
this._imageRequest = null;
subscriber.error(new Error(`Failed to fetch image (${key})`));
};
xmlHTTP.ontimeout = (e: Event) => {
this._imageRequest = null;
subscriber.error(new Error(`Image request timed out (${key})`));
};
xmlHTTP.onabort = (event: Event) => {
this._imageRequest = null;
subscriber.error(new Error(`Image request was aborted (${key})`));
};
this._imageRequest = xmlHTTP;
xmlHTTP.send(null);
const blob: Blob = new Blob([buffer]);
image.src = window.URL.createObjectURL(blob);
},
(error: Error): void => {
this._imageAborter = null;
subscriber.error(error);
});
});
}
@ -448,61 +427,30 @@ export class NodeCache {
return;
}
let xmlHTTP: XMLHttpRequest = new XMLHttpRequest();
xmlHTTP.open("GET", Urls.protoMesh(key), true);
xmlHTTP.responseType = "arraybuffer";
xmlHTTP.timeout = 15000;
const abort: Promise<void> = new Promise(
(_, reject): void => {
this._meshAborter = reject;
});
xmlHTTP.onload = (pe: ProgressEvent) => {
this._meshRequest = null;
this._provider.getMesh(key, abort)
.then(
(mesh: IMesh): void => {
this._meshAborter = null;
if (this._disposed) {
return;
}
if (this._disposed) {
return;
}
let mesh: IMesh = xmlHTTP.status === 200 ?
MeshReader.read(new Buffer(xmlHTTP.response)) :
{ faces: [], vertices: [] };
subscriber.next({ loaded: { loaded: pe.loaded, total: pe.total }, object: mesh });
subscriber.complete();
};
xmlHTTP.onprogress = (pe: ProgressEvent) => {
if (this._disposed) {
return;
}
subscriber.next({ loaded: { loaded: pe.loaded, total: pe.total }, object: null });
};
xmlHTTP.onerror = (e: Event) => {
this._meshRequest = null;
console.error(`Failed to cache mesh (${key})`);
subscriber.next(this._createEmptyMeshLoadStatus());
subscriber.complete();
};
xmlHTTP.ontimeout = (e: Event) => {
this._meshRequest = null;
console.error(`Mesh request timed out (${key})`);
subscriber.next(this._createEmptyMeshLoadStatus());
subscriber.complete();
};
xmlHTTP.onabort = (e: Event) => {
this._meshRequest = null;
subscriber.error(new Error(`Mesh request was aborted (${key})`));
};
this._meshRequest = xmlHTTP;
xmlHTTP.send(null);
subscriber.next({
loaded: { loaded: 1, total: 1 },
object: mesh,
});
subscriber.complete();
},
(error: Error): void => {
this._meshAborter = null;
subscriber.error(error);
});
});
}

View File

@ -2,4 +2,3 @@ export {IEdgeStatus} from "./IEdgeStatus";
export {IGraphConfiguration} from "./IGraphConfiguration";
export {ILoadStatus} from "./ILoadStatus";
export {ILoadStatusObject} from "./ILoadStatusObject";
export {IMesh} from "./IMesh";

View File

@ -1,4 +1,4 @@
import {IUrlOptions} from "../Viewer";
import { IUrlOptions } from "../Viewer";
export class Urls {
private static _apiHost: string = "a.mapillary.com";
@ -42,6 +42,10 @@ export class Urls {
return `${Urls._scheme}://${Urls._apiHost}/v3/model.json?client_id=${clientId}`;
}
public static imageTile(imageKey: string, coords: string, size: string): string {
return `${Urls.tileScheme}://${Urls.tileDomain}/${imageKey}/${coords}/${size}/0/default.jpg`;
}
public static protoMesh(key: string): string {
return `${Urls._scheme}://${Urls._meshHost}/v2/mesh/${key}`;
}