diff --git a/src/closest.ts b/src/closest.ts new file mode 100644 index 00000000..73c26c5e --- /dev/null +++ b/src/closest.ts @@ -0,0 +1,171 @@ +/** + * @module ol/geom/flat/closest + */ +import {lerp, squaredDistance as squaredDx} from 'ol/math.js'; + +/* + * Returns the point on the 2D line segment flatCoordinates[offset1] to + * flatCoordinates[offset2] that is closest to the point (x, y). Extra + * dimensions are linearly interpolated. + */ +function assignClosest( + flatCoordinates: number[], + offset1: number, + offset2: number, + stride: number, + x: number, + y: number, + closestPoint: number[] +) { + const x1 = flatCoordinates[offset1]; + const y1 = flatCoordinates[offset1 + 1]; + const dx = flatCoordinates[offset2] - x1; + const dy = flatCoordinates[offset2 + 1] - y1; + let offset; + if (dx === 0 && dy === 0) { + offset = offset1; + } else { + const t = ((x - x1) * dx + (y - y1) * dy) / (dx * dx + dy * dy); + if (t > 1) { + offset = offset2; + } else if (t > 0) { + for (let i = 0; i < stride; ++i) { + closestPoint[i] = lerp( + flatCoordinates[offset1 + i], + flatCoordinates[offset2 + i], + t + ); + } + closestPoint.length = stride; + return; + } else { + offset = offset1; + } + } + for (let i = 0; i < stride; ++i) { + closestPoint[i] = flatCoordinates[offset + i]; + } + closestPoint.length = stride; +} + +type ClosestPointResult = { + totalSquaredDistance: number; + squaredDistance: number; + closestPoint: number[]; +} + +export function getClosestPoint( + flatCoordinates: number[], + offset: number, + end: number, + stride: number, + maxDelta: number, + x: number, + y: number +): ClosestPointResult { + let minSquaredDistance = Infinity; + const closestPoint = [NaN, NaN]; + let minIndex; + if (offset == end) { + return { + closestPoint: closestPoint, + squaredDistance: minSquaredDistance, + totalSquaredDistance: minSquaredDistance, + }; + } + let i, squaredDistance; + if (maxDelta === 0) { + // All points are identical, so just test the first point. + squaredDistance = squaredDx( + x, + y, + flatCoordinates[offset], + flatCoordinates[offset + 1] + ); + if (squaredDistance < minSquaredDistance) { + for (i = 0; i < stride; ++i) { + closestPoint[i] = flatCoordinates[offset + i]; + } + closestPoint.length = stride; + return { + closestPoint: closestPoint, + squaredDistance: squaredDistance, + totalSquaredDistance: squaredDx( + flatCoordinates[offset], + flatCoordinates[offset + 1], + closestPoint[0], + closestPoint[1] + ), + }; + } + return { + closestPoint: closestPoint, + squaredDistance: minSquaredDistance, + totalSquaredDistance: minSquaredDistance, + }; + } + const tmpPoint = [NaN, NaN]; + let index = offset + stride; + while (index < end) { + assignClosest( + flatCoordinates, + index - stride, + index, + stride, + x, + y, + tmpPoint + ); + squaredDistance = squaredDx(x, y, tmpPoint[0], tmpPoint[1]); + if (squaredDistance < minSquaredDistance) { + minSquaredDistance = squaredDistance; + minIndex = index; + for (i = 0; i < stride; ++i) { + closestPoint[i] = tmpPoint[i]; + } + closestPoint.length = stride; + index += stride; + } else { + // console.log('no match', index); + // Skip ahead multiple points, because we know that all the skipped + // points cannot be any closer than the closest point we have found so + // far. We know this because we know how close the current point is, how + // close the closest point we have found so far is, and the maximum + // distance between consecutive points. For example, if we're currently + // at distance 10, the best we've found so far is 3, and that the maximum + // distance between consecutive points is 2, then we'll need to skip at + // least (10 - 3) / 2 == 3 (rounded down) points to have any chance of + // finding a closer point. We use Math.max(..., 1) to ensure that we + // always advance at least one point, to avoid an infinite loop. + index += + stride * + Math.max( + ((Math.sqrt(squaredDistance) - Math.sqrt(minSquaredDistance)) / + maxDelta) | + 0, + 1 + ); + } + } + let totalSquaredDistance = 0; + for (i = offset + stride; i < minIndex; ++i) { + totalSquaredDistance += squaredDx( + flatCoordinates[i - stride], + flatCoordinates[i - stride + 1], + flatCoordinates[i], + flatCoordinates[i + 1] + ) + } + totalSquaredDistance += squaredDx( + flatCoordinates[minIndex], + flatCoordinates[minIndex + 1], + closestPoint[0], + closestPoint[1] + ); + + return { + totalSquaredDistance: totalSquaredDistance, + squaredDistance: minSquaredDistance, + closestPoint: closestPoint, + } +} diff --git a/src/interaction/TrackData.ts b/src/interaction/TrackData.ts index be945bb6..90c2b340 100644 --- a/src/interaction/TrackData.ts +++ b/src/interaction/TrackData.ts @@ -1,10 +1,12 @@ import {equals} from 'ol/coordinate.js'; import Feature from 'ol/Feature.js'; import LineString from 'ol/geom/LineString.js'; -import MultiPoint from 'ol/geom/MultiPoint.js'; +import {maxSquaredDelta} from 'ol/geom/flat/closest.js'; +import {getClosestPoint} from '../closest.ts'; import type Point from 'ol/geom/Point.js'; import type {Coordinate} from 'ol/coordinate.js'; + interface ParsedFeatures { segments: Array>; controlPoints: Array>; @@ -257,17 +259,18 @@ export default class TrackData { } updatePOIIndexes() { - // build a multi point geometry from all segments coordinates - const points = new MultiPoint(this.segments.map((s) => s.getGeometry().getCoordinates()).flat()); - const pointsCoordinates = points.getCoordinates(); + const lineFlatCoordinates = this.segments.map(s => s.getGeometry().getFlatCoordinates()).flat() + const lineMaxDelta = maxSquaredDelta(lineFlatCoordinates, 0, lineFlatCoordinates.length, 2, 0) + const sorted = this.pois.map((poi) => { - // find the closest point to the POI and returns its index; that's it's "distance" from the start - const closestPoint = points.getClosestPoint(poi.getGeometry().getCoordinates()); + const point = poi.getGeometry().getCoordinates(); + const {totalSquaredDistance} = getClosestPoint(lineFlatCoordinates, 0, lineFlatCoordinates.length, 2, lineMaxDelta, point[0], point[1]); + return { poi: poi, - index: pointsCoordinates.findIndex((c) => equals(c, closestPoint)) + distance: totalSquaredDistance }; - }).sort((a, b) => a.index - b.index); + }).sort((a, b) => a.distance - b.distance); sorted.forEach((s, index) => s.poi.set('index', index)); }