import { bootstrap } from "../Bootstrap"; bootstrap(); import { Subject } from "rxjs"; import { WebGLRenderer } from "three"; import * as Container from "../../src/viewer/Container"; import * as Navigator from "../../src/viewer/Navigator"; import { ContainerMockCreator } from "../helper/ContainerMockCreator"; import { MockCreator } from "../helper/MockCreator"; import { NavigatorMockCreator } from "../helper/NavigatorMockCreator"; import { RendererMock } from "../helper/WebGLRenderer"; import { LngLatAlt } from "../../src/api/interfaces/LngLatAlt"; import { RenderCamera } from "../../src/render/RenderCamera"; import { RenderMode } from "../../src/render/RenderMode"; import { CustomCameraControls } from "../../src/viewer/CustomCameraControls"; import { State } from "../../src/state/State"; import { ViewportSize } from "../../src/render/interfaces/ViewportSize"; import { AnimationFrame } from "../../src/state/interfaces/AnimationFrame"; import { ICustomCameraControls } from "../../src/mapillary"; global.WebGL2RenderingContext = jest.fn(); type WebGLMocks = { context: WebGL2RenderingContext; renderer: WebGLRenderer; }; function createWebGLMocks(): WebGLMocks { const renderer = new RendererMock(); const context = new MockCreator() .create(WebGL2RenderingContext, "WebGL2RenderingContext"); spyOn(renderer, "getContext").and.returnValue(context); return { context, renderer }; } describe("CustomCameraControls.ctor", () => { test("should be definded", () => { const navigator = new NavigatorMockCreator().create(); const container = new ContainerMockCreator().create(); spyOn(Navigator, "Navigator").and.returnValue(navigator); spyOn(Container, "Container").and.returnValue(container); const controls = new CustomCameraControls( container, navigator); expect(controls).toBeDefined(); }); }); describe("CustomCameraControls.has", () => { test("should have camera controls immediately after attaching", () => { const navigator = new NavigatorMockCreator().create(); const container = new ContainerMockCreator().create(); spyOn(Navigator, "Navigator").and.returnValue(navigator); spyOn(Container, "Container").and.returnValue(container); const controls = new CustomCameraControls( container, navigator); const viewer = {}; const custom: ICustomCameraControls = { onActivate: () => { /* noop*/ }, onAnimationFrame: () => { /* noop*/ }, onAttach: () => { /* noop*/ }, onDeactivate: () => { /* noop*/ }, onDetach: () => { /* noop*/ }, onReference: () => { /* noop */ }, onResize: () => { /* noop */ }, }; expect(controls.has(custom)).toBe(false); controls.attach(custom, viewer); expect(controls.has(custom)).toBe(true); const other: ICustomCameraControls = { onActivate: () => { /* noop*/ }, onAnimationFrame: () => { /* noop*/ }, onAttach: () => { /* noop*/ }, onDeactivate: () => { /* noop*/ }, onDetach: () => { /* noop*/ }, onReference: () => { /* noop */ }, onResize: () => { /* noop */ }, }; expect(controls.has(other)).toBe(false); }); }); describe("CustomCameraControls.attach", () => { test("should invoke onAttach after gl initialization", done => { const navigator = new NavigatorMockCreator().create(); const container = new ContainerMockCreator().create(); spyOn(Navigator, "Navigator").and.returnValue(navigator); spyOn(Container, "Container").and.returnValue(container); const controls = new CustomCameraControls( container, navigator); const viewer = {}; const referenceMock: LngLatAlt = { alt: 1, lat: 2, lng: 3 }; controls.attach( { onActivate: () => { fail(); }, onAnimationFrame: () => { /* noop*/ }, onAttach: (v, vmCallback, pmCallback) => { expect(v).toBe(viewer); expect(vmCallback).toBeDefined(); expect(pmCallback).toBeDefined(); done(); }, onDeactivate: () => { fail(); }, onDetach: () => { fail(); }, onReference: () => { /* noop */ }, onResize: () => { /* noop */ }, }, viewer); const webGLMocks = createWebGLMocks(); (>container.glRenderer.webGLRenderer$) .next(webGLMocks.renderer); (>navigator.stateService.reference$) .next(referenceMock); (>navigator.stateService.state$) .next(State.Traversing); (>container.renderService.renderCamera$) .next(new RenderCamera(1, 1, RenderMode.Fill)); }); test("should invoke onActivate if custom state is set", done => { const navigator = new NavigatorMockCreator().create(); const container = new ContainerMockCreator().create(); spyOn(Navigator, "Navigator").and.returnValue(navigator); spyOn(Container, "Container").and.returnValue(container); const controls = new CustomCameraControls( container, navigator); const viewer = {}; const reference: LngLatAlt = { alt: 1, lat: 2, lng: 3 }; const camera = new RenderCamera(1, 1, RenderMode.Fill); controls.attach( { onActivate: (v, vm, pm, ref) => { expect(v).toBe(viewer); expect(vm).toEqual( camera.perspective.matrixWorldInverse.toArray()); expect(pm).toEqual( camera.perspective.projectionMatrix.toArray()); expect(pm).not.toEqual(vm); expect(ref).toBe(reference); done(); }, onAnimationFrame: () => { /* noop*/ }, onAttach: () => { /* noop*/ }, onDeactivate: () => { fail(); }, onDetach: () => { fail(); }, onReference: () => { /* noop */ }, onResize: () => { /* noop */ }, }, viewer); const webGLMocks = createWebGLMocks(); (>container.glRenderer.webGLRenderer$) .next(webGLMocks.renderer); (>navigator.stateService.reference$) .next(reference); (>navigator.stateService.state$) .next(State.Custom); (>container.renderService.renderCamera$) .next(camera); // Replay (>navigator.stateService.state$) .next(State.Custom); }); test("should invoke onReference after skipping replay", done => { const navigator = new NavigatorMockCreator().create(); const container = new ContainerMockCreator().create(); spyOn(Navigator, "Navigator").and.returnValue(navigator); spyOn(Container, "Container").and.returnValue(container); const controls = new CustomCameraControls( container, navigator); const viewer = {}; const initialReference: LngLatAlt = { alt: 1, lat: 2, lng: 3 }; const changedReference: LngLatAlt = { alt: 4, lat: 4, lng: 6 }; const camera = new RenderCamera(1, 1, RenderMode.Fill); controls.attach( { onActivate: () => { /* noop*/ }, onAnimationFrame: () => { /* noop*/ }, onAttach: () => { /* noop*/ }, onDeactivate: () => { fail(); }, onDetach: () => { fail(); }, onReference: (v, ref) => { expect(v).toBe(viewer); expect(ref).not.toBe(initialReference); expect(ref).toBe(changedReference); expect(ref.alt).toBe(changedReference.alt); done(); }, onResize: () => { /* noop */ }, }, viewer); // Attach const webGLMocks = createWebGLMocks(); (>container.glRenderer.webGLRenderer$) .next(webGLMocks.renderer); (>navigator.stateService.state$) .next(State.Custom); (>container.renderService.renderCamera$) .next(camera); (>navigator.stateService.reference$) .next(initialReference); // Replay (>navigator.stateService.state$) .next(State.Custom); (>navigator.stateService.reference$) .next(initialReference); // Change (>navigator.stateService.reference$) .next(changedReference); }); test("should invoke onResize after skipping replay", done => { const navigator = new NavigatorMockCreator().create(); const container = new ContainerMockCreator().create(); spyOn(Navigator, "Navigator").and.returnValue(navigator); spyOn(Container, "Container").and.returnValue(container); const controls = new CustomCameraControls( container, navigator); const viewer = {}; const reference: LngLatAlt = { alt: 1, lat: 2, lng: 3 }; const camera = new RenderCamera(1, 1, RenderMode.Fill); const initialSize: ViewportSize = { height: 1, width: 2 }; const changedSize: ViewportSize = { height: 3, width: 4 }; controls.attach( { onActivate: () => { /* noop*/ }, onAnimationFrame: () => { /* noop*/ }, onAttach: () => { /* noop*/ }, onDeactivate: () => { fail(); }, onDetach: () => { fail(); }, onReference: () => { /* noop */ }, onResize: (v) => { expect(v).toBe(viewer); done(); }, }, viewer); // Attach const webGLMocks = createWebGLMocks(); (>container.glRenderer.webGLRenderer$) .next(webGLMocks.renderer); (>navigator.stateService.state$) .next(State.Custom); (>container.renderService.renderCamera$) .next(camera); (>navigator.stateService.reference$) .next(reference); // Replay (>navigator.stateService.state$) .next(State.Custom); (>container.renderService.size$) .next(initialSize); // Change (>container.renderService.size$) .next(changedSize); }); test("should invoke onAnimationFrame after skipping replay", done => { const navigator = new NavigatorMockCreator().create(); const container = new ContainerMockCreator().create(); spyOn(Navigator, "Navigator").and.returnValue(navigator); spyOn(Container, "Container").and.returnValue(container); const controls = new CustomCameraControls( container, navigator); const viewer = {}; const reference: LngLatAlt = { alt: 1, lat: 2, lng: 3 }; const camera = new RenderCamera(1, 1, RenderMode.Fill); const initialFrame: AnimationFrame = { id: 1, fps: 120, state: null }; const changedFrame: AnimationFrame = { id: 2, fps: 120, state: null }; controls.attach( { onActivate: () => { /* noop*/ }, onAnimationFrame: (v, frameId) => { expect(v).toBe(viewer); expect(frameId).toBe(changedFrame.id); done(); }, onAttach: () => { /* noop*/ }, onDeactivate: () => { fail(); }, onDetach: () => { fail(); }, onReference: () => { /* noop */ }, onResize: (v) => { /* noop */ }, }, viewer); // Attach const webGLMocks = createWebGLMocks(); (>container.glRenderer.webGLRenderer$) .next(webGLMocks.renderer); (>navigator.stateService.state$) .next(State.Custom); (>container.renderService.renderCamera$) .next(camera); (>navigator.stateService.reference$) .next(reference); // Replay (>navigator.stateService.state$) .next(State.Custom); (>navigator.stateService.currentState$) .next(initialFrame); // Change (>navigator.stateService.currentState$) .next(changedFrame); }); test("should invoke onActivate when custom state is set", done => { const navigator = new NavigatorMockCreator().create(); const container = new ContainerMockCreator().create(); spyOn(Navigator, "Navigator").and.returnValue(navigator); spyOn(Container, "Container").and.returnValue(container); const controls = new CustomCameraControls( container, navigator); const viewer = {}; const initialReference: LngLatAlt = { alt: 1, lat: 2, lng: 3 }; const changedReference: LngLatAlt = { alt: 4, lat: 5, lng: 5 }; const initialCamera = new RenderCamera(1, 1, RenderMode.Fill); const changedCamera = new RenderCamera(4, 1, RenderMode.Fill); controls.attach( { onActivate: (v, vm, pm, ref) => { expect(v).toBe(viewer); expect(vm).toEqual( changedCamera.perspective.matrixWorldInverse.toArray()); expect(pm).toEqual( changedCamera.perspective.projectionMatrix.toArray()); expect(pm).not.toEqual(vm); expect(ref).toBe(changedReference); done(); }, onAnimationFrame: () => { /* noop*/ }, onAttach: () => { /* noop*/ }, onDeactivate: () => { fail(); }, onDetach: () => { fail(); }, onReference: () => { /* noop */ }, onResize: () => { /* noop */ }, }, viewer); // Attach const webGLMocks = createWebGLMocks(); (>container.glRenderer.webGLRenderer$) .next(webGLMocks.renderer); (>navigator.stateService.reference$) .next(initialReference); (>container.renderService.renderCamera$) .next(initialCamera); (>navigator.stateService.state$) .next(State.Earth); // Replay (>navigator.stateService.reference$) .next(changedReference); (>container.renderService.renderCamera$) .next(changedCamera); (>navigator.stateService.state$) .next(State.Custom); }); test("should invoke onDeactivate when non custom state is set", done => { const navigator = new NavigatorMockCreator().create(); const container = new ContainerMockCreator().create(); spyOn(Navigator, "Navigator").and.returnValue(navigator); spyOn(Container, "Container").and.returnValue(container); const controls = new CustomCameraControls( container, navigator); const viewer = {}; const reference: LngLatAlt = { alt: 1, lat: 2, lng: 3 }; const camera = new RenderCamera(1, 1, RenderMode.Fill); controls.attach( { onActivate: () => { /* noop*/ }, onAnimationFrame: () => { /* noop*/ }, onAttach: () => { /* noop*/ }, onDeactivate: (v) => { expect(v).toBe(viewer); done(); }, onDetach: () => { fail(); }, onReference: () => { /* noop */ }, onResize: () => { /* noop */ }, }, viewer); // Attach const webGLMocks = createWebGLMocks(); (>container.glRenderer.webGLRenderer$) .next(webGLMocks.renderer); (>navigator.stateService.reference$) .next(reference); (>container.renderService.renderCamera$) .next(camera); (>navigator.stateService.state$) .next(State.Custom); // Replay (>navigator.stateService.reference$) .next(reference); (>container.renderService.renderCamera$) .next(camera); (>navigator.stateService.state$) .next(State.Custom); // Emit (>navigator.stateService.state$) .next(State.Earth); }); test("should callback view matrix in custom state", done => { const navigator = new NavigatorMockCreator().create(); const container = new ContainerMockCreator().create(); spyOn(Navigator, "Navigator").and.returnValue(navigator); spyOn(Container, "Container").and.returnValue(container); const controls = new CustomCameraControls( container, navigator); const viewer = {}; const referenceMock: LngLatAlt = { alt: 1, lat: 2, lng: 3 }; controls.attach( { onActivate: () => { /* noop */ }, onAnimationFrame: () => { /* noop*/ }, onAttach: (_, vmCallback) => { vmCallback([0, 1]); (>navigator.stateService.state$) .next(State.Custom); expect( (navigator.stateService.setViewMatrix) .calls.count()) .toBe(1); expect( (navigator.stateService.setViewMatrix) .calls.first().args[0]) .toEqual([0, 1]); done(); }, onDeactivate: () => { fail(); }, onDetach: () => { fail(); }, onReference: () => { /* noop */ }, onResize: () => { /* noop */ }, }, viewer); const rendererMock = new RendererMock(); const contextMock = new MockCreator() .create(WebGL2RenderingContext, "WebGL2RenderingContext"); spyOn(rendererMock, "getContext").and.returnValue(contextMock); (>container.glRenderer.webGLRenderer$) .next(rendererMock); (>navigator.stateService.reference$) .next(referenceMock); (>navigator.stateService.state$) .next(State.Custom); (>container.renderService.renderCamera$) .next(new RenderCamera(1, 1, RenderMode.Fill)); }); test("should callback projection matrix in custom state", done => { const navigator = new NavigatorMockCreator().create(); const container = new ContainerMockCreator().create(); spyOn(Navigator, "Navigator").and.returnValue(navigator); spyOn(Container, "Container").and.returnValue(container); const controls = new CustomCameraControls( container, navigator); const viewer = {}; const referenceMock: LngLatAlt = { alt: 1, lat: 2, lng: 3 }; controls.attach( { onActivate: () => { /* noop */ }, onAnimationFrame: () => { /* noop*/ }, onAttach: (_, __, pmCallback) => { pmCallback([2, 3]); (>container .renderService.projectionMatrix$) .subscribe( pm => { expect(pm).toEqual([2, 3]); done(); }); (>navigator.stateService.state$) .next(State.Custom); }, onDeactivate: () => { fail(); }, onDetach: () => { fail(); }, onReference: () => { /* noop */ }, onResize: () => { /* noop */ }, }, viewer); const rendererMock = new RendererMock(); const contextMock = new MockCreator() .create(WebGL2RenderingContext, "WebGL2RenderingContext"); spyOn(rendererMock, "getContext").and.returnValue(contextMock); (>container.glRenderer.webGLRenderer$) .next(rendererMock); (>navigator.stateService.reference$) .next(referenceMock); (>navigator.stateService.state$) .next(State.Custom); (>container.renderService.renderCamera$) .next(new RenderCamera(1, 1, RenderMode.Fill)); }); test("should not callback in non custom state", done => { spyOn(console, "warn").and.stub(); const navigator = new NavigatorMockCreator().create(); const container = new ContainerMockCreator().create(); spyOn(Navigator, "Navigator").and.returnValue(navigator); spyOn(Container, "Container").and.returnValue(container); const controls = new CustomCameraControls( container, navigator); const viewer = {}; const referenceMock: LngLatAlt = { alt: 1, lat: 2, lng: 3 }; controls.attach( { onActivate: () => { /* noop */ }, onAnimationFrame: () => { /* noop*/ }, onAttach: (_, vmCallback, pmCallback) => { vmCallback([0, 1]); pmCallback([2, 3]); (>container .renderService.projectionMatrix$) .subscribe( () => { fail(); }); (>navigator.stateService.state$) .next(State.Earth); expect( (navigator.stateService.setViewMatrix) .calls.count()) .toBe(0); done(); }, onDeactivate: () => { fail(); }, onDetach: () => { fail(); }, onReference: () => { /* noop */ }, onResize: () => { /* noop */ }, }, viewer); const rendererMock = new RendererMock(); const contextMock = new MockCreator() .create(WebGL2RenderingContext, "WebGL2RenderingContext"); spyOn(rendererMock, "getContext").and.returnValue(contextMock); (>container.glRenderer.webGLRenderer$) .next(rendererMock); (>navigator.stateService.reference$) .next(referenceMock); (>navigator.stateService.state$) .next(State.Earth); (>container.renderService.renderCamera$) .next(new RenderCamera(1, 1, RenderMode.Fill)); }); }); describe("CustomCameraControls.detach", () => { test("should invoke onDetach when detatching", done => { const navigator = new NavigatorMockCreator().create(); const container = new ContainerMockCreator().create(); spyOn(Navigator, "Navigator").and.returnValue(navigator); spyOn(Container, "Container").and.returnValue(container); const controls = new CustomCameraControls( container, navigator); const viewer = {}; controls.attach( { onActivate: () => { fail(); }, onAnimationFrame: () => { fail(); }, onAttach: () => { fail(); }, onDeactivate: (v) => { fail(); }, onDetach: (v) => { expect(v).toBe(viewer); done(); }, onReference: () => { fail(); }, onResize: () => { fail(); }, }, viewer); controls.detach(viewer); (>navigator.stateService.state$) .next(State.Earth); }); test("should only invoke onDetach once", () => { const navigator = new NavigatorMockCreator().create(); const container = new ContainerMockCreator().create(); spyOn(Navigator, "Navigator").and.returnValue(navigator); spyOn(Container, "Container").and.returnValue(container); const controls = new CustomCameraControls( container, navigator); const viewer = {}; let detachCount = 0; controls.attach( { onActivate: () => { fail(); }, onAnimationFrame: () => { fail(); }, onAttach: () => { fail(); }, onDeactivate: (v) => { fail(); }, onDetach: (v) => { expect(v).toBe(viewer); detachCount++; }, onReference: () => { fail(); }, onResize: () => { fail(); }, }, viewer); controls.detach(viewer); (>navigator.stateService.state$) .next(State.Earth); (>navigator.stateService.state$) .next(State.Traversing); expect(detachCount).toBe(1); }); test("should invoke onDeactive when detatching if in custom state", done => { const navigator = new NavigatorMockCreator().create(); const container = new ContainerMockCreator().create(); spyOn(Navigator, "Navigator").and.returnValue(navigator); spyOn(Container, "Container").and.returnValue(container); const controls = new CustomCameraControls( container, navigator); const viewer = {}; let deactivateCount = 0; controls.attach( { onActivate: () => { fail(); }, onAnimationFrame: () => { fail(); }, onAttach: () => { fail(); }, onDeactivate: (v) => { expect(v).toBe(viewer); deactivateCount++; }, onDetach: (v) => { expect(v).toBe(viewer); expect(deactivateCount).toBe(1); done(); }, onReference: () => { fail(); }, onResize: () => { fail(); }, }, viewer); controls.detach(viewer); (>navigator.stateService.state$) .next(State.Custom); }); });