Refactor geometric math functions from geo.js to geom.js

This commit is contained in:
Bryan Housel
2017-12-28 01:28:38 -05:00
parent 96afbbd785
commit 7af73c10ef
7 changed files with 544 additions and 521 deletions
+13 -212
View File
@@ -39,15 +39,6 @@ export function geoMetersToLon(m, atLat) {
}
export function geoOffsetToMeters(offset, tileSize) {
tileSize = tileSize || 256;
return [
offset[0] * TAU * EQUATORIAL_RADIUS / tileSize,
-offset[1] * TAU * POLAR_RADIUS / tileSize
];
}
export function geoMetersToOffset(meters, tileSize) {
tileSize = tileSize || 256;
return [
@@ -57,6 +48,15 @@ export function geoMetersToOffset(meters, tileSize) {
}
export function geoOffsetToMeters(offset, tileSize) {
tileSize = tileSize || 256;
return [
offset[0] * TAU * EQUATORIAL_RADIUS / tileSize,
-offset[1] * TAU * POLAR_RADIUS / tileSize
];
}
// Equirectangular approximation of spherical distances on Earth
export function geoSphericalDistance(a, b) {
var x = geoLonToMeters(a[0] - b[0], (a[1] + b[1]) / 2);
@@ -65,13 +65,6 @@ export function geoSphericalDistance(a, b) {
}
// zoom to scale
export function geoZoomToScale(z, tileSize) {
tileSize = tileSize || 256;
return tileSize * Math.pow(2, z) / TAU;
}
// scale to zoom
export function geoScaleToZoom(k, tileSize) {
tileSize = tileSize || 256;
@@ -80,203 +73,11 @@ export function geoScaleToZoom(k, tileSize) {
}
export function geoEdgeEqual(a, b) {
return (a[0] === b[0] && a[1] === b[1]) ||
(a[0] === b[1] && a[1] === b[0]);
// zoom to scale
export function geoZoomToScale(z, tileSize) {
tileSize = tileSize || 256;
return tileSize * Math.pow(2, z) / TAU;
}
// Return the counterclockwise angle in the range (-pi, pi)
// between the positive X axis and the line intersecting a and b.
export function geoAngle(a, b, projection) {
return geoVecAngle(projection(a.loc), projection(b.loc));
}
// Rotate all points counterclockwise around a pivot point by given angle
export function geoRotate(points, angle, around) {
return points.map(function(point) {
var radial = [point[0] - around[0], point[1] - around[1]];
return [
radial[0] * Math.cos(angle) - radial[1] * Math.sin(angle) + around[0],
radial[0] * Math.sin(angle) + radial[1] * Math.cos(angle) + around[1]
];
});
}
// Choose the edge with the minimal distance from `point` to its orthogonal
// projection onto that edge, if such a projection exists, or the distance to
// the closest vertex on that edge. Returns an object with the `index` of the
// chosen edge, the chosen `loc` on that edge, and the `distance` to to it.
export function geoChooseEdge(nodes, point, projection, skipID) {
var dist = geoVecLength;
var points = nodes.map(function(n) { return projection(n.loc); });
var ids = nodes.map(function(n) { return n.id; });
var min = Infinity;
var idx;
var loc;
for (var i = 0; i < points.length - 1; i++) {
if (ids[i] === skipID || ids[i + 1] === skipID) continue;
var o = points[i];
var s = geoVecSubtract(points[i + 1], o);
var v = geoVecSubtract(point, o);
var proj = geoVecDot(v, s) / geoVecDot(s, s);
var p;
if (proj < 0) {
p = o;
} else if (proj > 1) {
p = points[i + 1];
} else {
p = [o[0] + proj * s[0], o[1] + proj * s[1]];
}
var d = dist(p, point);
if (d < min) {
min = d;
idx = i + 1;
loc = projection.invert(p);
}
}
if (idx !== undefined) {
return { index: idx, distance: min, loc: loc };
} else {
return null;
}
}
// Return the intersection point of 2 line segments.
// From https://github.com/pgkelley4/line-segments-intersect
// This uses the vector cross product approach described below:
// http://stackoverflow.com/a/565282/786339
export function geoLineIntersection(a, b) {
var p = [a[0][0], a[0][1]];
var p2 = [a[1][0], a[1][1]];
var q = [b[0][0], b[0][1]];
var q2 = [b[1][0], b[1][1]];
var r = geoVecSubtract(p2, p);
var s = geoVecSubtract(q2, q);
var uNumerator = geoVecCross(geoVecSubtract(q, p), r);
var denominator = geoVecCross(r, s);
if (uNumerator && denominator) {
var u = uNumerator / denominator;
var t = geoVecCross(geoVecSubtract(q, p), s) / denominator;
if ((t >= 0) && (t <= 1) && (u >= 0) && (u <= 1)) {
return geoVecInterp(p, p2, t);
}
}
return null;
}
export function geoPathIntersections(path1, path2) {
var intersections = [];
for (var i = 0; i < path1.length - 1; i++) {
for (var j = 0; j < path2.length - 1; j++) {
var a = [ path1[i], path1[i+1] ];
var b = [ path2[j], path2[j+1] ];
var hit = geoLineIntersection(a, b);
if (hit) {
intersections.push(hit);
}
}
}
return intersections;
}
// Return whether point is contained in polygon.
//
// `point` should be a 2-item array of coordinates.
// `polygon` should be an array of 2-item arrays of coordinates.
//
// From https://github.com/substack/point-in-polygon.
// ray-casting algorithm based on
// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
//
export function geoPointInPolygon(point, polygon) {
var x = point[0];
var y = point[1];
var inside = false;
for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
var xi = polygon[i][0], yi = polygon[i][1];
var xj = polygon[j][0], yj = polygon[j][1];
var intersect = ((yi > y) !== (yj > y)) &&
(x < (xj - xi) * (y - yi) / (yj - yi) + xi);
if (intersect) inside = !inside;
}
return inside;
}
export function geoPolygonContainsPolygon(outer, inner) {
return _every(inner, function(point) {
return geoPointInPolygon(point, outer);
});
}
export function geoPolygonIntersectsPolygon(outer, inner, checkSegments) {
function testSegments(outer, inner) {
for (var i = 0; i < outer.length - 1; i++) {
for (var j = 0; j < inner.length - 1; j++) {
var a = [ outer[i], outer[i + 1] ];
var b = [ inner[j], inner[j + 1] ];
if (geoLineIntersection(a, b)) return true;
}
}
return false;
}
function testPoints(outer, inner) {
return _some(inner, function(point) {
return geoPointInPolygon(point, outer);
});
}
return testPoints(outer, inner) || (!!checkSegments && testSegments(outer, inner));
}
export function geoPathLength(path) {
var length = 0;
for (var i = 0; i < path.length - 1; i++) {
length += geoVecLength(path[i], path[i + 1]);
}
return length;
}
// If the given point is at the edge of the padded viewport,
// return a vector that will nudge the viewport in that direction
export function geoViewportEdge(point, dimensions) {
var pad = [80, 20, 50, 20]; // top, right, bottom, left
var x = 0;
var y = 0;
if (point[0] > dimensions[0] - pad[1])
x = -10;
if (point[0] < pad[3])
x = 10;
if (point[1] > dimensions[1] - pad[2])
y = -10;
if (point[1] < pad[0])
y = 10;
if (x || y) {
return [x, y];
} else {
return null;
}
}
+213
View File
@@ -0,0 +1,213 @@
import _every from 'lodash-es/every';
import _some from 'lodash-es/some';
import {
geoVecAngle,
geoVecCross,
geoVecDot,
geoVecInterp,
geoVecLength,
geoVecSubtract
} from './vector.js';
// Return the counterclockwise angle in the range (-pi, pi)
// between the positive X axis and the line intersecting a and b.
export function geoAngle(a, b, projection) {
return geoVecAngle(projection(a.loc), projection(b.loc));
}
export function geoEdgeEqual(a, b) {
return (a[0] === b[0] && a[1] === b[1]) ||
(a[0] === b[1] && a[1] === b[0]);
}
// Rotate all points counterclockwise around a pivot point by given angle
export function geoRotate(points, angle, around) {
return points.map(function(point) {
var radial = [point[0] - around[0], point[1] - around[1]];
return [
radial[0] * Math.cos(angle) - radial[1] * Math.sin(angle) + around[0],
radial[0] * Math.sin(angle) + radial[1] * Math.cos(angle) + around[1]
];
});
}
// Choose the edge with the minimal distance from `point` to its orthogonal
// projection onto that edge, if such a projection exists, or the distance to
// the closest vertex on that edge. Returns an object with the `index` of the
// chosen edge, the chosen `loc` on that edge, and the `distance` to to it.
export function geoChooseEdge(nodes, point, projection, skipID) {
var dist = geoVecLength;
var points = nodes.map(function(n) { return projection(n.loc); });
var ids = nodes.map(function(n) { return n.id; });
var min = Infinity;
var idx;
var loc;
for (var i = 0; i < points.length - 1; i++) {
if (ids[i] === skipID || ids[i + 1] === skipID) continue;
var o = points[i];
var s = geoVecSubtract(points[i + 1], o);
var v = geoVecSubtract(point, o);
var proj = geoVecDot(v, s) / geoVecDot(s, s);
var p;
if (proj < 0) {
p = o;
} else if (proj > 1) {
p = points[i + 1];
} else {
p = [o[0] + proj * s[0], o[1] + proj * s[1]];
}
var d = dist(p, point);
if (d < min) {
min = d;
idx = i + 1;
loc = projection.invert(p);
}
}
if (idx !== undefined) {
return { index: idx, distance: min, loc: loc };
} else {
return null;
}
}
// Return the intersection point of 2 line segments.
// From https://github.com/pgkelley4/line-segments-intersect
// This uses the vector cross product approach described below:
// http://stackoverflow.com/a/565282/786339
export function geoLineIntersection(a, b) {
var p = [a[0][0], a[0][1]];
var p2 = [a[1][0], a[1][1]];
var q = [b[0][0], b[0][1]];
var q2 = [b[1][0], b[1][1]];
var r = geoVecSubtract(p2, p);
var s = geoVecSubtract(q2, q);
var uNumerator = geoVecCross(geoVecSubtract(q, p), r);
var denominator = geoVecCross(r, s);
if (uNumerator && denominator) {
var u = uNumerator / denominator;
var t = geoVecCross(geoVecSubtract(q, p), s) / denominator;
if ((t >= 0) && (t <= 1) && (u >= 0) && (u <= 1)) {
return geoVecInterp(p, p2, t);
}
}
return null;
}
export function geoPathIntersections(path1, path2) {
var intersections = [];
for (var i = 0; i < path1.length - 1; i++) {
for (var j = 0; j < path2.length - 1; j++) {
var a = [ path1[i], path1[i+1] ];
var b = [ path2[j], path2[j+1] ];
var hit = geoLineIntersection(a, b);
if (hit) {
intersections.push(hit);
}
}
}
return intersections;
}
// Return whether point is contained in polygon.
//
// `point` should be a 2-item array of coordinates.
// `polygon` should be an array of 2-item arrays of coordinates.
//
// From https://github.com/substack/point-in-polygon.
// ray-casting algorithm based on
// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
//
export function geoPointInPolygon(point, polygon) {
var x = point[0];
var y = point[1];
var inside = false;
for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
var xi = polygon[i][0];
var yi = polygon[i][1];
var xj = polygon[j][0];
var yj = polygon[j][1];
var intersect = ((yi > y) !== (yj > y)) &&
(x < (xj - xi) * (y - yi) / (yj - yi) + xi);
if (intersect) inside = !inside;
}
return inside;
}
export function geoPolygonContainsPolygon(outer, inner) {
return _every(inner, function(point) {
return geoPointInPolygon(point, outer);
});
}
export function geoPolygonIntersectsPolygon(outer, inner, checkSegments) {
function testSegments(outer, inner) {
for (var i = 0; i < outer.length - 1; i++) {
for (var j = 0; j < inner.length - 1; j++) {
var a = [ outer[i], outer[i + 1] ];
var b = [ inner[j], inner[j + 1] ];
if (geoLineIntersection(a, b)) return true;
}
}
return false;
}
function testPoints(outer, inner) {
return _some(inner, function(point) {
return geoPointInPolygon(point, outer);
});
}
return testPoints(outer, inner) || (!!checkSegments && testSegments(outer, inner));
}
export function geoPathLength(path) {
var length = 0;
for (var i = 0; i < path.length - 1; i++) {
length += geoVecLength(path[i], path[i + 1]);
}
return length;
}
// If the given point is at the edge of the padded viewport,
// return a vector that will nudge the viewport in that direction
export function geoViewportEdge(point, dimensions) {
var pad = [80, 20, 50, 20]; // top, right, bottom, left
var x = 0;
var y = 0;
if (point[0] > dimensions[0] - pad[1])
x = -10;
if (point[0] < pad[3])
x = 10;
if (point[1] > dimensions[1] - pad[2])
y = -10;
if (point[1] < pad[0])
y = 10;
if (x || y) {
return [x, y];
} else {
return null;
}
}
+18 -13
View File
@@ -1,23 +1,29 @@
export { geoAngle } from './geo.js';
export { geoChooseEdge } from './geo.js';
export { geoEdgeEqual } from './geo.js';
export { geoExtent } from './extent.js';
export { geoRawMercator } from './raw_mercator.js';
export { geoRotate } from './geo.js';
export { geoLatToMeters } from './geo.js';
export { geoLineIntersection } from './geo.js';
export { geoLonToMeters } from './geo.js';
export { geoMetersToLat } from './geo.js';
export { geoMetersToLon } from './geo.js';
export { geoMetersToOffset } from './geo.js';
export { geoOffsetToMeters } from './geo.js';
export { geoPathIntersections } from './geo.js';
export { geoPathLength } from './geo.js';
export { geoPointInPolygon } from './geo.js';
export { geoPolygonContainsPolygon } from './geo.js';
export { geoPolygonIntersectsPolygon } from './geo.js';
export { geoScaleToZoom } from './geo.js';
export { geoSphericalDistance } from './geo.js';
export { geoZoomToScale } from './geo.js';
export { geoAngle } from './geom.js';
export { geoChooseEdge } from './geom.js';
export { geoEdgeEqual } from './geom.js';
export { geoRotate } from './geom.js';
export { geoLineIntersection } from './geom.js';
export { geoPathIntersections } from './geom.js';
export { geoPathLength } from './geom.js';
export { geoPointInPolygon } from './geom.js';
export { geoPolygonContainsPolygon } from './geom.js';
export { geoPolygonIntersectsPolygon } from './geom.js';
export { geoViewportEdge } from './geom.js';
export { geoRawMercator } from './raw_mercator.js';
export { geoVecAdd } from './vector.js';
export { geoVecAngle } from './vector.js';
export { geoVecCross } from './vector.js';
@@ -28,5 +34,4 @@ export { geoVecInterp } from './vector.js';
export { geoVecLength } from './vector.js';
export { geoVecSubtract } from './vector.js';
export { geoVecScale } from './vector.js';
export { geoZoomToScale } from './geo.js';
export { geoViewportEdge } from './geo.js';