UNPKG

mapillary-js

Version:

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

273 lines (231 loc) 9.42 kB
import {combineLatest as observableCombineLatest} from "rxjs"; import { scan, filter, map, distinctUntilChanged, pluck, refCount, publishReplay, } from "rxjs/operators"; import * as vd from "virtual-dom"; import {Observable, Subject} from "rxjs"; import {ISize, IVNodeHash, RenderMode, RenderService} from "../Render"; import {IFrame} from "../State"; interface INodePatch { vnode: vd.VNode; vpatch: vd.VPatch[]; } interface IVNodeHashes { [name: string]: vd.VNode; } interface IOffset { bottom: number; left: number; right: number; top: number; } interface IAdaptive { elementHeight: number; elementWidth: number; imageAspect: number; renderMode: RenderMode; } interface IAdaptiveOperation { (adaptive: IAdaptive): IAdaptive; } export class DOMRenderer { private _renderService: RenderService; private _currentFrame$: Observable<IFrame>; private _adaptiveOperation$: Subject<IAdaptiveOperation> = new Subject<IAdaptiveOperation>(); private _offset$: Observable<IOffset>; private _element$: Observable<Element>; private _vPatch$: Observable<vd.VPatch[]>; private _vNode$: Observable<vd.VNode>; private _render$: Subject<IVNodeHash> = new Subject<IVNodeHash>(); private _renderAdaptive$: Subject<IVNodeHash> = new Subject<IVNodeHash>(); constructor (element: HTMLElement, renderService: RenderService, currentFrame$: Observable<IFrame>) { this._renderService = renderService; this._currentFrame$ = currentFrame$; let rootNode: Element = vd.create(vd.h("div.domRenderer", [])); element.appendChild(rootNode); this._offset$ = this._adaptiveOperation$.pipe( scan( (adaptive: IAdaptive, operation: IAdaptiveOperation): IAdaptive => { return operation(adaptive); }, { elementHeight: element.offsetHeight, elementWidth: element.offsetWidth, imageAspect: 0, renderMode: RenderMode.Fill, }), filter( (adaptive: IAdaptive): boolean => { return adaptive.imageAspect > 0 && adaptive.elementWidth > 0 && adaptive.elementHeight > 0; }), map( (adaptive: IAdaptive): IOffset => { let elementAspect: number = adaptive.elementWidth / adaptive.elementHeight; let ratio: number = adaptive.imageAspect / elementAspect; let verticalOffset: number = 0; let horizontalOffset: number = 0; if (adaptive.renderMode === RenderMode.Letterbox) { if (adaptive.imageAspect > elementAspect) { verticalOffset = adaptive.elementHeight * (1 - 1 / ratio) / 2; } else { horizontalOffset = adaptive.elementWidth * (1 - ratio) / 2; } } else { if (adaptive.imageAspect > elementAspect) { horizontalOffset = -adaptive.elementWidth * (ratio - 1) / 2; } else { verticalOffset = -adaptive.elementHeight * (1 / ratio - 1) / 2; } } return { bottom: verticalOffset, left: horizontalOffset, right: horizontalOffset, top: verticalOffset, }; })); this._currentFrame$.pipe( filter( (frame: IFrame): boolean => { return frame.state.currentNode != null; }), distinctUntilChanged( (k1: string, k2: string): boolean => { return k1 === k2; }, (frame: IFrame): string => { return frame.state.currentNode.key; }), map( (frame: IFrame): number => { return frame.state.currentTransform.basicAspect; }), map( (aspect: number): IAdaptiveOperation => { return (adaptive: IAdaptive): IAdaptive => { adaptive.imageAspect = aspect; return adaptive; }; })) .subscribe(this._adaptiveOperation$); observableCombineLatest( this._renderAdaptive$.pipe( scan( (vNodeHashes: IVNodeHashes, vNodeHash: IVNodeHash): IVNodeHashes => { if (vNodeHash.vnode == null) { delete vNodeHashes[vNodeHash.name]; } else { vNodeHashes[vNodeHash.name] = vNodeHash.vnode; } return vNodeHashes; }, {})), this._offset$).pipe( map( (vo: [IVNodeHashes, IOffset]): IVNodeHash => { let vNodes: vd.VNode[] = []; let hashes: IVNodeHashes = vo[0]; for (const name in hashes) { if (!hashes.hasOwnProperty(name)) { continue; } vNodes.push(hashes[name]); } let offset: IOffset = vo[1]; let properties: vd.createProperties = { style: { bottom: offset.bottom + "px", left: offset.left + "px", "pointer-events": "none", position: "absolute", right: offset.right + "px", top: offset.top + "px", }, }; return { name: "adaptiveDomRenderer", vnode: vd.h("div.adaptiveDomRenderer", properties, vNodes), }; })) .subscribe(this._render$); this._vNode$ = this._render$.pipe( scan( (vNodeHashes: IVNodeHashes, vNodeHash: IVNodeHash): IVNodeHashes => { if (vNodeHash.vnode == null) { delete vNodeHashes[vNodeHash.name]; } else { vNodeHashes[vNodeHash.name] = vNodeHash.vnode; } return vNodeHashes; }, {}), map( (hashes: IVNodeHashes): vd.VNode => { let vNodes: vd.VNode[] = []; for (const name in hashes) { if (!hashes.hasOwnProperty(name)) { continue; } vNodes.push(hashes[name]); } return vd.h("div.domRenderer", vNodes); })); this._vPatch$ = this._vNode$.pipe( scan( (nodePatch: INodePatch, vNode: vd.VNode): INodePatch => { nodePatch.vpatch = vd.diff(nodePatch.vnode, vNode); nodePatch.vnode = vNode; return nodePatch; }, {vnode: vd.h("div.domRenderer", []), vpatch: null}), pluck<INodePatch, vd.VPatch[]>("vpatch")); this._element$ = this._vPatch$.pipe( scan( (oldElement: Element, vPatch: vd.VPatch[]): Element => { return vd.patch(oldElement, vPatch); }, rootNode), publishReplay(1), refCount()); this._element$.subscribe(() => { /*noop*/ }); this._renderService.size$.pipe( map( (size: ISize): IAdaptiveOperation => { return (adaptive: IAdaptive): IAdaptive => { adaptive.elementWidth = size.width; adaptive.elementHeight = size.height; return adaptive; }; })) .subscribe(this._adaptiveOperation$); this._renderService.renderMode$.pipe( map( (renderMode: RenderMode): IAdaptiveOperation => { return (adaptive: IAdaptive): IAdaptive => { adaptive.renderMode = renderMode; return adaptive; }; })) .subscribe(this._adaptiveOperation$); } public get element$(): Observable<Element> { return this._element$; } public get render$(): Subject<IVNodeHash> { return this._render$; } public get renderAdaptive$(): Subject<IVNodeHash> { return this._renderAdaptive$; } public clear(name: string): void { this._renderAdaptive$.next({name: name, vnode: null}); this._render$.next({name: name, vnode: null}); } } export default DOMRenderer;