import { bootstrap } from "../Bootstrap"; bootstrap(); import { of as observableOf, zip as observableZip, Observable, Subject, } from "rxjs"; import { take, first, skip } from "rxjs/operators"; import { MockCreator } from "../helper/MockCreator"; import { ImageHelper } from "../helper/ImageHelper"; import { StateServiceMockCreator } from "../helper/StateServiceMockCreator"; import { FrameHelper } from "../helper/FrameHelper"; import { Image } from "../../src/graph/Image"; import { APIWrapper } from "../../src/api/APIWrapper"; import { ImageEnt } from "../../src/api/ents/ImageEnt"; import { Graph } from "../../src/graph/Graph"; import { GraphMode } from "../../src/graph/GraphMode"; import { GraphService } from "../../src/graph/GraphService"; import { NavigationEdgeStatus } from "../../src/graph/interfaces/NavigationEdgeStatus"; import { ImageCache } from "../../src/graph/ImageCache"; import { Sequence } from "../../src/graph/Sequence"; import { IAnimationState } from "../../src/state/interfaces/IAnimationState"; import { AnimationFrame } from "../../src/state/interfaces/AnimationFrame"; import { State } from "../../src/state/State"; import { StateService } from "../../src/state/StateService"; import { PlayService } from "../../src/viewer/PlayService"; import { NavigationDirection } from "../../src/graph/edge/NavigationDirection"; import { DataProvider } from "../helper/ProviderHelper"; describe("PlayService.ctor", () => { it("should be defined when constructed", () => { const api: APIWrapper = new APIWrapper(new DataProvider()); const graphService: GraphService = new GraphService(new Graph(api)); const stateService: StateService = new StateService(State.Traversing); const playService: PlayService = new PlayService(graphService, stateService); expect(playService).toBeDefined(); }); it("should emit default values", (done: () => void) => { const api: APIWrapper = new APIWrapper(new DataProvider()); const graphService: GraphService = new GraphService(new Graph(api)); const stateService: StateService = new StateService(State.Traversing); const playService: PlayService = new PlayService(graphService, stateService); observableZip( playService.direction$, playService.playing$, playService.speed$).pipe( first()) .subscribe( ([d, p, s]: [NavigationDirection, boolean, number]): void => { expect(d).toBe(NavigationDirection.Next); expect(p).toBe(false); expect(s).toBe(0.5); done(); }); }); }); describe("PlayService.playing", () => { it("should be playing after calling play", (done: () => void) => { const api: APIWrapper = new APIWrapper(new DataProvider()); const graphService: GraphService = new GraphService(new Graph(api)); const stateService: StateService = new StateService(State.Traversing); 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 api: APIWrapper = new APIWrapper(new DataProvider()); const graphService: GraphService = new GraphService(new Graph(api)); const stateService: StateService = new StateService(State.Traversing); const playService: PlayService = new PlayService(graphService, stateService); playService.play(); const setGraphModeSpy: jasmine.Spy = spyOn(graphService, "setGraphMode").and.stub(); const cutImagesSpy: jasmine.Spy = spyOn(stateService, "cutImages").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(cutImagesSpy.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 api: APIWrapper = new APIWrapper(new DataProvider()); const graphService: GraphService = new GraphService(new Graph(api)); const stateService: StateService = new StateService(State.Traversing); 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 api: APIWrapper = new APIWrapper(new DataProvider()); const graphService: GraphService = new GraphService(new Graph(api)); const stateService: StateService = new StateService(State.Traversing); 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 api: APIWrapper = new APIWrapper(new DataProvider()); const graphService: GraphService = new GraphService(new Graph(api)); const stateService: StateService = new StateService(State.Traversing); 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: () => IAnimationState = (): IAnimationState => { return { alpha: 0, camera: null, currentCamera: null, currentIndex: 0, currentImage: null, currentTransform: null, lastImage: null, motionless: false, imagesAhead: 0, previousImage: null, previousTransform: null, reference: null, state: State.Traversing, trajectory: null, zoom: 0, }; }; describe("PlayService.play", () => { let imageHelper: ImageHelper; let api: APIWrapper; let graphService: GraphService; let stateService: StateService; beforeEach(() => { imageHelper = new ImageHelper(); api = new APIWrapper(new DataProvider()); graphService = new GraphService(new Graph(api)); 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 image 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()); spyOn(graphService, "cacheSequenceImages$").and.returnValue(new Subject()); spyOn(graphService, "cacheBoundingBox$").and.returnValue(observableOf([])); playService.setDirection(NavigationDirection.Next); playService.play(); const frame: AnimationFrame = new FrameHelper().createFrame(); frame.state.currentImage.initializeCache(new ImageCache(undefined)); (>stateService.currentState$).next(frame); frame.state.currentImage.cacheSequenceEdges([]); expect(stopSpy.calls.count()).toBe(1); }); it("should stop if earth mode is emitted", () => { const playService: PlayService = new PlayService(graphService, stateService); const stopSpy: jasmine.Spy = spyOn(playService, "stop").and.callThrough(); spyOn(graphService, "cacheSequence$").and.returnValue(new Subject()); spyOn(graphService, "cacheSequenceImages$").and.returnValue(new Subject()); spyOn(graphService, "cacheBoundingBox$").and.returnValue(observableOf([])); playService.setDirection(NavigationDirection.Next); playService.play(); (>stateService.state$).next(State.Earth); 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()); spyOn(graphService, "cacheSequenceImages$").and.returnValue(new Subject()); spyOn(graphService, "cacheBoundingBox$").and.returnValue(new Subject()); playService.setDirection(NavigationDirection.Next); playService.play(); const frame: AnimationFrame = new FrameHelper().createFrame(); const image: Image = frame.state.currentImage; const sequenceEdgesSubject: Subject = new Subject(); new MockCreator().mockProperty(image, "sequenceEdges$", sequenceEdgesSubject); (>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); spyOn(graphService, "cacheSequence$").and.returnValue(new Subject()); spyOn(graphService, "cacheSequenceImages$").and.returnValue(new Subject()); spyOn(graphService, "cacheBoundingBox$").and.returnValue(observableOf([])); playService.setDirection(NavigationDirection.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: AnimationFrame = new FrameHelper().createFrame(); frame.state.currentImage.initializeCache(new ImageCache(undefined)); (>stateService.currentState$).next(frame); frame.state.currentImage.cacheSequenceEdges([]); }); it("should not stop if images 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()); spyOn(graphService, "cacheSequenceImages$").and.returnValue(new Subject()); spyOn(graphService, "cacheBoundingBox$").and.returnValue(observableOf([])); playService.setDirection(NavigationDirection.Next); playService.play(); const frame: AnimationFrame = new FrameHelper().createFrame(); const image: Image = frame.state.currentImage; image.initializeCache(new ImageCache(undefined)); const sequenceEdgesSubject: Subject = new Subject(); new MockCreator().mockProperty(image, "sequenceEdges$", sequenceEdgesSubject); (>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 stop if no more images", () => { const playService: PlayService = new PlayService(graphService, stateService); const stopSpy: jasmine.Spy = spyOn(playService, "stop").and.callThrough(); spyOn(graphService, "cacheSequence$").and.returnValue(new Subject()); spyOn(graphService, "cacheSequenceImages$").and.returnValue(new Subject()); playService.setDirection(NavigationDirection.Next); playService.play(); const frame: AnimationFrame = new FrameHelper().createFrame(); const image: Image = frame.state.currentImage; image.initializeCache(new ImageCache(undefined)); const sequenceEdgesSubject: Subject = new Subject(); const prevFullImage: ImageEnt = new ImageHelper().createImageEnt(); prevFullImage.captured_at = -1; const prevImage: Image = new Image(prevFullImage); prevImage.makeComplete(prevFullImage); frame.state.trajectory.splice(0, 0, prevImage); frame.state.currentIndex = 1; new MockCreator().mockProperty(image, "sequenceEdges$", sequenceEdgesSubject); (>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 append image when cached", () => { const playService: PlayService = new PlayService(graphService, stateService); const appendImagesSpy: jasmine.Spy = stateService.appendImagess; appendImagesSpy.and.callThrough(); const cacheImageSpy: jasmine.Spy = spyOn(graphService, "cacheImage$"); const cacheImageSubject: Subject = new Subject(); cacheImageSpy.and.returnValue(cacheImageSubject); spyOn(graphService, "cacheSequence$").and.returnValue(new Subject()); spyOn(graphService, "cacheSequenceImages$").and.returnValue(new Subject()); spyOn(graphService, "cacheBoundingBox$").and.returnValue(new Subject()); playService.setDirection(NavigationDirection.Next); playService.play(); const frame: AnimationFrame = new FrameHelper().createFrame(); const image: Image = frame.state.currentImage; image.initializeCache(new ImageCache(undefined)); const sequenceEdgesSubject: Subject = new Subject(); new MockCreator().mockProperty(image, "sequenceEdges$", sequenceEdgesSubject); (>stateService.currentState$).next(frame); const fullToImage: ImageEnt = imageHelper.createImageEnt(); fullToImage.id = "toKey"; const toImage: Image = new Image(fullToImage); sequenceEdgesSubject.next({ cached: true, edges: [{ data: { direction: NavigationDirection.Next, worldMotionAzimuth: 0 }, source: image.id, target: toImage.id, }], }); cacheImageSubject.next(toImage); expect(cacheImageSpy.calls.count()).toBe(1); expect(cacheImageSpy.calls.argsFor(0)[0]).toBe(toImage.id); expect(appendImagesSpy.calls.count()).toBe(1); expect(appendImagesSpy.calls.argsFor(0)[0].length).toBe(1); expect(appendImagesSpy.calls.argsFor(0)[0][0].id).toBe(toImage.id); }); it("should stop on image caching error", () => { spyOn(console, "error").and.stub(); const playService: PlayService = new PlayService(graphService, stateService); const appendImagesSpy: jasmine.Spy = stateService.appendImagess; appendImagesSpy.and.callThrough(); const cacheImageSpy: jasmine.Spy = spyOn(graphService, "cacheImage$"); const cacheImageSubject: Subject = new Subject(); cacheImageSpy.and.returnValue(cacheImageSubject); const stopSpy: jasmine.Spy = spyOn(playService, "stop").and.callThrough(); spyOn(graphService, "cacheSequence$").and.returnValue(new Subject()); spyOn(graphService, "cacheSequenceImages$").and.returnValue(new Subject()); spyOn(graphService, "cacheBoundingBox$").and.returnValue(new Subject()); playService.setDirection(NavigationDirection.Next); playService.play(); const frame: AnimationFrame = new FrameHelper().createFrame(); const image: Image = frame.state.currentImage; image.initializeCache(new ImageCache(undefined)); const sequenceEdgesSubject: Subject = new Subject(); new MockCreator().mockProperty(image, "sequenceEdges$", sequenceEdgesSubject); (>stateService.currentState$).next(frame); const fullToImage: ImageEnt = imageHelper.createImageEnt(); fullToImage.id = "toKey"; const toImage: Image = new Image(fullToImage); sequenceEdgesSubject.next({ cached: true, edges: [{ data: { direction: NavigationDirection.Next, worldMotionAzimuth: 0 }, source: image.id, target: toImage.id, }], }); cacheImageSubject.error(new Error()); expect(cacheImageSpy.calls.count()).toBe(1); expect(cacheImageSpy.calls.argsFor(0)[0]).toBe(toImage.id); expect(appendImagesSpy.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(NavigationDirection.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()); const cacheSequenceImagesSpy: jasmine.Spy = spyOn(graphService, "cacheSequenceImages$"); cacheSequenceImagesSpy.and.returnValue(new Subject()); playService.play(); const currentImage: Image = imageHelper.createImage(); new MockCreator().mockProperty(currentImage, "sequenceEdges$", new Subject()); const currentImageSubject: Subject = >stateService.currentImage$; currentImageSubject.next(currentImage); expect(cacheSequenceSpy.calls.count()).toBe(1); expect(cacheSequenceSpy.calls.argsFor(0)[0]).toBe(currentImage.sequenceId); expect(cacheSequenceImagesSpy.calls.count()).toBe(0); playService.stop(); }); it("should cache sequence images when in sequence graph mode", () => { const playService: PlayService = new PlayService(graphService, stateService); playService.setDirection(NavigationDirection.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()); const cacheSequenceImagesSpy: jasmine.Spy = spyOn(graphService, "cacheSequenceImages$"); cacheSequenceImagesSpy.and.returnValue(new Subject()); playService.play(); const currentImage: Image = imageHelper.createImage(); new MockCreator().mockProperty(currentImage, "sequenceEdges$", new Subject()); const currentImageSubject: Subject = >stateService.currentImage$; currentImageSubject.next(currentImage); expect(cacheSequenceSpy.calls.count()).toBe(0); expect(cacheSequenceImagesSpy.calls.count()).toBe(1); expect(cacheSequenceImagesSpy.calls.argsFor(0)[0]).toBe(currentImage.sequenceId); playService.stop(); }); it("should not pre-cache if current image is last sequence image", () => { graphService.setGraphMode(GraphMode.Spatial); const playService: PlayService = new PlayService(graphService, stateService); playService.setDirection(NavigationDirection.Next); const cacheSequenceSubject: Subject = new Subject(); spyOn(graphService, "cacheSequence$").and.returnValue(cacheSequenceSubject); playService.play(); const sequenceKey: string = "sequenceId"; const currentFullImage: ImageEnt = new ImageHelper().createImageEnt(); currentFullImage.sequence.id = sequenceKey; currentFullImage.id = "image0"; const currentImage: Image = new Image(currentFullImage); new MockCreator().mockProperty(currentImage, "sequenceEdges$", new Subject()); const prevImageKey: string = "image1"; const currentImageSubject: Subject = >stateService.currentImage$; currentImageSubject.next(currentImage); const sequence: Sequence = new Sequence({ id: sequenceKey, image_ids: [prevImageKey, currentImage.id] }); cacheSequenceSubject.next(sequence); const cacheImageSpy: jasmine.Spy = spyOn(graphService, "cacheImage$"); const cacheImageSubject: Subject = new Subject(); cacheImageSpy.and.returnValue(cacheImageSubject); const state: IAnimationState = createState(); state.trajectory = [currentImage]; state.lastImage = currentImage; state.currentImage = currentImage; state.imagesAhead = 0; (>stateService.currentState$).next({ fps: 60, id: 0, state: state }); expect(cacheImageSpy.calls.count()).toBe(0); playService.stop(); }); it("should pre-cache one trajectory image", () => { graphService.setGraphMode(GraphMode.Spatial); const playService: PlayService = new PlayService(graphService, stateService); playService.setDirection(NavigationDirection.Next); const cacheSequenceSubject: Subject = new Subject(); spyOn(graphService, "cacheSequence$").and.returnValue(cacheSequenceSubject); playService.play(); const sequenceKey: string = "sequenceId"; const currentFullImage: ImageEnt = new ImageHelper().createImageEnt(); currentFullImage.sequence.id = sequenceKey; currentFullImage.id = "image0"; const currentImage: Image = new Image(currentFullImage); new MockCreator().mockProperty(currentImage, "sequenceEdges$", new Subject()); const nextImageKey: string = "image1"; const currentImageSubject: Subject = >stateService.currentImage$; currentImageSubject.next(currentImage); const sequence: Sequence = new Sequence({ id: sequenceKey, image_ids: [currentImage.id, nextImageKey] }); cacheSequenceSubject.next(sequence); const cacheImageSpy: jasmine.Spy = spyOn(graphService, "cacheImage$"); const cacheImageSubject: Subject = new Subject(); cacheImageSpy.and.returnValue(cacheImageSubject); const state: IAnimationState = createState(); state.trajectory = [currentImage]; state.lastImage = currentImage; state.currentImage = currentImage; state.imagesAhead = 0; const currentStateSubject$: Subject = >stateService.currentState$; currentStateSubject$.next({ fps: 60, id: 0, state: state }); cacheImageSubject.next(new ImageHelper().createImage()); expect(cacheImageSpy.calls.count()).toBe(1); expect(cacheImageSpy.calls.argsFor(0)[0]).toBe(nextImageKey); playService.stop(); }); it("should pre-cache one trajectory image in prev direction", () => { graphService.setGraphMode(GraphMode.Spatial); const playService: PlayService = new PlayService(graphService, stateService); playService.setDirection(NavigationDirection.Prev); const cacheSequenceSubject: Subject = new Subject(); spyOn(graphService, "cacheSequence$").and.returnValue(cacheSequenceSubject); playService.play(); const sequenceKey: string = "sequenceId"; const currentFullImage: ImageEnt = new ImageHelper().createImageEnt(); currentFullImage.sequence.id = sequenceKey; currentFullImage.id = "image0"; const currentImage: Image = new Image(currentFullImage); new MockCreator().mockProperty(currentImage, "sequenceEdges$", new Subject()); const prevImageKey: string = "image1"; const currentImageSubject: Subject = >stateService.currentImage$; currentImageSubject.next(currentImage); const sequence: Sequence = new Sequence({ id: sequenceKey, image_ids: [prevImageKey, currentImage.id] }); cacheSequenceSubject.next(sequence); const cacheImageSpy: jasmine.Spy = spyOn(graphService, "cacheImage$"); const cacheImageSubject: Subject = new Subject(); cacheImageSpy.and.returnValue(cacheImageSubject); const state: IAnimationState = createState(); state.trajectory = [currentImage]; state.lastImage = currentImage; state.currentImage = currentImage; state.imagesAhead = 0; const currentStateSubject$: Subject = >stateService.currentState$; currentStateSubject$.next({ fps: 60, id: 0, state: state }); cacheImageSubject.next(new ImageHelper().createImage()); expect(cacheImageSpy.calls.count()).toBe(1); expect(cacheImageSpy.calls.argsFor(0)[0]).toBe(prevImageKey); // Sequence should not have changed because of internal reversing expect(sequence.imageIds[0]).toBe(prevImageKey); expect(sequence.imageIds[1]).toBe(currentImage.id); playService.stop(); }); it("should not pre-cache the same image twice", () => { graphService.setGraphMode(GraphMode.Spatial); const playService: PlayService = new PlayService(graphService, stateService); playService.setDirection(NavigationDirection.Next); const cacheSequenceSubject: Subject = new Subject(); spyOn(graphService, "cacheSequence$").and.returnValue(cacheSequenceSubject); playService.play(); const sequenceKey: string = "sequenceId"; const currentFullImage: ImageEnt = new ImageHelper().createImageEnt(); currentFullImage.sequence.id = sequenceKey; currentFullImage.id = "image0"; const currentImage: Image = new Image(currentFullImage); currentImage.makeComplete(currentFullImage); new MockCreator().mockProperty(currentImage, "sequenceEdges$", new Subject()); const nextImageKey: string = "image1"; const currentImageSubject: Subject = >stateService.currentImage$; currentImageSubject.next(currentImage); const sequence: Sequence = new Sequence({ id: sequenceKey, image_ids: [currentImage.id, nextImageKey] }); cacheSequenceSubject.next(sequence); const cacheImageSpy: jasmine.Spy = spyOn(graphService, "cacheImage$"); const cacheImageSubject: Subject = new Subject(); cacheImageSpy.and.returnValue(cacheImageSubject); const state: IAnimationState = createState(); state.trajectory = [currentImage]; state.lastImage = currentImage; state.currentImage = currentImage; state.imagesAhead = 0; const currentStateSubject$: Subject = >stateService.currentState$; currentStateSubject$.next({ fps: 60, id: 0, state: state }); const nextFullImage: ImageEnt = new ImageHelper().createImageEnt(); nextFullImage.sequence.id = sequenceKey; nextFullImage.id = nextImageKey; const nextImage: Image = new Image(nextFullImage); nextImage.makeComplete(nextFullImage); cacheImageSubject.next(nextImage); expect(cacheImageSpy.calls.count()).toBe(1); expect(cacheImageSpy.calls.argsFor(0)[0]).toBe(nextImageKey); currentStateSubject$.next({ fps: 60, id: 0, state: state }); expect(cacheImageSpy.calls.count()).toBe(1); playService.stop(); }); it("should not pre-cache if all sequence images in trajectory", () => { graphService.setGraphMode(GraphMode.Spatial); const playService: PlayService = new PlayService(graphService, stateService); playService.setDirection(NavigationDirection.Next); const cacheSequenceSubject: Subject = new Subject(); spyOn(graphService, "cacheSequence$").and.returnValue(cacheSequenceSubject); playService.play(); const sequenceKey: string = "sequenceId"; const currentFullImage: ImageEnt = new ImageHelper().createImageEnt(); currentFullImage.sequence.id = sequenceKey; currentFullImage.id = "image0"; const currentImage: Image = new Image(currentFullImage); currentImage.makeComplete(currentFullImage); new MockCreator().mockProperty(currentImage, "sequenceEdges$", new Subject()); const nextImageKey: string = "image1"; const nextFullImage: ImageEnt = new ImageHelper().createImageEnt(); nextFullImage.sequence.id = sequenceKey; nextFullImage.id = nextImageKey; const nextImage: Image = new Image(nextFullImage); nextImage.makeComplete(nextFullImage); new MockCreator().mockProperty(nextImage, "sequenceEdges$", new Subject()); const currentImageSubject: Subject = >stateService.currentImage$; currentImageSubject.next(currentImage); const sequence: Sequence = new Sequence({ id: sequenceKey, image_ids: [currentImage.id, nextImageKey] }); cacheSequenceSubject.next(sequence); const cacheImageSpy: jasmine.Spy = spyOn(graphService, "cacheImage$"); const cacheImageSubject: Subject = new Subject(); cacheImageSpy.and.returnValue(cacheImageSubject); const state: IAnimationState = createState(); state.trajectory = [currentImage, nextImage]; state.lastImage = nextImage; state.currentImage = currentImage; state.imagesAhead = 0; const currentStateSubject$: Subject = >stateService.currentState$; currentStateSubject$.next({ fps: 60, id: 0, state: state }); expect(cacheImageSpy.calls.count()).toBe(0); playService.stop(); }); it("should pre-cache up to specified images ahead", () => { graphService.setGraphMode(GraphMode.Spatial); const playService: PlayService = new PlayService(graphService, stateService); playService.setDirection(NavigationDirection.Next); // Zero speed means max ten images ahead playService.setSpeed(0); const cacheSequenceSubject: Subject = new Subject(); spyOn(graphService, "cacheSequence$").and.returnValue(cacheSequenceSubject); playService.play(); const sequenceKey: string = "sequenceId"; const currentFullImage: ImageEnt = new ImageHelper().createImageEnt(); currentFullImage.sequence.id = sequenceKey; currentFullImage.id = "currentImageKey"; const currentImage: Image = new Image(currentFullImage); currentImage.makeComplete(currentFullImage); new MockCreator().mockProperty(currentImage, "sequenceEdges$", new Subject()); const sequence: Sequence = new Sequence({ id: sequenceKey, image_ids: [currentImage.id] }); const sequenceImages: Image[] = []; for (let i: number = 0; i < 20; i++) { const sequenceImageKey: string = `image${i}`; const sequenceFullImage: ImageEnt = new ImageHelper().createImageEnt(); sequenceFullImage.sequence.id = sequenceKey; sequenceFullImage.id = sequenceImageKey; const sequenceImage: Image = new Image(sequenceFullImage); sequenceImage.makeComplete(sequenceFullImage); new MockCreator().mockProperty(sequenceImage, "sequenceEdges$", new Subject()); sequence.imageIds.push(sequenceImage.id); sequenceImages.push(sequenceImage); } const currentImageSubject: Subject = >stateService.currentImage$; currentImageSubject.next(currentImage); cacheSequenceSubject.next(sequence); const cacheImageSpy: jasmine.Spy = spyOn(graphService, "cacheImage$").and.callFake( (key: string): Observable => { const fullImage: ImageEnt = new ImageHelper().createImageEnt(); fullImage.sequence.id = sequenceKey; fullImage.id = key; const image: Image = new Image(fullImage); image.makeComplete(fullImage); return observableOf(image); }); const state: IAnimationState = createState(); state.trajectory = [currentImage]; state.lastImage = currentImage; state.currentImage = currentImage; state.currentIndex = 0; state.imagesAhead = 0; // Cache ten images immediately const currentStateSubject$: Subject = >stateService.currentState$; currentStateSubject$.next({ fps: 60, id: 0, state: state }); let cachedCount: number = 10; expect(cacheImageSpy.calls.count()).toBe(cachedCount); // Add one image to trajectory before current image has moved state.trajectory = state.trajectory.concat(sequenceImages.splice(0, 1)); state.lastImage = state.trajectory[state.trajectory.length - 1]; state.imagesAhead = 1; currentStateSubject$.next({ fps: 60, id: 0, state: state }); // No new images should be cached expect(cacheImageSpy.calls.count()).toBe(cachedCount); // Current image has moved one step in trajectory to the last image, images ahead // is zero and one new image should be cached state.currentIndex += 1; state.currentImage = state.trajectory[state.currentIndex]; state.imagesAhead = 0; currentStateSubject$.next({ fps: 60, id: 0, state: state }); cachedCount += 1; expect(cacheImageSpy.calls.count()).toBe(cachedCount); // Add 5 images to trajectory and move current image 3 steps state.trajectory = state.trajectory.concat(sequenceImages.splice(0, 5)); state.currentIndex += 3; state.currentImage = state.trajectory[state.currentIndex]; state.lastImage = state.trajectory[state.trajectory.length - 1]; state.imagesAhead = 2; currentStateSubject$.next({ fps: 60, id: 0, state: state }); // Three new images should be cached cachedCount += 3; expect(cacheImageSpy.calls.count()).toBe(cachedCount); // Add all 14 images cached so far to trajectory and move current image to last // trajectory image state.trajectory = state.trajectory.concat(sequenceImages.splice(0, 8)); state.currentIndex = state.trajectory.length - 1; expect(state.currentIndex).toBe(14); state.currentImage = state.trajectory[state.currentIndex]; state.lastImage = state.trajectory[state.trajectory.length - 1]; state.imagesAhead = 0; currentStateSubject$.next({ fps: 60, id: 0, state: state }); // Six last images should be cached cachedCount += 6; expect(cacheImageSpy.calls.count()).toBe(cachedCount); currentStateSubject$.next({ fps: 60, id: 0, state: state }); // No new images should be cached expect(cacheImageSpy.calls.count()).toBe(cachedCount); // Add all remaining images to trajectory and move current image one step state.trajectory = state.trajectory.concat(sequenceImages.splice(0, sequenceImages.length)); state.currentIndex += 1; state.currentImage = state.trajectory[state.currentIndex]; state.lastImage = state.trajectory[state.trajectory.length - 1]; state.imagesAhead = 5; currentStateSubject$.next({ fps: 60, id: 0, state: state }); // No new images should be cached expect(cacheImageSpy.calls.count()).toBe(cachedCount); // Move current image to last trajectory image state.trajectory = state.trajectory.concat(sequenceImages.splice(0, sequenceImages.length)); state.currentIndex = state.trajectory.length - 1; state.currentImage = state.trajectory[state.currentIndex]; state.lastImage = state.trajectory[state.trajectory.length - 1]; state.imagesAhead = 0; currentStateSubject$.next({ fps: 60, id: 0, state: state }); // No new images should be cached expect(cacheImageSpy.calls.count()).toBe(20); for (let i: number = 0; i < 20; i++) { expect(cacheImageSpy.calls.argsFor(i)[0]).toBe(sequence.imageIds[i + 1]); } playService.stop(); }); });