mapillary-js
Version:
WebGL JavaScript library for displaying street level imagery from mapillary.com
794 lines (712 loc) • 27.2 kB
text/typescript
import {
combineLatest as observableCombineLatest,
empty as observableEmpty,
merge as observableMerge,
from as observableFrom,
Observable,
Subscription,
} from "rxjs";
import {
startWith,
first,
tap,
map,
share,
skipWhile,
filter,
mergeMap,
refCount,
publishReplay,
switchMap,
distinctUntilChanged,
} from "rxjs/operators";
import * as when from "when";
import {
ComponentService,
Component,
CreateHandlerBase,
CreatePointHandler,
CreatePolygonHandler,
CreateRectHandler,
CreateRectDragHandler,
CreateTag,
EditVertexHandler,
Geometry,
ITagConfiguration,
RenderTag,
Tag,
TagCreator,
TagDOMRenderer,
TagMode,
TagScene,
TagSet,
CreatePointsHandler,
} from "../../Component";
import {
Transform,
ViewportCoords,
} from "../../Geo";
import {
GLRenderStage,
IGLRenderHash,
ISize,
IVNodeHash,
RenderCamera,
} from "../../Render";
import {IFrame} from "../../State";
import {
Container,
ISpriteAtlas,
Navigator,
} from "../../Viewer";
import PointsGeometry from "./geometry/PointsGeometry";
/**
* @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 node.
*
* To retrive and use the tag component
*
* @example
* ```
* var viewer = new Mapillary.Viewer(
* "<element-id>",
* "<client-id>",
* "<my key>",
* { component: { tag: true } });
*
* var tagComponent = viewer.getComponent("tag");
* ```
*/
export class TagComponent extends Component<ITagConfiguration> {
/** @inheritdoc */
public static componentName: string = "tag";
/**
* 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 TagComponent#creategeometryend
* @type {TagComponent} Tag component.
* @example
* ```
* tagComponent.on("creategeometryend", function(component) {
* console.log(component);
* });
* ```
*/
public static creategeometryend: string = "creategeometryend";
/**
* 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 TagComponent#creategeometrystart
* @type {TagComponent} Tag component.
* @example
* ```
* tagComponent.on("creategeometrystart", function(component) {
* console.log(component);
* });
* ```
*/
public static creategeometrystart: string = "creategeometrystart";
/**
* Event fired when the create mode is changed.
*
* @event TagComponent#modechanged
* @type {TagMode} Tag mode
* @example
* ```
* tagComponent.on("modechanged", function(mode) {
* console.log(mode);
* });
* ```
*/
public static modechanged: string = "modechanged";
/**
* Event fired when a geometry has been created.
*
* @event TagComponent#geometrycreated
* @type {Geometry} Created geometry.
* @example
* ```
* tagComponent.on("geometrycreated", function(geometry) {
* console.log(geometry);
* });
* ```
*/
public static geometrycreated: string = "geometrycreated";
/**
* Event fired when the tags collection has changed.
*
* @event TagComponent#tagschanged
* @type {TagComponent} Tag component.
* @example
* ```
* tagComponent.on("tagschanged", function(component) {
* console.log(component.getAll());
* });
* ```
*/
public static tagschanged: string = "tagschanged";
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<ITagConfiguration>;
private _updateGLObjectsSubscription: Subscription;
private _updateTagSceneSubscription: Subscription;
private _stopCreateSubscription: Subscription;
private _setGLCreateTagSubscription: Subscription;
private _createGLObjectsChangedSubscription: Subscription;
private _handlerStopCreateSubscription: Subscription;
private _handlerEnablerSubscription: Subscription;
private _domSubscription: Subscription;
private _glSubscription: Subscription;
private _fireCreateGeometryEventSubscription: Subscription;
private _fireGeometryCreatedSubscription: Subscription;
private _fireTagsChangedSubscription: Subscription;
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: ITagConfiguration, c2: ITagConfiguration): boolean => {
return c1.mode === c2.mode;
},
(configuration: ITagConfiguration): ITagConfiguration => {
return {
createColor: configuration.createColor,
mode: configuration.mode,
};
}),
publishReplay(1),
refCount());
this._creatingConfiguration$
.subscribe(
(configuration: ITagConfiguration): void => {
this.fire(TagComponent.modechanged, configuration.mode);
});
}
/**
* 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 ```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 node is an equirectangular panorama or not. If the
* current node is an equirectangular panorama 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): when.Promise<number[]> {
return when.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:
*
* TagMode.CreatePoints
* TagMode.CreatePolygon
* TagMode.CreateRect
* 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 TagComponent.geometrycreated
*
* @example
* ```
* tagComponent.on("geometrycreated", 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 TagComponent#modechanged
*
* @example ```tagComponent.changeMode(Mapillary.TagComponent.TagMode.CreateRect);```
*/
public changeMode(mode: TagMode): void {
this.configure({ mode: mode });
}
/**
* 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 ```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 ```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 panoramas or
* rectangles rendered in panoramas.
*
* 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
* ```
* tagComponent.getTagIdsAt([100, 100])
* .then((tagIds) => { console.log(tagIds); });
* ```
*/
public getTagIdsAt(pixelPoint: number[]): when.Promise<string[]> {
return when.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.element);
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 ```var tagExists = tagComponent.has("tagId");```
*/
public has(tagId: string): boolean {
return this._activated ? this._tagSet.has(tagId) : this._tagSet.hasDeactivated(tagId);
}
/**
* Remove tags with the specified ids from the tag set.
*
* @param {Array<string>} tagIds - Ids for tags to remove.
*
* @example ```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 ```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$: Observable<Geometry> =
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());
this._fireGeometryCreatedSubscription = handlerGeometryCreated$
.subscribe(
(geometry: Geometry): void => {
this.fire(TagComponent.geometrycreated, geometry);
});
this._fireCreateGeometryEventSubscription = this._tagCreator.tag$.pipe(
skipWhile(
(tag: CreateTag<Geometry>): boolean => {
return tag == null;
}),
distinctUntilChanged())
.subscribe(
(tag: CreateTag<Geometry>): void => {
const eventType: string = tag != null ?
TagComponent.creategeometrystart :
TagComponent.creategeometryend;
this.fire(eventType, this);
});
this._handlerStopCreateSubscription = handlerGeometryCreated$
.subscribe(
(): void => {
this.changeMode(TagMode.Default);
});
this._handlerEnablerSubscription = this._creatingConfiguration$
.subscribe(
(configuration: ITagConfiguration): void => {
this._disableCreateHandlers();
const mode: keyof typeof TagMode = <keyof typeof TagMode>TagMode[configuration.mode];
const handler: CreateHandlerBase = this._createHandlers[mode];
if (!!handler) {
handler.enable();
}
});
this._fireTagsChangedSubscription = this._renderTags$
.subscribe(
(): void => {
this.fire(TagComponent.tagschanged, this);
});
this._stopCreateSubscription = 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); });
this._setGLCreateTagSubscription = this._tagCreator.tag$
.subscribe(
(tag: CreateTag<Geometry>): void => {
if (this._tagScene.hasCreateTag()) {
this._tagScene.removeCreateTag();
}
if (tag != null) {
this._tagScene.addCreateTag(tag);
}
});
this._createGLObjectsChangedSubscription = this._createGLObjectsChanged$
.subscribe(
(tag: CreateTag<Geometry>): void => {
this._tagScene.updateCreateTagObjects(tag);
});
this._updateGLObjectsSubscription = this._renderTagGLChanged$
.subscribe(
(tag: RenderTag<Tag>): void => {
this._tagScene.updateObjects(tag);
});
this._updateTagSceneSubscription = this._tagChanged$
.subscribe(
(): void => {
this._tagScene.update();
});
this._domSubscription = 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, ISize, Tag, CreateTag<Geometry>]):
IVNodeHash => {
return {
name: this._name,
vnode: this._tagDomRenderer.render(renderTags, ct, atlas, rc.perspective, size),
};
}))
.subscribe(this._container.domRenderer.render$);
this._glSubscription = this._navigator.stateService.currentState$.pipe(
map(
(frame: IFrame): IGLRenderHash => {
const tagScene: TagScene = this._tagScene;
return {
name: this._name,
render: {
frameId: frame.id,
needsRender: tagScene.needsRender,
render: tagScene.render.bind(tagScene),
stage: GLRenderStage.Foreground,
},
};
}))
.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._updateGLObjectsSubscription.unsubscribe();
this._updateTagSceneSubscription.unsubscribe();
this._stopCreateSubscription.unsubscribe();
this._setGLCreateTagSubscription.unsubscribe();
this._createGLObjectsChangedSubscription.unsubscribe();
this._domSubscription.unsubscribe();
this._glSubscription.unsubscribe();
this._fireCreateGeometryEventSubscription.unsubscribe();
this._fireGeometryCreatedSubscription.unsubscribe();
this._fireTagsChangedSubscription.unsubscribe();
this._handlerStopCreateSubscription.unsubscribe();
this._handlerEnablerSubscription.unsubscribe();
this._container.element.classList.remove("component-tag-create");
}
protected _getDefaultConfiguration(): ITagConfiguration {
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: CreateHandlerBase = createHandlers[<keyof typeof TagMode>key];
if (!!handler) {
handler.disable();
}
}
}
}
ComponentService.register(TagComponent);
export default TagComponent;