Typescript-ifying turf-great-circle (#2733)

* Added typing to index.ts, tests to test.ts

* Update pnpm-lock.yaml after dependency updates

* Replace embedded arc.js with external TypeScript arc package

- Add arc@^0.2.0 (WIP bump) as dependency to replace embedded lib/arc.js
- Update import from './lib/arc.js' to 'arc' package
- Fix TypeScript type compatibility issues:
  - Handle null properties with fallback to empty object
  - Add proper type assertion for return value
- All tests pass (9/9) with new TypeScript arc.js integration
- Maintains 100% backward compatibility while adding full TypeScript support

* Remove arc.d.ts which is no longer needed, regenerate pnpm-lock

---------

Co-authored-by: mfedderly <24275386+mfedderly@users.noreply.github.com>
This commit is contained in:
Thomas Hervey 2025-12-08 08:47:38 -05:00 committed by GitHub
parent b43a4d51e8
commit 14f354ffca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 140 additions and 56 deletions

View File

@ -11,5 +11,5 @@ suite
.add("greatCircle", () => {
greatCircle(point1, point2);
})
.on("cycle", (e) => console.log(String(e.target)))
.on("cycle", (e: any) => console.log(String(e.target)))
.run();

View File

@ -1,23 +0,0 @@
import {
LineString,
MultiLineString,
Feature,
GeoJsonProperties,
} from "geojson";
import { Coord } from "@turf/helpers";
/**
* http://turfjs.org/docs/#greatcircle
*/
declare function greatCircle(
start: Coord,
end: Coord,
options?: {
properties?: GeoJsonProperties;
npoints?: number;
offset?: number;
}
): Feature<LineString | MultiLineString>;
export { greatCircle };
export default greatCircle;

View File

@ -1,6 +1,14 @@
import type {
Feature,
GeoJsonProperties,
LineString,
MultiLineString,
Point,
Position,
} from "geojson";
import { lineString } from "@turf/helpers";
import { getCoord } from "@turf/invariant";
import { GreatCircle } from "./lib/arc.js";
import { GreatCircle } from "arc";
/**
* Calculate great circles routes as {@link LineString} or {@link MultiLineString}.
@ -8,7 +16,7 @@ import { GreatCircle } from "./lib/arc.js";
* be split into a `MultiLineString`. If the `start` and `end` positions are the same
* then a `LineString` will be returned with duplicate coordinates the length of the `npoints` option.
*
* @function
* @name greatCircle
* @param {Coord} start source point feature
* @param {Coord} end destination point feature
* @param {Object} [options={}] Optional parameters
@ -26,37 +34,39 @@ import { GreatCircle } from "./lib/arc.js";
* //addToMap
* var addToMap = [start, end, greatCircle]
*/
function greatCircle(start, end, options) {
function greatCircle(
start: Feature<Point, GeoJsonProperties> | Point | Position,
end: Feature<Point, GeoJsonProperties> | Point | Position,
options: {
properties?: GeoJsonProperties;
npoints?: number;
offset?: number;
} = {}
): Feature<LineString | MultiLineString> {
// Optional parameters
options = options || {};
if (typeof options !== "object") throw new Error("options is invalid");
var properties = options.properties;
var npoints = options.npoints;
var offset = options.offset;
const { properties = {}, npoints = 100, offset = 10 } = options;
start = getCoord(start);
end = getCoord(end);
const startCoord = getCoord(start);
const endCoord = getCoord(end);
properties = properties || {};
npoints = npoints || 100;
if (start[0] === end[0] && start[1] === end[1]) {
const arr = Array(npoints);
arr.fill([start[0], start[1]]);
if (startCoord[0] === endCoord[0] && startCoord[1] === endCoord[1]) {
const arr = Array(npoints).fill([startCoord[0], startCoord[1]]);
return lineString(arr, properties);
}
offset = offset || 10;
var generator = new GreatCircle(
{ x: start[0], y: start[1] },
{ x: end[0], y: end[1] },
properties
const generator = new GreatCircle(
{ x: startCoord[0], y: startCoord[1] },
{ x: endCoord[0], y: endCoord[1] },
properties || {}
);
var line = generator.Arc(npoints, { offset: offset });
const line = generator.Arc(npoints, { offset: offset });
return line.json();
return line.json() as Feature<
LineString | MultiLineString,
GeoJsonProperties
>;
}
export { greatCircle };

View File

@ -6,7 +6,8 @@
"contributors": [
"Dane Springmeyer <@springmeyer>",
"Stepan Kuzmin <@stepankuzmin>",
"Denis Carriere <@DenisCarriere>"
"Denis Carriere <@DenisCarriere>",
"Thomas Hervey <@thomas-hervey>"
],
"license": "MIT",
"bugs": {
@ -66,11 +67,14 @@
"tape": "^5.9.0",
"tsup": "^8.4.0",
"tsx": "^4.19.4",
"typescript": "^5.8.3",
"write-json-file": "^6.0.0"
},
"dependencies": {
"@turf/helpers": "workspace:*",
"@turf/invariant": "workspace:*",
"@types/geojson": "^7946.0.10"
"@types/geojson": "^7946.0.10",
"arc": "^0.2.0",
"tslib": "^2.8.1"
}
}

View File

@ -4,6 +4,13 @@ import path from "path";
import { fileURLToPath } from "url";
import { loadJsonFileSync } from "load-json-file";
import { writeJsonFileSync } from "write-json-file";
import type {
FeatureCollection,
LineString,
Feature,
Geometry,
Point,
} from "geojson";
import { truncate } from "@turf/truncate";
import { featureCollection, point, lineString } from "@turf/helpers";
import { greatCircle } from "./index.js";
@ -15,23 +22,32 @@ const directories = {
out: path.join(__dirname, "test", "out") + path.sep,
};
let fixtures = fs.readdirSync(directories.in).map((filename) => {
const fixtures = fs.readdirSync(directories.in).map((filename) => {
return {
filename,
name: path.parse(filename).name,
geojson: loadJsonFileSync(path.join(directories.in, filename)),
geojson: loadJsonFileSync(
path.join(directories.in, filename)
) as FeatureCollection,
};
});
// Function to get the start and end points from the fixture
function getStartEndPoints(fixture: (typeof fixtures)[0]) {
const geojson = fixture.geojson;
const start = geojson.features[0] as Feature<Point>;
const end = geojson.features[1] as Feature<Point>;
return { start, end };
}
test("turf-great-circle", (t) => {
fixtures.forEach((fixture) => {
const name = fixture.name;
const filename = fixture.filename;
const geojson = fixture.geojson;
const start = geojson.features[0];
const end = geojson.features[1];
const { start, end } = getStartEndPoints(fixture);
const line = truncate(greatCircle(start, end));
const results = featureCollection([line, start, end]);
const results = featureCollection<Geometry>([line, start, end]);
if (process.env.REGEN)
writeJsonFileSync(directories.out + filename, results);
@ -54,12 +70,74 @@ test("turf-great-circle with same input and output", (t) => {
[0, 0],
[0, 0],
]),
line
line as Feature<LineString>
);
t.end();
});
test("turf-great-circle accepts Feature<Point> inputs", (t) => {
const { start, end } = getStartEndPoints(fixtures[0]);
t.doesNotThrow(
() => greatCircle(start, end),
"accepts Feature<Point> inputs"
);
t.end();
});
test("turf-great-circle accepts Point geometry inputs", (t) => {
const { start, end } = getStartEndPoints(fixtures[0]);
t.doesNotThrow(
() => greatCircle(start.geometry, end.geometry),
"accepts Point geometry inputs"
);
t.end();
});
test("turf-great-circle accepts Position inputs", (t) => {
const { start, end } = getStartEndPoints(fixtures[0]);
t.doesNotThrow(
() => greatCircle(start.geometry.coordinates, end.geometry.coordinates),
"accepts Position inputs"
);
t.end();
});
test("turf-great-circle applies custom properties", (t) => {
const { start, end } = getStartEndPoints(fixtures[0]);
const withProperties = greatCircle(start, end, {
properties: { name: "Test Route" },
});
t.equal(
withProperties.properties?.name,
"Test Route",
"applies custom properties"
);
t.end();
});
test("turf-great-circle respects npoints option", (t) => {
const { start, end } = getStartEndPoints(fixtures[0]);
const withCustomPoints = greatCircle(start, end, { npoints: 5 });
t.equal(
(withCustomPoints.geometry as LineString).coordinates.length,
5,
"respects npoints option"
);
t.end();
});
test("turf-great-circle respects offset and npoints options", (t) => {
const { start, end } = getStartEndPoints(fixtures[0]);
const withOffset = greatCircle(start, end, { offset: 100, npoints: 10 });
t.equal(
(withOffset.geometry as LineString).coordinates.length,
10,
"respects offset and npoints options"
);
t.end();
});
test("turf-great-circle with antipodal start and end", (t) => {
const start = point([0, 90]);
const end = point([0, -90]);

15
pnpm-lock.yaml generated
View File

@ -3047,6 +3047,12 @@ importers:
'@types/geojson':
specifier: ^7946.0.10
version: 7946.0.14
arc:
specifier: ^0.2.0
version: 0.2.0
tslib:
specifier: ^2.8.1
version: 2.8.1
devDependencies:
'@turf/truncate':
specifier: workspace:*
@ -3072,6 +3078,9 @@ importers:
tsx:
specifier: ^4.19.4
version: 4.19.4
typescript:
specifier: ^5.8.3
version: 5.8.3
write-json-file:
specifier: ^6.0.0
version: 6.0.0
@ -7962,6 +7971,10 @@ packages:
aproba@2.0.0:
resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==}
arc@0.2.0:
resolution: {integrity: sha512-8NFOo126uYKQJyXNSLY/jSklgfLQL+XWAcPXGo876JwEQ8nSOPXWNI3TV2jLZMN8QEw8uksJ1ZwS4npjBca8MA==}
engines: {node: '>=0.4.0'}
argparse@1.0.10:
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
@ -13778,6 +13791,8 @@ snapshots:
aproba@2.0.0: {}
arc@0.2.0: {}
argparse@1.0.10:
dependencies:
sprintf-js: 1.0.3