mapillary-js
Version:
WebGL JavaScript library for displaying street level imagery from mapillary.com
781 lines (642 loc) • 28 kB
text/typescript
import * as THREE from "three";
import {
Node,
Sequence,
} from "../../Graph";
import
{
EdgeDirection,
IStep,
ITurn,
IPano,
IEdge,
IPotentialEdge,
EdgeCalculatorSettings,
EdgeCalculatorDirections,
EdgeCalculatorCoefficients,
} from "../../Edge";
import {ArgumentMapillaryError} from "../../Error";
import {GeoCoords, Spatial} from "../../Geo";
/**
* @class EdgeCalculator
*
* @classdesc Represents a class for calculating node edges.
*/
export class EdgeCalculator {
private _spatial: Spatial;
private _geoCoords: GeoCoords;
private _settings: EdgeCalculatorSettings;
private _directions: EdgeCalculatorDirections;
private _coefficients: EdgeCalculatorCoefficients;
/**
* Create a new edge calculator instance.
*
* @param {EdgeCalculatorSettings} settings - Settings struct.
* @param {EdgeCalculatorDirections} directions - Directions struct.
* @param {EdgeCalculatorCoefficients} coefficients - Coefficients struct.
*/
constructor(
settings?: EdgeCalculatorSettings,
directions?: EdgeCalculatorDirections,
coefficients?: EdgeCalculatorCoefficients) {
this._spatial = new Spatial();
this._geoCoords = new GeoCoords();
this._settings = settings != null ? settings : new EdgeCalculatorSettings();
this._directions = directions != null ? directions : new EdgeCalculatorDirections();
this._coefficients = coefficients != null ? coefficients : new EdgeCalculatorCoefficients();
}
/**
* Returns the potential edges to destination nodes for a set
* of nodes with respect to a source node.
*
* @param {Node} node - Source node.
* @param {Array<Node>} nodes - Potential destination nodes.
* @param {Array<string>} fallbackKeys - Keys for destination nodes that should
* be returned even if they do not meet the criteria for a potential edge.
* @throws {ArgumentMapillaryError} If node is not full.
*/
public getPotentialEdges(node: Node, potentialNodes: Node[], fallbackKeys: string[]): IPotentialEdge[] {
if (!node.full) {
throw new ArgumentMapillaryError("Node has to be full.");
}
if (!node.merged) {
return [];
}
let currentDirection: THREE.Vector3 =
this._spatial.viewingDirection(node.rotation);
let currentVerticalDirection: number =
this._spatial.angleToPlane(currentDirection.toArray(), [0, 0, 1]);
let potentialEdges: IPotentialEdge[] = [];
for (let potential of potentialNodes) {
if (!potential.merged ||
potential.key === node.key) {
continue;
}
let enu: number[] = this._geoCoords.geodeticToEnu(
potential.latLon.lat,
potential.latLon.lon,
potential.alt,
node.latLon.lat,
node.latLon.lon,
node.alt);
let motion: THREE.Vector3 = new THREE.Vector3(enu[0], enu[1], enu[2]);
let distance: number = motion.length();
if (distance > this._settings.maxDistance &&
fallbackKeys.indexOf(potential.key) < 0) {
continue;
}
let motionChange: number = this._spatial.angleBetweenVector2(
currentDirection.x,
currentDirection.y,
motion.x,
motion.y);
let verticalMotion: number = this._spatial.angleToPlane(motion.toArray(), [0, 0, 1]);
let direction: THREE.Vector3 =
this._spatial.viewingDirection(potential.rotation);
let directionChange: number = this._spatial.angleBetweenVector2(
currentDirection.x,
currentDirection.y,
direction.x,
direction.y);
let verticalDirection: number = this._spatial.angleToPlane(direction.toArray(), [0, 0, 1]);
let verticalDirectionChange: number = verticalDirection - currentVerticalDirection;
let rotation: number = this._spatial.relativeRotationAngle(
node.rotation,
potential.rotation);
let worldMotionAzimuth: number =
this._spatial.angleBetweenVector2(1, 0, motion.x, motion.y);
let sameSequence: boolean = potential.sequenceKey != null &&
node.sequenceKey != null &&
potential.sequenceKey === node.sequenceKey;
let sameMergeCC: boolean =
(potential.mergeCC == null && node.mergeCC == null) ||
potential.mergeCC === node.mergeCC;
let sameUser: boolean =
potential.userKey === node.userKey;
let potentialEdge: IPotentialEdge = {
capturedAt: potential.capturedAt,
croppedPano: potential.pano && !potential.fullPano,
directionChange: directionChange,
distance: distance,
fullPano: potential.fullPano,
key: potential.key,
motionChange: motionChange,
rotation: rotation,
sameMergeCC: sameMergeCC,
sameSequence: sameSequence,
sameUser: sameUser,
sequenceKey: potential.sequenceKey,
verticalDirectionChange: verticalDirectionChange,
verticalMotion: verticalMotion,
worldMotionAzimuth: worldMotionAzimuth,
};
potentialEdges.push(potentialEdge);
}
return potentialEdges;
}
/**
* Computes the sequence edges for a node.
*
* @param {Node} node - Source node.
* @throws {ArgumentMapillaryError} If node is not full.
*/
public computeSequenceEdges(node: Node, sequence: Sequence): IEdge[] {
if (!node.full) {
throw new ArgumentMapillaryError("Node has to be full.");
}
if (node.sequenceKey !== sequence.key) {
throw new ArgumentMapillaryError("Node and sequence does not correspond.");
}
let edges: IEdge[] = [];
let nextKey: string = sequence.findNextKey(node.key);
if (nextKey != null) {
edges.push({
data: {
direction: EdgeDirection.Next,
worldMotionAzimuth: Number.NaN,
},
from: node.key,
to: nextKey,
});
}
let prevKey: string = sequence.findPrevKey(node.key);
if (prevKey != null) {
edges.push({
data: {
direction: EdgeDirection.Prev,
worldMotionAzimuth: Number.NaN,
},
from: node.key,
to: prevKey,
});
}
return edges;
}
/**
* Computes the similar edges for a node.
*
* @description Similar edges for perspective images and cropped panoramas
* look roughly in the same direction and are positioned closed to the node.
* Similar edges for full panoramas only target other full panoramas.
*
* @param {Node} node - Source node.
* @param {Array<IPotentialEdge>} potentialEdges - Potential edges.
* @throws {ArgumentMapillaryError} If node is not full.
*/
public computeSimilarEdges(node: Node, potentialEdges: IPotentialEdge[]): IEdge[] {
if (!node.full) {
throw new ArgumentMapillaryError("Node has to be full.");
}
let nodeFullPano: boolean = node.fullPano;
let sequenceGroups: { [key: string]: IPotentialEdge[] } = {};
for (let potentialEdge of potentialEdges) {
if (potentialEdge.sequenceKey == null) {
continue;
}
if (potentialEdge.sameSequence) {
continue;
}
if (nodeFullPano) {
if (!potentialEdge.fullPano) {
continue;
}
} else {
if (!potentialEdge.fullPano &&
Math.abs(potentialEdge.directionChange) > this._settings.similarMaxDirectionChange) {
continue;
}
}
if (potentialEdge.distance > this._settings.similarMaxDistance) {
continue;
}
if (potentialEdge.sameUser &&
Math.abs(potentialEdge.capturedAt - node.capturedAt) <
this._settings.similarMinTimeDifference) {
continue;
}
if (sequenceGroups[potentialEdge.sequenceKey] == null) {
sequenceGroups[potentialEdge.sequenceKey] = [];
}
sequenceGroups[potentialEdge.sequenceKey].push(potentialEdge);
}
let similarEdges: IPotentialEdge[] = [];
let calculateScore: (potentialEdge: IPotentialEdge) => number =
node.fullPano ?
(potentialEdge: IPotentialEdge): number => {
return potentialEdge.distance;
} :
(potentialEdge: IPotentialEdge): number => {
return this._coefficients.similarDistance * potentialEdge.distance +
this._coefficients.similarRotation * potentialEdge.rotation;
};
for (let sequenceKey in sequenceGroups) {
if (!sequenceGroups.hasOwnProperty(sequenceKey)) {
continue;
}
let lowestScore: number = Number.MAX_VALUE;
let similarEdge: IPotentialEdge = null;
for (let potentialEdge of sequenceGroups[sequenceKey]) {
let score: number = calculateScore(potentialEdge);
if (score < lowestScore) {
lowestScore = score;
similarEdge = potentialEdge;
}
}
if (similarEdge == null) {
continue;
}
similarEdges.push(similarEdge);
}
return similarEdges
.map<IEdge>(
(potentialEdge: IPotentialEdge): IEdge => {
return {
data: {
direction: EdgeDirection.Similar,
worldMotionAzimuth: potentialEdge.worldMotionAzimuth,
},
from: node.key,
to: potentialEdge.key,
};
});
}
/**
* Computes the step edges for a perspective node.
*
* @description Step edge targets can only be other perspective nodes.
* Returns an empty array for cropped and full panoramas.
*
* @param {Node} node - Source node.
* @param {Array<IPotentialEdge>} potentialEdges - Potential edges.
* @param {string} prevKey - Key of previous node in sequence.
* @param {string} prevKey - Key of next node in sequence.
* @throws {ArgumentMapillaryError} If node is not full.
*/
public computeStepEdges(
node: Node,
potentialEdges: IPotentialEdge[],
prevKey: string,
nextKey: string): IEdge[] {
if (!node.full) {
throw new ArgumentMapillaryError("Node has to be full.");
}
let edges: IEdge[] = [];
if (node.pano) {
return edges;
}
for (let k in this._directions.steps) {
if (!this._directions.steps.hasOwnProperty(k)) {
continue;
}
let step: IStep = this._directions.steps[k];
let lowestScore: number = Number.MAX_VALUE;
let edge: IPotentialEdge = null;
let fallback: IPotentialEdge = null;
for (let potential of potentialEdges) {
if (potential.croppedPano || potential.fullPano) {
continue;
}
if (Math.abs(potential.directionChange) > this._settings.stepMaxDirectionChange) {
continue;
}
let motionDifference: number =
this._spatial.angleDifference(step.motionChange, potential.motionChange);
let directionMotionDifference: number =
this._spatial.angleDifference(potential.directionChange, motionDifference);
let drift: number =
Math.max(Math.abs(motionDifference), Math.abs(directionMotionDifference));
if (Math.abs(drift) > this._settings.stepMaxDrift) {
continue;
}
let potentialKey: string = potential.key;
if (step.useFallback && (potentialKey === prevKey || potentialKey === nextKey)) {
fallback = potential;
}
if (potential.distance > this._settings.stepMaxDistance) {
continue;
}
motionDifference = Math.sqrt(
motionDifference * motionDifference +
potential.verticalMotion * potential.verticalMotion);
let score: number =
this._coefficients.stepPreferredDistance *
Math.abs(potential.distance - this._settings.stepPreferredDistance) /
this._settings.stepMaxDistance +
this._coefficients.stepMotion * motionDifference / this._settings.stepMaxDrift +
this._coefficients.stepRotation * potential.rotation / this._settings.stepMaxDirectionChange +
this._coefficients.stepSequencePenalty * (potential.sameSequence ? 0 : 1) +
this._coefficients.stepMergeCCPenalty * (potential.sameMergeCC ? 0 : 1);
if (score < lowestScore) {
lowestScore = score;
edge = potential;
}
}
edge = edge == null ? fallback : edge;
if (edge != null) {
edges.push({
data: {
direction: step.direction,
worldMotionAzimuth: edge.worldMotionAzimuth,
},
from: node.key,
to: edge.key,
});
}
}
return edges;
}
/**
* Computes the turn edges for a perspective node.
*
* @description Turn edge targets can only be other perspective images.
* Returns an empty array for cropped and full panoramas.
*
* @param {Node} node - Source node.
* @param {Array<IPotentialEdge>} potentialEdges - Potential edges.
* @throws {ArgumentMapillaryError} If node is not full.
*/
public computeTurnEdges(node: Node, potentialEdges: IPotentialEdge[]): IEdge[] {
if (!node.full) {
throw new ArgumentMapillaryError("Node has to be full.");
}
let edges: IEdge[] = [];
if (node.pano) {
return edges;
}
for (let k in this._directions.turns) {
if (!this._directions.turns.hasOwnProperty(k)) {
continue;
}
let turn: ITurn = this._directions.turns[k];
let lowestScore: number = Number.MAX_VALUE;
let edge: IPotentialEdge = null;
for (let potential of potentialEdges) {
if (potential.croppedPano || potential.fullPano) {
continue;
}
if (potential.distance > this._settings.turnMaxDistance) {
continue;
}
let rig: boolean =
turn.direction !== EdgeDirection.TurnU &&
potential.distance < this._settings.turnMaxRigDistance &&
Math.abs(potential.directionChange) > this._settings.turnMinRigDirectionChange;
let directionDifference: number = this._spatial.angleDifference(
turn.directionChange, potential.directionChange);
let score: number;
if (
rig &&
potential.directionChange * turn.directionChange > 0 &&
Math.abs(potential.directionChange) < Math.abs(turn.directionChange)) {
score = -Math.PI / 2 + Math.abs(potential.directionChange);
} else {
if (Math.abs(directionDifference) > this._settings.turnMaxDirectionChange) {
continue;
}
let motionDifference: number = turn.motionChange ?
this._spatial.angleDifference(turn.motionChange, potential.motionChange) : 0;
motionDifference = Math.sqrt(
motionDifference * motionDifference +
potential.verticalMotion * potential.verticalMotion);
score =
this._coefficients.turnDistance * potential.distance /
this._settings.turnMaxDistance +
this._coefficients.turnMotion * motionDifference / Math.PI +
this._coefficients.turnSequencePenalty * (potential.sameSequence ? 0 : 1) +
this._coefficients.turnMergeCCPenalty * (potential.sameMergeCC ? 0 : 1);
}
if (score < lowestScore) {
lowestScore = score;
edge = potential;
}
}
if (edge != null) {
edges.push({
data: {
direction: turn.direction,
worldMotionAzimuth: edge.worldMotionAzimuth,
},
from: node.key,
to: edge.key,
});
}
}
return edges;
}
/**
* Computes the pano edges for a perspective node.
*
* @description Perspective to pano edge targets can only be
* full pano nodes. Returns an empty array for cropped and full panoramas.
*
* @param {Node} node - Source node.
* @param {Array<IPotentialEdge>} potentialEdges - Potential edges.
* @throws {ArgumentMapillaryError} If node is not full.
*/
public computePerspectiveToPanoEdges(node: Node, potentialEdges: IPotentialEdge[]): IEdge[] {
if (!node.full) {
throw new ArgumentMapillaryError("Node has to be full.");
}
if (node.pano) {
return [];
}
let lowestScore: number = Number.MAX_VALUE;
let edge: IPotentialEdge = null;
for (let potential of potentialEdges) {
if (!potential.fullPano) {
continue;
}
let score: number =
this._coefficients.panoPreferredDistance *
Math.abs(potential.distance - this._settings.panoPreferredDistance) /
this._settings.panoMaxDistance +
this._coefficients.panoMotion * Math.abs(potential.motionChange) / Math.PI +
this._coefficients.panoMergeCCPenalty * (potential.sameMergeCC ? 0 : 1);
if (score < lowestScore) {
lowestScore = score;
edge = potential;
}
}
if (edge == null) {
return [];
}
return [
{
data: {
direction: EdgeDirection.Pano,
worldMotionAzimuth: edge.worldMotionAzimuth,
},
from: node.key,
to: edge.key,
},
];
}
/**
* Computes the full pano and step edges for a full pano node.
*
* @description Pano to pano edge targets can only be
* full pano nodes. Pano to step edge targets can only be perspective
* nodes.
* Returns an empty array for cropped panoramas and perspective nodes.
*
* @param {Node} node - Source node.
* @param {Array<IPotentialEdge>} potentialEdges - Potential edges.
* @throws {ArgumentMapillaryError} If node is not full.
*/
public computePanoEdges(node: Node, potentialEdges: IPotentialEdge[]): IEdge[] {
if (!node.full) {
throw new ArgumentMapillaryError("Node has to be full.");
}
if (!node.fullPano) {
return [];
}
let panoEdges: IEdge[] = [];
let potentialPanos: IPotentialEdge[] = [];
let potentialSteps: [EdgeDirection, IPotentialEdge][] = [];
for (let potential of potentialEdges) {
if (potential.distance > this._settings.panoMaxDistance) {
continue;
}
if (potential.fullPano) {
if (potential.distance < this._settings.panoMinDistance) {
continue;
}
potentialPanos.push(potential);
} else {
if (potential.croppedPano) {
continue;
}
for (let k in this._directions.panos) {
if (!this._directions.panos.hasOwnProperty(k)) {
continue;
}
let pano: IPano = this._directions.panos[k];
let turn: number = this._spatial.angleDifference(
potential.directionChange,
potential.motionChange);
let turnChange: number = this._spatial.angleDifference(pano.directionChange, turn);
if (Math.abs(turnChange) > this._settings.panoMaxStepTurnChange) {
continue;
}
potentialSteps.push([pano.direction, potential]);
// break if step direction found
break;
}
}
}
let maxRotationDifference: number = Math.PI / this._settings.panoMaxItems;
let occupiedAngles: number[] = [];
let stepAngles: number[] = [];
for (let index: number = 0; index < this._settings.panoMaxItems; index++) {
let rotation: number = index / this._settings.panoMaxItems * 2 * Math.PI;
let lowestScore: number = Number.MAX_VALUE;
let edge: IPotentialEdge = null;
for (let potential of potentialPanos) {
let motionDifference: number = this._spatial.angleDifference(rotation, potential.motionChange);
if (Math.abs(motionDifference) > maxRotationDifference) {
continue;
}
let occupiedDifference: number = Number.MAX_VALUE;
for (let occupiedAngle of occupiedAngles) {
let difference: number = Math.abs(this._spatial.angleDifference(occupiedAngle, potential.motionChange));
if (difference < occupiedDifference) {
occupiedDifference = difference;
}
}
if (occupiedDifference <= maxRotationDifference) {
continue;
}
let score: number =
this._coefficients.panoPreferredDistance *
Math.abs(potential.distance - this._settings.panoPreferredDistance) /
this._settings.panoMaxDistance +
this._coefficients.panoMotion * Math.abs(motionDifference) / maxRotationDifference +
this._coefficients.panoSequencePenalty * (potential.sameSequence ? 0 : 1) +
this._coefficients.panoMergeCCPenalty * (potential.sameMergeCC ? 0 : 1);
if (score < lowestScore) {
lowestScore = score;
edge = potential;
}
}
if (edge != null) {
occupiedAngles.push(edge.motionChange);
panoEdges.push({
data: {
direction: EdgeDirection.Pano,
worldMotionAzimuth: edge.worldMotionAzimuth,
},
from: node.key,
to: edge.key,
});
} else {
stepAngles.push(rotation);
}
}
let occupiedStepAngles: {[direction: string]: number[] } = {};
occupiedStepAngles[EdgeDirection.Pano] = occupiedAngles;
occupiedStepAngles[EdgeDirection.StepForward] = [];
occupiedStepAngles[EdgeDirection.StepLeft] = [];
occupiedStepAngles[EdgeDirection.StepBackward] = [];
occupiedStepAngles[EdgeDirection.StepRight] = [];
for (let stepAngle of stepAngles) {
let occupations: [EdgeDirection, IPotentialEdge][] = [];
for (let k in this._directions.panos) {
if (!this._directions.panos.hasOwnProperty(k)) {
continue;
}
let pano: IPano = this._directions.panos[k];
let allOccupiedAngles: number[] = occupiedStepAngles[EdgeDirection.Pano]
.concat(occupiedStepAngles[pano.direction])
.concat(occupiedStepAngles[pano.prev])
.concat(occupiedStepAngles[pano.next]);
let lowestScore: number = Number.MAX_VALUE;
let edge: [EdgeDirection, IPotentialEdge] = null;
for (let potential of potentialSteps) {
if (potential[0] !== pano.direction) {
continue;
}
let motionChange: number = this._spatial.angleDifference(stepAngle, potential[1].motionChange);
if (Math.abs(motionChange) > maxRotationDifference) {
continue;
}
let minOccupiedDifference: number = Number.MAX_VALUE;
for (let occupiedAngle of allOccupiedAngles) {
let occupiedDifference: number =
Math.abs(this._spatial.angleDifference(occupiedAngle, potential[1].motionChange));
if (occupiedDifference < minOccupiedDifference) {
minOccupiedDifference = occupiedDifference;
}
}
if (minOccupiedDifference <= maxRotationDifference) {
continue;
}
let score: number = this._coefficients.panoPreferredDistance *
Math.abs(potential[1].distance - this._settings.panoPreferredDistance) /
this._settings.panoMaxDistance +
this._coefficients.panoMotion * Math.abs(motionChange) / maxRotationDifference +
this._coefficients.panoMergeCCPenalty * (potential[1].sameMergeCC ? 0 : 1);
if (score < lowestScore) {
lowestScore = score;
edge = potential;
}
}
if (edge != null) {
occupations.push(edge);
panoEdges.push({
data: {
direction: edge[0],
worldMotionAzimuth: edge[1].worldMotionAzimuth,
},
from: node.key,
to: edge[1].key,
});
}
}
for (let occupation of occupations) {
occupiedStepAngles[occupation[0]].push(occupation[1].motionChange);
}
}
return panoEdges;
}
}
export default EdgeCalculator;