UNPKG

mapillary-js

Version:

WebGL JavaScript library for displaying street level imagery from mapillary.com

565 lines (466 loc) 21.6 kB
import {merge as observableMerge, Observable, Subject, Subscription} from "rxjs"; import {filter} from "rxjs/operators"; import * as vd from "virtual-dom"; import { SequenceMode, ISequenceConfiguration, SequenceComponent, } from "../../Component"; import {EdgeDirection} from "../../Edge"; import {AbortMapillaryError} from "../../Error"; import { IEdgeStatus, Node, } from "../../Graph"; import {ISize} from "../../Render"; import { Container, Navigator, } from "../../Viewer"; export class SequenceDOMRenderer { private _container: Container; private _minThresholdWidth: number; private _maxThresholdWidth: number; private _minThresholdHeight: number; private _maxThresholdHeight: number; private _stepperDefaultWidth: number; private _controlsDefaultWidth: number; private _defaultHeight: number; private _expandControls: boolean; private _mode: SequenceMode; private _speed: number; private _changingSpeed: boolean; private _index: number; private _changingPosition: boolean; private _mouseEnterDirection$: Subject<EdgeDirection>; private _mouseLeaveDirection$: Subject<EdgeDirection>; private _notifyChanged$: Subject<SequenceDOMRenderer>; private _notifyChangingPositionChanged$: Subject<boolean>; private _notifySpeedChanged$: Subject<number>; private _notifyIndexChanged$: Subject<number>; private _changingSubscription: Subscription; constructor(container: Container) { this._container = container; this._minThresholdWidth = 320; this._maxThresholdWidth = 1480; this._minThresholdHeight = 240; this._maxThresholdHeight = 820; this._stepperDefaultWidth = 108; this._controlsDefaultWidth = 88; this._defaultHeight = 30; this._expandControls = false; this._mode = SequenceMode.Default; this._speed = 0.5; this._changingSpeed = false; this._index = null; this._changingPosition = false; this._mouseEnterDirection$ = new Subject<EdgeDirection>(); this._mouseLeaveDirection$ = new Subject<EdgeDirection>(); this._notifyChanged$ = new Subject<SequenceDOMRenderer>(); this._notifyChangingPositionChanged$ = new Subject<boolean>(); this._notifySpeedChanged$ = new Subject<number>(); this._notifyIndexChanged$ = new Subject<number>(); } public get changed$(): Observable<SequenceDOMRenderer> { return this._notifyChanged$; } public get changingPositionChanged$(): Observable<boolean> { return this._notifyChangingPositionChanged$; } public get speed$(): Observable<number> { return this._notifySpeedChanged$; } public get index$(): Observable<number> { return this._notifyIndexChanged$; } public get mouseEnterDirection$(): Observable<EdgeDirection> { return this._mouseEnterDirection$; } public get mouseLeaveDirection$(): Observable<EdgeDirection> { return this._mouseLeaveDirection$; } public activate(): void { if (!!this._changingSubscription) { return; } this._changingSubscription = observableMerge( this._container.mouseService.documentMouseUp$, this._container.touchService.touchEnd$.pipe( filter( (touchEvent: TouchEvent): boolean => { return touchEvent.touches.length === 0; }))) .subscribe( (event: Event): void => { if (this._changingSpeed) { this._changingSpeed = false; } if (this._changingPosition) { this._setChangingPosition(false); } }); } public deactivate(): void { if (!this._changingSubscription) { return; } this._changingSpeed = false; this._changingPosition = false; this._expandControls = false; this._mode = SequenceMode.Default; this._changingSubscription.unsubscribe(); this._changingSubscription = null; } public render( edgeStatus: IEdgeStatus, configuration: ISequenceConfiguration, containerWidth: number, speed: number, index: number, max: number, component: SequenceComponent, navigator: Navigator): vd.VNode { if (configuration.visible === false) { return vd.h("div.SequenceContainer", {}, []); } const stepper: vd.VNode = this._createStepper(edgeStatus, configuration, containerWidth, component, navigator); const controls: vd.VNode = this._createSequenceControls(containerWidth); const playback: vd.VNode = this._createPlaybackControls(containerWidth, speed, component, configuration); const timeline: vd.VNode = this._createTimelineControls(containerWidth, index, max); return vd.h("div.SequenceContainer", [stepper, controls, playback, timeline]); } public getContainerWidth(size: ISize, configuration: ISequenceConfiguration): number { let minWidth: number = configuration.minWidth; let maxWidth: number = configuration.maxWidth; if (maxWidth < minWidth) { maxWidth = minWidth; } let relativeWidth: number = (size.width - this._minThresholdWidth) / (this._maxThresholdWidth - this._minThresholdWidth); let relativeHeight: number = (size.height - this._minThresholdHeight) / (this._maxThresholdHeight - this._minThresholdHeight); let coeff: number = Math.max(0, Math.min(1, Math.min(relativeWidth, relativeHeight))); return minWidth + coeff * (maxWidth - minWidth); } private _createPositionInput(index: number, max: number): vd.VNode { this._index = index; const onPosition: (e: Event) => void = (e: Event): void => { this._index = Number((<HTMLInputElement>e.target).value); this._notifyIndexChanged$.next(this._index); }; const boundingRect: ClientRect = this._container.domContainer.getBoundingClientRect(); const width: number = Math.max(276, Math.min(410, 5 + 0.8 * boundingRect.width)) - 65; const onStart: (e: Event) => void = (e: Event): void => { e.stopPropagation(); this._setChangingPosition(true); }; const onMove: (e: Event) => void = (e: Event): void => { if (this._changingPosition === true) { e.stopPropagation(); } }; const onKeyDown: (e: KeyboardEvent) => void = (e: KeyboardEvent): void => { if (e.key === "ArrowDown" || e.key === "ArrowLeft" || e.key === "ArrowRight" || e.key === "ArrowUp") { e.preventDefault(); } }; const positionInputProperties: vd.createProperties = { max: max != null ? max : 1, min: 0, onchange: onPosition, oninput: onPosition, onkeydown: onKeyDown, onmousedown: onStart, onmousemove: onMove, ontouchmove: onMove, ontouchstart: onStart, style: { width: `${width}px`, }, type: "range", value: index != null ? index : 0, }; const disabled: boolean = index == null || max == null || max <= 1; if (disabled) { positionInputProperties.disabled = "true"; } const positionInput: vd.VNode = vd.h("input.SequencePosition", positionInputProperties, []); const positionContainerClass: string = disabled ? ".SequencePositionContainerDisabled" : ".SequencePositionContainer"; return vd.h("div" + positionContainerClass, [positionInput]); } private _createSpeedInput(speed: number): vd.VNode { this._speed = speed; const onSpeed: (e: Event) => void = (e: Event): void => { this._speed = Number((<HTMLInputElement>e.target).value) / 1000; this._notifySpeedChanged$.next(this._speed); }; const boundingRect: ClientRect = this._container.domContainer.getBoundingClientRect(); const width: number = Math.max(276, Math.min(410, 5 + 0.8 * boundingRect.width)) - 160; const onStart: (e: Event) => void = (e: Event): void => { this._changingSpeed = true; e.stopPropagation(); }; const onMove: (e: Event) => void = (e: Event): void => { if (this._changingSpeed === true) { e.stopPropagation(); } }; const onKeyDown: (e: KeyboardEvent) => void = (e: KeyboardEvent): void => { if (e.key === "ArrowDown" || e.key === "ArrowLeft" || e.key === "ArrowRight" || e.key === "ArrowUp") { e.preventDefault(); } }; const speedInput: vd.VNode = vd.h( "input.SequenceSpeed", { max: 1000, min: 0, onchange: onSpeed, oninput: onSpeed, onkeydown: onKeyDown, onmousedown: onStart, onmousemove: onMove, ontouchmove: onMove, ontouchstart: onStart, style: { width: `${width}px`, }, type: "range", value: 1000 * speed, }, []); return vd.h("div.SequenceSpeedContainer", [speedInput]); } private _createPlaybackControls( containerWidth: number, speed: number, component: SequenceComponent, configuration: ISequenceConfiguration): vd.VNode { if (this._mode !== SequenceMode.Playback) { return vd.h("div.SequencePlayback", []); } const switchIcon: vd.VNode = vd.h("div.SequenceSwitchIcon.SequenceIconVisible", []); const direction: EdgeDirection = configuration.direction === EdgeDirection.Next ? EdgeDirection.Prev : EdgeDirection.Next; const playing: boolean = configuration.playing; const switchButtonProperties: vd.createProperties = { onclick: (): void => { if (!playing) { component.setDirection(direction); } }, }; const switchButtonClassName: string = configuration.playing ? ".SequenceSwitchButtonDisabled" : ".SequenceSwitchButton"; const switchButton: vd.VNode = vd.h("div" + switchButtonClassName, switchButtonProperties, [switchIcon]); const slowIcon: vd.VNode = vd.h("div.SequenceSlowIcon.SequenceIconVisible", []); const slowContainer: vd.VNode = vd.h("div.SequenceSlowContainer", [slowIcon]); const fastIcon: vd.VNode = vd.h("div.SequenceFastIcon.SequenceIconVisible", []); const fastContainer: vd.VNode = vd.h("div.SequenceFastContainer", [fastIcon]); const closeIcon: vd.VNode = vd.h("div.SequenceCloseIcon.SequenceIconVisible", []); const closeButtonProperties: vd.createProperties = { onclick: (): void => { this._mode = SequenceMode.Default; this._notifyChanged$.next(this); }, }; const closeButton: vd.VNode = vd.h("div.SequenceCloseButton", closeButtonProperties, [closeIcon]); const speedInput: vd.VNode = this._createSpeedInput(speed); const playbackChildren: vd.VNode[] = [switchButton, slowContainer, speedInput, fastContainer, closeButton]; const top: number = Math.round(containerWidth / this._stepperDefaultWidth * this._defaultHeight + 10); const playbackProperties: vd.createProperties = { style: { top: `${top}px` } }; return vd.h("div.SequencePlayback", playbackProperties, playbackChildren); } private _createPlayingButton( nextKey: string, prevKey: string, configuration: ISequenceConfiguration, component: SequenceComponent): vd.VNode { let canPlay: boolean = configuration.direction === EdgeDirection.Next && nextKey != null || configuration.direction === EdgeDirection.Prev && prevKey != null; let onclick: (e: Event) => void = configuration.playing ? (e: Event): void => { component.stop(); } : canPlay ? (e: Event): void => { component.play(); } : null; let buttonProperties: vd.createProperties = { onclick: onclick }; let iconClass: string = configuration.playing ? "Stop" : canPlay ? "Play" : "PlayDisabled"; let iconProperties: vd.createProperties = { className: iconClass }; if (configuration.direction === EdgeDirection.Prev) { iconProperties.style = { transform: "rotate(180deg) translate(50%, 50%)", }; } let icon: vd.VNode = vd.h("div.SequenceComponentIcon", iconProperties, []); let buttonClass: string = canPlay ? "SequencePlay" : "SequencePlayDisabled"; return vd.h("div." + buttonClass, buttonProperties, [icon]); } private _createSequenceControls(containerWidth: number): vd.VNode { const borderRadius: number = Math.round(8 / this._stepperDefaultWidth * containerWidth); const expanderProperties: vd.createProperties = { onclick: (): void => { this._expandControls = !this._expandControls; this._mode = SequenceMode.Default; this._notifyChanged$.next(this); }, style: { "border-bottom-right-radius": `${borderRadius}px`, "border-top-right-radius": `${borderRadius}px`, }, }; const expanderBar: vd.VNode = vd.h("div.SequenceExpanderBar", []); const expander: vd.VNode = vd.h("div.SequenceExpanderButton", expanderProperties, [expanderBar]); const fastIconClassName: string = this._mode === SequenceMode.Playback ? ".SequenceFastIconGrey.SequenceIconVisible" : ".SequenceFastIcon"; const fastIcon: vd.VNode = vd.h("div" + fastIconClassName, []); const playbackProperties: vd.createProperties = { onclick: (): void => { this._mode = this._mode === SequenceMode.Playback ? SequenceMode.Default : SequenceMode.Playback; this._notifyChanged$.next(this); }, }; const playback: vd.VNode = vd.h("div.SequencePlaybackButton", playbackProperties, [fastIcon]); const timelineIconClassName: string = this._mode === SequenceMode.Timeline ? ".SequenceTimelineIconGrey.SequenceIconVisible" : ".SequenceTimelineIcon"; const timelineIcon: vd.VNode = vd.h("div" + timelineIconClassName, []); const timelineProperties: vd.createProperties = { onclick: (): void => { this._mode = this._mode === SequenceMode.Timeline ? SequenceMode.Default : SequenceMode.Timeline; this._notifyChanged$.next(this); }, }; const timeline: vd.VNode = vd.h("div.SequenceTimelineButton", timelineProperties, [timelineIcon]); const properties: vd.createProperties = { style: { height: (this._defaultHeight / this._stepperDefaultWidth * containerWidth) + "px", transform: `translate(${containerWidth / 2 + 2}px, 0)`, width: (this._controlsDefaultWidth / this._stepperDefaultWidth * containerWidth) + "px", }, }; const className: string = ".SequenceControls" + (this._expandControls ? ".SequenceControlsExpanded" : ""); return vd.h("div" + className, properties, [playback, timeline, expander]); } private _createSequenceArrows( nextKey: string, prevKey: string, containerWidth: number, configuration: ISequenceConfiguration, navigator: Navigator): vd.VNode[] { let nextProperties: vd.createProperties = { onclick: nextKey != null ? (e: Event): void => { navigator.moveDir$(EdgeDirection.Next) .subscribe( undefined, (error: Error): void => { if (!(error instanceof AbortMapillaryError)) { console.error(error); } }); } : null, onmouseenter: (e: MouseEvent): void => { this._mouseEnterDirection$.next(EdgeDirection.Next); }, onmouseleave: (e: MouseEvent): void => { this._mouseLeaveDirection$.next(EdgeDirection.Next); }, }; const borderRadius: number = Math.round(8 / this._stepperDefaultWidth * containerWidth); let prevProperties: vd.createProperties = { onclick: prevKey != null ? (e: Event): void => { navigator.moveDir$(EdgeDirection.Prev) .subscribe( undefined, (error: Error): void => { if (!(error instanceof AbortMapillaryError)) { console.error(error); } }); } : null, onmouseenter: (e: MouseEvent): void => { this._mouseEnterDirection$.next(EdgeDirection.Prev); }, onmouseleave: (e: MouseEvent): void => { this._mouseLeaveDirection$.next(EdgeDirection.Prev); }, style: { "border-bottom-left-radius": `${borderRadius}px`, "border-top-left-radius": `${borderRadius}px`, }, }; let nextClass: string = this._getStepClassName(EdgeDirection.Next, nextKey, configuration.highlightKey); let prevClass: string = this._getStepClassName(EdgeDirection.Prev, prevKey, configuration.highlightKey); let nextIcon: vd.VNode = vd.h("div.SequenceComponentIcon", []); let prevIcon: vd.VNode = vd.h("div.SequenceComponentIcon", []); return [ vd.h("div." + prevClass, prevProperties, [prevIcon]), vd.h("div." + nextClass, nextProperties, [nextIcon]), ]; } private _createStepper( edgeStatus: IEdgeStatus, configuration: ISequenceConfiguration, containerWidth: number, component: SequenceComponent, navigator: Navigator, ): vd.VNode { let nextKey: string = null; let prevKey: string = null; for (let edge of edgeStatus.edges) { if (edge.data.direction === EdgeDirection.Next) { nextKey = edge.to; } if (edge.data.direction === EdgeDirection.Prev) { prevKey = edge.to; } } const playingButton: vd.VNode = this._createPlayingButton(nextKey, prevKey, configuration, component); const buttons: vd.VNode[] = this._createSequenceArrows(nextKey, prevKey, containerWidth, configuration, navigator); buttons.splice(1, 0, playingButton); const containerProperties: vd.createProperties = { oncontextmenu: (event: MouseEvent): void => { event.preventDefault(); }, style: { height: (this._defaultHeight / this._stepperDefaultWidth * containerWidth) + "px", width: containerWidth + "px", }, }; return vd.h("div.SequenceStepper", containerProperties, buttons); } private _createTimelineControls(containerWidth: number, index: number, max: number): vd.VNode { if (this._mode !== SequenceMode.Timeline) { return vd.h("div.SequenceTimeline", []); } const positionInput: vd.VNode = this._createPositionInput(index, max); const closeIcon: vd.VNode = vd.h("div.SequenceCloseIcon.SequenceIconVisible", []); const closeButtonProperties: vd.createProperties = { onclick: (): void => { this._mode = SequenceMode.Default; this._notifyChanged$.next(this); }, }; const closeButton: vd.VNode = vd.h("div.SequenceCloseButton", closeButtonProperties, [closeIcon]); const top: number = Math.round(containerWidth / this._stepperDefaultWidth * this._defaultHeight + 10); const playbackProperties: vd.createProperties = { style: { top: `${top}px` } }; return vd.h("div.SequenceTimeline", playbackProperties, [positionInput, closeButton]); } private _getStepClassName(direction: EdgeDirection, key: string, highlightKey: string): string { let className: string = direction === EdgeDirection.Next ? "SequenceStepNext" : "SequenceStepPrev"; if (key == null) { className += "Disabled"; } else { if (highlightKey === key) { className += "Highlight"; } } return className; } private _setChangingPosition(value: boolean): void { this._changingPosition = value; this._notifyChangingPositionChanged$.next(value); } } export default SequenceDOMRenderer;