Skip to content
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

Allow optional ellipsoid definition to be passed to distance allowing for calculation types other than great circle #2476

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 101 additions & 6 deletions packages/turf-distance/index.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,33 @@
import { Point } from "geojson";
import { getCoord } from "@turf/invariant";
import { radiansToLength, degreesToRadians, Coord, Units } from "@turf/helpers";

import type { Coord, Datum, Units } from "@turf/helpers";
import {
datums,
radiansToLength,
degreesToRadians,
convertLength,
} from "@turf/helpers";
import LatLon from "geodesy/latlon-ellipsoidal-vincenty.js";
//http://en.wikipedia.org/wiki/Haversine_formula
//http://www.movable-type.co.uk/scripts/latlong.html

/**
* Calculates the distance between two {@link Point|points} in degrees, radians, miles, or kilometers.
* This uses the [Haversine formula](http://en.wikipedia.org/wiki/Haversine_formula) to account for global curvature.
* Calculates the distance between two {@link Point|points}.
* If a specific datum is passed, uses a geodesic ellipsoid calculation.
Copy link
Collaborator

@twelch twelch Sep 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you are technically correct here in that if the user passes any valid datum object, that the code as it is now will run geodesicEllipsoidDistance. But that doesn't seem clean/clear. Technically, I could possibly make up a datum and it will still run that one function right? But why? It seems like it would be better to accept the one expected ellipsoid datum, and throw an error otherwise. Or see my comment below about exposing just a string parameter to specify the datum.

* If no datum is passed, performs a great circle calculation.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you think about keeping the ", using the Haversine formula". That seems to be a level of detail that people request having.

*
* @name distance
* @param {Coord | Point} from origin point or coordinate
* @param {Coord | Point} to destination point or coordinate
* @param {Object} [options={}] Optional parameters
* @param {string} [options.units='kilometers'] can be degrees, radians, miles, or kilometers
* @returns {number} distance between the two points
* @param {Datum} [options.datum=datums.WGS84] datum listed in {@link Helpers}
* @returns {number} distance between the two points in chosen units
* @example
* var from = turf.point([-75.343, 39.984]);
* var to = turf.point([-75.534, 39.123]);
* var options = {units: 'miles'};
*
* var options = {units: 'miles'};
* var distance = turf.distance(from, to, options);
*
* //addToMap
Expand All @@ -28,6 +36,46 @@ import { radiansToLength, degreesToRadians, Coord, Units } from "@turf/helpers";
* to.properties.distance = distance;
*/
function distance(
from: Coord | Point,
to: Coord | Point,
options: {
units?: Units;
datum?: Datum;
} = {}
) {
if (options?.datum) {
smallsaucepan marked this conversation as resolved.
Show resolved Hide resolved
return geodesicEllipsoidDistance(from, to, {
datum: options.datum, // Must pass a datum option
...options,
});
} else {
return greatCircleDistance(from, to, options);
}
}

/**
* Calculates the distance between two {@link Point|points} in degrees, radians, miles, or kilometers.
* Performs a great circle calculation using the [Haversine formula](http://en.wikipedia.org/wiki/Haversine_formula) to account for global curvature.
*
* @name greatCircleDistance
* @param {Coord | Point} from origin point or coordinate
* @param {Coord | Point} to destination point or coordinate
* @param {Object} [options={}] Optional parameters
* @param {string} [options.units='kilometers'] can be degrees, radians, miles, or kilometers
* @returns {number} distance between the two points
* @example
* var from = turf.point([-75.343, 39.984]);
* var to = turf.point([-75.534, 39.123]);
* var options = {units: 'miles'};
*
* var distance = turf.greatCircleDistance(from, to, options);
*
* //addToMap
* var addToMap = [from, to];
* from.properties.distance = distance;
* to.properties.distance = distance;
*/
function greatCircleDistance(
from: Coord | Point,
to: Coord | Point,
options: {
Expand All @@ -51,4 +99,51 @@ function distance(
);
}

/**
* Calculates the distance between two {@link Point|points}.
* Performs a geodesic ellipsoid calculation using [Vincenty's formulae](https://en.wikipedia.org/wiki/Vincenty%27s_formulae) to account for speroidal curvature.
*
* @name geodesicEllipsoidDistance
* @param {Coord | Point} from origin point or coordinate
* @param {Coord | Point} to destination point or coordinate
* @param {Object} [options={}] Optional parameters
* @param {string} [options.units='kilometers'] can be degrees, radians, miles, or kilometers
* @param {Datum} [options.datum=datums.WGS84] datum listed in {@link Helpers}
* @returns {number} distance between the two points in chosen units
* @example
* var from = turf.point([-75.343, 39.984]);
* var to = turf.point([-75.534, 39.123]);
* var options = {units: 'miles', datum: datums.WGS84};
*
* var distance = turf.geodesicEllipsoidDistance(from, to, options);
*
* //addToMap
* var addToMap = [from, to];
* from.properties.distance = distance;
* to.properties.distance = distance;
*/
function geodesicEllipsoidDistance(
from: Coord | Point,
to: Coord | Point,
options: {
units?: Units;
datum: Datum;
} = { datum: datums.WGS84 }
) {
const fromCoord = getCoord(from);
const toCoord = getCoord(to);

const datum = options.datum;

const fromLatLon = new LatLon(fromCoord[1], fromCoord[0]);
// datum on from point sets the tone.
fromLatLon.datum = datum;
const toLatLon = new LatLon(toCoord[1], toCoord[0]);

const meters = fromLatLon.distanceTo(toLatLon);
// geodesy lib result is in meters
return convertLength(meters, "meters", options.units);
}

export { greatCircleDistance, geodesicEllipsoidDistance };
export default distance;
2 changes: 2 additions & 0 deletions packages/turf-distance/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"test:tape": "tsx test.js"
},
"devDependencies": {
"@types/geodesy": "^2.2.3",
"@types/tape": "*",
"benchmark": "*",
"load-json-file": "*",
Expand All @@ -59,6 +60,7 @@
"dependencies": {
"@turf/helpers": "^7.0.0-alpha.2",
"@turf/invariant": "^7.0.0-alpha.2",
"geodesy": "^2.3.0",
"tslib": "^2.3.0"
}
}
59 changes: 58 additions & 1 deletion packages/turf-distance/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ const path = require("path");
const test = require("tape");
const load = require("load-json-file");
const write = require("write-json-file");
const { point } = require("@turf/helpers");
const { datums, point } = require("@turf/helpers");
const distance = require("./index").default;
const { geodesicEllipsoidDistance } = require("./index");

const directories = {
in: path.join(__dirname, "test", "in") + path.sep,
Expand Down Expand Up @@ -56,3 +57,59 @@ test("distance -- throws", (t) => {
);
t.end();
});

test("distance -- Issue #1726 line between poles", (t) => {
const p1 = point([-33.6, 81.1]);
const p2 = point([64.5, -80.8]);

let overallDistance = distance(p1, p2, {
units: "meters",
datum: datums.WGS84,
});

const expected = 18682436.875; // m from QGIS
const tolerance = 0.01; // 1 cm expressed as m
t.true(
Math.abs(overallDistance - expected) < tolerance,
`${overallDistance} within ${tolerance} of ${expected}`
);

t.end();
});

test("distance -- Issue #1726 line near equator", (t) => {
const p1 = point([34, 15.9]);
const p2 = point([21, 0.2]);

let overallDistance = distance(p1, p2, {
units: "meters",
datum: datums.WGS84,
});

const expected = 2248334.18; // m from QGIS
const tolerance = 0.01; // 1 cm expressed as m
t.true(
Math.abs(overallDistance - expected) < tolerance,
`${overallDistance} within ${tolerance} of ${expected}`
);

t.end();
});

test("distance -- calling ellipsoid distance with empty options", (t) => {
// Make sure calling geodesicEllipsoidDistance directly without a datum
// option defaults to WGS84.
const p1 = point([-33.6, 81.1]);
const p2 = point([64.5, -80.8]);

let overallDistance = geodesicEllipsoidDistance(p1, p2);

const expected = 18682.436875; // m from QGIS as km
const tolerance = 0.00001; // 1 cm expressed as km
t.true(
Math.abs(overallDistance - expected) < tolerance,
`${overallDistance} within ${tolerance} of ${expected}`
);

t.end();
});
15 changes: 15 additions & 0 deletions packages/turf-helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
Position,
GeoJsonProperties,
} from "geojson";
import type { Datum } from "geodesy";
import LatLonEllipsoidalVincenty from "geodesy/latlon-ellipsoidal-vincenty.js";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing this module isn't exported out of the top-level of the geodesy library.


import { Id } from "./lib/geojson";
export * from "./lib/geojson";
Expand Down Expand Up @@ -115,6 +117,19 @@ export const areaFactors: Record<AreaUnits, number> = {
yards: 1.195990046,
};

/**
* Common datum and ellipsoid definitions. Re-export verbatim from geodesy.
*
* @memberof helpers
* @type {Object}
*/
const datums = LatLonEllipsoidalVincenty.datums;
export { datums };
Comment on lines +126 to +127
Copy link
Collaborator

@twelch twelch Sep 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed this geodesy module defines only one datum, WGS84.

const datums = {
    WGS84: { ellipsoid: ellipsoids.WGS84 },
};

https://github.com/chrisveness/geodesy/blob/master/latlon-ellipsoidal.js

So this would export just the one datum to users via @turf/helpers for use in the distance function correct? And to use it, the user would need to know to import this datum and pass it. Does this feel like the right interface for the users? Do they need this low level of access? Is the intention of exposing the datum objects to be able to offer more datums via https://github.com/chrisveness/geodesy/blob/master/latlon-ellipsoidal-datum.js?

What do you think about just accepting a 'wgs84' string parameter in the distance function for the datum, which internally handles the geodesy dependency. This could cause a breaking change in the future.

Alternatively, can you see how the documentation will be improved alongside this PR to tell users how to access/use a datum object? I can see the param doc at least points at the Helpers module.


// Re-export type from geodesy so clients don't need to refer directly to
// geodesy types.
export type { Datum };

/**
* Wraps a GeoJSON {@link Geometry} in a GeoJSON {@link Feature}.
*
Expand Down
2 changes: 2 additions & 0 deletions packages/turf-helpers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"test:types": "tsc --esModuleInterop --noEmit --strict types.ts"
},
"devDependencies": {
"@types/geodesy": "^2.2.3",
"@types/tape": "*",
"benchmark": "*",
"npm-run-all": "*",
Expand All @@ -62,6 +63,7 @@
"typescript": "*"
},
"dependencies": {
"geodesy": "^2.3.0",
"tslib": "^2.3.0"
}
}
3 changes: 2 additions & 1 deletion packages/turf-length/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Feature, FeatureCollection, GeometryCollection } from "geojson";
import distance from "@turf/distance";
import { Units } from "@turf/helpers";
import type { Datum, Units } from "@turf/helpers";
import { segmentReduce } from "@turf/meta";

/**
Expand All @@ -23,6 +23,7 @@ export default function length(
geojson: Feature<any> | FeatureCollection<any> | GeometryCollection,
options: {
units?: Units;
datum?: Datum;
} = {}
): number {
// Calculate distance from 2-vertex line segments
Expand Down
Loading