From 5faa6f189fb1106eeece26c4248fa80cbb5b29bb Mon Sep 17 00:00:00 2001 From: James Beard Date: Mon, 8 Jul 2024 00:16:16 +1000 Subject: [PATCH 1/3] Renamed turf-mask/index.js to .ts in preparation for converting to Typescript. --- packages/turf-mask/{index.js => index.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/turf-mask/{index.js => index.ts} (100%) diff --git a/packages/turf-mask/index.js b/packages/turf-mask/index.ts similarity index 100% rename from packages/turf-mask/index.js rename to packages/turf-mask/index.ts From cf6a856de9e1545e82c1415af96f657fc83d3873 Mon Sep 17 00:00:00 2001 From: James Beard Date: Mon, 8 Jul 2024 00:46:43 +1000 Subject: [PATCH 2/3] Converted turf-mask to Typescript. Fixed an apparently unreported bug where the typings suggested it was ok to pass a Polygon or MultiPolygon Geometry (rather than a complete Feature), but the code would bomb out. --- packages/turf-mask/README.md | 23 +++++---- packages/turf-mask/bench.ts | 11 +++-- packages/turf-mask/index.d.ts | 12 ----- packages/turf-mask/index.ts | 82 +++++++++++++++++++++++---------- packages/turf-mask/package.json | 5 +- packages/turf-mask/test.ts | 78 ++++++++++++++++++++++++++++++- pnpm-lock.yaml | 9 ++++ 7 files changed, 164 insertions(+), 56 deletions(-) delete mode 100644 packages/turf-mask/index.d.ts diff --git a/packages/turf-mask/README.md b/packages/turf-mask/README.md index cfcfcbb16a..38aca306d6 100644 --- a/packages/turf-mask/README.md +++ b/packages/turf-mask/README.md @@ -4,36 +4,35 @@ ## mask -Takes any type of [polygon][1] and an optional mask and returns a [polygon][1] exterior ring with holes. +Takes polygons or multipolygons and an optional mask, and returns an exterior +ring polygon with holes. ### Parameters -* `polygon` **([FeatureCollection][2] | [Feature][3]<([Polygon][4] | [MultiPolygon][5])>)** GeoJSON Polygon used as interior rings or holes. -* `mask` **[Feature][3]<[Polygon][4]>?** GeoJSON Polygon used as the exterior ring (if undefined, the world extent is used) +* `polygon` **([Polygon][1] | [MultiPolygon][2] | [Feature][3]<([Polygon][1] | [MultiPolygon][2])> | [FeatureCollection][4]<([Polygon][1] | [MultiPolygon][2])>)** GeoJSON polygon used as interior rings or holes +* `mask` **([Polygon][1] | [Feature][3]<[Polygon][1]>)?** GeoJSON polygon used as the exterior ring (if undefined, the world extent is used) ### Examples ```javascript -var polygon = turf.polygon([[[112, -21], [116, -36], [146, -39], [153, -24], [133, -10], [112, -21]]]); -var mask = turf.polygon([[[90, -55], [170, -55], [170, 10], [90, 10], [90, -55]]]); +const polygon = turf.polygon([[[112, -21], [116, -36], [146, -39], [153, -24], [133, -10], [112, -21]]]); +const mask = turf.polygon([[[90, -55], [170, -55], [170, 10], [90, 10], [90, -55]]]); -var masked = turf.mask(polygon, mask); +const masked = turf.mask(polygon, mask); //addToMap -var addToMap = [masked] +const addToMap = [masked] ``` -Returns **[Feature][3]<[Polygon][4]>** Masked Polygon (exterior ring with holes). +Returns **[Feature][3]<[Polygon][1]>** Masked Polygon (exterior ring with holes). [1]: https://tools.ietf.org/html/rfc7946#section-3.1.6 -[2]: https://tools.ietf.org/html/rfc7946#section-3.3 +[2]: https://tools.ietf.org/html/rfc7946#section-3.1.7 [3]: https://tools.ietf.org/html/rfc7946#section-3.2 -[4]: https://tools.ietf.org/html/rfc7946#section-3.1.6 - -[5]: https://tools.ietf.org/html/rfc7946#section-3.1.7 +[4]: https://tools.ietf.org/html/rfc7946#section-3.3 diff --git a/packages/turf-mask/bench.ts b/packages/turf-mask/bench.ts index 7d56ab92a2..a6f1aec249 100644 --- a/packages/turf-mask/bench.ts +++ b/packages/turf-mask/bench.ts @@ -1,8 +1,9 @@ +import { Feature, FeatureCollection, Polygon, MultiPolygon } from "geojson"; import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; import { loadJsonFileSync } from "load-json-file"; -import Benchmark from "benchmark"; +import Benchmark, { Event } from "benchmark"; import { mask as turfMask } from "./index.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -17,13 +18,15 @@ const directories = { let fixtures = fs.readdirSync(directories.in).map((filename) => { return { name: path.parse(filename).name, - geojson: loadJsonFileSync(path.join(directories.in, filename)), + geojson: loadJsonFileSync( + path.join(directories.in, filename) + ) as FeatureCollection, }; }); for (const { name, geojson } of fixtures) { const [polygon, masking] = geojson.features; - suite.add(name, () => turfMask(polygon, masking)); + suite.add(name, () => turfMask(polygon, masking as Feature)); } // basic x 4,627 ops/sec ±25.23% (21 runs sampled) @@ -31,7 +34,7 @@ for (const { name, geojson } of fixtures) { // multi-polygon x 5,837 ops/sec ±3.03% (91 runs sampled) // overlapping x 22,326 ops/sec ±1.34% (90 runs sampled) suite - .on("cycle", (event) => { + .on("cycle", (event: Event) => { console.log(String(event.target)); }) .run(); diff --git a/packages/turf-mask/index.d.ts b/packages/turf-mask/index.d.ts deleted file mode 100644 index 3efb5d86e2..0000000000 --- a/packages/turf-mask/index.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Feature, Polygon, MultiPolygon, FeatureCollection } from "geojson"; - -/** - * http://turfjs.org/docs/#mask - */ -declare function mask( - poly: Feature | FeatureCollection | T, - mask?: Feature | Polygon -): Feature; - -export { mask }; -export default mask; diff --git a/packages/turf-mask/index.ts b/packages/turf-mask/index.ts index 5718c98268..ac28e2f185 100644 --- a/packages/turf-mask/index.ts +++ b/packages/turf-mask/index.ts @@ -1,32 +1,54 @@ +import { + Feature, + FeatureCollection, + Polygon, + Position, + MultiPolygon, +} from "geojson"; import { polygon as createPolygon, multiPolygon } from "@turf/helpers"; -import polygonClipping from "polygon-clipping"; +import polygonClipping, { Geom } from "polygon-clipping"; /** - * Takes any type of {@link Polygon|polygon} and an optional mask and returns a {@link Polygon|polygon} exterior ring with holes. + * Takes polygons or multipolygons and an optional mask, and returns an exterior + * ring polygon with holes. * * @name mask - * @param {FeatureCollection|Feature} polygon GeoJSON Polygon used as interior rings or holes. - * @param {Feature} [mask] GeoJSON Polygon used as the exterior ring (if undefined, the world extent is used) + * @param {Polygon|MultiPolygon|Feature|FeatureCollection} polygon GeoJSON polygon used as interior rings or holes + * @param {Polygon|Feature} [mask] GeoJSON polygon used as the exterior ring (if undefined, the world extent is used) * @returns {Feature} Masked Polygon (exterior ring with holes). * @example - * var polygon = turf.polygon([[[112, -21], [116, -36], [146, -39], [153, -24], [133, -10], [112, -21]]]); - * var mask = turf.polygon([[[90, -55], [170, -55], [170, 10], [90, 10], [90, -55]]]); + * const polygon = turf.polygon([[[112, -21], [116, -36], [146, -39], [153, -24], [133, -10], [112, -21]]]); + * const mask = turf.polygon([[[90, -55], [170, -55], [170, 10], [90, 10], [90, -55]]]); * - * var masked = turf.mask(polygon, mask); + * const masked = turf.mask(polygon, mask); * * //addToMap - * var addToMap = [masked] + * const addToMap = [masked] */ -function mask(polygon, mask) { +function mask( + polygon: T | Feature | FeatureCollection, + mask?: Polygon | Feature +): Feature { // Define mask - var maskPolygon = createMask(mask); + const maskPolygon = createMask(mask); var polygonOuters = null; - if (polygon.type === "FeatureCollection") polygonOuters = unionFc(polygon); - else + if (polygon.type === "FeatureCollection") { + polygonOuters = unionFc(polygon); + } else if (polygon.type === "Feature") { + // Need to cast below as Position[][] isn't quite as strict as Geom, even + // though they should be equivalent. polygonOuters = createGeomFromPolygonClippingOutput( - polygonClipping.union(polygon.geometry.coordinates) + polygonClipping.union(polygon.geometry.coordinates as Geom) ); + } else { + // Geometry + // Need to cast below as Position[][] isn't quite as strict as Geom, even + // though they should be equivalent. + polygonOuters = createGeomFromPolygonClippingOutput( + polygonClipping.union(polygon.coordinates as Geom) + ); + } polygonOuters.geometry.coordinates.forEach(function (contour) { maskPolygon.geometry.coordinates.push(contour[0]); @@ -35,23 +57,24 @@ function mask(polygon, mask) { return maskPolygon; } -function unionFc(fc) { - var unioned = +function unionFc(fc: FeatureCollection) { + // Need to cast below as Position[][] isn't quite as strict as Geom, even + // though they should be equivalent. + const unioned = fc.features.length === 2 ? polygonClipping.union( - fc.features[0].geometry.coordinates, - fc.features[1].geometry.coordinates + fc.features[0].geometry.coordinates as Geom, + fc.features[1].geometry.coordinates as Geom ) - : polygonClipping.union.apply( - polygonClipping, - fc.features.map(function (f) { + : polygonClipping.union( + ...(fc.features.map(function (f) { return f.geometry.coordinates; - }) + }) as [Geom, ...Geom[]]) ); return createGeomFromPolygonClippingOutput(unioned); } -function createGeomFromPolygonClippingOutput(unioned) { +function createGeomFromPolygonClippingOutput(unioned: Position[][][]) { return multiPolygon(unioned); } @@ -60,9 +83,9 @@ function createGeomFromPolygonClippingOutput(unioned) { * * @private * @param {Feature} [mask] default to world if undefined - * @returns {Feature} mask coordinate + * @returns {Feature} mask as a polygon */ -function createMask(mask) { +function createMask(mask: Feature | Polygon | undefined) { var world = [ [ [180, 90], @@ -72,7 +95,16 @@ function createMask(mask) { [180, 90], ], ]; - var coordinates = (mask && mask.geometry.coordinates) || world; + let coordinates = world; + if (mask) { + if (mask.type === "Feature") { + // polygon feature + coordinates = mask.geometry.coordinates; + } else { + // polygon geometry + coordinates = mask.coordinates; + } + } return createPolygon(coordinates); } diff --git a/packages/turf-mask/package.json b/packages/turf-mask/package.json index 3f3a07f787..f921c11a91 100644 --- a/packages/turf-mask/package.json +++ b/packages/turf-mask/package.json @@ -60,10 +60,13 @@ "tape": "^5.7.2", "tsup": "^8.0.1", "tsx": "^4.6.2", + "typescript": "^5.2.2", "write-json-file": "^5.0.0" }, "dependencies": { "@turf/helpers": "workspace:^", - "polygon-clipping": "^0.15.3" + "@types/geojson": "7946.0.8", + "polygon-clipping": "^0.15.3", + "tslib": "^2.6.2" } } diff --git a/packages/turf-mask/test.ts b/packages/turf-mask/test.ts index a9f893f299..51cf142bd0 100644 --- a/packages/turf-mask/test.ts +++ b/packages/turf-mask/test.ts @@ -1,3 +1,10 @@ +import { + Feature, + FeatureCollection, + Polygon, + Position, + MultiPolygon, +} from "geojson"; import fs from "fs"; import test from "tape"; import path from "path"; @@ -19,7 +26,9 @@ let 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, }; }); @@ -30,7 +39,7 @@ test("turf-mask", (t) => { } const [polygon, masking] = geojson.features; - const results = mask(polygon, masking); + const results = mask(polygon, masking as Feature); if (process.env.REGEN) writeJsonFileSync(directories.out + filename, results); @@ -38,3 +47,68 @@ test("turf-mask", (t) => { } t.end(); }); + +test("turf-mask polygon geometry", (t) => { + // A polygon somewhere + const polyCoords: Position[] = [ + [9, 13], + [68, 13], + [68, 50], + [9, 50], + [9, 13], + ]; + + const polygonGeometry: Polygon = { + type: "Polygon", + coordinates: [polyCoords], + }; + + let expectedResult = { + type: "Feature", + properties: {}, + geometry: { + type: "Polygon", + coordinates: [ + [ + [180, 90], + [-180, 90], + [-180, -90], + [180, -90], + [180, 90], + ], + polyCoords, + ], + }, + }; + + let result = mask(polygonGeometry); + t.deepEquals(result, expectedResult, "default mask"); + + // A slightly larger polygon surrounding the one above + const customMaskCoords: Position[] = [ + [6, 10], + [71, 10], + [71, 53], + [6, 53], + [6, 10], + ]; + + const maskGeometry: Polygon = { + type: "Polygon", + coordinates: [customMaskCoords], + }; + + expectedResult = { + type: "Feature", + properties: {}, + geometry: { + type: "Polygon", + coordinates: [customMaskCoords, polyCoords], + }, + }; + + result = mask(polygonGeometry, maskGeometry); + t.deepEquals(result, expectedResult, "custom mask"); + + t.end(); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b2a4bd38a2..3849df9af2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4033,9 +4033,15 @@ importers: '@turf/helpers': specifier: workspace:^ version: link:../turf-helpers + '@types/geojson': + specifier: 7946.0.8 + version: 7946.0.8 polygon-clipping: specifier: ^0.15.3 version: 0.15.3 + tslib: + specifier: ^2.6.2 + version: 2.6.2 devDependencies: '@types/benchmark': specifier: ^2.1.5 @@ -4064,6 +4070,9 @@ importers: tsx: specifier: ^4.6.2 version: 4.6.2 + typescript: + specifier: ^5.2.2 + version: 5.3.3 write-json-file: specifier: ^5.0.0 version: 5.0.0 From 2544132d65877200ac75606de5ed4279039d0a92 Mon Sep 17 00:00:00 2001 From: James Beard Date: Mon, 8 Jul 2024 16:40:10 +1000 Subject: [PATCH 3/3] Rolling back use of spread operator instead of apply() as performance took a hit. --- packages/turf-mask/index.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/turf-mask/index.ts b/packages/turf-mask/index.ts index ac28e2f185..5d83ce3c94 100644 --- a/packages/turf-mask/index.ts +++ b/packages/turf-mask/index.ts @@ -32,7 +32,7 @@ function mask( // Define mask const maskPolygon = createMask(mask); - var polygonOuters = null; + let polygonOuters = null; if (polygon.type === "FeatureCollection") { polygonOuters = unionFc(polygon); } else if (polygon.type === "Feature") { @@ -60,17 +60,23 @@ function mask( function unionFc(fc: FeatureCollection) { // Need to cast below as Position[][] isn't quite as strict as Geom, even // though they should be equivalent. + + // Stick with apply() below as spread operator degrades performance. Have + // to disable prefer-spread lint rule though. + /* eslint-disable prefer-spread */ const unioned = fc.features.length === 2 ? polygonClipping.union( fc.features[0].geometry.coordinates as Geom, fc.features[1].geometry.coordinates as Geom ) - : polygonClipping.union( - ...(fc.features.map(function (f) { + : polygonClipping.union.apply( + polygonClipping, + fc.features.map(function (f) { return f.geometry.coordinates; - }) as [Geom, ...Geom[]]) + }) as [Geom, ...Geom[]] ); + /* eslint-enable */ return createGeomFromPolygonClippingOutput(unioned); } @@ -86,7 +92,7 @@ function createGeomFromPolygonClippingOutput(unioned: Position[][][]) { * @returns {Feature} mask as a polygon */ function createMask(mask: Feature | Polygon | undefined) { - var world = [ + const world = [ [ [180, 90], [-180, 90],