Skip to content

Commit 17d5aa4

Browse files
committed
Initial implementation of poly-slice
0 parents  commit 17d5aa4

File tree

9 files changed

+4046
-0
lines changed

9 files changed

+4046
-0
lines changed

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/node_modules
2+
3+
# Ignore test-related files
4+
/coverage.data
5+
/coverage/
6+
7+
# Build files
8+
/dist

LICENSE

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
ISC License
2+
3+
Copyright (c) 2023 Patrick Ofilada
4+
5+
Permission to use, copy, modify, and/or distribute this software for any
6+
purpose with or without fee is hereby granted, provided that the above
7+
copyright notice and this permission notice appear in all copies.
8+
9+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15+
PERFORMANCE OF THIS SOFTWARE.

README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# poly-slice
2+
3+
PolySlice is a lightweight npm package designed to simplify the process of cutting and splitting GeoJSON polygon using a provided linestring. It offers a straightforward way to perform geometric operations on your spatial data.
4+
5+
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=pcofilada_poly-slice&metric=alert_status)](https://sonarcloud.io/dashboard?id=pcofilada_poly-slice)
6+
![ISC License](https://img.shields.io/static/v1.svg?label=📜%20License&message=ISC&color=informational)
7+
![npm](https://img.shields.io/npm/v/poly-slice?color=brightgreen)
8+
![npm bundle size](https://img.shields.io/bundlephobia/min/poly-slice)
9+
10+
## Installation
11+
12+
NPM
13+
14+
```bash
15+
npm install poly-slice
16+
```
17+
18+
Yarn
19+
20+
```bash
21+
yarn add poly-slice
22+
```
23+
24+
## Usage
25+
26+
```js
27+
import polySlice from "poly-slice";
28+
29+
// Your GeoJSON polygon and linestring features
30+
const polygon = /* your GeoJSON polygon feature */;
31+
const linestring = /* your GeoJSON linestring feature */;
32+
33+
// Use PolySlice to cut the polygon
34+
const slicedPolygons = polySlice(polygon, linestring);
35+
```
36+
37+
## Contributing
38+
39+
Feel free to contribute by opening issues or submitting pull requests. Contributions are always welcome!
40+
41+
## License
42+
43+
This project is licensed under the ISC License - see the [LICENSE](https://github.com/pcofilada/poly-slice/blob/main/LICENSE) file for details.

jest.config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/** @type {import('ts-jest').JestConfigWithTsJest} */
2+
module.exports = {
3+
preset: "ts-jest",
4+
testEnvironment: "node",
5+
};

package.json

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"name": "poly-slice",
3+
"version": "0.0.1",
4+
"description": "PolySlice is a lightweight npm package designed to simplify the process of cutting and splitting GeoJSON polygon using a provided linestring. It offers a straightforward way to perform geometric operations on your spatial data.",
5+
"repository": "https://github.com/pcofilada/poly-slice",
6+
"author": "Patrick Ofilada <pcofilada@gmail.com> (http://patrickofilada.com)",
7+
"keywords": [
8+
"geojson",
9+
"polygon",
10+
"split",
11+
"slice",
12+
"linestring",
13+
"geometry",
14+
"spatial",
15+
"gis",
16+
"shape",
17+
"divide",
18+
"turf",
19+
"map"
20+
],
21+
"files": [
22+
"dist",
23+
"LICENSE",
24+
"README.md"
25+
],
26+
"main": "./dist/src/index.js",
27+
"scripts": {
28+
"build": "tsc",
29+
"test": "jest"
30+
},
31+
"license": "ISC",
32+
"devDependencies": {
33+
"@types/geojson": "^7946.0.13",
34+
"@types/jest": "^29.5.10",
35+
"@types/node": "^20.10.1",
36+
"jest": "^29.7.0",
37+
"ts-jest": "^29.1.1",
38+
"typescript": "^5.3.2"
39+
},
40+
"dependencies": {
41+
"@turf/turf": "7.0.0-alpha.2"
42+
}
43+
}

src/index.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import {
2+
difference,
3+
featureCollection,
4+
lineIntersect,
5+
lineOffset,
6+
lineString,
7+
lineToPolygon,
8+
polygon,
9+
} from "@turf/turf";
10+
import {
11+
Feature,
12+
FeatureCollection,
13+
LineString,
14+
Point,
15+
Polygon,
16+
Position,
17+
} from "geojson";
18+
19+
const findIntersections = (
20+
poly: Feature<Polygon>,
21+
line: Feature<LineString>
22+
): FeatureCollection<Point> => {
23+
return lineIntersect(poly, line);
24+
};
25+
26+
const constructLineOffsets = (
27+
line: Feature<LineString>
28+
): Feature<LineString>[] => {
29+
const offsetWidth = 0.001;
30+
const offsetUnits = "kilometers";
31+
const startOffset = lineOffset(line, offsetWidth, { units: offsetUnits });
32+
const endOffset = lineOffset(line, -offsetWidth, { units: offsetUnits });
33+
34+
return [startOffset, endOffset];
35+
};
36+
37+
const constructCoordinates = (
38+
line: Feature<LineString>,
39+
lineOffsets: Feature<LineString>[],
40+
currentItem: number
41+
): Position[] => {
42+
const coords = line.geometry.coordinates.map((c) => c);
43+
44+
const reversed = lineOffsets[currentItem].geometry.coordinates
45+
.slice()
46+
.reverse()
47+
.map((c) => c);
48+
49+
return [...coords, ...reversed, coords[0]];
50+
};
51+
52+
const createNewPolygon = (coords: Position[]): Feature<Polygon> => {
53+
const newLinestring = lineString(coords);
54+
const newPolygon = lineToPolygon(newLinestring);
55+
56+
return newPolygon as Feature<Polygon>;
57+
};
58+
59+
const clipPolygon = (
60+
poly: Feature<Polygon>,
61+
newPolygon: Feature<Polygon>
62+
): Feature<Polygon> => {
63+
return difference(featureCollection([poly, newPolygon])) as Feature<Polygon>;
64+
};
65+
66+
const createNewGeometries = (
67+
clippedPolygon: Feature<Polygon>,
68+
lineOffsets: Feature<LineString>[],
69+
currentItem: number
70+
): Position[][][] => {
71+
const selectedIndex = (currentItem + 1) % 2;
72+
const coords = clippedPolygon.geometry.coordinates;
73+
const geoms: Position[][][] = [];
74+
75+
coords.forEach((coord) => {
76+
// @ts-ignore-next-line
77+
const newPolygon = polygon(coord);
78+
const intersection = lineIntersect(newPolygon, lineOffsets[selectedIndex]);
79+
80+
if (intersection.features.length > 0) {
81+
geoms.push(newPolygon.geometry.coordinates);
82+
}
83+
});
84+
85+
return geoms;
86+
};
87+
88+
const createNewFeatures = (geometries: Position[][][]): Feature[] => {
89+
return geometries.map((geometry) => polygon(geometry));
90+
};
91+
92+
const polySlice = (
93+
poly: Feature<Polygon>,
94+
line: Feature<LineString>
95+
): Feature<Polygon>[] => {
96+
if (!poly || poly.type !== "Feature" || poly.geometry.type !== "Polygon") {
97+
throw new Error("Invalid polygon");
98+
}
99+
100+
if (!line || line.type !== "Feature" || line.geometry.type !== "LineString") {
101+
throw new Error("Invalid linestring");
102+
}
103+
104+
const intersectionPoints = findIntersections(poly, line);
105+
106+
if (intersectionPoints.features.length === 0) {
107+
throw new Error("Line must intersect with polygon");
108+
}
109+
110+
const slicedFeatures: Feature<Polygon>[] = [];
111+
const lineOffsets = constructLineOffsets(line);
112+
113+
for (let i = 0; i <= 1; i++) {
114+
const coords = constructCoordinates(line, lineOffsets, i);
115+
const newPolygon = createNewPolygon(coords);
116+
const clippedPolygon = clipPolygon(poly, newPolygon);
117+
const newGeometries = createNewGeometries(clippedPolygon, lineOffsets, i);
118+
const newFeatures = createNewFeatures(newGeometries);
119+
120+
slicedFeatures.push(...(newFeatures as Feature<Polygon>[]));
121+
}
122+
123+
return slicedFeatures;
124+
};
125+
126+
export default polySlice;

tests/index.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { lineString, polygon } from "@turf/turf";
2+
import polySlice from "../src";
3+
4+
describe("polySlice", () => {
5+
const poly = polygon([
6+
[
7+
[0, 5],
8+
[10, 15],
9+
[20, 5],
10+
[10, -5],
11+
[0, 5],
12+
],
13+
]);
14+
const line = lineString([
15+
[0, 10],
16+
[20, -10],
17+
]);
18+
19+
it("should throw an error if the polygon is invalid", () => {
20+
// @ts-expect-error testing wrong argument type
21+
expect(() => polySlice(null, line)).toThrow("Invalid polygon");
22+
});
23+
24+
it("should throw an error if the line is invalid", () => {
25+
// @ts-expect-error testing wrong argument type
26+
expect(() => polySlice(poly, null)).toThrow("Invalid line");
27+
});
28+
29+
it("should throw an error if the line doesn't intersect the polygon", () => {
30+
const outsideLine = lineString([
31+
[15.834884892350203, -2.890594495287729],
32+
[25.457996014639605, 6.6301897930659806],
33+
]);
34+
expect(() => polySlice(poly, outsideLine)).toThrow(
35+
"Line must intersect with polygon"
36+
);
37+
});
38+
39+
it("should return sliced polygons", () => {
40+
const result = polySlice(poly, line);
41+
42+
expect(result).toHaveLength(2);
43+
expect(result[0].geometry.coordinates).toEqual([
44+
[
45+
[2.5, 7.5],
46+
[12.5, -2.500000000000001],
47+
[20, 5],
48+
[10, 15],
49+
[2.5, 7.5],
50+
],
51+
]);
52+
expect(result[1].geometry.coordinates).toEqual([
53+
[
54+
[0, 5],
55+
[10, -5],
56+
[12.5, -2.5],
57+
[2.5, 7.5],
58+
[0, 5],
59+
],
60+
]);
61+
});
62+
});

tsconfig.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"compilerOptions": {
3+
"target": "es2022",
4+
"module": "commonjs",
5+
"outDir": "./dist",
6+
"esModuleInterop": true,
7+
"forceConsistentCasingInFileNames": true,
8+
"strict": true,
9+
"skipLibCheck": true,
10+
"declaration": true
11+
}
12+
}

0 commit comments

Comments
 (0)