From 14f354ffcac20f8346b33bcd8032bf39baf4689e Mon Sep 17 00:00:00 2001 From: Thomas Hervey Date: Mon, 8 Dec 2025 08:47:38 -0500 Subject: [PATCH] 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> --- packages/turf-great-circle/bench.ts | 2 +- packages/turf-great-circle/index.d.ts | 23 ----- .../turf-great-circle/{index.js => index.ts} | 56 ++++++----- packages/turf-great-circle/package.json | 8 +- packages/turf-great-circle/test.ts | 92 +++++++++++++++++-- pnpm-lock.yaml | 15 +++ 6 files changed, 140 insertions(+), 56 deletions(-) delete mode 100644 packages/turf-great-circle/index.d.ts rename packages/turf-great-circle/{index.js => index.ts} (57%) diff --git a/packages/turf-great-circle/bench.ts b/packages/turf-great-circle/bench.ts index 8ca597d6a..87007d3f4 100644 --- a/packages/turf-great-circle/bench.ts +++ b/packages/turf-great-circle/bench.ts @@ -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(); diff --git a/packages/turf-great-circle/index.d.ts b/packages/turf-great-circle/index.d.ts deleted file mode 100644 index eae84e0d0..000000000 --- a/packages/turf-great-circle/index.d.ts +++ /dev/null @@ -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; - -export { greatCircle }; -export default greatCircle; diff --git a/packages/turf-great-circle/index.js b/packages/turf-great-circle/index.ts similarity index 57% rename from packages/turf-great-circle/index.js rename to packages/turf-great-circle/index.ts index c7d36243e..4824e49c1 100644 --- a/packages/turf-great-circle/index.js +++ b/packages/turf-great-circle/index.ts @@ -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 | Position, + end: Feature | Point | Position, + options: { + properties?: GeoJsonProperties; + npoints?: number; + offset?: number; + } = {} +): Feature { // 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 }; diff --git a/packages/turf-great-circle/package.json b/packages/turf-great-circle/package.json index 7509c8640..8711fe041 100644 --- a/packages/turf-great-circle/package.json +++ b/packages/turf-great-circle/package.json @@ -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" } } diff --git a/packages/turf-great-circle/test.ts b/packages/turf-great-circle/test.ts index cf896bc73..31ab691e2 100644 --- a/packages/turf-great-circle/test.ts +++ b/packages/turf-great-circle/test.ts @@ -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; + const end = geojson.features[1] as Feature; + 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([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 ); t.end(); }); +test("turf-great-circle accepts Feature inputs", (t) => { + const { start, end } = getStartEndPoints(fixtures[0]); + t.doesNotThrow( + () => greatCircle(start, end), + "accepts Feature 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]); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 964bd6bbf..191911662 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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