UNPKG

mapillary-js

Version:

WebGL JavaScript library for displaying street level imagery from mapillary.com

1,196 lines (892 loc) 48.8 kB
import {of as observableOf, zip as observableZip, Observable , Subject } from "rxjs"; import {take, first, skip} from "rxjs/operators"; import { APIv3, IFullNode, } from "../../src/API"; import { EdgeDirection } from "../../src/Edge"; import { Graph, GraphMode, GraphService, IEdgeStatus, ImageLoadingService, Node, NodeCache, Sequence, } from "../../src/Graph"; import { ICurrentState, IFrame, StateService, State, } from "../../src/State"; import { PlayService, } from "../../src/Viewer"; import { MockCreator } from "../helper/MockCreator.spec"; import { NodeHelper } from "../helper/NodeHelper.spec"; import { StateServiceMockCreator } from "../helper/StateServiceMockCreator.spec"; import { FrameHelper } from "../helper/FrameHelper.spec"; describe("PlayService.ctor", () => { it("should be defined when constructed", () => { const clientId: string = "clientId"; const apiV3: APIv3 = new APIv3(clientId); const imageLoadingService: ImageLoadingService = new ImageLoadingService(); const graphService: GraphService = new GraphService(new Graph(apiV3), imageLoadingService); const stateService: StateService = new StateService(); const playService: PlayService = new PlayService(graphService, stateService); expect(playService).toBeDefined(); }); it("should emit default values", (done: () => void) => { const clientId: string = "clientId"; const apiV3: APIv3 = new APIv3(clientId); const imageLoadingService: ImageLoadingService = new ImageLoadingService(); const graphService: GraphService = new GraphService(new Graph(apiV3), imageLoadingService); const stateService: StateService = new StateService(); const playService: PlayService = new PlayService(graphService, stateService); observableZip( playService.direction$, playService.playing$, playService.speed$).pipe( first()) .subscribe( ([d, p, s]: [EdgeDirection, boolean, number]): void => { expect(d).toBe(EdgeDirection.Next); expect(p).toBe(false); expect(s).toBe(0.5); done(); }); }); }); describe("PlayService.playing", () => { it("should be playing after calling play", (done: () => void) => { const clientId: string = "clientId"; const apiV3: APIv3 = new APIv3(clientId); const imageLoadingService: ImageLoadingService = new ImageLoadingService(); const graphService: GraphService = new GraphService(new Graph(apiV3), imageLoadingService); const stateService: StateService = new StateService(); const playService: PlayService = new PlayService(graphService, stateService); playService.play(); expect(playService.playing).toBe(true); playService.playing$ .subscribe( (playing: boolean): void => { expect(playing).toBe(true); done(); }); }); it("should not be playing after calling stop", (done: () => void) => { const clientId: string = "clientId"; const apiV3: APIv3 = new APIv3(clientId); const imageLoadingService: ImageLoadingService = new ImageLoadingService(); const graphService: GraphService = new GraphService(new Graph(apiV3), imageLoadingService); const stateService: StateService = new StateService(); const playService: PlayService = new PlayService(graphService, stateService); playService.play(); const setGraphModeSpy: jasmine.Spy = spyOn(graphService, "setGraphMode").and.stub(); const cutNodesSpy: jasmine.Spy = spyOn(stateService, "cutNodes").and.stub(); const setSpeedSpy: jasmine.Spy = spyOn(stateService, "setSpeed").and.stub(); playService.stop(); expect(setGraphModeSpy.calls.count()).toBe(1); expect(setGraphModeSpy.calls.argsFor(0)[0]).toBe(GraphMode.Spatial); expect(cutNodesSpy.calls.count()).toBe(1); expect(setSpeedSpy.calls.count()).toBe(1); expect(setSpeedSpy.calls.argsFor(0)[0]).toBe(1); expect(playService.playing).toBe(false); playService.playing$ .subscribe( (playing: boolean): void => { expect(playing).toBe(false); done(); }); }); }); describe("PlayService.speed$", () => { it("should emit when changing speed", (done: () => void) => { const clientId: string = "clientId"; const apiV3: APIv3 = new APIv3(clientId); const imageLoadingService: ImageLoadingService = new ImageLoadingService(); const graphService: GraphService = new GraphService(new Graph(apiV3), imageLoadingService); const stateService: StateService = new StateService(); const playService: PlayService = new PlayService(graphService, stateService); playService.speed$.pipe( skip(1)) .subscribe( (speed: number): void => { expect(speed).toBe(0); done(); }); playService.setSpeed(0); }); it("should not emit when setting current speed", () => { const clientId: string = "clientId"; const apiV3: APIv3 = new APIv3(clientId); const imageLoadingService: ImageLoadingService = new ImageLoadingService(); const graphService: GraphService = new GraphService(new Graph(apiV3), imageLoadingService); const stateService: StateService = new StateService(); const playService: PlayService = new PlayService(graphService, stateService); playService.setSpeed(1); let speedEmitCount: number = 0; let firstEmit: boolean = true; playService.speed$.pipe( skip(1)) .subscribe( (speed: number): void => { speedEmitCount ++; if (firstEmit) { expect(speed).toBe(0); firstEmit = false; } else { expect(speed).toBe(1); } }); playService.setSpeed(0); playService.setSpeed(0); playService.setSpeed(1); playService.setSpeed(1); expect(speedEmitCount).toBe(2); }); it("should clamp speed values to 0, 1 interval", (done: () => void) => { const clientId: string = "clientId"; const apiV3: APIv3 = new APIv3(clientId); const imageLoadingService: ImageLoadingService = new ImageLoadingService(); const graphService: GraphService = new GraphService(new Graph(apiV3), imageLoadingService); const stateService: StateService = new StateService(); const playService: PlayService = new PlayService(graphService, stateService); let firstEmit: boolean = true; playService.speed$.pipe( skip(1)) .subscribe( (speed: number): void => { if (firstEmit) { expect(speed).toBe(0); firstEmit = false; } else { expect(speed).toBe(1); done(); } }); playService.setSpeed(-1); playService.setSpeed(2); }); }); let createState: () => ICurrentState = (): ICurrentState => { return { alpha: 0, camera: null, currentCamera: null, currentIndex: 0, currentNode: null, currentTransform: null, lastNode: null, motionless: false, nodesAhead: 0, previousNode: null, previousTransform: null, reference: null, state: State.Traversing, trajectory: null, zoom: 0, }; }; describe("PlayService.play", () => { let nodeHelper: NodeHelper; let apiV3: APIv3; let imageLoadingService: ImageLoadingService; let graphService: GraphService; let stateService: StateService; beforeEach(() => { nodeHelper = new NodeHelper(); apiV3 = new APIv3("clientId"); imageLoadingService = new ImageLoadingService(); graphService = new GraphService(new Graph(apiV3), imageLoadingService); stateService = new StateServiceMockCreator().create(); }); it("should set graph mode when passing speed threshold", () => { const playService: PlayService = new PlayService(graphService, stateService); const setGraphModeSpy: jasmine.Spy = spyOn(graphService, "setGraphMode").and.stub(); playService.setSpeed(0); playService.play(); playService.setSpeed(1); playService.setSpeed(0); expect(setGraphModeSpy.calls.count()).toBe(3); expect(setGraphModeSpy.calls.argsFor(0)[0]).toBe(GraphMode.Spatial); expect(setGraphModeSpy.calls.argsFor(1)[0]).toBe(GraphMode.Sequence); expect(setGraphModeSpy.calls.argsFor(2)[0]).toBe(GraphMode.Spatial); }); it("should stop immediately if node does not have an edge in current direction and no bridge", () => { const playService: PlayService = new PlayService(graphService, stateService); const stopSpy: jasmine.Spy = spyOn(playService, "stop").and.callThrough(); spyOn(graphService, "cacheSequence$").and.returnValue(new Subject<Sequence>()); spyOn(graphService, "cacheSequenceNodes$").and.returnValue(new Subject<Sequence>()); spyOn(graphService, "cacheBoundingBox$").and.returnValue(observableOf([])); playService.setDirection(EdgeDirection.Next); playService.play(); const frame: IFrame = new FrameHelper().createFrame(); frame.state.currentNode.initializeCache(new NodeCache()); (<Subject<IFrame>>stateService.currentState$).next(frame); frame.state.currentNode.cacheSequenceEdges([]); expect(stopSpy.calls.count()).toBe(1); }); it("should stop if error occurs", () => { spyOn(console, "error").and.stub(); const playService: PlayService = new PlayService(graphService, stateService); const stopSpy: jasmine.Spy = spyOn(playService, "stop").and.callThrough(); spyOn(graphService, "cacheSequence$").and.returnValue(new Subject<Sequence>()); spyOn(graphService, "cacheSequenceNodes$").and.returnValue(new Subject<Sequence>()); spyOn(graphService, "cacheBoundingBox$").and.returnValue(new Subject<Node[]>()); playService.setDirection(EdgeDirection.Next); playService.play(); const frame: IFrame = new FrameHelper().createFrame(); const node: Node = frame.state.currentNode; const sequenceEdgesSubject: Subject<IEdgeStatus> = new Subject<IEdgeStatus>(); new MockCreator().mockProperty(node, "sequenceEdges$", sequenceEdgesSubject); (<Subject<IFrame>>stateService.currentState$).next(frame); sequenceEdgesSubject.error(new Error()); expect(stopSpy.calls.count()).toBe(1); }); it("should emit in correct order if stopping immediately", (done: () => void) => { const playService: PlayService = new PlayService(graphService, stateService); const stopSpy: jasmine.Spy = spyOn(playService, "stop").and.callThrough(); spyOn(graphService, "cacheSequence$").and.returnValue(new Subject<Sequence>()); spyOn(graphService, "cacheSequenceNodes$").and.returnValue(new Subject<Sequence>()); spyOn(graphService, "cacheBoundingBox$").and.returnValue(observableOf([])); playService.setDirection(EdgeDirection.Next); let firstEmit: boolean = true; playService.playing$.pipe( skip(1), take(2)) .subscribe( (playing: boolean): void => { expect(playing).toBe(playService.playing); if (firstEmit) { expect(playing).toBe(true); firstEmit = false; } else { expect(playing).toBe(false); done(); } }); playService.play(); const frame: IFrame = new FrameHelper().createFrame(); frame.state.currentNode.initializeCache(new NodeCache()); (<Subject<IFrame>>stateService.currentState$).next(frame); frame.state.currentNode.cacheSequenceEdges([]); }); it("should not stop if nodes are not cached", () => { const playService: PlayService = new PlayService(graphService, stateService); const stopSpy: jasmine.Spy = spyOn(playService, "stop").and.callThrough(); spyOn(graphService, "cacheSequence$").and.returnValue(new Subject<Sequence>()); spyOn(graphService, "cacheSequenceNodes$").and.returnValue(new Subject<Sequence>()); spyOn(graphService, "cacheBoundingBox$").and.returnValue(observableOf([])); playService.setDirection(EdgeDirection.Next); playService.play(); const frame: IFrame = new FrameHelper().createFrame(); const node: Node = frame.state.currentNode; node.initializeCache(new NodeCache()); const sequenceEdgesSubject: Subject<IEdgeStatus> = new Subject<IEdgeStatus>(); new MockCreator().mockProperty(node, "sequenceEdges$", sequenceEdgesSubject); (<Subject<IFrame>>stateService.currentState$).next(frame); sequenceEdgesSubject.next({ cached: false, edges: []}); expect(stopSpy.calls.count()).toBe(0); sequenceEdgesSubject.next({ cached: true, edges: []}); expect(stopSpy.calls.count()).toBe(1); }); it("should not stop until bridge call completes", () => { const playService: PlayService = new PlayService(graphService, stateService); const stopSpy: jasmine.Spy = spyOn(playService, "stop").and.callThrough(); spyOn(graphService, "cacheSequence$").and.returnValue(new Subject<Sequence>()); spyOn(graphService, "cacheSequenceNodes$").and.returnValue(new Subject<Sequence>()); const cacheBoundingBoxSubject: Subject<Node[]> = new Subject<Node[]>(); spyOn(graphService, "cacheBoundingBox$").and.returnValue(cacheBoundingBoxSubject); playService.setDirection(EdgeDirection.Next); playService.play(); const frame: IFrame = new FrameHelper().createFrame(); const node: Node = frame.state.currentNode; node.initializeCache(new NodeCache()); const sequenceEdgesSubject: Subject<IEdgeStatus> = new Subject<IEdgeStatus>(); const prevFullNode: IFullNode = new NodeHelper().createFullNode(); prevFullNode.captured_at = -1; const prevNode: Node = new Node(prevFullNode); prevNode.makeFull(prevFullNode); frame.state.trajectory.splice(0, 0, prevNode); frame.state.currentIndex = 1; new MockCreator().mockProperty(node, "sequenceEdges$", sequenceEdgesSubject); (<Subject<IFrame>>stateService.currentState$).next(frame); sequenceEdgesSubject.next({ cached: false, edges: []}); expect(stopSpy.calls.count()).toBe(0); sequenceEdgesSubject.next({ cached: true, edges: []}); expect(stopSpy.calls.count()).toBe(0); cacheBoundingBoxSubject.next([]); expect(stopSpy.calls.count()).toBe(1); }); it("should bridge if camera id corresponds and time monotonic", () => { const playService: PlayService = new PlayService(graphService, stateService); const stopSpy: jasmine.Spy = spyOn(playService, "stop").and.callThrough(); spyOn(graphService, "cacheSequence$").and.returnValue(new Subject<Sequence>()); spyOn(graphService, "cacheSequenceNodes$").and.returnValue(new Subject<Sequence>()); const cacheBoundingBoxSubject: Subject<Node[]> = new Subject<Node[]>(); spyOn(graphService, "cacheBoundingBox$").and.returnValue(cacheBoundingBoxSubject); const cacheNodeSubject: Subject<Node> = new Subject<Node>(); const cacheNodeSpy: jasmine.Spy = spyOn(graphService, "cacheNode$"); cacheNodeSpy.and.returnValue(cacheNodeSubject); const appendNodesSpy: jasmine.Spy = <jasmine.Spy>stateService.appendNodes; appendNodesSpy.and.stub(); playService.setDirection(EdgeDirection.Next); playService.play(); const cameraUuid: string = "camera_uuid"; const sequenceKey1: string = "sequence1"; const currentFullNode: IFullNode = new NodeHelper().createFullNode(); currentFullNode.captured_at = 0; currentFullNode.captured_with_camera_uuid = cameraUuid; currentFullNode.key = "currKey"; currentFullNode.sequence_key = sequenceKey1; const currentNode: Node = new Node(currentFullNode); currentNode.makeFull(currentFullNode); const sequenceEdgesSubject: Subject<IEdgeStatus> = new Subject<IEdgeStatus>(); new MockCreator().mockProperty(currentNode, "sequenceEdges$", sequenceEdgesSubject); const prevFullNode: IFullNode = new NodeHelper().createFullNode(); prevFullNode.captured_at = -1; prevFullNode.captured_with_camera_uuid = cameraUuid; prevFullNode.key = "prevKey"; prevFullNode.sequence_key = sequenceKey1; const prevNode: Node = new Node(prevFullNode); prevNode.makeFull(prevFullNode); const state: ICurrentState = createState(); state.trajectory = [prevNode, currentNode]; state.lastNode = currentNode; state.currentNode = currentNode; state.nodesAhead = 0; state.currentIndex = 1; (<Subject<IFrame>>stateService.currentState$).next({ fps: 60, id: 0, state: state }); sequenceEdgesSubject.next({ cached: true, edges: []}); expect(stopSpy.calls.count()).toBe(0); const bridgeFullNode: IFullNode = new NodeHelper().createFullNode(); bridgeFullNode.captured_at = 1; bridgeFullNode.captured_with_camera_uuid = cameraUuid; bridgeFullNode.key = "bridgeKey"; bridgeFullNode.sequence_key = "sequenceBrdige"; const bridgeNode: Node = new Node(bridgeFullNode); bridgeNode.makeFull(bridgeFullNode); cacheBoundingBoxSubject.next([bridgeNode]); cacheNodeSubject.next(bridgeNode); expect(stopSpy.calls.count()).toBe(0); expect(appendNodesSpy.calls.count()).toBe(1); expect(appendNodesSpy.calls.argsFor(0)[0][0].key).toBe(bridgeNode.key); expect(cacheNodeSpy.calls.count()).toBe(1); expect(cacheNodeSpy.calls.argsFor(0)[0]).toBe(bridgeNode.key); }); it("should bridge to closest node in decreasing time", () => { const playService: PlayService = new PlayService(graphService, stateService); const stopSpy: jasmine.Spy = spyOn(playService, "stop").and.callThrough(); spyOn(graphService, "cacheSequence$").and.returnValue(new Subject<Sequence>()); spyOn(graphService, "cacheSequenceNodes$").and.returnValue(new Subject<Sequence>()); const cacheBoundingBoxSubject: Subject<Node[]> = new Subject<Node[]>(); spyOn(graphService, "cacheBoundingBox$").and.returnValue(cacheBoundingBoxSubject); const cacheNodeSubject: Subject<Node> = new Subject<Node>(); const cacheNodeSpy: jasmine.Spy = spyOn(graphService, "cacheNode$"); cacheNodeSpy.and.returnValue(cacheNodeSubject); const appendNodesSpy: jasmine.Spy = <jasmine.Spy>stateService.appendNodes; appendNodesSpy.and.stub(); playService.setDirection(EdgeDirection.Next); playService.play(); const cameraUuid: string = "camera_uuid"; const sequenceKey1: string = "sequence1"; const currentFullNode: IFullNode = new NodeHelper().createFullNode(); currentFullNode.captured_at = -1; currentFullNode.captured_with_camera_uuid = cameraUuid; currentFullNode.key = "currKey"; currentFullNode.sequence_key = sequenceKey1; const currentNode: Node = new Node(currentFullNode); currentNode.makeFull(currentFullNode); const sequenceEdgesSubject: Subject<IEdgeStatus> = new Subject<IEdgeStatus>(); new MockCreator().mockProperty(currentNode, "sequenceEdges$", sequenceEdgesSubject); const prevFullNode: IFullNode = new NodeHelper().createFullNode(); prevFullNode.captured_at = 0; prevFullNode.captured_with_camera_uuid = cameraUuid; prevFullNode.key = "prevKey"; prevFullNode.sequence_key = sequenceKey1; const prevNode: Node = new Node(prevFullNode); prevNode.makeFull(prevFullNode); const state: ICurrentState = createState(); state.trajectory = [prevNode, currentNode]; state.lastNode = currentNode; state.currentNode = currentNode; state.nodesAhead = 0; state.currentIndex = 1; (<Subject<IFrame>>stateService.currentState$).next({ fps: 60, id: 0, state: state }); sequenceEdgesSubject.next({ cached: true, edges: []}); expect(stopSpy.calls.count()).toBe(0); const cacheBoudndingBoxNodex: Node[] = [1, -3, -5] .map( (capturedAt: number): Node => { const bridgeFullNode: IFullNode = new NodeHelper().createFullNode(); bridgeFullNode.captured_at = capturedAt; bridgeFullNode.captured_with_camera_uuid = cameraUuid; bridgeFullNode.key = "bridgeKey"; bridgeFullNode.sequence_key = "sequenceBrdige"; const bridgeNode: Node = new Node(bridgeFullNode); bridgeNode.makeFull(bridgeFullNode); return bridgeNode; }); const sameSequenceFullNode: IFullNode = new NodeHelper().createFullNode(); sameSequenceFullNode.captured_at = -2; sameSequenceFullNode.captured_with_camera_uuid = cameraUuid; sameSequenceFullNode.key = "bridgeKey"; sameSequenceFullNode.sequence_key = sequenceKey1; const sameSequenceNode: Node = new Node(sameSequenceFullNode); sameSequenceNode.makeFull(sameSequenceFullNode); cacheBoudndingBoxNodex.push(sameSequenceNode); cacheBoundingBoxSubject.next(cacheBoudndingBoxNodex); cacheNodeSubject.next(cacheBoudndingBoxNodex[1]); expect(stopSpy.calls.count()).toBe(0); expect(appendNodesSpy.calls.count()).toBe(1); expect(appendNodesSpy.calls.argsFor(0)[0][0].key).toBe(cacheBoudndingBoxNodex[1].key); expect(cacheNodeSpy.calls.count()).toBe(1); expect(cacheNodeSpy.calls.argsFor(0)[0]).toBe(cacheBoudndingBoxNodex[1].key); }); it("should not bridge if time direction cannot be determined", () => { const playService: PlayService = new PlayService(graphService, stateService); const stopSpy: jasmine.Spy = spyOn(playService, "stop").and.callThrough(); spyOn(graphService, "cacheSequence$").and.returnValue(new Subject<Sequence>()); spyOn(graphService, "cacheSequenceNodes$").and.returnValue(new Subject<Sequence>()); const cacheBoundingBoxSubject: Subject<Node[]> = new Subject<Node[]>(); const cacheBoudningBoxSpy: jasmine.Spy = spyOn(graphService, "cacheBoundingBox$"); cacheBoudningBoxSpy.and.returnValue(cacheBoundingBoxSubject); const cacheNodeSubject: Subject<Node> = new Subject<Node>(); const cacheNodeSpy: jasmine.Spy = spyOn(graphService, "cacheNode$"); cacheNodeSpy.and.returnValue(cacheNodeSubject); const appendNodesSpy: jasmine.Spy = <jasmine.Spy>stateService.appendNodes; appendNodesSpy.and.stub(); playService.setDirection(EdgeDirection.Next); playService.play(); const cameraUuid: string = "camera_uuid"; const sequenceKey1: string = "sequence1"; const currentFullNode: IFullNode = new NodeHelper().createFullNode(); currentFullNode.captured_at = 0; currentFullNode.captured_with_camera_uuid = cameraUuid; currentFullNode.key = "currKey"; currentFullNode.sequence_key = sequenceKey1; const currentNode: Node = new Node(currentFullNode); currentNode.makeFull(currentFullNode); const sequenceEdgesSubject: Subject<IEdgeStatus> = new Subject<IEdgeStatus>(); new MockCreator().mockProperty(currentNode, "sequenceEdges$", sequenceEdgesSubject); const state: ICurrentState = createState(); state.trajectory = [currentNode]; state.lastNode = currentNode; state.currentNode = currentNode; state.nodesAhead = 0; state.currentIndex = 0; (<Subject<IFrame>>stateService.currentState$).next({ fps: 60, id: 0, state: state }); sequenceEdgesSubject.next({ cached: true, edges: []}); expect(stopSpy.calls.count()).toBe(1); expect(cacheBoudningBoxSpy.calls.count()).toBe(0); expect(appendNodesSpy.calls.count()).toBe(0); expect(cacheNodeSpy.calls.count()).toBe(0); }); it("should append node when cached", () => { const playService: PlayService = new PlayService(graphService, stateService); const appendNodesSpy: jasmine.Spy = <jasmine.Spy>stateService.appendNodes; appendNodesSpy.and.callThrough(); const cacheNodeSpy: jasmine.Spy = spyOn(graphService, "cacheNode$"); const cacheNodeSubject: Subject<Node> = new Subject<Node>(); cacheNodeSpy.and.returnValue(cacheNodeSubject); spyOn(graphService, "cacheSequence$").and.returnValue(new Subject<Sequence>()); spyOn(graphService, "cacheSequenceNodes$").and.returnValue(new Subject<Sequence>()); spyOn(graphService, "cacheBoundingBox$").and.returnValue(new Subject<Node[]>()); playService.setDirection(EdgeDirection.Next); playService.play(); const frame: IFrame = new FrameHelper().createFrame(); const node: Node = frame.state.currentNode; node.initializeCache(new NodeCache()); const sequenceEdgesSubject: Subject<IEdgeStatus> = new Subject<IEdgeStatus>(); new MockCreator().mockProperty(node, "sequenceEdges$", sequenceEdgesSubject); (<Subject<IFrame>>stateService.currentState$).next(frame); const fullToNode: IFullNode = nodeHelper.createFullNode(); fullToNode.key = "toKey"; const toNode: Node = new Node(fullToNode); sequenceEdgesSubject.next({ cached: true, edges: [{ data: { direction: EdgeDirection.Next, worldMotionAzimuth: 0 }, from: node.key, to: toNode.key, }], }); cacheNodeSubject.next(toNode); expect(cacheNodeSpy.calls.count()).toBe(1); expect(cacheNodeSpy.calls.argsFor(0)[0]).toBe(toNode.key); expect(appendNodesSpy.calls.count()).toBe(1); expect(appendNodesSpy.calls.argsFor(0)[0].length).toBe(1); expect(appendNodesSpy.calls.argsFor(0)[0][0].key).toBe(toNode.key); }); it("should stop on node caching error", () => { spyOn(console, "error").and.stub(); const playService: PlayService = new PlayService(graphService, stateService); const appendNodesSpy: jasmine.Spy = <jasmine.Spy>stateService.appendNodes; appendNodesSpy.and.callThrough(); const cacheNodeSpy: jasmine.Spy = spyOn(graphService, "cacheNode$"); const cacheNodeSubject: Subject<Node> = new Subject<Node>(); cacheNodeSpy.and.returnValue(cacheNodeSubject); const stopSpy: jasmine.Spy = spyOn(playService, "stop").and.callThrough(); spyOn(graphService, "cacheSequence$").and.returnValue(new Subject<Sequence>()); spyOn(graphService, "cacheSequenceNodes$").and.returnValue(new Subject<Sequence>()); spyOn(graphService, "cacheBoundingBox$").and.returnValue(new Subject<Node[]>()); playService.setDirection(EdgeDirection.Next); playService.play(); const frame: IFrame = new FrameHelper().createFrame(); const node: Node = frame.state.currentNode; node.initializeCache(new NodeCache()); const sequenceEdgesSubject: Subject<IEdgeStatus> = new Subject<IEdgeStatus>(); new MockCreator().mockProperty(node, "sequenceEdges$", sequenceEdgesSubject); (<Subject<IFrame>>stateService.currentState$).next(frame); const fullToNode: IFullNode = nodeHelper.createFullNode(); fullToNode.key = "toKey"; const toNode: Node = new Node(fullToNode); sequenceEdgesSubject.next({ cached: true, edges: [{ data: { direction: EdgeDirection.Next, worldMotionAzimuth: 0 }, from: node.key, to: toNode.key, }], }); cacheNodeSubject.error(new Error()); expect(cacheNodeSpy.calls.count()).toBe(1); expect(cacheNodeSpy.calls.argsFor(0)[0]).toBe(toNode.key); expect(appendNodesSpy.calls.count()).toBe(0); expect(stopSpy.calls.count()).toBe(1); }); it("should cache sequence when in spatial graph mode", () => { const playService: PlayService = new PlayService(graphService, stateService); playService.setDirection(EdgeDirection.Next); // Set speed to zero so that graph mode is set to spatial when calling play playService.setSpeed(0); const cacheSequenceSpy: jasmine.Spy = spyOn(graphService, "cacheSequence$"); cacheSequenceSpy.and.returnValue(new Subject<Sequence>()); const cacheSequenceNodesSpy: jasmine.Spy = spyOn(graphService, "cacheSequenceNodes$"); cacheSequenceNodesSpy.and.returnValue(new Subject<Sequence>()); playService.play(); const currentNode: Node = nodeHelper.createNode(); new MockCreator().mockProperty(currentNode, "sequenceEdges$", new Subject<IEdgeStatus>()); const currentNodeSubject: Subject<Node> = <Subject<Node>>stateService.currentNode$; currentNodeSubject.next(currentNode); expect(cacheSequenceSpy.calls.count()).toBe(1); expect(cacheSequenceSpy.calls.argsFor(0)[0]).toBe(currentNode.sequenceKey); expect(cacheSequenceNodesSpy.calls.count()).toBe(0); playService.stop(); }); it("should cache sequence nodes when in sequence graph mode", () => { const playService: PlayService = new PlayService(graphService, stateService); playService.setDirection(EdgeDirection.Next); // Set speed to one so that graph mode is set to sequence when calling play playService.setSpeed(1); const cacheSequenceSpy: jasmine.Spy = spyOn(graphService, "cacheSequence$"); cacheSequenceSpy.and.returnValue(new Subject<Sequence>()); const cacheSequenceNodesSpy: jasmine.Spy = spyOn(graphService, "cacheSequenceNodes$"); cacheSequenceNodesSpy.and.returnValue(new Subject<Sequence>()); playService.play(); const currentNode: Node = nodeHelper.createNode(); new MockCreator().mockProperty(currentNode, "sequenceEdges$", new Subject<IEdgeStatus>()); const currentNodeSubject: Subject<Node> = <Subject<Node>>stateService.currentNode$; currentNodeSubject.next(currentNode); expect(cacheSequenceSpy.calls.count()).toBe(0); expect(cacheSequenceNodesSpy.calls.count()).toBe(1); expect(cacheSequenceNodesSpy.calls.argsFor(0)[0]).toBe(currentNode.sequenceKey); playService.stop(); }); it("should not pre-cache if current node is last sequence node", () => { graphService.setGraphMode(GraphMode.Spatial); const playService: PlayService = new PlayService(graphService, stateService); playService.setDirection(EdgeDirection.Next); const cacheSequenceSubject: Subject<Sequence> = new Subject<Sequence>(); spyOn(graphService, "cacheSequence$").and.returnValue(cacheSequenceSubject); playService.play(); const sequenceKey: string = "sequenceKey"; const currentFullNode: IFullNode = new NodeHelper().createFullNode(); currentFullNode.sequence_key = sequenceKey; currentFullNode.key = "node0"; const currentNode: Node = new Node(currentFullNode); new MockCreator().mockProperty(currentNode, "sequenceEdges$", new Subject<IEdgeStatus>()); const prevNodeKey: string = "node1"; const currentNodeSubject: Subject<Node> = <Subject<Node>>stateService.currentNode$; currentNodeSubject.next(currentNode); const sequence: Sequence = new Sequence({ key: sequenceKey, keys: [prevNodeKey, currentNode.key ]}); cacheSequenceSubject.next(sequence); const cacheNodeSpy: jasmine.Spy = spyOn(graphService, "cacheNode$"); const cacheNodeSubject: Subject<Node> = new Subject<Node>(); cacheNodeSpy.and.returnValue(cacheNodeSubject); const state: ICurrentState = createState(); state.trajectory = [currentNode]; state.lastNode = currentNode; state.currentNode = currentNode; state.nodesAhead = 0; (<Subject<IFrame>>stateService.currentState$).next({ fps: 60, id: 0, state: state }); expect(cacheNodeSpy.calls.count()).toBe(0); playService.stop(); }); it("should pre-cache one trajectory node", () => { graphService.setGraphMode(GraphMode.Spatial); const playService: PlayService = new PlayService(graphService, stateService); playService.setDirection(EdgeDirection.Next); const cacheSequenceSubject: Subject<Sequence> = new Subject<Sequence>(); spyOn(graphService, "cacheSequence$").and.returnValue(cacheSequenceSubject); playService.play(); const sequenceKey: string = "sequenceKey"; const currentFullNode: IFullNode = new NodeHelper().createFullNode(); currentFullNode.sequence_key = sequenceKey; currentFullNode.key = "node0"; const currentNode: Node = new Node(currentFullNode); new MockCreator().mockProperty(currentNode, "sequenceEdges$", new Subject<IEdgeStatus>()); const nextNodeKey: string = "node1"; const currentNodeSubject: Subject<Node> = <Subject<Node>>stateService.currentNode$; currentNodeSubject.next(currentNode); const sequence: Sequence = new Sequence({ key: sequenceKey, keys: [currentNode.key, nextNodeKey ]}); cacheSequenceSubject.next(sequence); const cacheNodeSpy: jasmine.Spy = spyOn(graphService, "cacheNode$"); const cacheNodeSubject: Subject<Node> = new Subject<Node>(); cacheNodeSpy.and.returnValue(cacheNodeSubject); const state: ICurrentState = createState(); state.trajectory = [currentNode]; state.lastNode = currentNode; state.currentNode = currentNode; state.nodesAhead = 0; const currentStateSubject$: Subject<IFrame> = <Subject<IFrame>>stateService.currentState$; currentStateSubject$.next({ fps: 60, id: 0, state: state }); cacheNodeSubject.next(new NodeHelper().createNode()); expect(cacheNodeSpy.calls.count()).toBe(1); expect(cacheNodeSpy.calls.argsFor(0)[0]).toBe(nextNodeKey); playService.stop(); }); it("should pre-cache one trajectory node in prev direction", () => { graphService.setGraphMode(GraphMode.Spatial); const playService: PlayService = new PlayService(graphService, stateService); playService.setDirection(EdgeDirection.Prev); const cacheSequenceSubject: Subject<Sequence> = new Subject<Sequence>(); spyOn(graphService, "cacheSequence$").and.returnValue(cacheSequenceSubject); playService.play(); const sequenceKey: string = "sequenceKey"; const currentFullNode: IFullNode = new NodeHelper().createFullNode(); currentFullNode.sequence_key = sequenceKey; currentFullNode.key = "node0"; const currentNode: Node = new Node(currentFullNode); new MockCreator().mockProperty(currentNode, "sequenceEdges$", new Subject<IEdgeStatus>()); const prevNodeKey: string = "node1"; const currentNodeSubject: Subject<Node> = <Subject<Node>>stateService.currentNode$; currentNodeSubject.next(currentNode); const sequence: Sequence = new Sequence({ key: sequenceKey, keys: [prevNodeKey, currentNode.key]}); cacheSequenceSubject.next(sequence); const cacheNodeSpy: jasmine.Spy = spyOn(graphService, "cacheNode$"); const cacheNodeSubject: Subject<Node> = new Subject<Node>(); cacheNodeSpy.and.returnValue(cacheNodeSubject); const state: ICurrentState = createState(); state.trajectory = [currentNode]; state.lastNode = currentNode; state.currentNode = currentNode; state.nodesAhead = 0; const currentStateSubject$: Subject<IFrame> = <Subject<IFrame>>stateService.currentState$; currentStateSubject$.next({ fps: 60, id: 0, state: state }); cacheNodeSubject.next(new NodeHelper().createNode()); expect(cacheNodeSpy.calls.count()).toBe(1); expect(cacheNodeSpy.calls.argsFor(0)[0]).toBe(prevNodeKey); // Sequence should not have changed because of internal reversing expect(sequence.keys[0]).toBe(prevNodeKey); expect(sequence.keys[1]).toBe(currentNode.key); playService.stop(); }); it("should not pre-cache the same node twice", () => { graphService.setGraphMode(GraphMode.Spatial); const playService: PlayService = new PlayService(graphService, stateService); playService.setDirection(EdgeDirection.Next); const cacheSequenceSubject: Subject<Sequence> = new Subject<Sequence>(); spyOn(graphService, "cacheSequence$").and.returnValue(cacheSequenceSubject); playService.play(); const sequenceKey: string = "sequenceKey"; const currentFullNode: IFullNode = new NodeHelper().createFullNode(); currentFullNode.sequence_key = sequenceKey; currentFullNode.key = "node0"; const currentNode: Node = new Node(currentFullNode); currentNode.makeFull(currentFullNode); new MockCreator().mockProperty(currentNode, "sequenceEdges$", new Subject<IEdgeStatus>()); const nextNodeKey: string = "node1"; const currentNodeSubject: Subject<Node> = <Subject<Node>>stateService.currentNode$; currentNodeSubject.next(currentNode); const sequence: Sequence = new Sequence({ key: sequenceKey, keys: [currentNode.key, nextNodeKey ]}); cacheSequenceSubject.next(sequence); const cacheNodeSpy: jasmine.Spy = spyOn(graphService, "cacheNode$"); const cacheNodeSubject: Subject<Node> = new Subject<Node>(); cacheNodeSpy.and.returnValue(cacheNodeSubject); const state: ICurrentState = createState(); state.trajectory = [currentNode]; state.lastNode = currentNode; state.currentNode = currentNode; state.nodesAhead = 0; const currentStateSubject$: Subject<IFrame> = <Subject<IFrame>>stateService.currentState$; currentStateSubject$.next({ fps: 60, id: 0, state: state }); const nextFullNode: IFullNode = new NodeHelper().createFullNode(); nextFullNode.sequence_key = sequenceKey; nextFullNode.key = nextNodeKey; const nextNode: Node = new Node(nextFullNode); nextNode.makeFull(nextFullNode); cacheNodeSubject.next(nextNode); expect(cacheNodeSpy.calls.count()).toBe(1); expect(cacheNodeSpy.calls.argsFor(0)[0]).toBe(nextNodeKey); currentStateSubject$.next({ fps: 60, id: 0, state: state }); expect(cacheNodeSpy.calls.count()).toBe(1); playService.stop(); }); it("should not pre-cache if all sequence nodes in trajectory", () => { graphService.setGraphMode(GraphMode.Spatial); const playService: PlayService = new PlayService(graphService, stateService); playService.setDirection(EdgeDirection.Next); const cacheSequenceSubject: Subject<Sequence> = new Subject<Sequence>(); spyOn(graphService, "cacheSequence$").and.returnValue(cacheSequenceSubject); playService.play(); const sequenceKey: string = "sequenceKey"; const currentFullNode: IFullNode = new NodeHelper().createFullNode(); currentFullNode.sequence_key = sequenceKey; currentFullNode.key = "node0"; const currentNode: Node = new Node(currentFullNode); currentNode.makeFull(currentFullNode); new MockCreator().mockProperty(currentNode, "sequenceEdges$", new Subject<IEdgeStatus>()); const nextNodeKey: string = "node1"; const nextFullNode: IFullNode = new NodeHelper().createFullNode(); nextFullNode.sequence_key = sequenceKey; nextFullNode.key = nextNodeKey; const nextNode: Node = new Node(nextFullNode); nextNode.makeFull(nextFullNode); new MockCreator().mockProperty(nextNode, "sequenceEdges$", new Subject<IEdgeStatus>()); const currentNodeSubject: Subject<Node> = <Subject<Node>>stateService.currentNode$; currentNodeSubject.next(currentNode); const sequence: Sequence = new Sequence({ key: sequenceKey, keys: [currentNode.key, nextNodeKey ]}); cacheSequenceSubject.next(sequence); const cacheNodeSpy: jasmine.Spy = spyOn(graphService, "cacheNode$"); const cacheNodeSubject: Subject<Node> = new Subject<Node>(); cacheNodeSpy.and.returnValue(cacheNodeSubject); const state: ICurrentState = createState(); state.trajectory = [currentNode, nextNode]; state.lastNode = nextNode; state.currentNode = currentNode; state.nodesAhead = 0; const currentStateSubject$: Subject<IFrame> = <Subject<IFrame>>stateService.currentState$; currentStateSubject$.next({ fps: 60, id: 0, state: state }); expect(cacheNodeSpy.calls.count()).toBe(0); playService.stop(); }); it("should pre-cache up to specified nodes ahead", () => { graphService.setGraphMode(GraphMode.Spatial); const playService: PlayService = new PlayService(graphService, stateService); playService.setDirection(EdgeDirection.Next); // Zero speed means max ten nodes ahead playService.setSpeed(0); const cacheSequenceSubject: Subject<Sequence> = new Subject<Sequence>(); spyOn(graphService, "cacheSequence$").and.returnValue(cacheSequenceSubject); playService.play(); const sequenceKey: string = "sequenceKey"; const currentFullNode: IFullNode = new NodeHelper().createFullNode(); currentFullNode.sequence_key = sequenceKey; currentFullNode.key = "currentNodeKey"; const currentNode: Node = new Node(currentFullNode); currentNode.makeFull(currentFullNode); new MockCreator().mockProperty(currentNode, "sequenceEdges$", new Subject<IEdgeStatus>()); const sequence: Sequence = new Sequence({ key: sequenceKey, keys: [currentNode.key]}); const sequenceNodes: Node[] = []; for (let i: number = 0; i < 20; i++) { const sequenceNodeKey: string = `node${i}`; const sequenceFullNode: IFullNode = new NodeHelper().createFullNode(); sequenceFullNode.sequence_key = sequenceKey; sequenceFullNode.key = sequenceNodeKey; const sequenceNode: Node = new Node(sequenceFullNode); sequenceNode.makeFull(sequenceFullNode); new MockCreator().mockProperty(sequenceNode, "sequenceEdges$", new Subject<IEdgeStatus>()); sequence.keys.push(sequenceNode.key); sequenceNodes.push(sequenceNode); } const currentNodeSubject: Subject<Node> = <Subject<Node>>stateService.currentNode$; currentNodeSubject.next(currentNode); cacheSequenceSubject.next(sequence); const cacheNodeSpy: jasmine.Spy = spyOn(graphService, "cacheNode$").and.callFake( (key: string): Observable<Node> => { const fullNode: IFullNode = new NodeHelper().createFullNode(); fullNode.sequence_key = sequenceKey; fullNode.key = key; const node: Node = new Node(fullNode); node.makeFull(fullNode); return observableOf(node); }); const state: ICurrentState = createState(); state.trajectory = [currentNode]; state.lastNode = currentNode; state.currentNode = currentNode; state.currentIndex = 0; state.nodesAhead = 0; // Cache ten nodes immediately const currentStateSubject$: Subject<IFrame> = <Subject<IFrame>>stateService.currentState$; currentStateSubject$.next({ fps: 60, id: 0, state: state }); let cachedCount: number = 10; expect(cacheNodeSpy.calls.count()).toBe(cachedCount); // Add one node to trajectory before current node has moved state.trajectory = state.trajectory.concat(sequenceNodes.splice(0, 1)); state.lastNode = state.trajectory[state.trajectory.length - 1]; state.nodesAhead = 1; currentStateSubject$.next({ fps: 60, id: 0, state: state }); // No new nodes should be cached expect(cacheNodeSpy.calls.count()).toBe(cachedCount); // Current node has moved one step in trajectory to the last node, nodes ahead // is zero and one new node should be cached state.currentIndex += 1; state.currentNode = state.trajectory[state.currentIndex]; state.nodesAhead = 0; currentStateSubject$.next({ fps: 60, id: 0, state: state }); cachedCount += 1; expect(cacheNodeSpy.calls.count()).toBe(cachedCount); // Add 5 nodes to trajectory and move current node 3 steps state.trajectory = state.trajectory.concat(sequenceNodes.splice(0, 5)); state.currentIndex += 3; state.currentNode = state.trajectory[state.currentIndex]; state.lastNode = state.trajectory[state.trajectory.length - 1]; state.nodesAhead = 2; currentStateSubject$.next({ fps: 60, id: 0, state: state }); // Three new nodes should be cached cachedCount += 3; expect(cacheNodeSpy.calls.count()).toBe(cachedCount); // Add all 14 nodes cached so far to trajectory and move current node to last // trajectory node state.trajectory = state.trajectory.concat(sequenceNodes.splice(0, 8)); state.currentIndex = state.trajectory.length - 1; expect(state.currentIndex).toBe(14); state.currentNode = state.trajectory[state.currentIndex]; state.lastNode = state.trajectory[state.trajectory.length - 1]; state.nodesAhead = 0; currentStateSubject$.next({ fps: 60, id: 0, state: state }); // Six last nodes should be cached cachedCount += 6; expect(cacheNodeSpy.calls.count()).toBe(cachedCount); currentStateSubject$.next({ fps: 60, id: 0, state: state }); // No new nodes should be cached expect(cacheNodeSpy.calls.count()).toBe(cachedCount); // Add all remaining nodes to trajectory and move current node one step state.trajectory = state.trajectory.concat(sequenceNodes.splice(0, sequenceNodes.length)); state.currentIndex += 1; state.currentNode = state.trajectory[state.currentIndex]; state.lastNode = state.trajectory[state.trajectory.length - 1]; state.nodesAhead = 5; currentStateSubject$.next({ fps: 60, id: 0, state: state }); // No new nodes should be cached expect(cacheNodeSpy.calls.count()).toBe(cachedCount); // Move current node to last trajectory node state.trajectory = state.trajectory.concat(sequenceNodes.splice(0, sequenceNodes.length)); state.currentIndex = state.trajectory.length - 1; state.currentNode = state.trajectory[state.currentIndex]; state.lastNode = state.trajectory[state.trajectory.length - 1]; state.nodesAhead = 0; currentStateSubject$.next({ fps: 60, id: 0, state: state }); // No new nodes should be cached expect(cacheNodeSpy.calls.count()).toBe(20); for (let i: number = 0; i < 20; i++) { expect(cacheNodeSpy.calls.argsFor(i)[0]).toBe(sequence.keys[i + 1]); } playService.stop(); }); });