diff --git a/spec/utils/Urls.spec.ts b/spec/utils/Urls.spec.ts deleted file mode 100644 index 7c3104664..000000000 --- a/spec/utils/Urls.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Urls } from "../../src/utils/Urls"; -import { UrlOptions } from "../../src/viewer/options/UrlOptions"; - -describe("Urls.setOptions", () => { - it("should set all option properties", () => { - const options: UrlOptions = { - exploreHost: "test-explore", - scheme: "test-scheme", - }; - - Urls.setOptions(options); - - expect(Urls.explore).toContain(options.exploreHost); - expect(Urls.explore).toContain(options.scheme); - }); -}); diff --git a/spec/utils/ViewerConfiguration.spec.ts b/spec/utils/ViewerConfiguration.spec.ts new file mode 100644 index 000000000..a68dce822 --- /dev/null +++ b/spec/utils/ViewerConfiguration.spec.ts @@ -0,0 +1,28 @@ +import { notDeepEqual } from "assert"; +import { ViewerOptions } from "../../src/Mapillary"; +import { ViewerConfiguration } from "../../src/utils/ViewerConfiguration"; + +describe("Urls.setOptions", () => { + it("should set all option properties", () => { + const options: ViewerOptions = { + apiClient: "api-client", + container: "container-id", + imageTiling: false, + url: { + exploreHost: "test-explore", + scheme: "test-scheme", + }, + }; + + expect(ViewerConfiguration.imageTiling).toBe(true); + expect(ViewerConfiguration.explore).toBe("https://www.mapillary.com"); + + ViewerConfiguration.setOptions(options); + + expect(ViewerConfiguration.imageTiling).toBe(false); + expect(ViewerConfiguration.explore).not.toContain("https"); + expect(ViewerConfiguration.explore).not.toContain("mapillary"); + expect(ViewerConfiguration.explore).toContain(options.url.scheme); + expect(ViewerConfiguration.explore).toContain(options.url.exploreHost); + }); +}); diff --git a/src/component/attribution/AttributionComponent.ts b/src/component/attribution/AttributionComponent.ts index 1471efac9..8fe82c0d0 100644 --- a/src/component/attribution/AttributionComponent.ts +++ b/src/component/attribution/AttributionComponent.ts @@ -1,23 +1,25 @@ import * as vd from "virtual-dom"; import { combineLatest as observableCombineLatest } from "rxjs"; - import { map } from "rxjs/operators"; -import { Component } from "../Component"; -import { ComponentConfiguration } from "../interfaces/ComponentConfiguration"; - import { Node } from "../../graph/Node"; import { ViewportSize } from "../../render/interfaces/ViewportSize"; import { VirtualNodeHash } from "../../render/interfaces/VirtualNodeHash"; -import { Urls } from "../../utils/Urls"; +import { ViewerConfiguration } from "../../utils/ViewerConfiguration"; import { Container } from "../../viewer/Container"; import { Navigator } from "../../viewer/Navigator"; +import { Component } from "../Component"; +import { ComponentConfiguration } from "../interfaces/ComponentConfiguration"; + export class AttributionComponent extends Component { public static componentName: string = "attribution"; - constructor(name: string, container: Container, navigator: Navigator) { + constructor( + name: string, + container: Container, + navigator: Navigator) { super(name, container, navigator); } @@ -29,7 +31,11 @@ export class AttributionComponent extends Component { ([node, size]: [Node, ViewportSize]): VirtualNodeHash => { return { name: this._name, - vnode: this._getAttributionNode(node.creatorUsername, node.id, node.capturedAt, size.width), + vnode: this._getAttributionNode( + node.creatorUsername, + node.id, + node.capturedAt, + size.width), }; })) .subscribe(this._container.domRenderer.render$)); @@ -43,35 +49,58 @@ export class AttributionComponent extends Component { return {}; } - private _getAttributionNode(username: string, id: string, capturedAt: number, width: number): vd.VNode { - const compact: boolean = width <= 640; - - const mapillaryIcon: vd.VNode = vd.h("div.AttributionMapillaryLogo", []); - const mapillaryLink: vd.VNode = vd.h( + private _getAttributionNode( + username: string, + id: string, + capturedAt: number, + width: number) + : vd.VNode { + const compact = width <= 640; + + const mapillaryIcon = vd.h( + "div.AttributionMapillaryLogo", + []); + const mapillaryLink = vd.h( "a.AttributionIconContainer", - { href: Urls.explore, target: "_blank" }, + { href: ViewerConfiguration.explore, target: "_blank" }, [mapillaryIcon]); - const imageBy: string = compact ? `${username}` : `image by ${username}`; - const imageByContent: vd.VNode = vd.h("div.AttributionUsername", { textContent: imageBy }, []); - - const date: string[] = new Date(capturedAt).toDateString().split(" "); - const formatted: string = (date.length > 3 ? + const imageBy = compact ? + `${username}` : `image by ${username}`; + const imageByContent = vd.h( + "div.AttributionUsername", + { textContent: imageBy }, + []); + + const date = new Date(capturedAt) + .toDateString() + .split(" "); + const formatted = (date.length > 3 ? compact ? [date[3]] : [date[1], date[2] + ",", date[3]] : date).join(" "); - const dateContent: vd.VNode = vd.h("div.AttributionDate", { textContent: formatted }, []); + const dateContent = vd.h( + "div.AttributionDate", + { textContent: formatted }, + []); - const imageLink: vd.VNode = + const imageLink = vd.h( "a.mapillary-attribution-image-container", - { href: Urls.exploreImage(id), target: "_blank" }, + { + href: ViewerConfiguration.exploreImage(id), + target: "_blank", + }, [imageByContent, dateContent]); - const compactClass: string = compact ? ".mapillary-attribution-compact" : ""; + const compactClass = compact ? + ".mapillary-attribution-compact" : ""; - return vd.h("div.mapillary-attribution-container" + compactClass, {}, [mapillaryLink, imageLink]); + return vd.h( + "div.mapillary-attribution-container" + compactClass, + {}, + [mapillaryLink, imageLink]); } } diff --git a/src/component/cover/CoverComponent.ts b/src/component/cover/CoverComponent.ts index f0aa2dacb..6dbb2193e 100644 --- a/src/component/cover/CoverComponent.ts +++ b/src/component/cover/CoverComponent.ts @@ -5,7 +5,6 @@ import { empty as observableEmpty, of as observableOf, Observable, - Subscription, Subscriber, } from "rxjs"; @@ -27,7 +26,7 @@ import { MapillaryError } from "../../error/MapillaryError"; import { Node } from "../../graph/Node"; import { ViewportSize } from "../../render/interfaces/ViewportSize"; import { VirtualNodeHash } from "../../render/interfaces/VirtualNodeHash"; -import { Urls } from "../../utils/Urls"; +import { ViewerConfiguration } from "../../utils/ViewerConfiguration"; import { Container } from "../../viewer/Container"; import { Navigator } from "../../viewer/Navigator"; import { ImagesContract } from "../../api/contracts/ImagesContract"; @@ -168,7 +167,7 @@ export class CoverComponent extends Component { "div.mapillary-cover-button", [vd.h("div.mapillary-cover-button-icon", [])]); - const coverLogo: vd.VNode = vd.h("a.mapillary-cover-logo", { href: Urls.explore, target: "_blank" }, []); + const coverLogo: vd.VNode = vd.h("a.mapillary-cover-logo", { href: ViewerConfiguration.explore, target: "_blank" }, []); const coverIndicator: vd.VNode = vd.h( "div.mapillary-cover-indicator", { onclick: (): void => { this.configure({ state: CoverState.Loading }); } }, diff --git a/src/component/imageplane/ImagePlaneComponent.ts b/src/component/imageplane/ImagePlaneComponent.ts index 704d77f79..bf7ce2be5 100644 --- a/src/component/imageplane/ImagePlaneComponent.ts +++ b/src/component/imageplane/ImagePlaneComponent.ts @@ -49,6 +49,7 @@ import { RegionOfInterestCalculator } import { TextureProvider } from "../../tiles/TextureProvider"; import { ComponentConfiguration } from "../interfaces/ComponentConfiguration"; import { Transform } from "../../geo/Transform"; +import { ViewerConfiguration } from "../../utils/ViewerConfiguration"; interface ImagePlaneGLRendererOperation { (renderer: ImagePlaneGLRenderer): ImagePlaneGLRenderer; @@ -152,33 +153,35 @@ export class ImagePlaneComponent extends Component { })) .subscribe(this._rendererOperation$)); - const textureProvider$ = this._navigator.stateService.currentState$.pipe( - distinctUntilChanged( - undefined, - (frame: AnimationFrame): string => { - return frame.state.currentNode.id; - }), - withLatestFrom( - this._container.glRenderer.webGLRenderer$, - this._container.renderService.size$), - map( - ([frame, renderer, size]: [AnimationFrame, THREE.WebGLRenderer, ViewportSize]): TextureProvider => { - let state = frame.state; - let currentNode = state.currentNode; - let currentTransform = state.currentTransform; - let tileSize = 1024; - - return new TextureProvider( - currentNode.id, - currentTransform.basicWidth, - currentTransform.basicHeight, - currentNode.image, - this._imageTileLoader, - new TileStore(), - renderer); - }), - publishReplay(1), - refCount()); + const textureProvider$ = this._navigator.stateService.currentState$ + .pipe( + filter(() => ViewerConfiguration.imageTiling), + distinctUntilChanged( + undefined, + (frame: AnimationFrame): string => { + return frame.state.currentNode.id; + }), + withLatestFrom( + this._container.glRenderer.webGLRenderer$, + this._container.renderService.size$), + map( + ([frame, renderer, size]: [AnimationFrame, THREE.WebGLRenderer, ViewportSize]): TextureProvider => { + let state = frame.state; + let currentNode = state.currentNode; + let currentTransform = state.currentTransform; + let tileSize = 1024; + + return new TextureProvider( + currentNode.id, + currentTransform.basicWidth, + currentTransform.basicHeight, + currentNode.image, + this._imageTileLoader, + new TileStore(), + renderer); + }), + publishReplay(1), + refCount()); subs.push(textureProvider$.subscribe(() => { /*noop*/ })); @@ -204,6 +207,7 @@ export class ImagePlaneComponent extends Component { const roiTrigger$ = observableCombineLatest( this._container.renderService.renderCameraFrame$, this._container.renderService.size$.pipe(debounceTime(250))).pipe( + filter(() => ViewerConfiguration.imageTiling), map( ([camera, size]: [RenderCamera, ViewportSize]): PositionLookat => { return [ diff --git a/src/component/slider/SliderComponent.ts b/src/component/slider/SliderComponent.ts index 99fd6c1f6..d0cb78e59 100644 --- a/src/component/slider/SliderComponent.ts +++ b/src/component/slider/SliderComponent.ts @@ -64,6 +64,7 @@ import { SliderGLRenderer } from "./SliderGLRenderer"; import { Transform } from "../../geo/Transform"; import { SliderDOMRenderer } from "./SliderDOMRenderer"; import { isSpherical } from "../../geo/Geo"; +import { ViewerConfiguration } from "../../utils/ViewerConfiguration"; /** * @class SliderComponent @@ -391,35 +392,37 @@ export class SliderComponent extends Component { })); - const textureProvider$ = this._navigator.stateService.currentState$.pipe( - distinctUntilChanged( - undefined, - (frame: AnimationFrame): string => { - return frame.state.currentNode.id; - }), - withLatestFrom( - this._container.glRenderer.webGLRenderer$, - this._container.renderService.size$), - map( - ([frame, renderer, size]: [AnimationFrame, THREE.WebGLRenderer, ViewportSize]): TextureProvider => { - const state: IAnimationState = frame.state; - const viewportSize: number = Math.max(size.width, size.height); - - const currentNode: Node = state.currentNode; - const currentTransform: Transform = state.currentTransform; - const tileSize: number = viewportSize > 2048 ? 2048 : viewportSize > 1024 ? 1024 : 512; - - return new TextureProvider( - currentNode.id, - currentTransform.basicWidth, - currentTransform.basicHeight, - currentNode.image, - this._imageTileLoader, - new TileStore(), - renderer); - }), - publishReplay(1), - refCount()); + const textureProvider$ = this._navigator.stateService.currentState$ + .pipe( + filter(() => ViewerConfiguration.imageTiling), + distinctUntilChanged( + undefined, + (frame: AnimationFrame): string => { + return frame.state.currentNode.id; + }), + withLatestFrom( + this._container.glRenderer.webGLRenderer$, + this._container.renderService.size$), + map( + ([frame, renderer, size]: [AnimationFrame, THREE.WebGLRenderer, ViewportSize]): TextureProvider => { + const state: IAnimationState = frame.state; + const viewportSize: number = Math.max(size.width, size.height); + + const currentNode: Node = state.currentNode; + const currentTransform: Transform = state.currentTransform; + const tileSize: number = viewportSize > 2048 ? 2048 : viewportSize > 1024 ? 1024 : 512; + + return new TextureProvider( + currentNode.id, + currentTransform.basicWidth, + currentTransform.basicHeight, + currentNode.image, + this._imageTileLoader, + new TileStore(), + renderer); + }), + publishReplay(1), + refCount()); subs.push(textureProvider$.subscribe(() => { /*noop*/ })); @@ -445,6 +448,7 @@ export class SliderComponent extends Component { const roiTrigger$ = observableCombineLatest( this._container.renderService.renderCameraFrame$, this._container.renderService.size$.pipe(debounceTime(250))).pipe( + filter(() => ViewerConfiguration.imageTiling), map( ([camera, size]: [RenderCamera, ViewportSize]): PositionLookat => { return [ @@ -520,6 +524,7 @@ export class SliderComponent extends Component { subs.push(hasTexture$.subscribe(() => { /*noop*/ })); const textureProviderPrev$ = this._navigator.stateService.currentState$.pipe( + filter(() => ViewerConfiguration.imageTiling), filter( (frame: AnimationFrame): boolean => { return !!frame.state.previousNode; @@ -574,6 +579,7 @@ export class SliderComponent extends Component { const roiTriggerPrev$ = observableCombineLatest( this._container.renderService.renderCameraFrame$, this._container.renderService.size$.pipe(debounceTime(250))).pipe( + filter(() => ViewerConfiguration.imageTiling), map( ([camera, size]: [RenderCamera, ViewportSize]): PositionLookat => { return [ diff --git a/src/render/RenderService.ts b/src/render/RenderService.ts index 311f8e447..8799ccc0f 100644 --- a/src/render/RenderService.ts +++ b/src/render/RenderService.ts @@ -48,7 +48,12 @@ export class RenderService { private _subscriptions: SubscriptionHolder = new SubscriptionHolder(); - constructor(element: HTMLElement, currentFrame$: Observable, renderMode: RenderMode, renderCamera?: RenderCamera) { + constructor( + element: HTMLElement, + currentFrame$: Observable, + renderMode: RenderMode, + renderCamera?: RenderCamera) { + this._element = element; this._currentFrame$ = currentFrame$; @@ -57,20 +62,23 @@ export class RenderService { renderMode = renderMode != null ? renderMode : RenderMode.Fill; this._resize$ = new Subject(); - this._renderCameraOperation$ = new Subject(); + this._renderCameraOperation$ = + new Subject(); this._size$ = - new BehaviorSubject( - { - height: this._element.offsetHeight, - width: this._element.offsetWidth, - }); + new BehaviorSubject({ + height: this._element.offsetHeight, + width: this._element.offsetWidth, + }); const subs = this._subscriptions; subs.push(this._resize$.pipe( map( (): ViewportSize => { - return { height: this._element.offsetHeight, width: this._element.offsetWidth }; + return { + height: this._element.offsetHeight, + width: this._element.offsetWidth, + }; })) .subscribe(this._size$)); @@ -85,7 +93,12 @@ export class RenderService { (rc: RenderCamera, operation: RenderCameraOperation): RenderCamera => { return operation(rc); }, - !!renderCamera ? renderCamera : new RenderCamera(this._element.offsetWidth, this._element.offsetHeight, renderMode)), + !!renderCamera ? + renderCamera : + new RenderCamera( + this._element.offsetWidth, + this._element.offsetHeight, + renderMode)), publishReplay(1), refCount()); diff --git a/src/utils/Urls.ts b/src/utils/Urls.ts deleted file mode 100644 index d7f1ca37a..000000000 --- a/src/utils/Urls.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { UrlOptions } from "../viewer/options/UrlOptions"; - -export class Urls { - private static _exploreHost: string = "www.mapillary.com"; - private static _scheme: string = "https"; - - public static get explore(): string { - return `${Urls._scheme}://${Urls._exploreHost}`; - } - - public static exploreImage(id: string): string { - return `${Urls._scheme}://${Urls._exploreHost}/app/?pKey=${id}&focus=photo`; - } - - public static exploreUser(username: string): string { - return `${Urls._scheme}://${Urls._exploreHost}/app/user/${username}`; - } - - public static setOptions(options: UrlOptions): void { - if (!options) { - return; - } - - if (!!options.exploreHost) { - Urls._exploreHost = options.exploreHost; - } - - if (!!options.scheme) { - Urls._scheme = options.scheme; - } - } -} diff --git a/src/utils/ViewerConfiguration.ts b/src/utils/ViewerConfiguration.ts new file mode 100644 index 000000000..9cda3f30c --- /dev/null +++ b/src/utils/ViewerConfiguration.ts @@ -0,0 +1,40 @@ +import { ViewerOptions } from "../viewer/options/ViewerOptions"; + +export class ViewerConfiguration { + private static _exploreHost: string = "www.mapillary.com"; + private static _scheme: string = "https"; + private static _imageTiling: boolean = true; + + public static get explore(): string { + const scheme = ViewerConfiguration._scheme; + const host = ViewerConfiguration._exploreHost; + return `${scheme}://${host}`; + } + + public static get imageTiling(): boolean { + return ViewerConfiguration._imageTiling; + } + + public static exploreImage(id: string): string { + return `${ViewerConfiguration.explore}/app/?pKey=${id}&focus=photo`; + } + + public static exploreUser(username: string): string { + return `${ViewerConfiguration.explore}/app/user/${username}`; + } + + public static setOptions(options: ViewerOptions): void { + if (!options) { return; } + if (options.imageTiling === false) { + ViewerConfiguration._imageTiling = false; + } + + if (!options.url) { return; } + if (!!options.url.exploreHost) { + ViewerConfiguration._exploreHost = options.url.exploreHost; + } + if (!!options.url.scheme) { + ViewerConfiguration._scheme = options.url.scheme; + } + } +} diff --git a/src/viewer/Container.ts b/src/viewer/Container.ts index 76d53b568..bd9923cbc 100644 --- a/src/viewer/Container.ts +++ b/src/viewer/Container.ts @@ -36,60 +36,86 @@ export class Container { stateService: StateService, dom?: DOM) { - this._dom = !!dom ? dom : new DOM(); + this._dom = dom ?? new DOM(); if (typeof options.container === "string") { - this._container = - this._dom.document.getElementById(options.container); + this._container = this._dom.document + .getElementById(options.container); if (!this._container) { - throw new Error(`Container "${options.container}" not found.`); + throw new Error( + `Container "${options.container}" not found.`); } } else if (options.container instanceof HTMLElement) { this._container = options.container; } else { - throw new Error(`Invalid type: "container" must be a String or HTMLElement.`); + throw new Error( + `Invalid type: "container" must be ` + + `a String or HTMLElement.`); } - this.id = !!this._container.id ? this._container.id : "mapillary-fallback-container-id"; + this.id = this._container.id ?? + "mapillary-fallback-container-id"; - this._container.classList.add("mapillary-viewer"); - this._canvasContainer = this._dom.createElement("div", "mapillary-interactive", this._container); + this._container.classList + .add("mapillary-viewer"); - this._canvas = this._dom.createElement("canvas", "mapillary-canvas"); + this._canvasContainer = this._dom + .createElement( + "div", + "mapillary-interactive", + this._container); + + this._canvas = this._dom + .createElement( + "canvas", + "mapillary-canvas"); this._canvas.style.position = "absolute"; this._canvas.setAttribute("tabindex", "0"); - // Add DOM container after canvas container to render DOM - // elements on top of the interactive canvas. - this._domContainer = this._dom.createElement("div", "mapillary-dom", this._container); - - this.renderService = new RenderService( - this._container, - stateService.currentState$, - options.renderMode); - - this.glRenderer = new GLRenderer( - this._canvas, - this._canvasContainer, - this.renderService); - - this.domRenderer = new DOMRenderer( - this._domContainer, - this.renderService, - stateService.currentState$); - - this.keyboardService = new KeyboardService(this._canvasContainer); - this.mouseService = new MouseService( - this._container, - this._canvasContainer, - this._domContainer, - document); - - this.touchService = new TouchService( - this._canvasContainer, - this._domContainer); - - this.spriteService = new SpriteService(options.sprite); + // Add DOM container after canvas container to + // render DOM elements on top of the interactive + // canvas. + this._domContainer = this._dom + .createElement( + "div", + "mapillary-dom", + this._container); + + this.renderService = + new RenderService( + this._container, + stateService.currentState$, + options.renderMode); + + this.glRenderer = + new GLRenderer( + this._canvas, + this._canvasContainer, + this.renderService); + + this.domRenderer = + new DOMRenderer( + this._domContainer, + this.renderService, + stateService.currentState$); + + this.keyboardService = + new KeyboardService(this._canvasContainer); + + this.mouseService = + new MouseService( + this._container, + this._canvasContainer, + this._domContainer, + document); + + this.touchService = + new TouchService( + this._canvasContainer, + this._domContainer); + + this.spriteService = + new SpriteService(options.sprite); } public get canvas(): HTMLCanvasElement { @@ -122,7 +148,8 @@ export class Container { this._removeNode(this._canvasContainer); this._removeNode(this._domContainer); - this._container.classList.remove("mapillary-viewer"); + this._container.classList + .remove("mapillary-viewer"); } private _removeNode(node: Node): void { diff --git a/src/viewer/Viewer.ts b/src/viewer/Viewer.ts index 4f75d3a4c..84f71fee1 100644 --- a/src/viewer/Viewer.ts +++ b/src/viewer/Viewer.ts @@ -18,7 +18,7 @@ import { RenderCamera } from "../render/RenderCamera"; import { RenderMode } from "../render/RenderMode"; import { TransitionMode } from "../state/TransitionMode"; import { EventEmitter } from "../utils/EventEmitter"; -import { Urls } from "../utils/Urls"; +import { ViewerConfiguration } from "../utils/ViewerConfiguration"; import { ICustomRenderer } from "./interfaces/ICustomRenderer"; import { PointOfView } from "./interfaces/PointOfView"; @@ -160,17 +160,34 @@ export class Viewer extends EventEmitter implements IViewer { constructor(options: ViewerOptions) { super(); - Urls.setOptions(options.url); - this._navigator = new Navigator(options); - this._container = new Container(options, this._navigator.stateService); - this._observer = new Observer(this, this._navigator, this._container); - this._componentController = new ComponentController( - this._container, - this._navigator, - this._observer, - options.imageId, - options.component); - this._customRenderer = new CustomRenderer(this._container, this._navigator); + ViewerConfiguration.setOptions(options); + + this._navigator = + new Navigator(options); + + this._container = + new Container( + options, + this._navigator.stateService); + + this._observer = + new Observer( + this, + this._navigator, + this._container); + + this._componentController = + new ComponentController( + this._container, + this._navigator, + this._observer, + options.imageId, + options.component); + + this._customRenderer = + new CustomRenderer( + this._container, + this._navigator); } /** diff --git a/src/viewer/options/ViewerOptions.ts b/src/viewer/options/ViewerOptions.ts index c2f12dba7..8a8af36c9 100644 --- a/src/viewer/options/ViewerOptions.ts +++ b/src/viewer/options/ViewerOptions.ts @@ -50,6 +50,19 @@ export interface ViewerOptions { */ imageId?: string; + /** + * Value indicating if the viewer should fetch high resolution + * image tiles. + * + * @description Can be used when extending `mapillary-js` with + * a custom data provider. If no image tiling server exists + * the image tiling can be inactivated to avoid error + * messages about non-existing tiles in the console. + * + * @default true + */ + imageTiling?: boolean + /** * The render mode in the viewer. * @default {RenderMode.Fill}