UNPKG

mapillary-js

Version:

A WebGL interactive street imagery library

915 lines (844 loc) 30.5 kB
import { combineLatest as observableCombineLatest, empty as observableEmpty, merge as observableMerge, from as observableFrom, Observable, } from "rxjs"; import { distinctUntilChanged, filter, first, map, mergeMap, publishReplay, refCount, share, skipWhile, startWith, switchMap, tap, } from "rxjs/operators"; import { TagCreator } from "./TagCreator"; import { TagDOMRenderer } from "./TagDOMRenderer"; import { TagScene } from "./TagScene"; import { TagMode } from "./TagMode"; import { TagSet } from "./TagSet"; import { Geometry } from "./geometry/Geometry"; import { PointsGeometry } from "./geometry/PointsGeometry"; import { CreateHandlerBase } from "./handlers/CreateHandlerBase"; import { CreatePointHandler } from "./handlers/CreatePointHandler"; import { CreatePointsHandler } from "./handlers/CreatePointsHandler"; import { CreatePolygonHandler } from "./handlers/CreatePolygonHandler"; import { CreateRectHandler } from "./handlers/CreateRectHandler"; import { CreateRectDragHandler } from "./handlers/CreateRectDragHandler"; import { EditVertexHandler } from "./handlers/EditVertexHandler"; import { Tag } from "./tag/Tag"; import { RenderTag } from "./tag/RenderTag"; import { CreateTag } from "./tag/CreateTag"; import { Component } from "../Component"; import { TagConfiguration } from "../interfaces/TagConfiguration"; import { Transform } from "../../geo/Transform"; import { ViewportCoords } from "../../geo/ViewportCoords"; import { Navigator } from "../../viewer/Navigator"; import { RenderPass } from "../../render/RenderPass"; import { RenderCamera } from "../../render/RenderCamera"; import { GLRenderHash } from "../../render/interfaces/IGLRenderHash"; import { ViewportSize } from "../../render/interfaces/ViewportSize"; import { VirtualNodeHash } from "../../render/interfaces/VirtualNodeHash"; import { AnimationFrame } from "../../state/interfaces/AnimationFrame"; import { Container } from "../../viewer/Container"; import { ISpriteAtlas } from "../../viewer/interfaces/ISpriteAtlas"; import { ComponentEventType } from "../events/ComponentEventType"; import { ComponentTagModeEvent } from "../events/ComponentTagModeEvent"; import { ComponentGeometryEvent } from "../events/ComponentGeometryEvent"; import { ComponentStateEvent } from "../events/ComponentStateEvent"; import { ComponentName } from "../ComponentName"; /** * @class TagComponent * * @classdesc Component for showing and editing tags with different * geometries composed from 2D basic image coordinates (see the * {@link Viewer} class documentation for more information about coordinate * systems). * * The `add` method is used for adding new tags or replacing * tags already in the set. Tags are removed by id. * * If a tag already in the set has the same * id as one of the tags added, the old tag will be removed and * the added tag will take its place. * * The tag component mode can be set to either be non interactive or * to be in creating mode of a certain geometry type. * * The tag properties can be updated at any time and the change will * be visibile immediately. * * Tags are only relevant to a single image because they are based on * 2D basic image coordinates. Tags related to a certain image should * be removed when the viewer is moved to another image. * * To retrive and use the tag component * * @example * ```js * var viewer = new Viewer({ component: { tag: true } }, ...); * * var tagComponent = viewer.getComponent("tag"); * ``` */ export class TagComponent extends Component<TagConfiguration> { /** @inheritdoc */ public static componentName: ComponentName = "tag"; private _tagDomRenderer: TagDOMRenderer; private _tagScene: TagScene; private _tagSet: TagSet; private _tagCreator: TagCreator; private _viewportCoords: ViewportCoords; private _renderTags$: Observable<RenderTag<Tag>[]>; private _tagChanged$: Observable<Tag>; private _renderTagGLChanged$: Observable<RenderTag<Tag>>; private _createGeometryChanged$: Observable<CreateTag<Geometry>>; private _createGLObjectsChanged$: Observable<CreateTag<Geometry>>; private _creatingConfiguration$: Observable<TagConfiguration>; private _createHandlers: { [K in keyof typeof TagMode]: CreateHandlerBase; }; private _editVertexHandler: EditVertexHandler; /** @ignore */ constructor( name: string, container: Container, navigator: Navigator) { super(name, container, navigator); this._tagDomRenderer = new TagDOMRenderer(); this._tagScene = new TagScene(); this._tagSet = new TagSet(); this._tagCreator = new TagCreator(this, navigator); this._viewportCoords = new ViewportCoords(); this._createHandlers = { "CreatePoint": new CreatePointHandler( this, container, navigator, this._viewportCoords, this._tagCreator), "CreatePoints": new CreatePointsHandler( this, container, navigator, this._viewportCoords, this._tagCreator), "CreatePolygon": new CreatePolygonHandler( this, container, navigator, this._viewportCoords, this._tagCreator), "CreateRect": new CreateRectHandler( this, container, navigator, this._viewportCoords, this._tagCreator), "CreateRectDrag": new CreateRectDragHandler( this, container, navigator, this._viewportCoords, this._tagCreator), "Default": undefined, }; this._editVertexHandler = new EditVertexHandler( this, container, navigator, this._viewportCoords, this._tagSet); this._renderTags$ = this._tagSet.changed$.pipe( map( (tagSet: TagSet): RenderTag<Tag>[] => { const tags: RenderTag<Tag>[] = tagSet.getAll(); // ensure that tags are always rendered in the same order // to avoid hover tracking problems on first resize. tags.sort( (t1: RenderTag<Tag>, t2: RenderTag<Tag>): number => { const id1: string = t1.tag.id; const id2: string = t2.tag.id; if (id1 < id2) { return -1; } if (id1 > id2) { return 1; } return 0; }); return tags; }), share()); this._tagChanged$ = this._renderTags$.pipe( switchMap( (tags: RenderTag<Tag>[]): Observable<Tag> => { return observableFrom(tags).pipe( mergeMap( (tag: RenderTag<Tag>): Observable<Tag> => { return observableMerge( tag.tag.changed$, tag.tag.geometryChanged$); })); }), share()); this._renderTagGLChanged$ = this._renderTags$.pipe( switchMap( (tags: RenderTag<Tag>[]): Observable<RenderTag<Tag>> => { return observableFrom(tags).pipe( mergeMap( (tag: RenderTag<Tag>): Observable<RenderTag<Tag>> => { return tag.glObjectsChanged$; })); }), share()); this._createGeometryChanged$ = this._tagCreator.tag$.pipe( switchMap( (tag: CreateTag<Geometry>): Observable<CreateTag<Geometry>> => { return tag != null ? tag.geometryChanged$ : observableEmpty(); }), share()); this._createGLObjectsChanged$ = this._tagCreator.tag$.pipe( switchMap( (tag: CreateTag<Geometry>): Observable<CreateTag<Geometry>> => { return tag != null ? tag.glObjectsChanged$ : observableEmpty(); }), share()); this._creatingConfiguration$ = this._configuration$.pipe( distinctUntilChanged( (c1: TagConfiguration, c2: TagConfiguration): boolean => { return c1.mode === c2.mode; }, (configuration: TagConfiguration): TagConfiguration => { return { createColor: configuration.createColor, mode: configuration.mode, }; }), publishReplay(1), refCount()); this._creatingConfiguration$ .subscribe( (configuration: TagConfiguration): void => { const type: ComponentEventType = "tagmode"; const event: ComponentTagModeEvent = { mode: configuration.mode, target: this, type, }; this.fire(type, event); }); } /** * Add tags to the tag set or replace tags in the tag set. * * @description If a tag already in the set has the same * id as one of the tags added, the old tag will be removed * the added tag will take its place. * * @param {Array<Tag>} tags - Tags to add. * * @example * ```js * tagComponent.add([tag1, tag2]); * ``` */ public add(tags: Tag[]): void { if (this._activated) { this._navigator.stateService.currentTransform$.pipe( first()) .subscribe( (transform: Transform): void => { this._tagSet.add(tags, transform); const renderTags: RenderTag<Tag>[] = tags .map( (tag: Tag): RenderTag<Tag> => { return this._tagSet.get(tag.id); }); this._tagScene.add(renderTags); }); } else { this._tagSet.addDeactivated(tags); } } /** * Calculate the smallest rectangle containing all the points * in the points geometry. * * @description The result may be different depending on if the * current image is an spherical or not. If the * current image is an spherical the rectangle may * wrap the horizontal border of the image. * * @returns {Promise<Array<number>>} Promise to the rectangle * on the format specified for the {@link RectGeometry} in basic * coordinates. */ public calculateRect(geometry: PointsGeometry): Promise<number[]> { return new Promise<number[]>((resolve: (value: number[]) => void, reject: (reason: Error) => void): void => { this._navigator.stateService.currentTransform$.pipe( first(), map( (transform: Transform): number[] => { return geometry.getRect2d(transform); })) .subscribe( (rect: number[]): void => { resolve(rect); }, (error: Error): void => { reject(error); }); }); } /** * Force the creation of a geometry programatically using its * current vertices. * * @description The method only has an effect when the tag * mode is either of the following modes: * * {@link TagMode.CreatePoints} * {@link TagMode.CreatePolygon} * {@link TagMode.CreateRect} * {@link TagMode.CreateRectDrag} * * In the case of points or polygon creation, only the created * vertices are used, i.e. the mouse position is disregarded. * * In the case of rectangle creation the position of the mouse * at the time of the method call is used as one of the vertices * defining the rectangle. * * @fires geometrycreate * * @example * ```js * tagComponent.on("geometrycreate", function(geometry) { * console.log(geometry); * }); * * tagComponent.create(); * ``` */ public create(): void { this._tagCreator.replayedTag$.pipe( first(), filter( (tag: CreateTag<Geometry>): boolean => { return !!tag; })) .subscribe( (tag: CreateTag<Geometry>): void => { tag.create(); }); } /** * Change the current tag mode. * * @description Change the tag mode to one of the create modes for creating new geometries. * * @param {TagMode} mode - New tag mode. * * @fires tagmode * * @example * ```js * tagComponent.changeMode(TagMode.CreateRect); * ``` */ public changeMode(mode: TagMode): void { this.configure({ mode: mode }); } public fire( type: "geometrycreate", event: ComponentGeometryEvent) : void; public fire( type: "tagmode", event: ComponentTagModeEvent) : void; /** @ignore */ public fire( type: | "tagcreateend" | "tagcreatestart" | "tags", event: ComponentStateEvent) : void; public fire<T>( type: ComponentEventType, event: T) : void { super.fire(type, event); } /** * Returns the tag in the tag set with the specified id, or * undefined if the id matches no tag. * * @param {string} tagId - Id of the tag. * * @example * ```js * var tag = tagComponent.get("tagId"); * ``` */ public get(tagId: string): Tag { if (this._activated) { const renderTag: RenderTag<Tag> = this._tagSet.get(tagId); return renderTag !== undefined ? renderTag.tag : undefined; } else { return this._tagSet.getDeactivated(tagId); } } /** * Returns an array of all tags. * * @example * ```js * var tags = tagComponent.getAll(); * ``` */ public getAll(): Tag[] { if (this.activated) { return this._tagSet .getAll() .map( (renderTag: RenderTag<Tag>): Tag => { return renderTag.tag; }); } else { return this._tagSet.getAllDeactivated(); } } /** * Returns an array of tag ids for tags that contain the specified point. * * @description The pixel point must lie inside the polygon or rectangle * of an added tag for the tag id to be returned. Tag ids for * tags that do not have a fill will also be returned if the point is inside * the geometry of the tag. Tags with point geometries can not be retrieved. * * No tag ids will be returned for polygons rendered in cropped spherical or * rectangles rendered in spherical. * * 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 tag component. * * If no tag at exist the pixel point, an empty array will be returned. * * @param {Array<number>} pixelPoint - Pixel coordinates on the viewer element. * @returns {Promise<Array<string>>} Promise to the ids of the tags that * contain the specified pixel point. * * @example * ```js * tagComponent.getTagIdsAt([100, 100]) * .then((tagIds) => { console.log(tagIds); }); * ``` */ public getTagIdsAt(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: number[] = this._viewportCoords .canvasToViewport( pixelPoint[0], pixelPoint[1], this._container.container); const ids: string[] = this._tagScene.intersectObjects(viewport, render.perspective); return ids; })) .subscribe( (ids: string[]): void => { resolve(ids); }, (error: Error): void => { reject(error); }); }); } /** * Check if a tag exist in the tag set. * * @param {string} tagId - Id of the tag. * * @example * ```js * var tagExists = tagComponent.has("tagId"); * ``` */ public has(tagId: string): boolean { return this._activated ? this._tagSet.has(tagId) : this._tagSet.hasDeactivated(tagId); } public off( type: "geometrycreate", handler: (event: ComponentGeometryEvent) => void) : void; public off( type: "tagmode", handler: (event: ComponentTagModeEvent) => void) : void; public off( type: | "tagcreateend" | "tagcreatestart" | "tags", handler: (event: ComponentStateEvent) => void) : void; public off<T>( type: ComponentEventType, handler: (event: T) => void) : void { super.off(type, handler); } /** * Event fired when a geometry has been created. * * @event geometrycreated * @example * ```js * // Initialize the viewer * var viewer = new Viewer({ // viewer options }); * var component = viewer.getComponent('<component-name>'); * // Set an event listener * component.on('geometrycreated', function() { * console.log("A geometrycreated event has occurred."); * }); * ``` */ public on( type: "geometrycreate", handler: (event: ComponentGeometryEvent) => void) : void; /** * Event fired when an interaction to create a geometry ends. * * @description A create interaction can by a geometry being created * or by the creation being aborted. * * @event tagcreateend * @example * ```js * // Initialize the viewer * var viewer = new Viewer({ // viewer options }); * var component = viewer.getComponent('<component-name>'); * // Set an event listener * component.on('tagcreateend', function() { * console.log("A tagcreateend event has occurred."); * }); * ``` */ public on( type: "tagcreateend", handler: (event: ComponentStateEvent) => void) : void; /** * Event fired when an interaction to create a geometry starts. * * @description A create interaction starts when the first vertex * is created in the geometry. * * @event tagcreatestart * @example * ```js * // Initialize the viewer * var viewer = new Viewer({ // viewer options }); * var component = viewer.getComponent('<component-name>'); * // Set an event listener * component.on('tagcreatestart', function() { * console.log("A tagcreatestart event has occurred."); * }); * ``` */ public on( type: "tagcreatestart", handler: (event: ComponentStateEvent) => void) : void; /** * Event fired when the create mode is changed. * * @event tagmode * @example * ```js * // Initialize the viewer * var viewer = new Viewer({ // viewer options }); * var component = viewer.getComponent('<component-name>'); * // Set an event listener * component.on('tagmode', function() { * console.log("A tagmode event has occurred."); * }); * ``` */ public on( type: "tagmode", handler: (event: ComponentTagModeEvent) => void) : void; /** * Event fired when the tags collection has changed. * * @event tags * @example * ```js * // Initialize the viewer * var viewer = new Viewer({ // viewer options }); * var component = viewer.getComponent('<component-name>'); * // Set an event listener * component.on('tags', function() { * console.log("A tags event has occurred."); * }); * ``` */ public on( type: "tags", handler: (event: ComponentStateEvent) => void) : void; public on<T>( type: ComponentEventType, handler: (event: T) => void) : void { super.on(type, handler); } /** * Remove tags with the specified ids from the tag set. * * @param {Array<string>} tagIds - Ids for tags to remove. * * @example * ```js * tagComponent.remove(["id-1", "id-2"]); * ``` */ public remove(tagIds: string[]): void { if (this._activated) { this._tagSet.remove(tagIds); this._tagScene.remove(tagIds); } else { this._tagSet.removeDeactivated(tagIds); } } /** * Remove all tags from the tag set. * * @example * ```js * tagComponent.removeAll(); * ``` */ public removeAll(): void { if (this._activated) { this._tagSet.removeAll(); this._tagScene.removeAll(); } else { this._tagSet.removeAllDeactivated(); } } protected _activate(): void { this._editVertexHandler.enable(); const handlerGeometryCreated$ = observableFrom(<(keyof typeof TagMode)[]>Object.keys(this._createHandlers)).pipe( map( (key: keyof typeof TagMode): CreateHandlerBase => { return this._createHandlers[key]; }), filter( (handler: CreateHandlerBase): boolean => { return !!handler; }), mergeMap( (handler: CreateHandlerBase): Observable<Geometry> => { return handler.geometryCreated$; }), share()); const subs = this._subscriptions; subs.push(handlerGeometryCreated$ .subscribe( (geometry: Geometry): void => { const type: ComponentEventType = "geometrycreate"; const event: ComponentGeometryEvent = { geometry, target: this, type, }; this.fire(type, event); })); subs.push(this._tagCreator.tag$.pipe( skipWhile( (tag: CreateTag<Geometry>): boolean => { return tag == null; }), distinctUntilChanged()) .subscribe( (tag: CreateTag<Geometry>): void => { const type: ComponentEventType = tag != null ? "tagcreatestart" : "tagcreateend"; const event: ComponentStateEvent = { target: this, type, }; this.fire(type, event); })); subs.push(handlerGeometryCreated$ .subscribe( (): void => { this.changeMode(TagMode.Default); })); subs.push(this._creatingConfiguration$ .subscribe( (configuration: TagConfiguration): void => { this._disableCreateHandlers(); const mode: keyof typeof TagMode = <keyof typeof TagMode>TagMode[configuration.mode]; const handler: CreateHandlerBase = this._createHandlers[mode]; if (!!handler) { handler.enable(); } })); subs.push(this._renderTags$ .subscribe( (): void => { const type: ComponentEventType = "tags"; const event: ComponentStateEvent = { target: this, type, }; this.fire(type, event); })); subs.push(this._tagCreator.tag$.pipe( switchMap( (tag: CreateTag<Geometry>): Observable<void> => { return tag != null ? tag.aborted$.pipe( map((): void => { return null; })) : observableEmpty(); })) .subscribe((): void => { this.changeMode(TagMode.Default); })); subs.push(this._tagCreator.tag$ .subscribe( (tag: CreateTag<Geometry>): void => { if (this._tagScene.hasCreateTag()) { this._tagScene.removeCreateTag(); } if (tag != null) { this._tagScene.addCreateTag(tag); } })); subs.push(this._createGLObjectsChanged$ .subscribe( (tag: CreateTag<Geometry>): void => { this._tagScene.updateCreateTagObjects(tag); })); subs.push(this._renderTagGLChanged$ .subscribe( (tag: RenderTag<Tag>): void => { this._tagScene.updateObjects(tag); })); subs.push(this._tagChanged$ .subscribe( (): void => { this._tagScene.update(); })); subs.push(observableCombineLatest( this._renderTags$.pipe( startWith([]), tap( (): void => { this._container.domRenderer.render$.next({ name: this._name, vNode: this._tagDomRenderer.clear(), }); })), this._container.renderService.renderCamera$, this._container.spriteService.spriteAtlas$, this._container.renderService.size$, this._tagChanged$.pipe(startWith(null)), observableMerge( this._tagCreator.tag$, this._createGeometryChanged$).pipe(startWith(null))).pipe( map( ([renderTags, rc, atlas, size, , ct]: [RenderTag<Tag>[], RenderCamera, ISpriteAtlas, ViewportSize, Tag, CreateTag<Geometry>]): VirtualNodeHash => { return { name: this._name, vNode: this._tagDomRenderer.render(renderTags, ct, atlas, rc.perspective, size), }; })) .subscribe(this._container.domRenderer.render$)); subs.push(this._navigator.stateService.currentState$.pipe( map( (frame: AnimationFrame): GLRenderHash => { const tagScene: TagScene = this._tagScene; return { name: this._name, renderer: { frameId: frame.id, needsRender: tagScene.needsRender, render: tagScene.render.bind(tagScene), pass: RenderPass.Opaque, }, }; })) .subscribe(this._container.glRenderer.render$)); this._navigator.stateService.currentTransform$.pipe( first()) .subscribe( (transform: Transform): void => { this._tagSet.activate(transform); this._tagScene.add(this._tagSet.getAll()); }); } protected _deactivate(): void { this._editVertexHandler.disable(); this._disableCreateHandlers(); this._tagScene.clear(); this._tagSet.deactivate(); this._tagCreator.delete$.next(null); this._subscriptions.unsubscribe(); this._container.container.classList.remove("component-tag-create"); } protected _getDefaultConfiguration(): TagConfiguration { return { createColor: 0xFFFFFF, indicatePointsCompleter: true, mode: TagMode.Default, }; } private _disableCreateHandlers(): void { const createHandlers: { [K in keyof typeof TagMode]: CreateHandlerBase } = this._createHandlers; for (const key in createHandlers) { if (!createHandlers.hasOwnProperty(key)) { continue; } const handler = createHandlers[<keyof typeof TagMode>key]; if (!!handler) { handler.disable(); } } } }