mapillary-js
Version:
WebGL JavaScript library for displaying street level imagery from mapillary.com
319 lines (278 loc) • 11.8 kB
text/typescript
import {
combineLatest as observableCombineLatest,
of as observableOf,
from as observableFrom,
Observable,
Subscription,
} from "rxjs";
import {
map,
filter,
distinctUntilChanged,
mergeMap,
distinct,
scan,
pluck,
} from "rxjs/operators";
import * as vd from "virtual-dom";
import {ISequence} from "../API";
import {IRouteConfiguration, IRoutePath, ComponentService, Component} from "../Component";
import {Node} from "../Graph";
import {IVNodeHash} from "../Render";
import {IFrame} from "../State";
import {Container, Navigator} from "../Viewer";
interface IRtAndFrame {
routeTrack: RouteTrack;
frame: IFrame;
conf: IRouteConfiguration;
}
interface IConfAndNode {
conf: IRouteConfiguration;
node: Node;
}
interface INodeInstruction {
key: string;
description: string;
}
interface IInstructionPlace {
place: number;
nodeInstructions: INodeInstruction[];
}
class DescriptionState {
public description: string;
public showsLeft: number;
}
class RouteState {
public routeTrack: RouteTrack;
public currentNode: Node;
public lastNode: Node;
public playing: boolean;
}
class RouteTrack {
public nodeInstructions: INodeInstruction[] = [];
public nodeInstructionsOrdered: INodeInstruction[][] = [];
}
export class RouteComponent extends Component<IRouteConfiguration> {
public static componentName: string = "route";
private _disposable: Subscription;
private _disposableDescription: Subscription;
constructor(name: string, container: Container, navigator: Navigator) {
super(name, container, navigator);
}
public play(): void {
this.configure({ playing: true });
}
public stop(): void {
this.configure({ playing: false });
}
protected _activate(): void {
let slowedStream$: Observable<IFrame> = this._navigator.stateService.currentState$.pipe(
filter(
(frame: IFrame) => {
return (frame.id % 2) === 0;
}),
filter(
(frame: IFrame) => {
return frame.state.nodesAhead < 15;
}),
distinctUntilChanged(
undefined,
(frame: IFrame): string => {
return frame.state.lastNode.key;
}));
let routeTrack$: Observable<RouteTrack> = observableCombineLatest(
this.configuration$.pipe(
mergeMap(
(conf: IRouteConfiguration): Observable<IRoutePath> => {
return observableFrom(conf.paths);
}),
distinct(
(p: IRoutePath): string => {
return p.sequenceKey;
}),
mergeMap(
(path: IRoutePath): Observable<ISequence> => {
return this._navigator.apiV3.sequenceByKey$([path.sequenceKey]).pipe(
map(
(sequenceByKey: { [sequenceKey: string]: ISequence }): ISequence => {
return sequenceByKey[path.sequenceKey];
}));
})),
this.configuration$).pipe(
map(
([sequence, conf]: [ISequence, IRouteConfiguration]): IInstructionPlace[] => {
let i: number = 0;
let instructionPlaces: IInstructionPlace[] = [];
for (let path of conf.paths) {
if (path.sequenceKey === sequence.key) {
let nodeInstructions: INodeInstruction[] = [];
let saveKey: boolean = false;
for (let key of sequence.keys) {
if (path.startKey === key) {
saveKey = true;
}
if (saveKey) {
let description: string = null;
for (let infoKey of path.infoKeys) {
if (infoKey.key === key) {
description = infoKey.description;
}
}
nodeInstructions.push({description: description, key: key});
}
if (path.stopKey === key) {
saveKey = false;
}
}
instructionPlaces.push({nodeInstructions: nodeInstructions, place: i});
}
i++;
}
return instructionPlaces;
}),
scan(
(routeTrack: RouteTrack, instructionPlaces: IInstructionPlace[]): RouteTrack => {
for (let instructionPlace of instructionPlaces) {
routeTrack.nodeInstructionsOrdered[instructionPlace.place] = instructionPlace.nodeInstructions;
}
for (const place in routeTrack.nodeInstructionsOrdered) {
if (!routeTrack.nodeInstructionsOrdered.hasOwnProperty(place)) {
continue;
}
const instructionGroup: INodeInstruction[] = routeTrack.nodeInstructionsOrdered[place];
for (const instruction of instructionGroup) {
routeTrack.nodeInstructions.push(instruction);
}
}
return routeTrack;
},
new RouteTrack()));
const cacheNode$: any = observableCombineLatest(
slowedStream$,
routeTrack$,
this.configuration$).pipe(
map(
([frame, routeTrack, conf]: [IFrame, RouteTrack, IRouteConfiguration]): IRtAndFrame => {
return {conf: conf, frame: frame, routeTrack: routeTrack};
}),
scan(
(routeState: RouteState, rtAndFrame: IRtAndFrame): RouteState => {
if (rtAndFrame.conf.playing === undefined || rtAndFrame.conf.playing) {
routeState.routeTrack = rtAndFrame.routeTrack;
routeState.currentNode = rtAndFrame.frame.state.currentNode;
routeState.lastNode = rtAndFrame.frame.state.lastNode;
routeState.playing = true;
} else {
this._navigator.stateService.cutNodes();
routeState.playing = false;
}
return routeState;
},
new RouteState()),
filter(
(routeState: RouteState): boolean => {
return routeState.playing;
}),
filter(
(routeState: RouteState): boolean => {
for (let nodeInstruction of routeState.routeTrack.nodeInstructions) {
if (!nodeInstruction) {
continue;
}
if (nodeInstruction.key === routeState.lastNode.key) {
return true;
}
}
return false;
}),
distinctUntilChanged(
undefined,
(routeState: RouteState): string => {
return routeState.lastNode.key;
}),
mergeMap(
(routeState: RouteState): Observable<Node> => {
let i: number = 0;
for (let nodeInstruction of routeState.routeTrack.nodeInstructions) {
if (nodeInstruction.key === routeState.lastNode.key) {
break;
}
i++;
}
let nextInstruction: INodeInstruction = routeState.routeTrack.nodeInstructions[i + 1];
if (!nextInstruction) {
return observableOf<Node>(null);
}
return this._navigator.graphService.cacheNode$(nextInstruction.key);
}));
this._disposable = observableCombineLatest(
cacheNode$,
this.configuration$).pipe(
map(
([node, conf]: [Node, IRouteConfiguration]): IConfAndNode => {
return {conf: conf, node: node};
}),
filter(
(cAN: IConfAndNode) => {
return cAN.node !== null && cAN.conf.playing;
}),
pluck<IConfAndNode, Node>("node"))
.subscribe(this._navigator.stateService.appendNode$);
this._disposableDescription = observableCombineLatest(
this._navigator.stateService.currentNode$,
routeTrack$,
this.configuration$).pipe(
map(
([node, routeTrack, conf]: [Node, RouteTrack, IRouteConfiguration]): string => {
if (conf.playing !== undefined && !conf.playing) {
return "quit";
}
let description: string = null;
for (let nodeInstruction of routeTrack.nodeInstructions) {
if (nodeInstruction.key === node.key) {
description = nodeInstruction.description;
break;
}
}
return description;
}),
scan(
(descriptionState: DescriptionState, description: string): DescriptionState => {
if (description !== descriptionState.description && description !== null) {
descriptionState.description = description;
descriptionState.showsLeft = 6;
} else {
descriptionState.showsLeft--;
}
if (description === "quit") {
descriptionState.description = null;
}
return descriptionState;
},
new DescriptionState(),
),
map(
(descriptionState: DescriptionState): IVNodeHash => {
if (descriptionState.showsLeft > 0 && descriptionState.description) {
return {name: this._name, vnode: this._getRouteAnnotationNode(descriptionState.description)};
} else {
return {name: this._name, vnode: vd.h("div", [])};
}
}))
.subscribe(this._container.domRenderer.render$);
}
protected _deactivate(): void {
this._disposable.unsubscribe();
this._disposableDescription.unsubscribe();
}
protected _getDefaultConfiguration(): IRouteConfiguration {
return {};
}
private _getRouteAnnotationNode(description: string): vd.VNode {
return vd.h("div.RouteFrame", {}, [
vd.h("p", {textContent: description}, []),
]);
}
}
ComponentService.register(RouteComponent);
export default RouteComponent;