ol
Version:
OpenLayers mapping library
410 lines (372 loc) • 12.9 kB
JavaScript
/**
* Coordinate type when drawing lines.
* @typedef {Array<import("../coordinate.js").Coordinate>} LineCoordType
*/
import {distance} from '../coordinate.js';
import {
GeometryCollection,
LineString,
MultiLineString,
MultiPolygon,
Polygon,
} from '../geom.js';
import {clamp, squaredDistance, toFixed} from '../math.js';
/**
* @param {LineCoordType} coordinates The ring coordinates.
* @param {number} index The index. May be wrapped.
* @return {import("../coordinate.js").Coordinate} The coordinate.
*/
export function getCoordinate(coordinates, index) {
const count = coordinates.length;
if (index < 0) {
return coordinates[index + count];
}
if (index >= count) {
return coordinates[index - count];
}
return coordinates[index];
}
/**
* @param {LineCoordType} coordinates The coordinates.
* @param {number} index The index. May be fractional and may wrap.
* @return {import("../coordinate.js").Coordinate} The interpolated coordinate.
*/
export function interpolateCoordinate(coordinates, index) {
const count = coordinates.length;
let startIndex = Math.floor(index);
const along = index - startIndex;
if (startIndex >= count) {
startIndex -= count;
} else if (startIndex < 0) {
startIndex += count;
}
let endIndex = startIndex + 1;
if (endIndex >= count) {
endIndex -= count;
}
const start = coordinates[startIndex];
const x0 = start[0];
const y0 = start[1];
const end = coordinates[endIndex];
const dx = end[0] - x0;
const dy = end[1] - y0;
return [x0 + dx * along, y0 + dy * along];
}
/**
* @typedef {Object} TraceTarget
* @property {Array<import("../coordinate.js").Coordinate>} coordinates Target coordinates.
* @property {boolean} ring The target coordinates are a linear ring.
* @property {number} startIndex The index of first traced coordinate. A fractional index represents an
* edge intersection. Index values for rings will wrap (may be negative or larger than coordinates length).
* @property {number} endIndex The index of last traced coordinate. Details from startIndex also apply here.
*/
/**
* @typedef {Object} TraceState
* @property {boolean} active Tracing active.
* @property {import("../coordinate.js").Coordinate} [startCoord] The initially clicked coordinate.
* @property {Array<TraceTarget>} [targets] Targets available for tracing.
* @property {number} [targetIndex] The index of the currently traced target. A value of -1 indicates
* that no trace target is active.
*/
/**
* @typedef {Object} TraceTargetUpdateInfo
* @property {number} index The new target index.
* @property {number} endIndex The new segment end index.
* @property {number} closestTargetDistance The squared distance to the closest target.
*/
/**
* @type {TraceTargetUpdateInfo}
*/
const sharedUpdateInfo = {
index: -1,
endIndex: NaN,
closestTargetDistance: Infinity,
};
/**
* @param {import("../coordinate.js").Coordinate} coordinate The coordinate.
* @param {TraceState} traceState The trace state.
* @param {import("../Map.js").default} map The map.
* @param {number} snapTolerance The snap tolerance.
* @return {TraceTargetUpdateInfo} Information about the new trace target. The returned
* object is reused between calls and must not be modified by the caller.
*/
export function getTraceTargetUpdate(
coordinate,
traceState,
map,
snapTolerance,
) {
const x = coordinate[0];
const y = coordinate[1];
let closestTargetDistance = Infinity;
let newTargetIndex = -1;
let newEndIndex = NaN;
for (
let targetIndex = 0;
targetIndex < traceState.targets.length;
++targetIndex
) {
const target = traceState.targets[targetIndex];
const coordinates = target.coordinates;
let minSegmentDistance = Infinity;
let endIndex;
for (
let coordinateIndex = 0;
coordinateIndex < coordinates.length - 1;
++coordinateIndex
) {
const start = coordinates[coordinateIndex];
const end = coordinates[coordinateIndex + 1];
const rel = getPointSegmentRelationship(x, y, start, end);
if (rel.squaredDistance < minSegmentDistance) {
minSegmentDistance = rel.squaredDistance;
endIndex = coordinateIndex + rel.along;
}
}
if (minSegmentDistance < closestTargetDistance) {
closestTargetDistance = minSegmentDistance;
if (target.ring && traceState.targetIndex === targetIndex) {
// same target, maintain the same trace direction
if (target.endIndex > target.startIndex) {
// forward trace
if (endIndex < target.startIndex) {
endIndex += coordinates.length;
}
} else if (target.endIndex < target.startIndex) {
// reverse trace
if (endIndex > target.startIndex) {
endIndex -= coordinates.length;
}
}
}
newEndIndex = endIndex;
newTargetIndex = targetIndex;
}
}
const newTarget = traceState.targets[newTargetIndex];
let considerBothDirections = newTarget.ring;
if (traceState.targetIndex === newTargetIndex && considerBothDirections) {
// only consider switching trace direction if close to the start
const newCoordinate = interpolateCoordinate(
newTarget.coordinates,
newEndIndex,
);
const pixel = map.getPixelFromCoordinate(newCoordinate);
const startPx = map.getPixelFromCoordinate(traceState.startCoord);
if (distance(pixel, startPx) > snapTolerance) {
considerBothDirections = false;
}
}
if (considerBothDirections) {
const coordinates = newTarget.coordinates;
const count = coordinates.length;
const startIndex = newTarget.startIndex;
const endIndex = newEndIndex;
if (startIndex < endIndex) {
const forwardDistance = getCumulativeSquaredDistance(
coordinates,
startIndex,
endIndex,
);
const reverseDistance = getCumulativeSquaredDistance(
coordinates,
startIndex,
endIndex - count,
);
if (reverseDistance < forwardDistance) {
newEndIndex -= count;
}
} else {
const reverseDistance = getCumulativeSquaredDistance(
coordinates,
startIndex,
endIndex,
);
const forwardDistance = getCumulativeSquaredDistance(
coordinates,
startIndex,
endIndex + count,
);
if (forwardDistance < reverseDistance) {
newEndIndex += count;
}
}
}
sharedUpdateInfo.index = newTargetIndex;
sharedUpdateInfo.endIndex = newEndIndex;
sharedUpdateInfo.closestTargetDistance = closestTargetDistance;
return sharedUpdateInfo;
}
/**
* @param {import("../coordinate.js").Coordinate} coordinate The coordinate.
* @param {Array<import("../Feature.js").default>} features The candidate features.
* @return {Array<TraceTarget>} The trace targets.
*/
export function getTraceTargets(coordinate, features) {
/**
* @type {Array<TraceTarget>}
*/
const targets = [];
for (let i = 0; i < features.length; ++i) {
const feature = features[i];
const geometry = feature.getGeometry();
appendGeometryTraceTargets(coordinate, geometry, targets);
}
return targets;
}
/**
* @param {import("../coordinate.js").Coordinate} coordinate The coordinate.
* @param {import("../geom/Geometry.js").default} geometry The candidate geometry.
* @param {Array<TraceTarget>} targets The trace targets.
*/
function appendGeometryTraceTargets(coordinate, geometry, targets) {
if (geometry instanceof LineString) {
appendTraceTarget(coordinate, geometry.getCoordinates(), false, targets);
return;
}
if (geometry instanceof MultiLineString) {
const coordinates = geometry.getCoordinates();
for (let i = 0, ii = coordinates.length; i < ii; ++i) {
appendTraceTarget(coordinate, coordinates[i], false, targets);
}
return;
}
if (geometry instanceof Polygon) {
const coordinates = geometry.getCoordinates();
for (let i = 0, ii = coordinates.length; i < ii; ++i) {
appendTraceTarget(coordinate, coordinates[i], true, targets);
}
return;
}
if (geometry instanceof MultiPolygon) {
const polys = geometry.getCoordinates();
for (let i = 0, ii = polys.length; i < ii; ++i) {
const coordinates = polys[i];
for (let j = 0, jj = coordinates.length; j < jj; ++j) {
appendTraceTarget(coordinate, coordinates[j], true, targets);
}
}
return;
}
if (geometry instanceof GeometryCollection) {
const geometries = geometry.getGeometries();
for (let i = 0; i < geometries.length; ++i) {
appendGeometryTraceTargets(coordinate, geometries[i], targets);
}
return;
}
// other types cannot be traced
}
/**
* @param {import("../coordinate.js").Coordinate} coordinate The clicked coordinate.
* @param {Array<import("../coordinate.js").Coordinate>} coordinates The geometry component coordinates.
* @param {boolean} ring The coordinates represent a linear ring.
* @param {Array<TraceTarget>} targets The trace targets.
*/
function appendTraceTarget(coordinate, coordinates, ring, targets) {
const x = coordinate[0];
const y = coordinate[1];
for (let i = 0, ii = coordinates.length - 1; i < ii; ++i) {
const start = coordinates[i];
const end = coordinates[i + 1];
const rel = getPointSegmentRelationship(x, y, start, end);
if (rel.squaredDistance === 0) {
const index = i + rel.along;
targets.push({
coordinates: coordinates,
ring: ring,
startIndex: index,
endIndex: index,
});
return;
}
}
}
/**
* @param {import("../coordinate.js").Coordinate} a One coordinate.
* @param {import("../coordinate.js").Coordinate} b Another coordinate.
* @return {number} The squared distance between the two coordinates.
*/
function getSquaredDistance(a, b) {
return squaredDistance(a[0], a[1], b[0], b[1]);
}
/**
* Get the cumulative squared distance along a ring path. The end index index may be "wrapped" and it may
* be less than the start index to indicate the direction of travel. The start and end index may have
* a fractional part to indicate a point between two coordinates.
* @param {LineCoordType} coordinates Ring coordinates.
* @param {number} startIndex The start index.
* @param {number} endIndex The end index.
* @return {number} The cumulative squared distance along the ring path.
*/
function getCumulativeSquaredDistance(coordinates, startIndex, endIndex) {
let lowIndex, highIndex;
if (startIndex < endIndex) {
lowIndex = startIndex;
highIndex = endIndex;
} else {
lowIndex = endIndex;
highIndex = startIndex;
}
const lowWholeIndex = Math.ceil(lowIndex);
const highWholeIndex = Math.floor(highIndex);
if (lowWholeIndex > highWholeIndex) {
// both start and end are on the same segment
const start = interpolateCoordinate(coordinates, lowIndex);
const end = interpolateCoordinate(coordinates, highIndex);
return getSquaredDistance(start, end);
}
let sd = 0;
if (lowIndex < lowWholeIndex) {
const start = interpolateCoordinate(coordinates, lowIndex);
const end = getCoordinate(coordinates, lowWholeIndex);
sd += getSquaredDistance(start, end);
}
if (highWholeIndex < highIndex) {
const start = getCoordinate(coordinates, highWholeIndex);
const end = interpolateCoordinate(coordinates, highIndex);
sd += getSquaredDistance(start, end);
}
for (let i = lowWholeIndex; i < highWholeIndex - 1; ++i) {
const start = getCoordinate(coordinates, i);
const end = getCoordinate(coordinates, i + 1);
sd += getSquaredDistance(start, end);
}
return sd;
}
/**
* @typedef {Object} PointSegmentRelationship
* @property {number} along The closest point expressed as a fraction along the segment length.
* @property {number} squaredDistance The squared distance of the point to the segment.
*/
/**
* @type {PointSegmentRelationship}
*/
const sharedRel = {along: 0, squaredDistance: 0};
/**
* @param {number} x The point x.
* @param {number} y The point y.
* @param {import("../coordinate.js").Coordinate} start The segment start.
* @param {import("../coordinate.js").Coordinate} end The segment end.
* @return {PointSegmentRelationship} The point segment relationship. The returned object is
* shared between calls and must not be modified by the caller.
*/
export function getPointSegmentRelationship(x, y, start, end) {
const x1 = start[0];
const y1 = start[1];
const x2 = end[0];
const y2 = end[1];
const dx = x2 - x1;
const dy = y2 - y1;
let along = 0;
let px = x1;
let py = y1;
if (dx !== 0 || dy !== 0) {
along = clamp(((x - x1) * dx + (y - y1) * dy) / (dx * dx + dy * dy), 0, 1);
px += dx * along;
py += dy * along;
}
sharedRel.along = along;
sharedRel.squaredDistance = toFixed(squaredDistance(x, y, px, py), 10);
return sharedRel;
}