Skip to content

Commit

Permalink
Converted turf-mask to Typescript and added support for geometry para…
Browse files Browse the repository at this point in the history
…meters (#2644)

* 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.
  • Loading branch information
smallsaucepan authored Jul 8, 2024
1 parent a8038d0 commit 2786f69
Show file tree
Hide file tree
Showing 8 changed files with 225 additions and 111 deletions.
23 changes: 11 additions & 12 deletions packages/turf-mask/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

<!-- This file is automatically generated. Please don't edit it directly. If you find an error, edit the source file of the module in question (likely index.js or index.ts), and re-run "yarn docs" from the root of the turf project. -->

Expand Down
11 changes: 7 additions & 4 deletions packages/turf-mask/bench.ts
Original file line number Diff line number Diff line change
@@ -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));
Expand All @@ -17,21 +18,23 @@ 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<Polygon | MultiPolygon>,
};
});

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<Polygon>));
}

// basic x 4,627 ops/sec ±25.23% (21 runs sampled)
// mask-outside x 3,892 ops/sec ±34.80% (15 runs sampled)
// 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();
12 changes: 0 additions & 12 deletions packages/turf-mask/index.d.ts

This file was deleted.

80 changes: 0 additions & 80 deletions packages/turf-mask/index.js

This file was deleted.

118 changes: 118 additions & 0 deletions packages/turf-mask/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import {
Feature,
FeatureCollection,
Polygon,
Position,
MultiPolygon,
} from "geojson";
import { polygon as createPolygon, multiPolygon } from "@turf/helpers";
import polygonClipping, { Geom } from "polygon-clipping";

/**
* Takes polygons or multipolygons and an optional mask, and returns an exterior
* ring polygon with holes.
*
* @name mask
* @param {Polygon|MultiPolygon|Feature<Polygon|MultiPolygon>|FeatureCollection<Polygon|MultiPolygon>} polygon GeoJSON polygon used as interior rings or holes
* @param {Polygon|Feature<Polygon>} [mask] GeoJSON polygon used as the exterior ring (if undefined, the world extent is used)
* @returns {Feature<Polygon>} Masked Polygon (exterior ring with holes).
* @example
* 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]]]);
*
* const masked = turf.mask(polygon, mask);
*
* //addToMap
* const addToMap = [masked]
*/
function mask<T extends Polygon | MultiPolygon>(
polygon: T | Feature<T> | FeatureCollection<T>,
mask?: Polygon | Feature<Polygon>
): Feature<Polygon> {
// Define mask
const maskPolygon = createMask(mask);

let polygonOuters = null;
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 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]);
});

return maskPolygon;
}

function unionFc(fc: FeatureCollection<Polygon | MultiPolygon>) {
// 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.apply(
polygonClipping,
fc.features.map(function (f) {
return f.geometry.coordinates;
}) as [Geom, ...Geom[]]
);
/* eslint-enable */
return createGeomFromPolygonClippingOutput(unioned);
}

function createGeomFromPolygonClippingOutput(unioned: Position[][][]) {
return multiPolygon(unioned);
}

/**
* Create Mask Coordinates
*
* @private
* @param {Feature<Polygon>} [mask] default to world if undefined
* @returns {Feature<Polygon>} mask as a polygon
*/
function createMask(mask: Feature<Polygon> | Polygon | undefined) {
const world = [
[
[180, 90],
[-180, 90],
[-180, -90],
[180, -90],
[180, 90],
],
];
let coordinates = world;
if (mask) {
if (mask.type === "Feature") {
// polygon feature
coordinates = mask.geometry.coordinates;
} else {
// polygon geometry
coordinates = mask.coordinates;
}
}
return createPolygon(coordinates);
}

export { mask };
export default mask;
5 changes: 4 additions & 1 deletion packages/turf-mask/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
Loading

0 comments on commit 2786f69

Please sign in to comment.