mapillary-js
Version:
WebGL JavaScript library for displaying street level imagery from mapillary.com
610 lines (535 loc) • 24.9 kB
text/typescript
import * as THREE from "three";
import {
empty as observableEmpty,
of as observableOf,
from as observableFrom,
combineLatest as observableCombineLatest,
Observable,
Subscription,
Subject,
} from "rxjs";
import {
switchMap,
pairwise,
debounceTime,
refCount,
publishReplay,
withLatestFrom,
scan,
filter,
first,
catchError,
takeUntil,
startWith,
skipWhile,
map,
publish,
distinctUntilChanged,
mergeMap,
share,
} from "rxjs/operators";
import {
ComponentService,
Component,
IComponentConfiguration,
ImagePlaneGLRenderer,
} from "../../Component";
import {
GeoCoords,
Transform,
Geo,
} from "../../Geo";
import {
ICurrentState,
IFrame,
} from "../../State";
import {
Container,
Navigator,
ImageSize,
} from "../../Viewer";
import {
GLRenderStage,
IGLRenderHash,
ISize,
RenderCamera,
} from "../../Render";
import {
Node as GraphNode,
} from "../../Graph";
import {
ImageTileLoader,
ImageTileStore,
IRegionOfInterest,
RegionOfInterestCalculator,
TextureProvider,
} from "../../Tiles";
import {
Settings,
Urls,
} from "../../Utils";
import ViewportCoords from "../../geo/ViewportCoords";
import Spatial from "../../geo/Spatial";
interface IImagePlaneGLRendererOperation {
(renderer: ImagePlaneGLRenderer): ImagePlaneGLRenderer;
}
type PositionLookat = [THREE.Vector3, THREE.Vector3, number, number, number];
export class ImagePlaneComponent extends Component<IComponentConfiguration> {
public static componentName: string = "imagePlane";
private _rendererOperation$: Subject<IImagePlaneGLRendererOperation>;
private _renderer$: Observable<ImagePlaneGLRenderer>;
private _rendererCreator$: Subject<void>;
private _rendererDisposer$: Subject<void>;
private _abortTextureProviderSubscription: Subscription;
private _hasTextureSubscription: Subscription;
private _rendererSubscription: Subscription;
private _setRegionOfInterestSubscription: Subscription;
private _setTextureProviderSubscription: Subscription;
private _setTileSizeSubscription: Subscription;
private _stateSubscription: Subscription;
private _textureProviderSubscription: Subscription;
private _updateBackgroundSubscription: Subscription;
private _updateTextureImageSubscription: Subscription;
private _clearPeripheryPlaneSubscription: Subscription;
private _addPeripheryPlaneSubscription: Subscription;
private _updatePeripheryPlaneTextureSubscription: Subscription;
private _moveToPeripheryNodeSubscription: Subscription;
private _imageTileLoader: ImageTileLoader;
private _roiCalculator: RegionOfInterestCalculator;
constructor (name: string, container: Container, navigator: Navigator) {
super(name, container, navigator);
this._imageTileLoader = new ImageTileLoader(Urls.tileScheme, Urls.tileDomain, Urls.origin);
this._roiCalculator = new RegionOfInterestCalculator();
this._rendererOperation$ = new Subject<IImagePlaneGLRendererOperation>();
this._rendererCreator$ = new Subject<void>();
this._rendererDisposer$ = new Subject<void>();
this._renderer$ = this._rendererOperation$.pipe(
scan(
(renderer: ImagePlaneGLRenderer, operation: IImagePlaneGLRendererOperation): ImagePlaneGLRenderer => {
return operation(renderer);
},
null),
filter(
(renderer: ImagePlaneGLRenderer): boolean => {
return renderer != null;
}),
distinctUntilChanged(
undefined,
(renderer: ImagePlaneGLRenderer): number => {
return renderer.frameId;
}));
this._rendererCreator$.pipe(
map(
(): IImagePlaneGLRendererOperation => {
return (renderer: ImagePlaneGLRenderer): ImagePlaneGLRenderer => {
if (renderer != null) {
throw new Error("Multiple image plane states can not be created at the same time");
}
return new ImagePlaneGLRenderer();
};
}))
.subscribe(this._rendererOperation$);
this._rendererDisposer$.pipe(
map(
(): IImagePlaneGLRendererOperation => {
return (renderer: ImagePlaneGLRenderer): ImagePlaneGLRenderer => {
renderer.dispose();
return null;
};
}))
.subscribe(this._rendererOperation$);
}
protected _activate(): void {
this._rendererSubscription = this._renderer$.pipe(
map(
(renderer: ImagePlaneGLRenderer): IGLRenderHash => {
let renderHash: IGLRenderHash = {
name: this._name,
render: {
frameId: renderer.frameId,
needsRender: renderer.needsRender,
render: renderer.render.bind(renderer),
stage: GLRenderStage.Background,
},
};
renderer.clearNeedsRender();
return renderHash;
}))
.subscribe(this._container.glRenderer.render$);
this._rendererCreator$.next(null);
this._stateSubscription = this._navigator.stateService.currentState$.pipe(
map(
(frame: IFrame): IImagePlaneGLRendererOperation => {
return (renderer: ImagePlaneGLRenderer): ImagePlaneGLRenderer => {
renderer.updateFrame(frame);
return renderer;
};
}))
.subscribe(this._rendererOperation$);
let textureProvider$: Observable<TextureProvider> = this._navigator.stateService.currentState$.pipe(
distinctUntilChanged(
undefined,
(frame: IFrame): string => {
return frame.state.currentNode.key;
}),
withLatestFrom(
this._container.glRenderer.webGLRenderer$,
this._container.renderService.size$),
map(
([frame, renderer, size]: [IFrame, THREE.WebGLRenderer, ISize]): TextureProvider => {
let state: ICurrentState = frame.state;
let viewportSize: number = Math.max(size.width, size.height);
let currentNode: GraphNode = state.currentNode;
let currentTransform: Transform = state.currentTransform;
let tileSize: number = viewportSize > 2048 ? 2048 : viewportSize > 1024 ? 1024 : 512;
return new TextureProvider(
currentNode.key,
currentTransform.basicWidth,
currentTransform.basicHeight,
tileSize,
currentNode.image,
this._imageTileLoader,
new ImageTileStore(),
renderer);
}),
publishReplay(1),
refCount());
this._textureProviderSubscription = textureProvider$.subscribe(() => { /*noop*/ });
this._setTextureProviderSubscription = textureProvider$.pipe(
map(
(provider: TextureProvider): IImagePlaneGLRendererOperation => {
return (renderer: ImagePlaneGLRenderer): ImagePlaneGLRenderer => {
renderer.setTextureProvider(provider.key, provider);
return renderer;
};
}))
.subscribe(this._rendererOperation$);
this._setTileSizeSubscription = this._container.renderService.size$.pipe(
switchMap(
(size: ISize): Observable<[TextureProvider, ISize]> => {
return observableCombineLatest(
textureProvider$,
observableOf<ISize>(size)).pipe(
first());
}))
.subscribe(
([provider, size]: [TextureProvider, ISize]): void => {
let viewportSize: number = Math.max(size.width, size.height);
let tileSize: number = viewportSize > 2048 ? 2048 : viewportSize > 1024 ? 1024 : 512;
provider.setTileSize(tileSize);
});
this._abortTextureProviderSubscription = textureProvider$.pipe(
pairwise())
.subscribe(
(pair: [TextureProvider, TextureProvider]): void => {
let previous: TextureProvider = pair[0];
previous.abort();
});
let roiTrigger$: Observable<[RenderCamera, ISize, Transform]> = observableCombineLatest(
this._container.renderService.renderCameraFrame$,
this._container.renderService.size$.pipe(debounceTime(250))).pipe(
map(
([camera, size]: [RenderCamera, ISize]): PositionLookat => {
return [
camera.camera.position.clone(),
camera.camera.lookat.clone(),
camera.zoom.valueOf(),
size.height.valueOf(),
size.width.valueOf()];
}),
pairwise(),
skipWhile(
(pls: [PositionLookat, PositionLookat]): boolean => {
return pls[1][2] - pls[0][2] < 0 || pls[1][2] === 0;
}),
map(
(pls: [PositionLookat, PositionLookat]): boolean => {
let samePosition: boolean = pls[0][0].equals(pls[1][0]);
let sameLookat: boolean = pls[0][1].equals(pls[1][1]);
let sameZoom: boolean = pls[0][2] === pls[1][2];
let sameHeight: boolean = pls[0][3] === pls[1][3];
let sameWidth: boolean = pls[0][4] === pls[1][4];
return samePosition && sameLookat && sameZoom && sameHeight && sameWidth;
}),
distinctUntilChanged(),
filter(
(stalled: boolean): boolean => {
return stalled;
}),
switchMap(
(stalled: boolean): Observable<RenderCamera> => {
return this._container.renderService.renderCameraFrame$.pipe(
first());
}),
withLatestFrom(
this._container.renderService.size$,
this._navigator.stateService.currentTransform$));
this._setRegionOfInterestSubscription = textureProvider$.pipe(
switchMap(
(provider: TextureProvider): Observable<[IRegionOfInterest, TextureProvider]> => {
return roiTrigger$.pipe(
map(
([camera, size, transform]: [RenderCamera, ISize, Transform]):
[IRegionOfInterest, TextureProvider] => {
const basic: number[] = 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: [IRegionOfInterest, TextureProvider]): boolean => {
return !!args;
}));
}),
filter(
(args: [IRegionOfInterest, TextureProvider]): boolean => {
return !args[1].disposed;
}))
.subscribe(
(args: [IRegionOfInterest, TextureProvider]): void => {
let roi: IRegionOfInterest = args[0];
let provider: TextureProvider = args[1];
provider.setRegionOfInterest(roi);
});
let hasTexture$: Observable<boolean> = textureProvider$.pipe(
switchMap(
(provider: TextureProvider): Observable<boolean> => {
return provider.hasTexture$;
}),
startWith(false),
publishReplay(1),
refCount());
this._hasTextureSubscription = hasTexture$.subscribe(() => { /*noop*/ });
let nodeImage$: Observable<[HTMLImageElement, GraphNode]> = this._navigator.stateService.currentState$.pipe(
filter(
(frame: IFrame): boolean => {
return frame.state.nodesAhead === 0;
}),
map(
(frame: IFrame): GraphNode => {
return frame.state.currentNode;
}),
distinctUntilChanged(
undefined,
(node: GraphNode): string => {
return node.key;
}),
debounceTime(1000),
withLatestFrom(hasTexture$),
filter(
(args: [GraphNode, boolean]): boolean => {
return !args[1];
}),
map(
(args: [GraphNode, boolean]): GraphNode => {
return args[0];
}),
filter(
(node: GraphNode): boolean => {
return node.pano ?
Settings.maxImageSize > Settings.basePanoramaSize :
Settings.maxImageSize > Settings.baseImageSize;
}),
switchMap(
(node: GraphNode): Observable<[HTMLImageElement, GraphNode]> => {
let baseImageSize: ImageSize = node.pano ?
Settings.basePanoramaSize :
Settings.baseImageSize;
if (Math.max(node.image.width, node.image.height) > baseImageSize) {
return observableEmpty();
}
let image$: Observable<[HTMLImageElement, GraphNode]> = node
.cacheImage$(Settings.maxImageSize).pipe(
map(
(n: GraphNode): [HTMLImageElement, GraphNode] => {
return [n.image, n];
}));
return image$.pipe(
takeUntil(
hasTexture$.pipe(
filter(
(hasTexture: boolean): boolean => {
return hasTexture;
}))),
catchError(
(error: Error, caught: Observable<[HTMLImageElement, GraphNode]>):
Observable<[HTMLImageElement, GraphNode]> => {
console.error(`Failed to fetch high res image (${node.key})`, error);
return observableEmpty();
}));
})).pipe(
publish(),
refCount());
this._updateBackgroundSubscription = nodeImage$.pipe(
withLatestFrom(textureProvider$))
.subscribe(
(args: [[HTMLImageElement, GraphNode], TextureProvider]): void => {
if (args[0][1].key !== args[1].key ||
args[1].disposed) {
return;
}
args[1].updateBackground(args[0][0]);
});
this._updateTextureImageSubscription = nodeImage$.pipe(
map(
(imn: [HTMLImageElement, GraphNode]): IImagePlaneGLRendererOperation => {
return (renderer: ImagePlaneGLRenderer): ImagePlaneGLRenderer => {
renderer.updateTextureImage(imn[0], imn[1]);
return renderer;
};
}))
.subscribe(this._rendererOperation$);
this._clearPeripheryPlaneSubscription = this._navigator.panService.panNodes$.pipe(
filter(
(panNodes: []): boolean => {
return panNodes.length === 0;
}),
map(
(): IImagePlaneGLRendererOperation => {
return (renderer: ImagePlaneGLRenderer): ImagePlaneGLRenderer => {
renderer.clearPeripheryPlanes();
return renderer;
};
}))
.subscribe(this._rendererOperation$);
const cachedPanNodes$: Observable<[GraphNode, Transform]> = this._navigator.panService.panNodes$.pipe(
switchMap(
(nts: [GraphNode, Transform, number][]): Observable<[GraphNode, Transform]> => {
return observableFrom(nts).pipe(
mergeMap(
([n, t]: [GraphNode, Transform, number]): Observable<[GraphNode, Transform]> => {
return observableCombineLatest(
this._navigator.graphService.cacheNode$(n.key).pipe(
catchError(
(error: Error): Observable<GraphNode> => {
console.error(`Failed to cache periphery node (${n.key})`, error);
return observableEmpty();
})),
observableOf(t));
}));
}),
share());
this._addPeripheryPlaneSubscription = cachedPanNodes$.pipe(
map(
([n, t]: [GraphNode, Transform]): IImagePlaneGLRendererOperation => {
return (renderer: ImagePlaneGLRenderer): ImagePlaneGLRenderer => {
renderer.addPeripheryPlane(n, t);
return renderer;
};
}))
.subscribe(this._rendererOperation$);
this._updatePeripheryPlaneTextureSubscription = cachedPanNodes$.pipe(
mergeMap(
([n]: [GraphNode, Transform]): Observable<GraphNode> => {
return ImageSize.Size2048 > Math.max(n.image.width, n.image.height) ?
n.cacheImage$(ImageSize.Size2048).pipe(
catchError(
(): Observable<GraphNode> => {
return observableEmpty();
})) :
observableEmpty();
}),
map(
(n: GraphNode): IImagePlaneGLRendererOperation => {
return (renderer: ImagePlaneGLRenderer): ImagePlaneGLRenderer => {
renderer.updateTextureImage(n.image, n);
return renderer;
};
}))
.subscribe(this._rendererOperation$);
const inTransition$: Observable<boolean> = this._navigator.stateService.currentState$.pipe(
map(
(frame: IFrame): boolean => {
return frame.state.alpha < 1;
}),
distinctUntilChanged());
const panTrigger$: Observable<boolean> = 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;
}));
this._moveToPeripheryNodeSubscription = this._navigator.panService.panNodes$.pipe(
switchMap(
(nts: [GraphNode, Transform, number][]):
Observable<[RenderCamera, GraphNode, Transform, [GraphNode, Transform, number][]]> => {
return panTrigger$.pipe(
withLatestFrom(
this._container.renderService.renderCamera$,
this._navigator.stateService.currentNode$,
this._navigator.stateService.currentTransform$),
mergeMap(
([, renderCamera, currentNode, currentTransform]: [boolean, RenderCamera, GraphNode, Transform]):
Observable<[RenderCamera, GraphNode, Transform, [GraphNode, Transform, number][]]> => {
return observableOf(
[
renderCamera,
currentNode,
currentTransform,
nts,
] as [RenderCamera, GraphNode, Transform, [GraphNode, Transform, number][]]);
}));
}),
switchMap(
([camera, cn, ct, nts]: [RenderCamera, GraphNode, Transform, [GraphNode, Transform, number][]]): Observable<GraphNode> => {
const direction: THREE.Vector3 = camera.camera.lookat.clone().sub(camera.camera.position);
const cd: THREE.Vector3 = new Spatial().viewingDirection(cn.rotation);
const ca: number = cd.angleTo(direction);
const closest: [number, string] = [ca, undefined];
const basic: number[] = 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: THREE.Vector3 = new Spatial().viewingDirection(n.rotation);
const a: number = d.angleTo(direction);
if (a < closest[0]) {
closest[0] = a;
closest[1] = n.key;
}
}
if (!closest[1]) {
return observableEmpty();
}
return this._navigator.moveToKey$(closest[1]).pipe(
catchError(
(): Observable<GraphNode> => {
return observableEmpty();
}));
}))
.subscribe();
}
protected _deactivate(): void {
this._rendererDisposer$.next(null);
this._abortTextureProviderSubscription.unsubscribe();
this._hasTextureSubscription.unsubscribe();
this._rendererSubscription.unsubscribe();
this._setRegionOfInterestSubscription.unsubscribe();
this._setTextureProviderSubscription.unsubscribe();
this._setTileSizeSubscription.unsubscribe();
this._stateSubscription.unsubscribe();
this._textureProviderSubscription.unsubscribe();
this._updateBackgroundSubscription.unsubscribe();
this._updateTextureImageSubscription.unsubscribe();
this._clearPeripheryPlaneSubscription.unsubscribe();
this._addPeripheryPlaneSubscription.unsubscribe();
this._updatePeripheryPlaneTextureSubscription.unsubscribe();
this._moveToPeripheryNodeSubscription.unsubscribe();
}
protected _getDefaultConfiguration(): IComponentConfiguration {
return { };
}
}
ComponentService.register(ImagePlaneComponent);
export default ImagePlaneComponent;