mapillary-js
Version:
A WebGL interactive street imagery library
148 lines (121 loc) • 5.94 kB
text/typescript
import * as THREE from "three";
import {
empty as observableEmpty,
combineLatest as observableCombineLatest,
Observable,
Subscription,
} from "rxjs";
import {
first,
map,
distinctUntilChanged,
switchMap,
withLatestFrom,
} from "rxjs/operators";
import { Transform } from "../../geo/Transform";
import { Image } from "../../graph/Image";
import { ViewportCoords } from "../../geo/ViewportCoords";
import { RenderCamera } from "../../render/RenderCamera";
import { AnimationFrame } from "../../state/interfaces/AnimationFrame";
import { Container } from "../../viewer/Container";
import { Navigator } from "../../viewer/Navigator";
import { Component } from "../Component";
import { PointerConfiguration } from "../interfaces/PointerConfiguration";
import { HandlerBase } from "../util/HandlerBase";
import { Spatial } from "../../geo/Spatial";
import * as ImageBoundary from "./ImageBoundary";
/**
* The `BounceHandler` ensures that the viewer bounces back to the image
* when drag panning outside of the image edge.
*/
export class BounceHandler extends HandlerBase<PointerConfiguration> {
private _spatial: Spatial;
private _viewportCoords: ViewportCoords;
private _bounceSubscription: Subscription;
constructor(
component: Component<PointerConfiguration>,
container: Container,
navigator: Navigator,
viewportCoords: ViewportCoords,
spatial: Spatial) {
super(component, container, navigator);
this._spatial = spatial;
this._viewportCoords = viewportCoords;
}
protected _enable(): void {
const inTransition$: Observable<boolean> = this._navigator.stateService.currentState$.pipe(
map(
(frame: AnimationFrame): boolean => {
return frame.state.alpha < 1;
}),
distinctUntilChanged());
this._bounceSubscription = observableCombineLatest(
inTransition$,
this._navigator.stateService.inTranslation$,
this._container.mouseService.active$,
this._container.touchService.active$).pipe(
map(
(noForce: boolean[]): boolean => {
return noForce[0] || noForce[1] || noForce[2] || noForce[3];
}),
distinctUntilChanged(),
switchMap(
(noForce: boolean): Observable<[RenderCamera, Transform]> => {
return noForce ?
observableEmpty() :
observableCombineLatest(
this._container.renderService.renderCamera$,
this._navigator.stateService.currentTransform$.pipe(first()));
}),
withLatestFrom(this._navigator.panService.panImages$))
.subscribe(
([[render, transform], nts]: [[RenderCamera, Transform], [Image, Transform, number][]]): void => {
if (!transform.hasValidScale && render.camera.focal < 0.1) {
return;
}
if (render.perspective.aspect === 0 || render.perspective.aspect === Number.POSITIVE_INFINITY) {
return;
}
const distances: number[] = ImageBoundary.viewportDistances(transform, render.perspective, this._viewportCoords);
const basic: number[] = this._viewportCoords.viewportToBasic(0, 0, transform, render.perspective);
if ((basic[0] < 0 || basic[0] > 1) && nts.length > 0) {
distances[0] = distances[2] = 0;
}
for (const [, t] of nts) {
const d: number[] = ImageBoundary.viewportDistances(t, render.perspective, this._viewportCoords);
for (let i: number = 1; i < distances.length; i += 2) {
if (d[i] < distances[i]) {
distances[i] = d[i];
}
}
}
if (Math.max(...distances) < 0.01) {
return;
}
const horizontalDistance: number = distances[1] - distances[3];
const verticalDistance: number = distances[0] - distances[2];
const currentDirection: THREE.Vector3 = this._viewportCoords
.unprojectFromViewport(0, 0, render.perspective)
.sub(render.perspective.position);
const directionPhi: THREE.Vector3 = this._viewportCoords
.unprojectFromViewport(horizontalDistance, 0, render.perspective)
.sub(render.perspective.position);
const directionTheta: THREE.Vector3 = this._viewportCoords
.unprojectFromViewport(0, verticalDistance, render.perspective)
.sub(render.perspective.position);
let phi: number = (horizontalDistance > 0 ? 1 : -1) * directionPhi.angleTo(currentDirection);
let theta: number = (verticalDistance > 0 ? 1 : -1) * directionTheta.angleTo(currentDirection);
const threshold: number = Math.PI / 60;
const coeff: number = 1e-1;
phi = this._spatial.clamp(coeff * phi, -threshold, threshold);
theta = this._spatial.clamp(coeff * theta, -threshold, threshold);
this._navigator.stateService.rotateUnbounded({ phi: phi, theta: theta });
});
}
protected _disable(): void {
this._bounceSubscription.unsubscribe();
}
protected _getConfiguration(): PointerConfiguration {
return {};
}
}