From b63fbe3799c0cd8be74d65d3e535fbf1debb1d54 Mon Sep 17 00:00:00 2001 From: Matthew Fedderly <24275386+mfedderly@users.noreply.github.com> Date: Mon, 20 Jul 2026 17:22:24 -0400 Subject: [PATCH] Rework projection public API to allow arbitrary projections --- packages/turf-projection/README.md | 16 +++++++ packages/turf-projection/index.ts | 67 ++++++++++++++++-------------- packages/turf-projection/test.ts | 34 ++++++++++++++- 3 files changed, 84 insertions(+), 33 deletions(-) diff --git a/packages/turf-projection/README.md b/packages/turf-projection/README.md index c6cd943277..c64245808b 100644 --- a/packages/turf-projection/README.md +++ b/packages/turf-projection/README.md @@ -48,6 +48,20 @@ var addToMap = [pt, converted]; Returns **[GeoJSON][1]** Projected GeoJSON +## project + +Projects GeoJSON or Position objects using an arbitrary projection function + +### Parameters + +* `geojson` **[GeoJSON][1]** GeoJSON or Position object +* `projection` **[string][5]** A function that implements the projection +* `options` **[Object][3]?** Optional parameters (optional, default `{}`) + + * `options.mutate` **[boolean][4]** allows GeoJSON input to be mutated (significant performance increase if true) (optional, default `false`) + +Returns **[GeoJSON][1]** Converted GeoJSON in the same shape as the original + [1]: https://tools.ietf.org/html/rfc7946#section-3 [2]: https://developer.mozilla.org/docs/Web/API/Position @@ -56,6 +70,8 @@ Returns **[GeoJSON][1]** Projected GeoJSON [4]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean +[5]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String + --- diff --git a/packages/turf-projection/index.ts b/packages/turf-projection/index.ts index 232ff0c99a..7c5b832609 100644 --- a/packages/turf-projection/index.ts +++ b/packages/turf-projection/index.ts @@ -1,6 +1,6 @@ -import { Position } from "geojson"; +import type { Position, GeoJSON } from "geojson"; import { coordEach } from "@turf/meta"; -import { AllGeoJSON, isNumber } from "@turf/helpers"; +import { type AllGeoJSON, isNumber } from "@turf/helpers"; import { clone } from "@turf/clone"; /** @@ -22,7 +22,11 @@ function toMercator( geojson: G, options: { mutate?: boolean } = {} ): G { - return convert(geojson, "mercator", options); + return project( + geojson as GeoJSON | Position, + convertToMercator, + options + ) as G; } /** @@ -44,47 +48,46 @@ function toWgs84( geojson: G, options: { mutate?: boolean } = {} ): G { - return convert(geojson, "wgs84", options); + return project( + geojson as AllGeoJSON | Position, + convertToWgs84, + options + ) as G; } /** - * Converts a GeoJSON coordinates to the defined `projection` + * Projects GeoJSON or Position objects using an arbitrary projection function * - * @private - * @param {GeoJSON} geojson GeoJSON Feature or Geometry - * @param {string} projection defines the projection system to convert the coordinates to + * @param {GeoJSON} geojson GeoJSON or Position object + * @param {string} projection A function that implements the projection * @param {Object} [options] Optional parameters * @param {boolean} [options.mutate=false] allows GeoJSON input to be mutated (significant performance increase if true) - * @returns {GeoJSON} Converted GeoJSON + * @returns {GeoJSON} Converted GeoJSON in the same shape as the original */ -function convert( - geojson: any, - projection: string, +function project( + geojson: G, + projection: (input: number[]) => number[], options: { mutate?: boolean } = {} -): any { - // Optional parameters - options = options || {}; - var mutate = options.mutate; +): G { + const mutate = options?.mutate ?? false; // Validation - if (!geojson) throw new Error("geojson is required"); + if (!geojson) { + throw new Error("geojson is required"); + } - // Handle Position - if (Array.isArray(geojson) && isNumber(geojson[0])) - geojson = - projection === "mercator" - ? convertToMercator(geojson) - : convertToWgs84(geojson); - // Handle GeoJSON - else { + if (Array.isArray(geojson) && isNumber(geojson[0])) { + // Handle Position + geojson = projection(geojson) as G; + } else { + // Handle GeoJSON // Handle possible data mutation - if (mutate !== true) geojson = clone(geojson); + if (mutate !== true) { + geojson = clone(geojson as GeoJSON) as G; + } - coordEach(geojson, function (coord) { - var newCoord = - projection === "mercator" - ? convertToMercator(coord) - : convertToWgs84(coord); + coordEach(geojson as GeoJSON, function (coord) { + var newCoord = projection(coord); coord[0] = newCoord[0]; coord[1] = newCoord[1]; }); @@ -154,4 +157,4 @@ function sign(x: number) { return x < 0 ? -1 : x > 0 ? 1 : 0; } -export { toMercator, toWgs84 }; +export { toMercator, toWgs84, project }; diff --git a/packages/turf-projection/test.ts b/packages/turf-projection/test.ts index ee4ebf34ce..27771a00f9 100644 --- a/packages/turf-projection/test.ts +++ b/packages/turf-projection/test.ts @@ -9,7 +9,8 @@ import { clone } from "@turf/clone"; import { point } from "@turf/helpers"; import { truncate } from "@turf/truncate"; import { coordEach } from "@turf/meta"; -import { toMercator, toWgs84 } from "./index.js"; +import { toMercator, toWgs84, project } from "./index.js"; +import { FeatureCollection, Point } from "geojson"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -129,3 +130,34 @@ test("projection -- handle Position", (t) => { t.deepEqual(coord, wgs84, "coord equal same as wgs84"); t.end(); }); + +test("projection - proj4 usage", (t) => { + const input: FeatureCollection = { + type: "FeatureCollection", + features: [ + { + type: "Feature", + geometry: { + type: "Point", + coordinates: [0, 0], + }, + properties: {}, + }, + ], + }; + + // proj4 projection usage example + const projected = project( + input, + (input) => { + return proj4("WGS84", "EPSG:3857", input); + }, + { mutate: true } + ); + + t.deepEqual( + projected.features[0].geometry.coordinates, + [0, -7.081154551613622e-10] + ); + t.end(); +});