From 2b4617bd7afe034268b4fe6372f3b671e932eee2 Mon Sep 17 00:00:00 2001 From: Oscar Lorentzon Date: Mon, 5 Oct 2020 18:06:40 +0200 Subject: [PATCH] feat(api): get static resources through data provider Use data provider in node caching. --- spec/api/DataProvider.spec.ts | 185 ++++++++++++- .../direction/DirectionComponent.spec.ts | 8 +- spec/graph/Node.spec.ts | 11 +- spec/graph/NodeCache.spec.ts | 75 +++--- spec/viewer/PlayService.spec.ts | 12 +- src/Graph.ts | 1 - src/api/DataProvider.ts | 174 +++++++++--- src/{graph => api}/MeshReader.ts | 4 +- src/api/interfaces/IDataProvider.ts | 16 ++ src/{graph => api}/interfaces/IMesh.ts | 0 src/graph/Graph.ts | 8 +- src/graph/Node.ts | 2 +- src/graph/NodeCache.ts | 252 +++++++----------- src/graph/interfaces/interfaces.ts | 1 - src/utils/Urls.ts | 6 +- 15 files changed, 500 insertions(+), 255 deletions(-) rename src/{graph => api}/MeshReader.ts (88%) rename src/{graph => api}/interfaces/IMesh.ts (100%) diff --git a/spec/api/DataProvider.spec.ts b/spec/api/DataProvider.spec.ts index 42708409..1a8eb5c1 100644 --- a/spec/api/DataProvider.spec.ts +++ b/spec/api/DataProvider.spec.ts @@ -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, "XMLHttpRequest").and.returnValue(requestMock); + + const abort: Promise = 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, "XMLHttpRequest").and.returnValue(requestMock); + + let aborter: Function; + const abort: Promise = 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, "XMLHttpRequest").and.returnValue(requestMock); + + const abort: Promise = 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, "XMLHttpRequest").and.returnValue(requestMock); + + const abort: Promise = 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, "XMLHttpRequest").and.returnValue(requestMock); + + const abort: Promise = 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, "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); + }); +}); diff --git a/spec/component/direction/DirectionComponent.spec.ts b/spec/component/direction/DirectionComponent.spec.ts index b3b4039e..9b9618f1 100644 --- a/spec/component/direction/DirectionComponent.spec.ts +++ b/spec/component/direction/DirectionComponent.spec.ts @@ -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([]); (>navigatorMock.stateService.currentNode$).next(node); @@ -88,7 +88,7 @@ describe("DirectionComponent.activate", () => { (navigatorMock.graphService.cacheSequence$).and.returnValue(observableOf(sequence)); const node: Node = new NodeHelper().createNode(); - node.initializeCache(new NodeCache()); + node.initializeCache(new NodeCache(undefined)); node.cacheSpatialEdges([]); (>navigatorMock.stateService.currentNode$).next(node); @@ -117,7 +117,7 @@ describe("DirectionComponent.activate", () => { (navigatorMock.graphService.cacheSequence$).and.returnValue(cacheSequence$); const node: Node = new NodeHelper().createNode(); - node.initializeCache(new NodeCache()); + node.initializeCache(new NodeCache(undefined)); node.cacheSpatialEdges([]); (>navigatorMock.stateService.currentNode$).next(node); @@ -149,7 +149,7 @@ describe("DirectionComponent.activate", () => { (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([]); (>navigatorMock.stateService.currentNode$).next(node); diff --git a/spec/graph/Node.spec.ts b/spec/graph/Node.spec.ts index 1fb874a3..4f7e42da 100644 --- a/spec/graph/Node.spec.ts +++ b/spec/graph/Node.spec.ts @@ -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; } diff --git a/spec/graph/NodeCache.spec.ts b/spec/graph/NodeCache.spec.ts index 651646f8..6d283590 100644 --- a/spec/graph/NodeCache.spec.ts +++ b/spec/graph/NodeCache.spec.ts @@ -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, "XMLHttpRequest").and.returnValue(requestMock); + const dataProvider: IDataProvider = new DataProvider("cid"); + spyOn(dataProvider, "getImage").and.returnValue(promise); const imageMock: HTMLImageElement = new Image(); spyOn(window, "Image").and.returnValue(imageMock); @@ -225,7 +230,7 @@ describe("NodeCache.cacheImage$", () => { spyOn(window, "Blob").and.returnValue({}); 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, "XMLHttpRequest").and.returnValue(requestMock); + const dataProvider: IDataProvider = new DataProvider("cid"); + spyOn(dataProvider, "getImage").and.returnValue(promise); const imageMock: HTMLImageElement = new Image(); spyOn(window, "Image").and.returnValue(imageMock); @@ -259,15 +264,12 @@ describe("NodeCache.cacheImage$", () => { spyOn(window, "Blob").and.returnValue({}); 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, "XMLHttpRequest").and.returnValue(requestMock); + const dataProvider: IDataProvider = new DataProvider("cid"); + spyOn(dataProvider, "getImage").and.returnValue(promise); const imageMock: HTMLImageElement = new Image(); spyOn(window, "Image").and.returnValue(imageMock); @@ -289,7 +294,7 @@ describe("NodeCache.cacheImage$", () => { spyOn(window, "Blob").and.returnValue({}); 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")); }); }); diff --git a/spec/viewer/PlayService.spec.ts b/spec/viewer/PlayService.spec.ts index 5482f481..119b8377 100644 --- a/spec/viewer/PlayService.spec.ts +++ b/spec/viewer/PlayService.spec.ts @@ -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)); (>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)); (>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 = new Subject(); 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 = new Subject(); 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 = new Subject(); 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 = new Subject(); new MockCreator().mockProperty(node, "sequenceEdges$", sequenceEdgesSubject); diff --git a/src/Graph.ts b/src/Graph.ts index e78e4a10..89823e74 100644 --- a/src/Graph.ts +++ b/src/Graph.ts @@ -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"; diff --git a/src/api/DataProvider.ts b/src/api/DataProvider.ts index 65dfae66..076bf654 100644 --- a/src/api/DataProvider.ts +++ b/src/api/DataProvider.ts @@ -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 { imageByKey: { [key: string]: T }; @@ -125,29 +130,53 @@ export class DataProvider implements IDataProvider { Promise<{ [geohash: string]: { [imageKey: string]: ICoreNode } }> { return Promise.resolve(>>>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>): { [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>): { [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): Promise { + return this._getArrayBuffer(Urls.clusterReconstruction(clusterKey), abort) + .then( + (buffer: ArrayBuffer): IClusterReconstruction => { + const inflated: string = + pako.inflate(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>): - { [key: string]: IFillNode } => { + (value: falcor.JSONEnvelope>): { [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>): - { [key: string]: IFullNode } => { + (value: falcor.JSONEnvelope>): { [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): Promise { + 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): Promise { + 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): Promise { + 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): Promise { + const xhr: XMLHttpRequest = new XMLHttpRequest(); + + const promise: Promise = 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]); } diff --git a/src/graph/MeshReader.ts b/src/api/MeshReader.ts similarity index 88% rename from src/graph/MeshReader.ts rename to src/api/MeshReader.ts index 12046eb8..fc530120 100644 --- a/src/graph/MeshReader.ts +++ b/src/api/MeshReader.ts @@ -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; diff --git a/src/api/interfaces/IDataProvider.ts b/src/api/interfaces/IDataProvider.ts index 9816bb1a..b2c19f1c 100644 --- a/src/api/interfaces/IDataProvider.ts +++ b/src/api/interfaces/IDataProvider.ts @@ -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): + Promise; getFillImages(imageKeys: string[]): Promise<{ [imageKey: string]: IFillNode }>; getFullImages(imageKeys: string[]): Promise<{ [imageKey: string]: IFullNode }>; + getImage(imageKey: string, size: number, abort?: Promise): + Promise; + getImageTile( + imageKey: string, + x: number, + y: number, + w: number, + h: number, + scaledW: number, + scaledH: number, + abort?: Promise): Promise; + getMesh(imageKey: string, abort?: Promise): Promise; getSequences(sequenceKeys: string[]): Promise<{ [sequenceKey: string]: ISequence }>; setToken(token?: string): void; diff --git a/src/graph/interfaces/IMesh.ts b/src/api/interfaces/IMesh.ts similarity index 100% rename from src/graph/interfaces/IMesh.ts rename to src/api/interfaces/IMesh.ts diff --git a/src/graph/Graph.ts b/src/graph/Graph.ts index d6fd1425..42546a42 100644 --- a/src/graph/Graph.ts +++ b/src/graph/Graph.ts @@ -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); diff --git a/src/graph/Node.ts b/src/graph/Node.ts index 104bbe9b..d861424c 100644 --- a/src/graph/Node.ts +++ b/src/graph/Node.ts @@ -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 diff --git a/src/graph/NodeCache.ts b/src/graph/NodeCache.ts index 2e10ff23..7d6942e2 100644 --- a/src/graph/NodeCache.ts +++ b/src/graph/NodeCache.ts @@ -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; private _image$: Observable; @@ -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, ILoadStatusObject]): NodeCache => { - this._loadStatus.loaded = 0; - this._loadStatus.total = 0; + this._cacheImage$(key, imageSize), + this._cacheMesh$(key, merged)).pipe( + map( + ([imageStatus, meshStatus]: [ILoadStatusObject, ILoadStatusObject]): 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> { return Observable.create( (subscriber: Subscriber>): 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 = 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 = 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); + }); }); } diff --git a/src/graph/interfaces/interfaces.ts b/src/graph/interfaces/interfaces.ts index 9c820e9b..5139baf8 100644 --- a/src/graph/interfaces/interfaces.ts +++ b/src/graph/interfaces/interfaces.ts @@ -2,4 +2,3 @@ export {IEdgeStatus} from "./IEdgeStatus"; export {IGraphConfiguration} from "./IGraphConfiguration"; export {ILoadStatus} from "./ILoadStatus"; export {ILoadStatusObject} from "./ILoadStatusObject"; -export {IMesh} from "./IMesh"; diff --git a/src/utils/Urls.ts b/src/utils/Urls.ts index c725a65b..c46345c2 100644 --- a/src/utils/Urls.ts +++ b/src/utils/Urls.ts @@ -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}`; }