public start()

in src/viewer/PanService.ts [119:305]


    public start(): void {
        if (this._mode !== PanMode.Enabled) {
            return;
        }

        const panImages$ = this._stateService.currentImage$.pipe(
            switchMap(
                (current: Image): Observable<[Image, Transform, number][]> => {
                    if (!current.merged || isSpherical(current.cameraType)) {
                        return observableOf([]);
                    }

                    const current$: Observable<Image> = observableOf(current);

                    const bounds: LngLat[] = this._graphCalculator.boundingBoxCorners(current.lngLat, 20);

                    const adjacent$: Observable<Image[]> = this._graphService
                        .cacheBoundingBox$(bounds[0], bounds[1]).pipe(
                            catchError(
                                (error: Error): Observable<Image> => {
                                    console.error(`Failed to cache periphery bounding box (${current.id})`, error);

                                    return observableEmpty();
                                }),
                            map(
                                (images: Image[]): Image[] => {
                                    if (isSpherical(current.cameraType)) {
                                        return [];
                                    }

                                    const potential: Image[] = [];

                                    for (const image of images) {
                                        if (image.id === current.id) {
                                            continue;
                                        }

                                        if (image.mergeId !== current.mergeId) {
                                            continue;
                                        }

                                        if (isSpherical(image.cameraType)) {
                                            continue;
                                        }

                                        if (this._distance(image, current) > 4) {
                                            continue;
                                        }

                                        potential.push(image);
                                    }

                                    return potential;
                                }));

                    return observableCombineLatest(current$, adjacent$).pipe(
                        withLatestFrom(this._stateService.reference$),
                        map(
                            ([[cn, adjacent], reference]: [[Image, Image[]], LngLatAlt]): [Image, Transform, number][] => {
                                const currentDirection: THREE.Vector3 = this._spatial.viewingDirection(cn.rotation);
                                const currentTranslation: number[] = Geo.computeTranslation(
                                    { lat: cn.lngLat.lat, lng: cn.lngLat.lng, alt: cn.computedAltitude },
                                    cn.rotation,
                                    reference);
                                const currentTransform: Transform = this._createTransform(cn, currentTranslation);
                                const currentAzimuthal: number = this._spatial.wrap(
                                    this._spatial.azimuthal(
                                        currentDirection.toArray(),
                                        currentTransform.upVector().toArray()),
                                    0,
                                    2 * Math.PI);

                                const currentProjectedPoints: number[][] = this._computeProjectedPoints(currentTransform);

                                const currentHFov: number = this._computeHorizontalFov(currentProjectedPoints) / 180 * Math.PI;

                                const preferredOverlap: number = Math.PI / 8;
                                let left: [number, Image, Transform, number] = undefined;
                                let right: [number, Image, Transform, number] = undefined;

                                for (const a of adjacent) {
                                    const translation: number[] = Geo.computeTranslation(
                                        { lat: a.lngLat.lat, lng: a.lngLat.lng, alt: a.computedAltitude },
                                        a.rotation,
                                        reference);

                                    const transform: Transform = this._createTransform(a, translation);
                                    const projectedPoints: number[][] = this._computeProjectedPoints(transform);
                                    const hFov: number = this._computeHorizontalFov(projectedPoints) / 180 * Math.PI;

                                    const direction: THREE.Vector3 = this._spatial.viewingDirection(a.rotation);
                                    const azimuthal: number = this._spatial.wrap(
                                        this._spatial.azimuthal(
                                            direction.toArray(),
                                            transform.upVector().toArray()),
                                        0,
                                        2 * Math.PI);

                                    const directionChange: number = this._spatial.angleBetweenVector2(
                                        currentDirection.x,
                                        currentDirection.y,
                                        direction.x,
                                        direction.y);

                                    let overlap: number = Number.NEGATIVE_INFINITY;
                                    if (directionChange > 0) {
                                        if (currentAzimuthal > azimuthal) {
                                            overlap = currentAzimuthal - 2 * Math.PI + currentHFov / 2 - (azimuthal - hFov / 2);
                                        } else {
                                            overlap = currentAzimuthal + currentHFov / 2 - (azimuthal - hFov / 2);
                                        }
                                    } else {
                                        if (currentAzimuthal < azimuthal) {
                                            overlap = azimuthal + hFov / 2 - (currentAzimuthal + 2 * Math.PI - currentHFov / 2);
                                        } else {
                                            overlap = azimuthal + hFov / 2 - (currentAzimuthal - currentHFov / 2);
                                        }
                                    }

                                    const nonOverlap: number = Math.abs(hFov - overlap);

                                    const distanceCost: number = this._distance(a, cn);
                                    const timeCost: number = Math.min(this._timeDifference(a, cn), 4);
                                    const overlapCost: number = 20 * Math.abs(overlap - preferredOverlap);
                                    const fovCost: number = Math.min(5, 1 / Math.min(hFov / currentHFov, 1));
                                    const nonOverlapCost: number = overlap > 0 ? -2 * nonOverlap : 0;

                                    const cost: number = distanceCost + timeCost + overlapCost + fovCost + nonOverlapCost;

                                    if (overlap > 0 &&
                                        overlap < 0.5 * currentHFov &&
                                        overlap < 0.5 * hFov &&
                                        nonOverlap > 0.5 * currentHFov) {

                                        if (directionChange > 0) {
                                            if (!left) {
                                                left = [cost, a, transform, hFov];
                                            } else {
                                                if (cost < left[0]) {
                                                    left = [cost, a, transform, hFov];
                                                }
                                            }
                                        } else {
                                            if (!right) {
                                                right = [cost, a, transform, hFov];
                                            } else {
                                                if (cost < right[0]) {
                                                    right = [cost, a, transform, hFov];
                                                }
                                            }
                                        }
                                    }
                                }

                                const panImagess:
                                    [Image, Transform, number][] = [];

                                if (!!left) {
                                    panImagess.push([left[1], left[2], left[3]]);
                                }

                                if (!!right) {
                                    panImagess.push([right[1], right[2], right[3]]);
                                }

                                return panImagess;
                            }),
                        startWith([]));
                }));

        this._panImagesSubscription = this._stateService.currentState$.pipe(
            map(
                (frame: AnimationFrame): boolean => {
                    return frame.state.imagesAhead > 0;
                }),
            distinctUntilChanged(),
            switchMap(
                (traversing: boolean): Observable<[Image, Transform, number][]> => {
                    return traversing ? observableOf([]) : panImages$;
                }))
            .subscribe(
                (panImages: [Image, Transform, number][]): void => {
                    this._panImagesSubject$.next(panImages);
                });

        this._mode = PanMode.Started;
    }