UNPKG

mapillary-js

Version:

A WebGL interactive street imagery library

794 lines (714 loc) 28.7 kB
import * as THREE from "three"; import { combineLatest as observableCombineLatest, concat as observableConcat, merge as observableMerge, of as observableOf, Observable, } from "rxjs"; import { distinctUntilChanged, first, map, pairwise, publishReplay, refCount, skip, startWith, switchMap, withLatestFrom, } from "rxjs/operators"; import { Component } from "../Component"; import { Image } from "../../graph/Image"; import { Container } from "../../viewer/Container"; import { Navigator } from "../../viewer/Navigator"; import { LngLat } from "../../api/interfaces/LngLat"; import { LngLatAlt } from "../../api/interfaces/LngLatAlt"; import { ViewportCoords } from "../../geo/ViewportCoords"; import { GraphCalculator } from "../../graph/GraphCalculator"; import { RenderPass } from "../../render/RenderPass"; import { GLRenderHash } from "../../render/interfaces/IGLRenderHash"; import { RenderCamera } from "../../render/RenderCamera"; import { AnimationFrame } from "../../state/interfaces/AnimationFrame"; import { MarkerConfiguration } from "../interfaces/MarkerConfiguration"; import { Marker } from "./marker/Marker"; import { MarkerSet } from "./MarkerSet"; import { MarkerScene } from "./MarkerScene"; import { ComponentEventType } from "../events/ComponentEventType"; import { enuToGeodetic, geodeticToEnu, } from "../../geo/GeoCoords"; import { ComponentMarkerEvent } from "../events/ComponentMarkerEvent"; import { ComponentEvent } from "../events/ComponentEvent"; import { ComponentName } from "../ComponentName"; /** * @class MarkerComponent * * @classdesc Component for showing and editing 3D marker objects. * * The `add` method is used for adding new markers or replacing * markers already in the set. * * If a marker already in the set has the same * id as one of the markers added, the old marker will be removed and * the added marker will take its place. * * It is not possible to update markers in the set by updating any properties * directly on the marker object. Markers need to be replaced by * re-adding them for updates to geographic position or configuration * to be reflected. * * Markers added to the marker component can be either interactive * or non-interactive. Different marker types define their behavior. * Markers with interaction support can be configured with options * to respond to dragging inside the viewer and be detected when * retrieving markers from pixel points with the `getMarkerIdAt` method. * * To retrive and use the marker component * * @example * ```js * var viewer = new Viewer({ component: { marker: true }, ... }); * * var markerComponent = viewer.getComponent("marker"); * ``` */ export class MarkerComponent extends Component<MarkerConfiguration> { public static componentName: ComponentName = "marker"; private _graphCalculator: GraphCalculator; private _markerScene: MarkerScene; private _markerSet: MarkerSet; private _viewportCoords: ViewportCoords; private _relativeGroundAltitude: number; /** @ignore */ constructor( name: string, container: Container, navigator: Navigator) { super(name, container, navigator); this._graphCalculator = new GraphCalculator(); this._markerScene = new MarkerScene(); this._markerSet = new MarkerSet(); this._viewportCoords = new ViewportCoords(); this._relativeGroundAltitude = -2; } /** * Add markers to the marker set or replace markers in the marker set. * * @description If a marker already in the set has the same * id as one of the markers added, the old marker will be removed * the added marker will take its place. * * Any marker inside the visible bounding bbox * will be initialized and placed in the viewer. * * @param {Array<Marker>} markers - Markers to add. * * @example * ```js * markerComponent.add([marker1, marker2]); * ``` */ public add(markers: Marker[]): void { this._markerSet.add(markers); } public fire( type: | "markerdragend" | "markerdragstart" | "markerposition", event: ComponentMarkerEvent) : void; /** @ignore */ public fire( type: ComponentEventType, event: ComponentEvent) : void; public fire<T>( type: ComponentEventType, event: T) : void { super.fire(type, event); } /** * Returns the marker in the marker set with the specified id, or * undefined if the id matches no marker. * * @param {string} markerId - Id of the marker. * * @example * ```js * var marker = markerComponent.get("markerId"); * ``` * */ public get(markerId: string): Marker { return this._markerSet.get(markerId); } /** * Returns an array of all markers. * * @example * ```js * var markers = markerComponent.getAll(); * ``` */ public getAll(): Marker[] { return this._markerSet.getAll(); } /** * Returns the id of the interactive marker closest to the current camera * position at the specified point. * * @description Notice that the pixelPoint argument requires x, y * coordinates from pixel space. * * With this function, you can use the coordinates provided by mouse * events to get information out of the marker component. * * If no interactive geometry of an interactive marker exist at the pixel * point, `null` will be returned. * * @param {Array<number>} pixelPoint - Pixel coordinates on the viewer element. * @returns {string} Id of the interactive marker closest to the camera. If no * interactive marker exist at the pixel point, `null` will be returned. * * @example * ```js * markerComponent.getMarkerIdAt([100, 100]) * .then((markerId) => { console.log(markerId); }); * ``` */ public getMarkerIdAt(pixelPoint: number[]): Promise<string> { return new Promise<string>((resolve: (value: string) => void, reject: (reason: Error) => void): void => { this._container.renderService.renderCamera$.pipe( first(), map( (render: RenderCamera): string => { const viewport = this._viewportCoords .canvasToViewport( pixelPoint[0], pixelPoint[1], this._container.container); const id: string = this._markerScene.intersectObjects(viewport, render.perspective); return id; })) .subscribe( (id: string): void => { resolve(id); }, (error: Error): void => { reject(error); }); }); } /** * Check if a marker exist in the marker set. * * @param {string} markerId - Id of the marker. * * @example * ```js * var markerExists = markerComponent.has("markerId"); * ``` */ public has(markerId: string): boolean { return this._markerSet.has(markerId); } public off( type: | "markerdragend" | "markerdragstart" | "markerposition", handler: (event: ComponentMarkerEvent) => void) : void; /** @ignore */ public off( type: ComponentEventType, handler: (event: ComponentEvent) => void) : void; public off<T>( type: ComponentEventType, handler: (event: T) => void) : void { super.off(type, handler); } /** * Fired when a marker drag interaction ends. * * @event markerdragend * @example * ```js * // Initialize the viewer * var viewer = new Viewer({ // viewer options }); * var component = viewer.getComponent('<component-name>'); * // Set an event listener * component.on('markerdragend', function() { * console.log("A markerdragend event has occurred."); * }); * ``` */ public on( type: "markerdragend", handler: (event: ComponentMarkerEvent) => void) : void; /** * Fired when a marker drag interaction starts. * * @event markerdragstart * @example * ```js * // Initialize the viewer * var viewer = new Viewer({ // viewer options }); * var component = viewer.getComponent('<component-name>'); * // Set an event listener * component.on('markerdragstart', function() { * console.log("A markerdragstart event has occurred."); * }); * ``` */ public on( type: "markerdragstart", handler: (event: ComponentMarkerEvent) => void) : void; /** * Fired when the position of a marker is changed. * * @event markerposition * @example * ```js * // Initialize the viewer * var viewer = new Viewer({ // viewer options }); * var component = viewer.getComponent('<component-name>'); * // Set an event listener * component.on('markerposition', function() { * console.log("A markerposition event has occurred."); * }); * ``` */ public on( type: "markerposition", handler: (event: ComponentMarkerEvent) => void) : void; public on<T>( type: ComponentEventType, handler: (event: T) => void) : void { super.on(type, handler); } /** * Remove markers with the specified ids from the marker set. * * @param {Array<string>} markerIds - Ids for markers to remove. * * @example * ```js * markerComponent.remove(["id-1", "id-2"]); * ``` */ public remove(markerIds: string[]): void { this._markerSet.remove(markerIds); } /** * Remove all markers from the marker set. * * @example * ```js * markerComponent.removeAll(); * ``` */ public removeAll(): void { this._markerSet.removeAll(); } protected _activate(): void { const groundAltitude$ = this._navigator.stateService.currentState$.pipe( map( (frame: AnimationFrame): number => { return frame.state.camera.position.z + this._relativeGroundAltitude; }), distinctUntilChanged( (a1: number, a2: number): boolean => { return Math.abs(a1 - a2) < 0.01; }), publishReplay(1), refCount()); const geoInitiated$ = observableCombineLatest( groundAltitude$, this._navigator.stateService.reference$).pipe( first(), map((): void => { /* noop */ }), publishReplay(1), refCount()); const clampedConfiguration$ = this._configuration$.pipe( map( (configuration: MarkerConfiguration): MarkerConfiguration => { return { visibleBBoxSize: Math.max(1, Math.min(200, configuration.visibleBBoxSize)) }; })); const currentLngLat$ = this._navigator.stateService.currentImage$.pipe( map((image: Image): LngLat => { return image.lngLat; }), publishReplay(1), refCount()); const visibleBBox$ = observableCombineLatest( clampedConfiguration$, currentLngLat$).pipe( map( ([configuration, lngLat]: [MarkerConfiguration, LngLat]): [LngLat, LngLat] => { return this._graphCalculator .boundingBoxCorners(lngLat, configuration.visibleBBoxSize / 2); }), publishReplay(1), refCount()); const visibleMarkers$ = observableCombineLatest( observableConcat( observableOf<MarkerSet>(this._markerSet), this._markerSet.changed$), visibleBBox$).pipe( map( ([set, bbox]: [MarkerSet, [LngLat, LngLat]]): Marker[] => { return set.search(bbox); })); const subs = this._subscriptions; subs.push(geoInitiated$.pipe( switchMap( (): Observable<[Marker[], LngLatAlt, number]> => { return visibleMarkers$.pipe( withLatestFrom( this._navigator.stateService.reference$, groundAltitude$)); })) .subscribe( ( [markers, reference, alt] : [Marker[], LngLatAlt, number]) : void => { const markerScene: MarkerScene = this._markerScene; const sceneMarkers: { [id: string]: Marker } = markerScene.markers; const markersToRemove: { [id: string]: Marker } = Object.assign({}, sceneMarkers); for (const marker of markers) { if (marker.id in sceneMarkers) { delete markersToRemove[marker.id]; } else { const point3d = geodeticToEnu( marker.lngLat.lng, marker.lngLat.lat, reference.alt + alt, reference.lng, reference.lat, reference.alt); markerScene.add(marker, point3d); } } for (const id in markersToRemove) { if (!markersToRemove.hasOwnProperty(id)) { continue; } markerScene.remove(id); } })); subs.push(geoInitiated$.pipe( switchMap( (): Observable<[Marker[], [LngLat, LngLat], LngLatAlt, number]> => { return this._markerSet.updated$.pipe( withLatestFrom( visibleBBox$, this._navigator.stateService.reference$, groundAltitude$)); })) .subscribe( ( [markers, [sw, ne], reference, alt] : [Marker[], [LngLat, LngLat], LngLatAlt, number]) : void => { const markerScene: MarkerScene = this._markerScene; for (const marker of markers) { const exists = markerScene.has(marker.id); const visible = marker.lngLat.lat > sw.lat && marker.lngLat.lat < ne.lat && marker.lngLat.lng > sw.lng && marker.lngLat.lng < ne.lng; if (visible) { const point3d = geodeticToEnu( marker.lngLat.lng, marker.lngLat.lat, reference.alt + alt, reference.lng, reference.lat, reference.alt); markerScene.add(marker, point3d); } else if (!visible && exists) { markerScene.remove(marker.id); } } })); subs.push(this._navigator.stateService.reference$.pipe( skip(1), withLatestFrom(groundAltitude$)) .subscribe( ([reference, alt]: [LngLatAlt, number]): void => { const markerScene: MarkerScene = this._markerScene; for (const marker of markerScene.getAll()) { const point3d = geodeticToEnu( marker.lngLat.lng, marker.lngLat.lat, reference.alt + alt, reference.lng, reference.lat, reference.alt); markerScene.update(marker.id, point3d); } })); subs.push(groundAltitude$.pipe( skip(1), withLatestFrom( this._navigator.stateService.reference$, currentLngLat$)) .subscribe( ( [alt, reference, lngLat] : [number, LngLatAlt, LngLat]) : void => { const markerScene = this._markerScene; const position = geodeticToEnu( lngLat.lng, lngLat.lat, reference.alt + alt, reference.lng, reference.lat, reference.alt); for (const marker of markerScene.getAll()) { const point3d = geodeticToEnu( marker.lngLat.lng, marker.lngLat.lat, reference.alt + alt, reference.lng, reference.lat, reference.alt); const distanceX = point3d[0] - position[0]; const distanceY = point3d[1] - position[1]; const groundDistance = Math .sqrt(distanceX * distanceX + distanceY * distanceY); if (groundDistance > 50) { continue; } markerScene.lerpAltitude(marker.id, alt, Math.min(1, Math.max(0, 1.2 - 1.2 * groundDistance / 50))); } })); subs.push(this._navigator.stateService.currentState$ .pipe( map( (frame: AnimationFrame): GLRenderHash => { const scene = this._markerScene; return { name: this._name, renderer: { frameId: frame.id, needsRender: scene.needsRender, render: scene.render.bind(scene), pass: RenderPass.Opaque, }, }; })) .subscribe(this._container.glRenderer.render$)); const hoveredMarkerId$: Observable<string> = observableCombineLatest( this._container.renderService.renderCamera$, this._container.mouseService.mouseMove$) .pipe( map( ([render, event]: [RenderCamera, MouseEvent]): string => { const element = this._container.container; const [canvasX, canvasY] = this._viewportCoords.canvasPosition(event, element); const viewport = this._viewportCoords .canvasToViewport( canvasX, canvasY, element); const markerId: string = this._markerScene.intersectObjects(viewport, render.perspective); return markerId; }), publishReplay(1), refCount()); const draggingStarted$: Observable<boolean> = this._container.mouseService .filtered$(this._name, this._container.mouseService.mouseDragStart$).pipe( map( (): boolean => { return true; })); const draggingStopped$: Observable<boolean> = this._container.mouseService .filtered$(this._name, this._container.mouseService.mouseDragEnd$).pipe( map( (): boolean => { return false; })); const filteredDragging$: Observable<boolean> = observableMerge( draggingStarted$, draggingStopped$) .pipe( startWith(false)); subs.push(observableMerge( draggingStarted$.pipe( withLatestFrom(hoveredMarkerId$)), observableCombineLatest( draggingStopped$, observableOf<string>(null))).pipe( startWith<[boolean, string]>([false, null]), pairwise()) .subscribe( ([previous, current]: [boolean, string][]): void => { const dragging = current[0]; const type: ComponentEventType = dragging ? "markerdragstart" : "markerdragend"; const id = dragging ? current[1] : previous[1]; const marker = this._markerScene.get(id); const event: ComponentMarkerEvent = { marker, target: this, type, }; this.fire(type, event); })); const mouseDown$: Observable<boolean> = observableMerge( this._container.mouseService.mouseDown$.pipe( map((): boolean => { return true; })), this._container.mouseService.documentMouseUp$.pipe( map((): boolean => { return false; }))).pipe( startWith(false)); subs.push( observableCombineLatest( this._container.mouseService.active$, hoveredMarkerId$.pipe(distinctUntilChanged()), mouseDown$, filteredDragging$) .pipe( map( ( [active, markerId, mouseDown, filteredDragging] : [boolean, string, boolean, boolean]) : boolean => { return (!active && markerId != null && mouseDown) || filteredDragging; }), distinctUntilChanged()) .subscribe( (claim: boolean): void => { if (claim) { this._container.mouseService.claimMouse(this._name, 1); this._container.mouseService.claimWheel(this._name, 1); } else { this._container.mouseService.unclaimMouse(this._name); this._container.mouseService.unclaimWheel(this._name); } })); const offset$: Observable<[Marker, number[], RenderCamera]> = this._container.mouseService .filtered$(this._name, this._container.mouseService.mouseDragStart$).pipe( withLatestFrom( hoveredMarkerId$, this._container.renderService.renderCamera$), map( ( [e, id, r]: [MouseEvent, string, RenderCamera]) : [Marker, number[], RenderCamera] => { const marker: Marker = this._markerScene.get(id); const element = this._container.container; const [groundCanvasX, groundCanvasY]: number[] = this._viewportCoords .projectToCanvas( marker.geometry.position .toArray(), element, r.perspective); const [canvasX, canvasY] = this._viewportCoords .canvasPosition(e, element); const offset = [canvasX - groundCanvasX, canvasY - groundCanvasY]; return [marker, offset, r]; }), publishReplay(1), refCount()); subs.push(this._container.mouseService .filtered$( this._name, this._container.mouseService.mouseDrag$) .pipe( withLatestFrom( offset$, this._navigator.stateService.reference$, clampedConfiguration$)) .subscribe( ([event, [marker, offset, render], reference, configuration]: [MouseEvent, [Marker, number[], RenderCamera], LngLatAlt, MarkerConfiguration]): void => { if (!this._markerScene.has(marker.id)) { return; } const element = this._container.container; const [canvasX, canvasY] = this._viewportCoords .canvasPosition(event, element); const groundX = canvasX - offset[0]; const groundY = canvasY - offset[1]; const [viewportX, viewportY] = this._viewportCoords .canvasToViewport( groundX, groundY, element); const direction = new THREE.Vector3(viewportX, viewportY, 1) .unproject(render.perspective) .sub(render.perspective.position) .normalize(); const distance = Math.min( this._relativeGroundAltitude / direction.z, configuration.visibleBBoxSize / 2 - 0.1); if (distance < 0) { return; } const intersection = direction .clone() .multiplyScalar(distance) .add(render.perspective.position); intersection.z = render.perspective.position.z + this._relativeGroundAltitude; const [lng, lat] = enuToGeodetic( intersection.x, intersection.y, intersection.z, reference.lng, reference.lat, reference.alt); this._markerScene .update( marker.id, intersection.toArray(), { lat, lng }); this._markerSet.update(marker); const type: ComponentEventType = "markerposition"; const markerEvent: ComponentMarkerEvent = { marker, target: this, type, }; this.fire(type, markerEvent); })); } protected _deactivate(): void { this._subscriptions.unsubscribe(); this._markerScene.clear(); } protected _getDefaultConfiguration(): MarkerConfiguration { return { visibleBBoxSize: 100 }; } }