mapillary-js
Version:
WebGL JavaScript library for displaying street level imagery from mapillary.com
256 lines (217 loc) • 8.9 kB
text/typescript
import * as THREE from "three";
import {
combineLatest as observableCombinLatest,
empty as observableEmpty,
merge as observableMerge,
of as observableOf,
Subscription,
Observable,
} from "rxjs";
import {
map,
filter,
withLatestFrom,
share,
switchMap,
startWith,
distinctUntilChanged,
} from "rxjs/operators";
import {
Component,
IMouseConfiguration,
HandlerBase,
MouseOperator,
} from "../../Component";
import {
Spatial,
Transform,
ViewportCoords,
} from "../../Geo";
import {
RenderCamera,
} from "../../Render";
import {
IRotation,
State,
} from "../../State";
import {
Container,
Navigator,
} from "../../Viewer";
export class EarthControlHandler extends HandlerBase<IMouseConfiguration> {
private _viewportCoords: ViewportCoords;
private _spatial: Spatial;
private _dollySubscription: Subscription;
private _orbitSubscription: Subscription;
private _preventDefaultSubscription: Subscription;
private _truckSubscription: Subscription;
constructor(
component: Component<IMouseConfiguration>,
container: Container,
navigator: Navigator,
viewportCoords: ViewportCoords,
spatial: Spatial) {
super(component, container, navigator);
this._spatial = spatial;
this._viewportCoords = viewportCoords;
}
protected _enable(): void {
const earth$: Observable<boolean> = this._navigator.stateService.state$.pipe(
map(
(state: State): boolean => {
return state === State.Earth;
}),
share());
this._preventDefaultSubscription = earth$.pipe(
switchMap(
(earth: boolean): Observable<MouseEvent> => {
return earth ?
this._container.mouseService.mouseWheel$ :
observableEmpty();
}))
.subscribe(
(event: WheelEvent): void => {
event.preventDefault();
});
this._truckSubscription = earth$.pipe(
switchMap(
(earth: boolean): Observable<[MouseEvent, MouseEvent]> => {
if (!earth) {
return observableEmpty();
}
return MouseOperator.filteredPairwiseMouseDrag$(this._component.name, this._container.mouseService).pipe(
filter(
([e1, e2]: [MouseEvent, MouseEvent]): boolean => {
return !(e1.ctrlKey && e2.ctrlKey);
}));
}),
withLatestFrom(
this._container.renderService.renderCamera$,
this._navigator.stateService.currentTransform$),
map(
([[previous, current], render, transform]: [[MouseEvent, MouseEvent], RenderCamera, Transform]): number[] => {
const planeNormal: number[] = [0, 0, 1];
const planePoint: number[] = transform.unprojectBasic([0.5, 0.5], 0);
planePoint[2] -= 2;
const currentIntersection: THREE.Vector3 = this._planeIntersection(
current,
planeNormal,
planePoint,
render.perspective,
this._container.element);
const previousIntersection: THREE.Vector3 = this._planeIntersection(
previous,
planeNormal,
planePoint,
render.perspective,
this._container.element);
if (!currentIntersection || !previousIntersection) {
return null;
}
const direction: number[] = new THREE.Vector3()
.subVectors(currentIntersection, previousIntersection)
.multiplyScalar(-1)
.toArray();
return direction;
}),
filter(
(direction: number[]): boolean => {
return !!direction;
}))
.subscribe(
(direction: number[]): void => {
this._navigator.stateService.truck(direction);
});
this._orbitSubscription = earth$.pipe(
switchMap(
(earth: boolean): Observable<[MouseEvent, MouseEvent]> => {
if (!earth) {
return observableEmpty();
}
return MouseOperator.filteredPairwiseMouseDrag$(this._component.name, this._container.mouseService).pipe(
filter(
([e1, e2]: [MouseEvent, MouseEvent]): boolean => {
return e1.ctrlKey && e2.ctrlKey;
}));
}),
map(
([previous, current]: [MouseEvent, MouseEvent]): IRotation => {
const [currentX, currentY]: number[] = this._eventToViewport(current, this._container.element);
const [previousX, previousY]: number[] = this._eventToViewport(previous, this._container.element);
const phi: number = (previousX - currentX) * Math.PI;
const theta: number = (currentY - previousY) * Math.PI / 2;
return { phi: phi, theta: theta };
}))
.subscribe(
(rotation: IRotation): void => {
this._navigator.stateService.orbit(rotation);
});
this._dollySubscription = earth$.pipe(
switchMap(
(earth: boolean): Observable<WheelEvent> => {
if (!earth) {
return observableEmpty();
}
return this._container.mouseService
.filteredWheel$(this._component.name, this._container.mouseService.mouseWheel$);
}),
map(
(event: WheelEvent): number => {
let delta: number = event.deltaY;
if (event.deltaMode === 1) {
delta = 40 * delta;
} else if (event.deltaMode === 2) {
delta = 800 * delta;
}
const canvasSize: number[] = this._viewportCoords.containerToCanvas(this._container.element);
return -delta / canvasSize[1];
}))
.subscribe(
(delta: number): void => {
this._navigator.stateService.dolly(delta);
});
}
protected _disable(): void {
this._dollySubscription.unsubscribe();
this._orbitSubscription.unsubscribe();
this._preventDefaultSubscription.unsubscribe();
this._truckSubscription.unsubscribe();
}
protected _getConfiguration(): IMouseConfiguration {
return { };
}
private _eventToViewport(event: MouseEvent, element: HTMLElement): number[] {
const previousCanvas: number[] = this._viewportCoords.canvasPosition(event, element);
return this._viewportCoords.canvasToViewport(previousCanvas[0], previousCanvas[1], element);
}
private _planeIntersection(
event: MouseEvent,
planeNormal: number[],
planePoint: number[],
camera: THREE.Camera,
element: HTMLElement): THREE.Vector3 {
const [canvasX, canvasY]: number[] = this._viewportCoords.canvasPosition(event, element);
const direction: THREE.Vector3 =
this._viewportCoords
.unprojectFromCanvas(
canvasX,
canvasY,
element,
camera)
.sub(camera.position)
.normalize();
if (Math.abs(this._spatial.angleToPlane(direction.toArray(), planeNormal)) < Math.PI / 90) {
return null;
}
const l0: THREE.Vector3 = camera.position.clone();
const n: THREE.Vector3 = new THREE.Vector3().fromArray(planeNormal);
const p0: THREE.Vector3 = new THREE.Vector3().fromArray(planePoint);
const d: number = new THREE.Vector3().subVectors(p0, l0).dot(n) / direction.clone().dot(n);
const intersection: THREE.Vector3 = new THREE.Vector3().addVectors(l0, direction.multiplyScalar(d));
if (this._viewportCoords.worldToCamera(intersection.toArray(), camera)[2] > 0) {
return null;
}
return intersection;
}
}
export default EarthControlHandler;