-
Notifications
You must be signed in to change notification settings - Fork 990
Ported union, difference, and intersect to clipper2-ts to improve performance #2997
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
e4ac32f
e6c7431
13bc9fc
ba8b43e
00c2074
39035eb
495bc0d
fc80ef3
6b45ae3
f9641d3
61fe366
08b2034
5032695
b4e0728
1ea01aa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| The MIT License (MIT) | ||
|
|
||
| Copyright (c) 2017 TurfJS | ||
|
|
||
| Permission is hereby granted, free of charge, to any person obtaining a copy of | ||
| this software and associated documentation files (the "Software"), to deal in | ||
| the Software without restriction, including without limitation the rights to | ||
| use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of | ||
| the Software, and to permit persons to whom the Software is furnished to do so, | ||
| subject to the following conditions: | ||
|
|
||
| The above copyright notice and this permission notice shall be included in all | ||
| copies or substantial portions of the Software. | ||
|
|
||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS | ||
| FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR | ||
| COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER | ||
| IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN | ||
| CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| # @turf/internal | ||
|
|
||
| <!-- Generated by documentation.js. Update this documentation by updating the source code. --> | ||
|
|
||
| <!-- 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. --> | ||
|
|
||
| --- | ||
|
|
||
| This module is part of the [Turfjs project](https://turfjs.org/), an open source module collection dedicated to geographic algorithms. It is maintained in the [Turfjs/turf](https://github.com/Turfjs/turf) repository, where you can create PRs and issues. | ||
|
|
||
| ### Installation | ||
|
|
||
| Install this single module individually: | ||
|
|
||
| ```sh | ||
| $ npm install @turf/internal | ||
| ``` | ||
|
|
||
| Or install the all-encompassing @turf/turf module that includes all modules as functions: | ||
|
|
||
| ```sh | ||
| $ npm install @turf/turf | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| { | ||
| "name": "@turf/internal", | ||
| "version": "7.3.1", | ||
| "description": "Common functionality used across multiple Turf packages.", | ||
| "author": "Turf Authors", | ||
| "contributors": [ | ||
| "James Beard <@smallsaucepan>" | ||
| ], | ||
| "license": "MIT", | ||
| "bugs": { | ||
| "url": "https://github.com/Turfjs/turf/issues" | ||
| }, | ||
| "homepage": "https://github.com/Turfjs/turf", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git://github.com/Turfjs/turf.git" | ||
| }, | ||
| "funding": "https://opencollective.com/turf", | ||
| "publishConfig": { | ||
| "access": "public" | ||
| }, | ||
| "type": "module", | ||
| "exports": { | ||
| "./package.json": "./package.json", | ||
| "./clipper2": { | ||
| "import": { | ||
| "types": "./dist/esm/clipper2.d.ts", | ||
| "default": "./dist/esm/clipper2.js" | ||
| }, | ||
| "require": { | ||
| "types": "./dist/cjs/clipper2.d.cts", | ||
| "default": "./dist/cjs/clipper2.cjs" | ||
| } | ||
| } | ||
| }, | ||
| "sideEffects": false, | ||
| "files": [ | ||
| "dist" | ||
| ], | ||
| "scripts": { | ||
| "build": "tsup", | ||
| "test": "pnpm run /test:.*/", | ||
| "test:tape": "tsx test.ts" | ||
| }, | ||
| "devDependencies": { | ||
| "@babel/core": "^7.26.10", | ||
| "@babel/types": "^7.26.10", | ||
| "@types/babel__core": "^7.20.5", | ||
| "@types/tape": "^5.8.1", | ||
| "tape": "^5.9.0", | ||
| "tsup": "^8.4.0" | ||
| }, | ||
| "dependencies": { | ||
| "@types/geojson": "^7946.0.10", | ||
| "clipper2-ts": "^2.0.1", | ||
| "jsbi": "^4.3.2", | ||
| "tslib": "^2.8.1" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,188 @@ | ||
| import { PolyPathD, PathD, PathsD, PolyTreeD, areaD } from "clipper2-ts"; | ||
| import { Polygon, MultiPolygon, Position } from "geojson"; | ||
|
|
||
| /* | ||
| * Clipper2 scaling factor used when converting between decimal and integer | ||
| * values for internal clipper2 calculations. | ||
| * For Turf's purposes defaults to 8 decimal places retained, which is approx | ||
| * 1mm if using decimal degrees at the equator. | ||
| */ | ||
| const TURF_CLIPPER2_SCALE_FACTOR = 8; | ||
|
|
||
| /** | ||
| * Converts a multipolygon to a flattened array of clipper2 paths. | ||
| */ | ||
| function multiPolygonToPaths(coords: Position[][][]): PathsD { | ||
| const paths: PathsD = []; | ||
|
|
||
| for (const polygon of coords) { | ||
| for (const ring of polygonToPaths(polygon)) { | ||
| paths.push(ring); | ||
| } | ||
| } | ||
|
|
||
| return paths; | ||
| } | ||
|
|
||
| /** | ||
| * Converts a polygon to a flattened array of clipper2 paths. | ||
| */ | ||
| function polygonToPaths(coords: Position[][]): PathsD { | ||
| const paths: PathsD = []; | ||
|
|
||
| for (const [idx, ring] of coords.entries()) { | ||
| // Defensive checking against incorrectly wound Geojson polygons. | ||
| const checkedRing = | ||
| idx === 0 | ||
| ? enforceOuterRing(ringToPath(ring)) | ||
| : enforceInnerRing(ringToPath(ring)); | ||
|
|
||
| paths.push(checkedRing); | ||
| } | ||
|
|
||
| return paths; | ||
| } | ||
|
|
||
| /** | ||
| * Make sure this ring is wound as an outer ring, according to clipper2 | ||
| * expectations. That is, clockwise. | ||
| */ | ||
| function enforceOuterRing(path: PathD): PathD { | ||
| if (areaD(path) < 0) { | ||
| return path.reverse(); | ||
| } | ||
|
|
||
| return path; | ||
| } | ||
|
|
||
| /** | ||
| * Make sure this ring is wound as an inner ring, according to clipper2 | ||
| * expectations. That is, counter clockwise. | ||
| */ | ||
| function enforceInnerRing(path: PathD): PathD { | ||
| if (areaD(path) > 0) { | ||
| // Leave original array untouched. | ||
| return [...path].reverse(); | ||
| } | ||
|
|
||
| return path; | ||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed that we should start telling the users to get the winding right before passing them in, but that's likely a major version issue. |
||
|
|
||
| /** | ||
| * Converts a ring to a clipper2 path. | ||
| */ | ||
| function ringToPath(ring: Position[]): PathD { | ||
| return ring.map(([x, y]) => ({ x, y })); | ||
| } | ||
|
|
||
| /** | ||
| * Construct the output Geojson based on a clipper2 tree. The tree is useful | ||
| * for propertly handing holes. | ||
| * | ||
| * @param polyTree hierarchy of outer and inner contours found by clipper2 | ||
| */ | ||
| function polyTreeToGeoJSON(polyTree: PolyTreeD): Polygon | MultiPolygon | null { | ||
| const polygons: Position[][][] = []; | ||
|
|
||
| // Process each top-level polygon (initally all outer contours) | ||
| for (let i = 0; i < polyTree.count; i++) { | ||
| const child = polyTree.child(i); | ||
| processPolyPath(child, polygons); | ||
| } | ||
|
|
||
| if (polygons.length === 0) { | ||
| return null; | ||
| } | ||
|
|
||
| // If exactly 1 polygon return as Geojson Polygon | ||
| if (polygons.length === 1) { | ||
| return { | ||
| type: "Polygon", | ||
| coordinates: polygons[0], | ||
| }; | ||
| } | ||
|
|
||
| // If anything else return as MultiPolygon | ||
| return { | ||
| type: "MultiPolygon", | ||
| coordinates: polygons, | ||
| }; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I kind of want to just always return MultiPolygon's even if there's only 1 polygon, but again... major version.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would make for a simpler interface. Plenty of options for what to return for 0, depending on how many liberties we're willing to take. MultiPolygon with empty coordinates array (not kosher from what I understand but used in the wild). Feature with null geometry (more correct, but back to where we started as far as return type conditional logic). Straight up null as we are now? |
||
| } | ||
|
|
||
| /** | ||
| * Processes a polyPath. Depending on whether the path denotes an outer ring or a hole, will add a polygon to the list of polygons. | ||
| * Recurses to children of this polyPath in turn. | ||
| * | ||
| * @param polyPath outer or inner contour of a polygon to process | ||
| * @param polygons array of completed polygons that this function may add to | ||
| */ | ||
| function processPolyPath(polyPath: PolyPathD, polygons: Position[][][]) { | ||
| // Don't look closely at holes during recursion ... | ||
| if (!polyPath.isHole) { | ||
| const rings: Position[][] = []; | ||
| // Add the outer ring. | ||
| const outerRing = pathToCoordinates(polyPath.poly); | ||
| if (outerRing.length > 0) { | ||
| rings.push(outerRing); | ||
| } | ||
|
|
||
| // ... instead add holes here. | ||
| // Add any holes (direct children are the holes). Do this now rather than | ||
| // during recursion given we already have the outer ring handy. | ||
| for (let i = 0; i < polyPath.count; i++) { | ||
| const child = polyPath.child(i); | ||
|
|
||
| const holeRing = pathToCoordinates(child.poly); | ||
| if (holeRing.length > 0) { | ||
| rings.push(holeRing); | ||
| } | ||
| } | ||
| polygons.push(rings); | ||
| } | ||
|
|
||
| // Now recurse into each child to handle nested levels. | ||
| for (let i = 0; i < polyPath.count; i++) { | ||
| processPolyPath(polyPath.child(i), polygons); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Converts a clipper2 path to an array of Geojson Positions. | ||
| * Automatically closes the path unless overridden e.g. for a lineString. | ||
| * | ||
| * @param path clipper2 path to convert to Positions | ||
| * @param [closeIt=true] close the path by making sure first and last positions are equal | ||
| */ | ||
| function pathToCoordinates( | ||
| path: PathD | null, | ||
| closeIt: boolean = true | ||
| ): Position[] { | ||
| const coords: Position[] = []; | ||
|
|
||
| if (!path || typeof path.length !== "number") { | ||
| return coords; | ||
| } | ||
|
|
||
| for (let i = 0; i < path.length; i++) { | ||
| const pt = path[i]; | ||
| coords.push([pt.x, pt.y]); | ||
| } | ||
|
|
||
| // GeoJSON requires the first and last coordinates to be identical (closed ring) | ||
| if (coords.length > 0 && closeIt) { | ||
| const first = coords[0]; | ||
| const last = coords[coords.length - 1]; | ||
| if (first[0] !== last[0] || first[1] !== last[1]) { | ||
| coords.push([first[0], first[1]]); | ||
| } | ||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is fine, as long as we never do it with a LineString, but everything going through here should be Polygon rings.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a parameter to at least highlight to future generations that it's closing the ring by default. |
||
|
|
||
| return coords; | ||
| } | ||
|
|
||
| export { | ||
| multiPolygonToPaths, | ||
| polygonToPaths, | ||
| polyTreeToGeoJSON, | ||
| TURF_CLIPPER2_SCALE_FACTOR, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import test from "tape"; | ||
| import { FillRule, ClipType, PolyTreeD, ClipperD } from "clipper2-ts"; | ||
| import { | ||
| TURF_CLIPPER2_SCALE_FACTOR, | ||
| multiPolygonToPaths, | ||
| polygonToPaths, | ||
| polyTreeToGeoJSON, | ||
| } from "./src/clipper2/index.js"; | ||
|
|
||
| test("clipper2", (t) => { | ||
| const clipper = new ClipperD(TURF_CLIPPER2_SCALE_FACTOR); | ||
|
|
||
| clipper.clear(); | ||
|
|
||
| t.end(); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| { | ||
| "extends": "../../tsconfig.shared.json" | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added this under
srcrather than reusinglibas feel copy pasting external source into lib is an anti-pattern we may like to keep easily auditable in turf-* packages.