Skip to content

Commit c1a18e1

Browse files
committed
Add slim version of cheap-ruler impl
1 parent 4d4df6e commit c1a18e1

File tree

2 files changed

+178
-4
lines changed

2 files changed

+178
-4
lines changed

src/style-spec/expression/definitions/distance.js

+2-3
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,8 @@ import type {
1515
GeoJSONPolygon,
1616
GeoJSONMultiPolygon
1717
} from "@mapbox/geojson-types";
18-
import {classifyRings, updateBBox, boxWithinBox, pointWithinPolygon, segmentIntersectSegment} from '../../util/geometry_util.js';
19-
import CheapRuler from "cheap-ruler";
20-
import type {GeometryPoint} from "cheap-ruler";
18+
import {classifyRings, updateBBox, boxWithinBox, pointWithinPolygon, segmentIntersectSegment, CheapRuler} from '../../util/geometry_util.js';
19+
import type {GeometryPoint} from '../../util/geometry_util.js';
2120
import Point from "@mapbox/point-geometry";
2221
import Queue from "tinyqueue";
2322

src/style-spec/util/geometry_util.js

+176-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
import quickselect from 'quickselect';
44

55
import type Point from '@mapbox/point-geometry';
6-
import type {GeometryPoint} from "cheap-ruler";
6+
7+
export type GeometryPoint = [number, number] | [number, number, number];
78
/**
89
* Returns the signed area for the polygon ring. Postive areas are exterior rings and
910
* have a clockwise winding. Negative areas are interior rings and have a counter clockwise
@@ -145,3 +146,177 @@ export function segmentIntersectSegment(a: GeometryPoint, b: GeometryPoint, c: G
145146
if (twoSided(a, b, c, d) && twoSided(c, d, a, b)) return true;
146147
return false;
147148
}
149+
150+
// normalize a degree value into [-180..180] range
151+
function wrap(deg) {
152+
while (deg < -180) deg += 360;
153+
while (deg > 180) deg -= 360;
154+
return deg;
155+
}
156+
157+
const factors = {
158+
kilometers: 1,
159+
miles: 1000 / 1609.344,
160+
nauticalmiles: 1000 / 1852,
161+
meters: 1000,
162+
metres: 1000,
163+
yards: 1000 / 0.9144,
164+
feet: 1000 / 0.3048,
165+
inches: 1000 / 0.0254
166+
};
167+
168+
// Values that define WGS84 ellipsoid model of the Earth
169+
const RE = 6378.137; // equatorial radius
170+
const FE = 1 / 298.257223563; // flattening
171+
172+
const E2 = FE * (2 - FE);
173+
const RAD = Math.PI / 180;
174+
175+
/**
176+
* A collection of very fast approximations to common geodesic measurements. Useful for performance-sensitive code that measures things on a city scale.
177+
* This is the slim version of https://github.com/mapbox/cheap-ruler
178+
* @param {number} lat latitude
179+
* @param {string} [units='kilometers']
180+
* @returns {CheapRuler}
181+
* @example
182+
* const ruler = cheapRuler(35.05, 'miles');
183+
* //=ruler
184+
*/
185+
export class CheapRuler {
186+
kx: number;
187+
ky: number;
188+
/**
189+
* Creates a ruler instance for very fast approximations to common geodesic measurements around a certain latitude.
190+
*
191+
* @param {number} lat latitude
192+
* @param {string} [units='kilometers']
193+
* @returns {CheapRuler}
194+
* @example
195+
* const ruler = cheapRuler(35.05, 'miles');
196+
* //=ruler
197+
*/
198+
constructor(lat: number, units: string) {
199+
if (lat === undefined) throw new Error('No latitude given.');
200+
if (units && !factors[units]) throw new Error(`Unknown unit ${units}. Use one of: ${Object.keys(factors).join(', ')}`);
201+
202+
// Curvature formulas from https://en.wikipedia.org/wiki/Earth_radius#Meridional
203+
const m = RAD * RE * (units ? factors[units] : 1);
204+
const coslat = Math.cos(lat * RAD);
205+
const w2 = 1 / (1 - E2 * (1 - coslat * coslat));
206+
const w = Math.sqrt(w2);
207+
208+
// multipliers for converting longitude and latitude degrees into distance
209+
this.kx = m * w * coslat; // based on normal radius of curvature
210+
this.ky = m * w * w2 * (1 - E2); // based on meridonal radius of curvature
211+
}
212+
213+
/**
214+
* Given two points of the form [longitude, latitude], returns the distance.
215+
*
216+
* @param {GeometryPoint} a point [longitude, latitude]
217+
* @param {GeometryPoint} b point [longitude, latitude]
218+
* @returns {number} distance
219+
* @example
220+
* const distance = ruler.distance([30.5, 50.5], [30.51, 50.49]);
221+
* //=distance
222+
*/
223+
distance(a: GeometryPoint, b: GeometryPoint): number {
224+
const dx = wrap(a[0] - b[0]) * this.kx;
225+
const dy = (a[1] - b[1]) * this.ky;
226+
return Math.sqrt(dx * dx + dy * dy);
227+
}
228+
229+
/**
230+
* Returns the distance from a point `p` to a line segment `a` to `b`.
231+
*
232+
* @param {GeometryPoint} p point [longitude, latitude]
233+
* @param {GeometryPoint} a segment point 1 [longitude, latitude]
234+
* @param {GeometryPoint} b segment point 2 [longitude, latitude]
235+
* @returns {number} distance
236+
* @example
237+
* const distance = ruler.pointToSegmentDistance([-67.04, 50.5], [-67.05, 50.57], [-67.03, 50.54]);
238+
* //=distance
239+
*/
240+
pointToSegmentDistance(p: GeometryPoint, a: GeometryPoint, b: GeometryPoint): number {
241+
let [x, y] = a;
242+
let dx = wrap(b[0] - x) * this.kx;
243+
let dy = (b[1] - y) * this.ky;
244+
let t = 0;
245+
246+
if (dx !== 0 || dy !== 0) {
247+
t = (wrap(p[0] - x) * this.kx * dx + (p[1] - y) * this.ky * dy) / (dx * dx + dy * dy);
248+
249+
if (t > 1) {
250+
x = b[0];
251+
y = b[1];
252+
253+
} else if (t > 0) {
254+
x += (dx / this.kx) * t;
255+
y += (dy / this.ky) * t;
256+
}
257+
}
258+
259+
dx = wrap(p[0] - x) * this.kx;
260+
dy = (p[1] - y) * this.ky;
261+
262+
return Math.sqrt(dx * dx + dy * dy);
263+
}
264+
265+
/**
266+
* Returns an object of the form {point, index, t}, where point is closest point on the line
267+
* from the given point, index is the start index of the segment with the closest point,
268+
* and t is a parameter from 0 to 1 that indicates where the closest point is on that segment.
269+
*
270+
* @param {Array<GeometryPoint>} line
271+
* @param {GeometryPoint} p point [longitude, latitude]
272+
* @returns {Object} {point, index, t}
273+
* @example
274+
* const point = ruler.pointOnLine(line, [-67.04, 50.5]).point;
275+
* //=point
276+
*/
277+
pointOnLine(line: Array<GeometryPoint>, p: GeometryPoint): Object {
278+
let minDist = Infinity;
279+
let minX = Infinity, minY = Infinity, minI = Infinity, minT = Infinity;
280+
281+
for (let i = 0; i < line.length - 1; i++) {
282+
283+
let x = line[i][0];
284+
let y = line[i][1];
285+
let dx = wrap(line[i + 1][0] - x) * this.kx;
286+
let dy = (line[i + 1][1] - y) * this.ky;
287+
let t = 0;
288+
289+
if (dx !== 0 || dy !== 0) {
290+
t = (wrap(p[0] - x) * this.kx * dx + (p[1] - y) * this.ky * dy) / (dx * dx + dy * dy);
291+
292+
if (t > 1) {
293+
x = line[i + 1][0];
294+
y = line[i + 1][1];
295+
296+
} else if (t > 0) {
297+
x += (dx / this.kx) * t;
298+
y += (dy / this.ky) * t;
299+
}
300+
}
301+
302+
dx = wrap(p[0] - x) * this.kx;
303+
dy = (p[1] - y) * this.ky;
304+
305+
const sqDist = dx * dx + dy * dy;
306+
if (sqDist < minDist) {
307+
minDist = sqDist;
308+
minX = x;
309+
minY = y;
310+
minI = i;
311+
minT = t;
312+
}
313+
}
314+
315+
return {
316+
point: [minX, minY],
317+
index: minI,
318+
t: Math.max(0, Math.min(1, minT))
319+
};
320+
}
321+
322+
}

0 commit comments

Comments
 (0)