diff --git a/packages/turf-projection/index.ts b/packages/turf-projection/index.ts index 232ff0c99..32d707f95 100644 --- a/packages/turf-projection/index.ts +++ b/packages/turf-projection/index.ts @@ -2,9 +2,35 @@ import { Position } from "geojson"; import { coordEach } from "@turf/meta"; import { AllGeoJSON, isNumber } from "@turf/helpers"; import { clone } from "@turf/clone"; +import proj4 from "proj4"; /** - * Converts a WGS84 GeoJSON object into Mercator (EPSG:900913) projection + * Converts from any Proj4 projection to any Proj4 projection. + * * @function + * @param {GeoJSON|Position} geojson GeoJSON object with coordinates in inProjection + * @param {string} inProjection defines the proj4 projection of the input coordinates, e.g. "WGS84" + * @param {string} outProjection defines the proj4 projection of the output coordinates, e.g. "EPSG:900913" + * @param {Object} [options] Optional parameters + * @param {boolean} [options.mutate=false] allows GeoJSON input to be mutated (significant performance increase if true) + * @returns {GeoJSON} Projected GeoJSON + * @example + * var pt = turf.point([-71,41]); + * var converted = turf.toProj4(pt, "EPSG:3857"); + * + * //addToMap + * var addToMap = [pt, converted]; + */ +function proj4ToProj4( + geojson: G, + inProjection: string, + outProjection: string, + options: { mutate?: boolean } = {} +): G { + return convert(geojson, inProjection, outProjection, options); +} + +/** + * Converts a GeoJSON object into Mercator (EPSG:900913) projection * * @function * @param {GeoJSON|Position} geojson WGS84 GeoJSON object @@ -22,7 +48,7 @@ function toMercator( geojson: G, options: { mutate?: boolean } = {} ): G { - return convert(geojson, "mercator", options); + return convert(geojson, "wgs84", "mercator", options); } /** @@ -44,7 +70,7 @@ function toWgs84( geojson: G, options: { mutate?: boolean } = {} ): G { - return convert(geojson, "wgs84", options); + return convert(geojson, "mercator", "wgs84", options); } /** @@ -52,14 +78,16 @@ function toWgs84( * * @private * @param {GeoJSON} geojson GeoJSON Feature or Geometry - * @param {string} projection defines the projection system to convert the coordinates to + * @param {string} inProjection defines the projection of the input coordinates, e.g. "WGS84" + * @param {string} outProjection defines the projection of the output coordinates, e.g. "EPSG:3857" * @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 */ function convert( geojson: any, - projection: string, + inProjection: string, + outProjection: string, options: { mutate?: boolean } = {} ): any { // Optional parameters @@ -68,23 +96,34 @@ function convert( // Validation if (!geojson) throw new Error("geojson is required"); + if (inProjection == undefined) throw new Error("inProjection is required"); + if (outProjection == undefined) throw new Error("outProjection is required"); // Handle Position - if (Array.isArray(geojson) && isNumber(geojson[0])) - geojson = - projection === "mercator" - ? convertToMercator(geojson) - : convertToWgs84(geojson); + const isPosition = Array.isArray(geojson) && isNumber(geojson[0]); + if (isPosition) { + if (inProjection === "wgs84" && outProjection === "mercator") { + geojson = convertToMercator(geojson); + } else if (inProjection === "mercator" && outProjection === "wgs84") { + geojson = convertToWgs84(geojson); + } else { + geojson = convertToProj4(geojson, inProjection, outProjection); + } + } // Handle GeoJSON else { // Handle possible data mutation if (mutate !== true) geojson = clone(geojson); coordEach(geojson, function (coord) { - var newCoord = - projection === "mercator" - ? convertToMercator(coord) - : convertToWgs84(coord); + let newCoord = []; + if (inProjection === "wgs84" && outProjection === "mercator") { + newCoord = convertToMercator(coord); + } else if (inProjection === "mercator" && outProjection === "wgs84") { + newCoord = convertToWgs84(coord); + } else { + newCoord = convertToProj4(coord, inProjection, outProjection); + } coord[0] = newCoord[0]; coord[1] = newCoord[1]; }); @@ -143,6 +182,19 @@ function convertToWgs84(xy: number[]) { ]; } +/** + * Convert lon/lat values to any Proj4 projection. + * + * @private + * @param {Array} lonLat WGS84 point + * @param {string} inProjection defines the proj4 projection of the input coordinates, e.g. "WGS84" + * @param {string} outProjection defines the proj4 projection to convert the coordinates to, e.g. "EPSG:3857" + * @returns {Array} Projected [x, y] point + */ +function convertToProj4(lonLat: number[], inProjection: string, outProjection: string) { + return proj4(inProjection, outProjection, lonLat); +} + /** * Returns the sign of the input, or zero * @@ -154,4 +206,4 @@ function sign(x: number) { return x < 0 ? -1 : x > 0 ? 1 : 0; } -export { toMercator, toWgs84 }; +export { toMercator, toWgs84, proj4ToProj4 }; diff --git a/packages/turf-projection/package.json b/packages/turf-projection/package.json index 3955d6266..fa9fd8aab 100644 --- a/packages/turf-projection/package.json +++ b/packages/turf-projection/package.json @@ -4,7 +4,8 @@ "description": "Provides tools for conversion between geographic coordinates and projected coordinates.", "author": "Turf Authors", "contributors": [ - "Stefano Borghi <@stebogit>" + "Stefano Borghi <@stebogit>", + "Benjamin W. Portner" ], "license": "MIT", "bugs": { @@ -68,7 +69,6 @@ "@types/tape": "^5.8.1", "benchmark": "^2.1.4", "load-json-file": "^7.0.1", - "proj4": "^2.9.2", "tape": "^5.9.0", "tsup": "^8.4.0", "tsx": "^4.19.4", @@ -80,6 +80,7 @@ "@turf/helpers": "workspace:*", "@turf/meta": "workspace:*", "@types/geojson": "^7946.0.10", + "proj4": "^2.20.4", "tslib": "^2.8.1" } } diff --git a/packages/turf-projection/test.ts b/packages/turf-projection/test.ts index ee4ebf34c..4ac40a42c 100644 --- a/packages/turf-projection/test.ts +++ b/packages/turf-projection/test.ts @@ -9,7 +9,7 @@ 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, proj4ToProj4 } from "./index.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -77,6 +77,24 @@ test("to-wgs84", (t) => { t.end(); }); +test("proj4-to-proj4", (t) => { + // test wgs84 to mercator + // compare against saved toMercator results + for (const { filename, name, geojson } of fromWgs84) { + const expected = loadJsonFileSync(directories.out + "mercator-" + filename); + const results = truncate(proj4ToProj4(geojson, "WGS84", "EPSG:900913")); + t.deepEqual(results, expected); + } + // test mercator to wgs84 + // compare against saved toWgs84 results + for (const { filename, name, geojson } of fromMercator) { + const expected = loadJsonFileSync(directories.out + "wgs84-" + filename); + const results = truncate(proj4ToProj4(geojson, "EPSG:900913", "WGS84")); + t.deepEqual(results, expected); + } + t.end(); +}); + test("projection -- throws", (t) => { t.throws( () => toMercator(null), @@ -88,37 +106,65 @@ test("projection -- throws", (t) => { /geojson is required/, "throws missing geojson" ); + t.throws( + () => proj4ToProj4(null, "WGS84", "EPSG:900913"), + /geojson is required/, + "throws missing geojson" + ); + t.throws( + () => proj4ToProj4(point([0, 0]), null, "EPSG:900913"), + /inProjection is required/, + "throws missing inProjection" + ); + t.throws( + () => proj4ToProj4(point([0, 0]), "WGS84", null), + /outProjection is required/, + "throws missing outProjection" + ); t.end(); }); test("projection -- verify mutation", (t) => { const pt1 = point([10, 10]); const pt2 = point([15, 15]); + const pt3 = point([20, 20]); const pt1Before = clone(pt1); const pt2Before = clone(pt2); + const pt3Before = clone(pt3); toMercator(pt1); - toMercator(pt1, { mutate: false }); t.deepEqual( pt1, pt1Before, "mutate = undefined - input should NOT be mutated" ); + toMercator(pt1, { mutate: false }); t.deepEqual(pt1, pt1Before, "mutate = false - input should NOT be mutated"); toMercator(pt1, { mutate: true }); t.notEqual(pt1, pt1Before, "input should be mutated"); toWgs84(pt2); - toWgs84(pt2, { mutate: false }); t.deepEqual( pt2, pt2Before, "mutate = undefined - input should NOT be mutated" ); + toWgs84(pt2, { mutate: false }); t.deepEqual(pt2, pt2Before, "mutate = false - input should NOT be mutated"); toWgs84(pt2, { mutate: true }); t.notEqual(pt2, pt2Before, "input should be mutated"); + proj4ToProj4(pt3, "WGS84", "EPSG:900913"); + t.deepEqual( + pt3, + pt3Before, + "mutate = undefined - input should NOT be mutated" + ); + proj4ToProj4(pt3, "WGS84", "EPSG:900913", { mutate: false }); + t.deepEqual(pt3, pt3Before, "mutate = false - input should NOT be mutated"); + proj4ToProj4(pt3, "WGS84", "EPSG:900913", { mutate: true }); + t.notEqual(pt3, pt3Before, "input should be mutated"); + t.end(); }); @@ -126,6 +172,9 @@ test("projection -- handle Position", (t) => { const coord = [10, 40]; const mercator = toMercator(coord); const wgs84 = toWgs84(mercator); - t.deepEqual(coord, wgs84, "coord equal same as wgs84"); + t.deepEqual(coord, wgs84, "coord equal to wgs84"); + const proj4Mercator = proj4ToProj4(coord, "WGS84", "EPSG:900913"); + const proj4Wgs84 = proj4ToProj4(proj4Mercator, "EPSG:900913", "WGS84"); + t.deepEqual(coord, proj4Wgs84,"coord equal to proj4 wgs84"); t.end(); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f18631ed4..03d000bcf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4945,6 +4945,9 @@ importers: '@types/geojson': specifier: ^7946.0.10 version: 7946.0.14 + proj4: + specifier: ^2.20.4 + version: 2.20.4 tslib: specifier: ^2.8.1 version: 2.8.1 @@ -4964,9 +4967,6 @@ importers: load-json-file: specifier: ^7.0.1 version: 7.0.1 - proj4: - specifier: ^2.9.2 - version: 2.9.2 tape: specifier: ^5.9.0 version: 5.9.0 @@ -7185,6 +7185,7 @@ packages: '@lerna/create@9.0.3': resolution: {integrity: sha512-hUTEWrR8zH+/Z3bp/R1aLm6DW8vB/BB7KH7Yeg4fMfrvSwxegiLVW9uJFAzWkK4mzEagmj/Dti85Yg9MN13t0g==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + deprecated: This package is an implementation detail of Lerna and is no longer published separately. '@ljharb/resumer@0.1.3': resolution: {integrity: sha512-d+tsDgfkj9X5QTriqM4lKesCkMMJC3IrbPKHvayP00ELx2axdXvDfWkqjxrLXIzGcQzmj7VAUT1wopqARTvafw==} @@ -9058,6 +9059,7 @@ packages: git-raw-commits@3.0.0: resolution: {integrity: sha512-b5OHmZ3vAgGrDn/X0kS+9qCfNKWe4K/jFnhwzVWWg0/k5eLa3060tZShrRg8Dja5kPc+YjS0Gc6y7cRr44Lpjw==} engines: {node: '>=14'} + deprecated: This package is no longer maintained. For the JavaScript API, please use @conventional-changelog/git-client instead. hasBin: true git-remote-origin-url@2.0.0: @@ -9067,6 +9069,7 @@ packages: git-semver-tags@5.0.1: resolution: {integrity: sha512-hIvOeZwRbQ+7YEUmCkHqo8FOLQZCEn18yevLHADlFPZY02KJGsu5FZt9YW/lybfK2uhWFI7Qg/07LekJiTv7iA==} engines: {node: '>=14'} + deprecated: This package is no longer maintained. For the JavaScript API, please use @conventional-changelog/git-client instead. hasBin: true git-up@7.0.0: @@ -10666,8 +10669,8 @@ packages: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} - proj4@2.9.2: - resolution: {integrity: sha512-bdyfNmtlWjQN/rHEHEiqFvpTUHhuzDaeQ6Uu1G4sPGqk+Xkxae6ahh865fClJokSGPBmlDOQWWaO6465TCfv5Q==} + proj4@2.20.4: + resolution: {integrity: sha512-/EEBoXjBw+zW1Lofinw0YFQ4OFOqC6XThnwRAgjEHw8WBN+wSy/6aeqRRdjy4b2igy3X0k3RNDuwcYqcQq7nDw==} promise-all-reject-late@1.0.1: resolution: {integrity: sha512-vuf0Lf0lOxyQREH7GDIOUMLS7kz+gs8i6B+Yi8dC68a2sychGrHTJYghMBD6k7eUcH0H5P73EckCA48xijWqXw==} @@ -11264,10 +11267,12 @@ packages: tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me tar@7.5.2: resolution: {integrity: sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==} engines: {node: '>=18'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me temp-dir@1.0.0: resolution: {integrity: sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ==} @@ -11662,8 +11667,8 @@ packages: resolution: {integrity: sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==} engines: {node: '>= 12.0.0'} - wkt-parser@1.3.3: - resolution: {integrity: sha512-ZnV3yH8/k58ZPACOXeiHaMuXIiaTk1t0hSUVisbO0t4RjA5wPpUytcxeyiN2h+LZRrmuHIh/1UlrR9e7DHDvTw==} + wkt-parser@1.5.5: + resolution: {integrity: sha512-/zMYi94/7D7fxcOSlVmWn6vnOMj3Gq5d1xvVjaYOS9n6h0qOJ4I7YYVxBWYcH1vq9+suhqzXkn05Yx47zQNUIA==} word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} @@ -17019,10 +17024,10 @@ snapshots: progress@2.0.3: {} - proj4@2.9.2: + proj4@2.20.4: dependencies: mgrs: 1.0.0 - wkt-parser: 1.3.3 + wkt-parser: 1.5.5 promise-all-reject-late@1.0.1: {} @@ -18167,7 +18172,7 @@ snapshots: triple-beam: 1.4.1 winston-transport: 4.9.0 - wkt-parser@1.3.3: {} + wkt-parser@1.5.5: {} word-wrap@1.2.5: {}