UNPKG

mapillary-js

Version:

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

377 lines (303 loc) 13.2 kB
import {merge as observableMerge, combineLatest as observableCombineLatest, Observable, Subject, Subscription} from "rxjs"; import {mergeMap, scan, filter, share, startWith, distinctUntilChanged, map, first, publishReplay, refCount} from "rxjs/operators"; import * as THREE from "three"; import { GLRenderStage, IGLRenderFunction, IGLRender, IGLRenderHash, RenderCamera, RenderService, ISize, } from "../Render"; import {DOM} from "../Utils"; interface IGLRenderer { needsRender: boolean; renderer: THREE.WebGLRenderer; } interface IRenderCamera { frameId: number; needsRender: boolean; perspective: THREE.PerspectiveCamera; } interface IGLRenderHashes { [name: string]: IGLRender; } interface IEraser { needsRender: boolean; } interface IGLRendererOperation { (renderer: IGLRenderer): IGLRenderer; } interface IRenderCameraOperation { (camera: IRenderCamera): IRenderCamera; } interface IGLRenderHashesOperation extends Function { (hashes: IGLRenderHashes): IGLRenderHashes; } interface IEraserOperation { (eraser: IEraser): IEraser; } interface ICombination { camera: IRenderCamera; eraser: IEraser; renderer: IGLRenderer; renders: IGLRender[]; } export class GLRenderer { private _renderService: RenderService; private _dom: DOM; private _renderFrame$: Subject<RenderCamera> = new Subject<RenderCamera>(); private _renderCameraOperation$: Subject<IRenderCameraOperation> = new Subject<IRenderCameraOperation>(); private _renderCamera$: Observable<IRenderCamera>; private _render$: Subject<IGLRenderHash> = new Subject<IGLRenderHash>(); private _clear$: Subject<string> = new Subject<string>(); private _renderOperation$: Subject<IGLRenderHashesOperation> = new Subject<IGLRenderHashesOperation>(); private _renderCollection$: Observable<IGLRenderHashes>; private _rendererOperation$: Subject<IGLRendererOperation> = new Subject<IGLRendererOperation>(); private _renderer$: Observable<IGLRenderer>; private _eraserOperation$: Subject<IEraserOperation> = new Subject<IEraserOperation>(); private _eraser$: Observable<IEraser>; private _webGLRenderer$: Observable<THREE.WebGLRenderer>; private _renderFrameSubscription: Subscription; constructor (canvasContainer: HTMLElement, renderService: RenderService, dom?: DOM) { this._renderService = renderService; this._dom = !!dom ? dom : new DOM(); this._renderer$ = this._rendererOperation$.pipe( scan( (renderer: IGLRenderer, operation: IGLRendererOperation): IGLRenderer => { return operation(renderer); }, { needsRender: false, renderer: null }), filter( (renderer: IGLRenderer): boolean => { return !!renderer.renderer; })); this._renderCollection$ = this._renderOperation$.pipe( scan( (hashes: IGLRenderHashes, operation: IGLRenderHashesOperation): IGLRenderHashes => { return operation(hashes); }, {}), share()); this._renderCamera$ = this._renderCameraOperation$.pipe( scan( (rc: IRenderCamera, operation: IRenderCameraOperation): IRenderCamera => { return operation(rc); }, { frameId: -1, needsRender: false, perspective: null })); this._eraser$ = this._eraserOperation$.pipe( startWith( (eraser: IEraser): IEraser => { return eraser; }), scan( (eraser: IEraser, operation: IEraserOperation): IEraser => { return operation(eraser); }, { needsRender: false })); observableCombineLatest( this._renderer$, this._renderCollection$, this._renderCamera$, this._eraser$).pipe( map( ([renderer, hashes, rc, eraser]: [IGLRenderer, IGLRenderHashes, IRenderCamera, IEraser]): ICombination => { let renders: IGLRender[] = Object.keys(hashes) .map((key: string): IGLRender => { return hashes[key]; }); return { camera: rc, eraser: eraser, renderer: renderer, renders: renders }; }), filter( (co: ICombination): boolean => { let needsRender: boolean = co.renderer.needsRender || co.camera.needsRender || co.eraser.needsRender; let frameId: number = co.camera.frameId; for (let render of co.renders) { if (render.frameId !== frameId) { return false; } needsRender = needsRender || render.needsRender; } return needsRender; }), distinctUntilChanged( (n1: number, n2: number): boolean => { return n1 === n2; }, (co: ICombination): number => { return co.eraser.needsRender ? -1 : co.camera.frameId; })) .subscribe( (co: ICombination): void => { co.renderer.needsRender = false; co.camera.needsRender = false; co.eraser.needsRender = false; let perspectiveCamera: THREE.PerspectiveCamera = co.camera.perspective; let backgroundRenders: IGLRenderFunction[] = []; let foregroundRenders: IGLRenderFunction[] = []; for (let render of co.renders) { if (render.stage === GLRenderStage.Background) { backgroundRenders.push(render.render); } else if (render.stage === GLRenderStage.Foreground) { foregroundRenders.push(render.render); } } let renderer: THREE.WebGLRenderer = co.renderer.renderer; renderer.clear(); for (let render of backgroundRenders) { render(perspectiveCamera, renderer); } renderer.clearDepth(); for (let render of foregroundRenders) { render(perspectiveCamera, renderer); } }); this._renderFrame$.pipe( map( (rc: RenderCamera): IRenderCameraOperation => { return (irc: IRenderCamera): IRenderCamera => { irc.frameId = rc.frameId; irc.perspective = rc.perspective; if (rc.changed === true) { irc.needsRender = true; } return irc; }; })) .subscribe(this._renderCameraOperation$); this._renderFrameSubscribe(); let renderHash$: Observable<IGLRenderHashesOperation> = this._render$.pipe( map( (hash: IGLRenderHash) => { return (hashes: IGLRenderHashes): IGLRenderHashes => { hashes[hash.name] = hash.render; return hashes; }; })); let clearHash$: Observable<IGLRenderHashesOperation> = this._clear$.pipe( map( (name: string) => { return (hashes: IGLRenderHashes): IGLRenderHashes => { delete hashes[name]; return hashes; }; })); observableMerge(renderHash$, clearHash$) .subscribe(this._renderOperation$); this._webGLRenderer$ = this._render$.pipe( first(), map( (hash: IGLRenderHash): THREE.WebGLRenderer => { const canvas: HTMLCanvasElement = this._dom.createElement("canvas", "mapillary-js-canvas"); canvas.style.position = "absolute"; canvas.setAttribute("tabindex", "0"); canvasContainer.appendChild(canvas); const element: HTMLElement = renderService.element; const webGLRenderer: THREE.WebGLRenderer = new THREE.WebGLRenderer({ canvas: canvas }); webGLRenderer.setPixelRatio(window.devicePixelRatio); webGLRenderer.setSize(element.offsetWidth, element.offsetHeight); webGLRenderer.setClearColor(new THREE.Color(0x202020), 1.0); webGLRenderer.autoClear = false; return webGLRenderer; }), publishReplay(1), refCount()); this._webGLRenderer$.subscribe(() => { /*noop*/ }); let createRenderer$: Observable<IGLRendererOperation> = this._webGLRenderer$.pipe( first(), map( (webGLRenderer: THREE.WebGLRenderer): IGLRendererOperation => { return (renderer: IGLRenderer): IGLRenderer => { renderer.needsRender = true; renderer.renderer = webGLRenderer; return renderer; }; })); let resizeRenderer$: Observable<IGLRendererOperation> = this._renderService.size$.pipe( map( (size: ISize): IGLRendererOperation => { return (renderer: IGLRenderer): IGLRenderer => { if (renderer.renderer == null) { return renderer; } renderer.renderer.setSize(size.width, size.height); renderer.needsRender = true; return renderer; }; })); let clearRenderer$: Observable<IGLRendererOperation> = this._clear$.pipe( map( (name: string) => { return (renderer: IGLRenderer): IGLRenderer => { if (renderer.renderer == null) { return renderer; } renderer.needsRender = true; return renderer; }; })); observableMerge(createRenderer$, resizeRenderer$, clearRenderer$) .subscribe(this._rendererOperation$); let renderCollectionEmpty$: Observable<IGLRenderHashes> = this._renderCollection$.pipe( filter( (hashes: IGLRenderHashes): boolean => { return Object.keys(hashes).length === 0; }), share()); renderCollectionEmpty$ .subscribe( (hashes: IGLRenderHashes): void => { if (this._renderFrameSubscription == null) { return; } this._renderFrameSubscription.unsubscribe(); this._renderFrameSubscription = null; this._renderFrameSubscribe(); }); renderCollectionEmpty$.pipe( map( (hashes: IGLRenderHashes): IEraserOperation => { return (eraser: IEraser): IEraser => { eraser.needsRender = true; return eraser; }; })) .subscribe(this._eraserOperation$); } public get render$(): Subject<IGLRenderHash> { return this._render$; } public get webGLRenderer$(): Observable<THREE.WebGLRenderer> { return this._webGLRenderer$; } public clear(name: string): void { this._clear$.next(name); } private _renderFrameSubscribe(): void { this._render$.pipe( first(), map( (renderHash: IGLRenderHash): IRenderCameraOperation => { return (irc: IRenderCamera): IRenderCamera => { irc.needsRender = true; return irc; }; })) .subscribe( (operation: IRenderCameraOperation): void => { this._renderCameraOperation$.next(operation); }); this._renderFrameSubscription = this._render$.pipe( first(), mergeMap( (hash: IGLRenderHash): Observable<RenderCamera> => { return this._renderService.renderCameraFrame$; })) .subscribe(this._renderFrame$); } } export default GLRenderer;