mapillary-js
Version:
A WebGL interactive street imagery library
508 lines (447 loc) • 20.4 kB
text/typescript
import * as THREE from "three";
import {
combineLatest as observableCombineLatest,
empty as observableEmpty,
from as observableFrom,
of as observableOf,
Observable,
Subject,
} from "rxjs";
import {
catchError,
distinctUntilChanged,
filter,
map,
mergeMap,
pairwise,
publishReplay,
refCount,
scan,
share,
startWith,
switchMap,
withLatestFrom,
} from "rxjs/operators";
import { Component } from "../Component";
import { Image as ImageNode } from "../../graph/Image";
import { Container } from "../../viewer/Container";
import { Navigator } from "../../viewer/Navigator";
import { ImageGLRenderer } from "./ImageGLRenderer";
import { Spatial } from "../../geo/Spatial";
import { ViewportCoords } from "../../geo/ViewportCoords";
import { RenderPass } from "../../render/RenderPass";
import { GLRenderHash } from "../../render/interfaces/IGLRenderHash";
import { ViewportSize } from "../../render/interfaces/ViewportSize";
import { RenderCamera } from "../../render/RenderCamera";
import { AnimationFrame } from "../../state/interfaces/AnimationFrame";
import { TileLoader } from "../../tile/TileLoader";
import { TileStore } from "../../tile/TileStore";
import { TileRegionOfInterest }
from "../../tile/interfaces/TileRegionOfInterest";
import { RegionOfInterestCalculator }
from "../../tile/RegionOfInterestCalculator";
import { TextureProvider } from "../../tile/TextureProvider";
import { ComponentConfiguration } from "../interfaces/ComponentConfiguration";
import { Transform } from "../../geo/Transform";
import { ComponentName } from "../ComponentName";
import { State } from "../../state/State";
interface ImageGLRendererOperation {
(renderer: ImageGLRenderer): ImageGLRenderer;
}
type PositionLookat = {
camera: RenderCamera,
height: number,
lookat: THREE.Vector3,
width: number,
zoom: number,
};
type TextureProviderInput = [AnimationFrame, THREE.WebGLRenderer];
type RoiTrigger = [StalledCamera, ViewportSize, Transform];
type StalledCamera = {
camera: RenderCamera,
stalled: boolean,
};
export class ImageComponent extends Component<ComponentConfiguration> {
public static componentName: ComponentName = "image";
private _rendererOperation$: Subject<ImageGLRendererOperation>;
private _renderer$: Observable<ImageGLRenderer>;
private _rendererCreator$: Subject<void>;
private _rendererDisposer$: Subject<void>;
private _imageTileLoader: TileLoader;
private _roiCalculator: RegionOfInterestCalculator;
constructor(
name: string,
container: Container,
navigator: Navigator) {
super(name, container, navigator);
this._imageTileLoader = new TileLoader(navigator.api);
this._roiCalculator = new RegionOfInterestCalculator();
this._rendererOperation$ = new Subject<ImageGLRendererOperation>();
this._rendererCreator$ = new Subject<void>();
this._rendererDisposer$ = new Subject<void>();
this._renderer$ = this._rendererOperation$.pipe(
scan(
(renderer: ImageGLRenderer, operation: ImageGLRendererOperation): ImageGLRenderer => {
return operation(renderer);
},
null),
filter(
(renderer: ImageGLRenderer): boolean => {
return renderer != null;
}),
distinctUntilChanged(
undefined,
(renderer: ImageGLRenderer): number => {
return renderer.frameId;
}));
this._rendererCreator$.pipe(
map(
(): ImageGLRendererOperation => {
return (renderer: ImageGLRenderer): ImageGLRenderer => {
if (renderer != null) {
throw new Error("Multiple image plane states can not be created at the same time");
}
return new ImageGLRenderer();
};
}))
.subscribe(this._rendererOperation$);
this._rendererDisposer$.pipe(
map(
(): ImageGLRendererOperation => {
return (renderer: ImageGLRenderer): ImageGLRenderer => {
renderer.dispose();
return null;
};
}))
.subscribe(this._rendererOperation$);
}
protected _activate(): void {
const subs = this._subscriptions;
subs.push(this._renderer$.pipe(
map(
(renderer: ImageGLRenderer): GLRenderHash => {
const renderHash: GLRenderHash = {
name: this._name,
renderer: {
frameId: renderer.frameId,
needsRender: renderer.needsRender,
render: renderer.render.bind(renderer),
pass: RenderPass.Background,
},
};
renderer.clearNeedsRender();
return renderHash;
}))
.subscribe(this._container.glRenderer.render$));
this._rendererCreator$.next(null);
subs.push(this._navigator.stateService.currentState$.pipe(
map(
(frame: AnimationFrame): ImageGLRendererOperation => {
return (renderer: ImageGLRenderer): ImageGLRenderer => {
renderer.updateFrame(frame);
return renderer;
};
}))
.subscribe(this._rendererOperation$));
const textureProvider$ =
this._container.configurationService.imageTiling$.pipe(
switchMap(
(active): Observable<AnimationFrame> => {
return active ?
this._navigator.stateService.currentState$ :
new Subject();
}),
distinctUntilChanged(
undefined,
(frame: AnimationFrame): string => {
return frame.state.currentImage.id;
}),
withLatestFrom(
this._container.glRenderer.webGLRenderer$),
map(
([frame, renderer]: TextureProviderInput)
: TextureProvider => {
const state = frame.state;
const currentNode = state.currentImage;
const currentTransform = state.currentTransform;
return new TextureProvider(
currentNode.id,
currentTransform.basicWidth,
currentTransform.basicHeight,
currentNode.image,
this._imageTileLoader,
new TileStore(),
renderer);
}),
publishReplay(1),
refCount());
subs.push(textureProvider$.subscribe(() => { /*noop*/ }));
subs.push(textureProvider$.pipe(
map(
(provider: TextureProvider): ImageGLRendererOperation => {
return (renderer: ImageGLRenderer): ImageGLRenderer => {
renderer.setTextureProvider(provider.id, provider);
return renderer;
};
}))
.subscribe(this._rendererOperation$));
subs.push(textureProvider$.pipe(
pairwise())
.subscribe(
(pair: [TextureProvider, TextureProvider]): void => {
const previous = pair[0];
previous.abort();
}));
const roiTrigger$ =
this._container.configurationService.imageTiling$.pipe(
switchMap(
(active): Observable<[State, boolean]> => {
return active ?
observableCombineLatest(
this._navigator.stateService.state$,
this._navigator.stateService.inTranslation$) :
new Subject();
}),
switchMap(
([state, inTranslation]: [State, boolean]) => {
const streetState =
state === State.Traversing ||
state === State.Waiting ||
state === State.WaitingInteractively;
const active = streetState && !inTranslation;
return active ?
this._container.renderService.renderCameraFrame$ :
observableEmpty();
}),
map(
(camera: RenderCamera): PositionLookat => {
return {
camera,
height: camera.size.height.valueOf(),
lookat: camera.camera.lookat.clone(),
width: camera.size.width.valueOf(),
zoom: camera.zoom.valueOf(),
};
}),
pairwise(),
map(
([pl0, pl1]: [PositionLookat, PositionLookat])
: StalledCamera => {
const stalled =
pl0.width === pl1.width &&
pl0.height === pl1.height &&
pl0.zoom === pl1.zoom &&
pl0.lookat.equals(pl1.lookat);
return { camera: pl1.camera, stalled };
}),
distinctUntilChanged(
(x, y): boolean => {
return x.stalled === y.stalled;
}),
filter(
(camera: StalledCamera): boolean => {
return camera.stalled;
}),
withLatestFrom(
this._container.renderService.size$,
this._navigator.stateService.currentTransform$));
subs.push(textureProvider$.pipe(
switchMap(
(provider: TextureProvider):
Observable<[TileRegionOfInterest, TextureProvider]> => {
return roiTrigger$.pipe(
map(
([stalled, size, transform]: RoiTrigger)
: [TileRegionOfInterest, TextureProvider] => {
const camera = stalled.camera;
const basic = new ViewportCoords()
.viewportToBasic(
0,
0,
transform,
camera.perspective);
if (basic[0] < 0 ||
basic[1] < 0 ||
basic[0] > 1 ||
basic[1] > 1) {
return undefined;
}
return [
this._roiCalculator
.computeRegionOfInterest(
camera,
size,
transform),
provider,
];
}),
filter(
(args: [TileRegionOfInterest, TextureProvider]): boolean => {
return !!args;
}));
}),
filter(
(args: [TileRegionOfInterest, TextureProvider]): boolean => {
return !args[1].disposed;
}))
.subscribe(
([roi, provider]: [TileRegionOfInterest, TextureProvider])
: void => {
provider.setRegionOfInterest(roi);
}));
const hasTexture$ = textureProvider$
.pipe(
switchMap(
(provider: TextureProvider): Observable<boolean> => {
return provider.hasTexture$;
}),
startWith(false),
publishReplay(1),
refCount());
subs.push(hasTexture$.subscribe(() => { /*noop*/ }));
subs.push(this._navigator.panService.panImages$.pipe(
filter(
(panNodes: []): boolean => {
return panNodes.length === 0;
}),
map(
(): ImageGLRendererOperation => {
return (renderer: ImageGLRenderer): ImageGLRenderer => {
renderer.clearPeripheryPlanes();
return renderer;
};
}))
.subscribe(this._rendererOperation$));
const cachedPanNodes$ = this._navigator.panService.panImages$.pipe(
switchMap(
(nts: [ImageNode, Transform, number][]): Observable<[ImageNode, Transform]> => {
return observableFrom(nts).pipe(
mergeMap(
([n, t]: [ImageNode, Transform, number]): Observable<[ImageNode, Transform]> => {
return observableCombineLatest(
this._navigator.graphService.cacheImage$(n.id).pipe(
catchError(
(error: Error): Observable<ImageNode> => {
console.error(`Failed to cache periphery image (${n.id})`, error);
return observableEmpty();
})),
observableOf(t));
}));
}),
share());
subs.push(cachedPanNodes$.pipe(
map(
([n, t]: [ImageNode, Transform]): ImageGLRendererOperation => {
return (renderer: ImageGLRenderer): ImageGLRenderer => {
renderer.addPeripheryPlane(n, t);
return renderer;
};
}))
.subscribe(this._rendererOperation$));
subs.push(cachedPanNodes$.pipe(
mergeMap(
([n]: [ImageNode, Transform]): Observable<ImageNode> => {
return n.cacheImage$().pipe(
catchError(
(): Observable<ImageNode> => {
return observableEmpty();
}));
}),
map(
(n: ImageNode): ImageGLRendererOperation => {
return (renderer: ImageGLRenderer): ImageGLRenderer => {
renderer.updateTextureImage(n.image, n);
return renderer;
};
}))
.subscribe(this._rendererOperation$));
const inTransition$ = this._navigator.stateService.currentState$.pipe(
map(
(frame: AnimationFrame): boolean => {
return frame.state.alpha < 1;
}),
distinctUntilChanged());
const panTrigger$ = observableCombineLatest(
this._container.mouseService.active$,
this._container.touchService.active$,
this._navigator.stateService.inMotion$,
inTransition$).pipe(
map(
([mouseActive, touchActive, inMotion, inTransition]: [boolean, boolean, boolean, boolean]): boolean => {
return !(mouseActive || touchActive || inMotion || inTransition);
}),
filter(
(trigger: boolean): boolean => {
return trigger;
}));
subs.push(this._navigator.stateService.state$
.pipe(
switchMap(
state => {
return state === State.Traversing ?
this._navigator.panService.panImages$ :
observableEmpty();
}),
switchMap(
(nts: [ImageNode, Transform, number][]):
Observable<[RenderCamera, ImageNode, Transform, [ImageNode, Transform, number][]]> => {
return panTrigger$.pipe(
withLatestFrom(
this._container.renderService.renderCamera$,
this._navigator.stateService.currentImage$,
this._navigator.stateService.currentTransform$),
mergeMap(
([, renderCamera, currentNode, currentTransform]: [boolean, RenderCamera, ImageNode, Transform]):
Observable<[RenderCamera, ImageNode, Transform, [ImageNode, Transform, number][]]> => {
return observableOf(
[
renderCamera,
currentNode,
currentTransform,
nts,
] as [RenderCamera, ImageNode, Transform, [ImageNode, Transform, number][]]);
}));
}),
switchMap(
([camera, cn, ct, nts]:
[
RenderCamera,
ImageNode,
Transform,
[ImageNode, Transform, number][],
]): Observable<ImageNode> => {
const direction = camera.camera.lookat.clone().sub(camera.camera.position);
const cd = new Spatial().viewingDirection(cn.rotation);
const ca = cd.angleTo(direction);
const closest: [number, string] = [ca, undefined];
const basic = new ViewportCoords().viewportToBasic(0, 0, ct, camera.perspective);
if (basic[0] >= 0 && basic[0] <= 1 && basic[1] >= 0 && basic[1] <= 1) {
closest[0] = Number.NEGATIVE_INFINITY;
}
for (const [n] of nts) {
const d = new Spatial().viewingDirection(n.rotation);
const a = d.angleTo(direction);
if (a < closest[0]) {
closest[0] = a;
closest[1] = n.id;
}
}
if (!closest[1]) {
return observableEmpty();
}
return this._navigator.moveTo$(closest[1]).pipe(
catchError(
(): Observable<ImageNode> => {
return observableEmpty();
}));
}))
.subscribe());
}
protected _deactivate(): void {
this._rendererDisposer$.next(null);
this._subscriptions.unsubscribe();
}
protected _getDefaultConfiguration(): ComponentConfiguration {
return {};
}
}