-
Notifications
You must be signed in to change notification settings - Fork 949
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
lineSlice is very imprecise #2023
Comments
This is caused by inaccuracy in nearestPointOnLine |
Let me know if there's anything I can do to help with helping improve the inaccuracy of |
@JamesLMilner as I said in #1440 (comment) , this is the big blocker to use turf for anything serious. I fell back to python / qgis for my application, even if it's less convenient in a JS app. I think a good starting point would be to add my code above as a test case, and then modify Then I am not sure if the root problem is in If I had time, I'd fix it myself by looking at these 5 functions, and implement them "by the book", applying the mathematical formula directly, maybe using Mathematica to derive a general closed-form solution to each of these functions. With a clearly documented and consistent hypothesis on the geoid model being used. |
To clarify I'm not on the Turf.js team - however I also rely on it for some important parts of our project and am currently experiencing issues with the One thing I'd like to understand is what I've implemented Chris Vernesses nearestPointOnSegment, which takes nearest point on a great circle line between two points, as a pure function in JavaScript (technically TypeScript), but would be happy to share if it would be useful here. |
@JamesLMilner I'm not familiar with the details on this issue, but just to answer your question, As far as I can tell it uses internally |
@stebogit you said this:
The problem is that this is wrong! In particular, the Point C is NOT along LineString L , it is NOT on the line, which is clearly a bug, given that the name of the function is See this comment who expresses the problem clearly #1726 (comment) Basically if we have point from "nearestPointOnLine" and check it with "booleanPointOnLine", we will get "false"... And this is one aspect of the general problem expressed here #1440 (comment) |
And @JamesLMilner yes you are right, I think some parts of Turf use Rhumb, some other use great circle (Haversine). This is implicit and never expressed clearly, so it leads to bugs and inconsistencies... |
@JamesLMilner if you could share your implementation that would be great. It would be interesting to see how large & different the implementation is from what's currently in Turf. Potentially if it's small enough we could add an option to the existing module as to whether we use one implementation or the other, we'll just need to see what makes sense I guess... |
@rowanwins here's the implementation - it uses [longittude, latatutide] ordering as per the GeoJSON spec and has no dependencies - inputs are coordinates rather than GeoJSON points/lines, but that could easily be changed: function equals(coord0: [number, number], coord1: [number, number]) {
if (Math.abs(coord0[1] - coord1[1]) > Number.EPSILON) return false;
if (Math.abs(coord0[0] - coord1[0]) > Number.EPSILON) return false;
return true;
}
const toRadians = (lngLat: number) => {
return (lngLat * Math.PI) / 180;
};
function cross(first: { x: number; y: number; z: number }, v: { x: number; y: number; z: number }) {
const x = first.y * v.z - first.z * v.y;
const y = first.z * v.x - first.x * v.z;
const z = first.x * v.y - first.y * v.x;
return { x, y, z };
}
function toNvector(coord: [number, number]) {
const φ = toRadians(coord[1]);
const λ = toRadians(coord[0]);
const sinφ = Math.sin(φ),
cosφ = Math.cos(φ);
const sinλ = Math.sin(λ),
cosλ = Math.cos(λ);
// right-handed vector: x -> 0°E,0°N; y -> 90°E,0°N, z -> 90°N
const x = cosφ * cosλ;
const y = cosφ * sinλ;
const z = sinφ;
return { x, y, z };
}
function minus(first: { x: number; y: number; z: number }, v: { x: number; y: number; z: number }) {
return { x: first.x - v.x, y: first.y - v.y, z: first.z - v.z };
}
function dot(first: { x: number; y: number; z: number }, v: { x: number; y: number; z: number }) {
return first.x * v.x + first.y * v.y + first.z * v.z;
}
function isWithinExtent(coord0: [number, number], coord1: [number, number], coord2: [number, number]) {
if (equals(coord1, coord2)) {
return equals(coord0, coord1); // null segment
}
const n0 = toNvector(coord0),
n1 = toNvector(coord1),
n2 = toNvector(coord2); // n-vectors
// get vectors representing p0->p1, p0->p2, p1->p2, p2->p1
const δ10 = minus(n0, n1),
δ12 = minus(n2, n1);
const δ20 = minus(n0, n2),
δ21 = minus(n1, n2);
// dot product δ10⋅δ12 tells us if p0 is on p2 side of p1, similarly for δ20⋅δ21
const extent1 = dot(δ10, δ12);
const extent2 = dot(δ20, δ21);
const isSameHemisphere = dot(n0, n1) >= 0 && dot(n0, n2) >= 0;
return extent1 >= 0 && extent2 >= 0 && isSameHemisphere;
}
const toDegrees = function (vector3d: number) {
return (vector3d * 180) / Math.PI;
};
function nVectorToLatLon(vector: { x: number; y: number; z: number }) {
// tanφ = z / √(x²+y²), tanλ = y / x (same as ellipsoidal calculation)
const x = vector.x,
y = vector.y,
z = vector.z;
const φ = Math.atan2(z, Math.sqrt(x * x + y * y));
const λ = Math.atan2(y, x);
return [toDegrees(λ), toDegrees(φ)];
}
function vectorLength(v: { x: number; y: number; z: number }) {
return Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
}
export function distanceTo(coord1: [number, number], coord2: [number, number], radius = 6371e3) {
const R = Number(radius);
const n1 = toNvector(coord1);
const n2 = toNvector(coord2);
const sinθ = vectorLength(cross(n1, n2));
const cosθ = dot(n1, n2);
const δ = Math.atan2(sinθ, cosθ); // tanδ = |n₁×n₂| / n₁⋅n₂
return δ * R;
}
/**
* Returns closest coordinate on great circle segment between lineCoordOne & lineCoordTwo to coord.
*
* If this coord is ‘within’ the extent of the segment, the coord is on the segment between coord1 &
* coord2; otherwise, it is the closer of the endcoords defining the segment.
*/
export function nearestCoordinateOnSegment(
coord: [number, number],
lineCoordOne: [number, number],
lineCoordTwo: [number, number]
) {
let closestCoords = null;
const isBetweenLineCoords = isWithinExtent(coord, lineCoordOne, lineCoordTwo);
const isNotEqualToLineCoords = !equals(lineCoordOne, lineCoordTwo);
if (isBetweenLineCoords && isNotEqualToLineCoords) {
// closer to segment than to its endcoords, find closest coord on segment
const n0 = toNvector(coord),
n1 = toNvector(lineCoordOne),
n2 = toNvector(lineCoordTwo);
const c1 = cross(n1, n2); // n1×n2 = vector representing great circle through p1, p2
const c2 = cross(n0, c1); // n0×c1 = vector representing great circle through p0 normal to c1
const n = cross(c1, c2); // c2×c1 = nearest coord on c1 to n0
closestCoords = nVectorToLatLon(n);
} else {
// beyond segment extent, take closer endcoord
const d1 = distanceTo(coord, lineCoordOne);
const d2 = distanceTo(coord, lineCoordTwo);
closestCoords = d1 < d2 ? lineCoordOne : lineCoordTwo;
}
return closestCoords;
} It passes the unit tests in the original code. Interestingly compared to the Turf implementation here is the difference: Above implementation results:
Turf
I would assume this means that Turf implementation is trying to find the point on a Great Circle as per the above implementation. The precision on the latitude there concerns me slightly as 0.0003 degrees is probably in the region of 33 meters of error. |
Hi @crubier. Testing a fix for this issue, and have some comments to add.
pointToLineDistance suffered from a similar issue to nearestPointOnLine (which impacted lineSlice). So the above wasn't actually true. Looking at the below close up of your example, the blue pin is the point mentioned in the double check above, the red line is the path of the line you want to split, and the red pin is the second coordinate that was VERY far from the slice point: With the fix mentioned, the green pin is now the slice point, and the yellow line coming in from the top is the slice you would get. Hope that all makes sense. If that's not what you would expect, please let me know. |
On turf 6.3.0: lineSlice is very imprecise:
The text was updated successfully, but these errors were encountered: