simplepolygon 1.2.4 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,34 +6,39 @@ The algorithm is based on a thesis submitted by Lavanya Subramaniam: *Subramania
6
6
 
7
7
  Here, the algorithm was implemented in `JavaScript`, and was extended to work with GeoJSON polygons, in the sense that it can handle outer *and* inner rings self- and cross-intersecting.
8
8
 
9
- ## How to use it
9
+ ## Installation
10
10
 
11
- Install Node.js, then simply use the node package manager 'npm' to
11
+ This package works in browsers and in Node.js as an ESM module.
12
12
 
13
- ```bash
13
+ Install Node.js or any compatible JavaScript runtime, then install this package with your favorite package manager.
14
+
15
+ ```sh
14
16
  npm install simplepolygon
15
17
  ```
16
18
 
17
- In your javascript, you can then use simplepolygon like so:
19
+ You can optionally build this package locally by running:
20
+
21
+ ```sh
22
+ npm run build
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ```js
28
+ import simplepolygon from 'simplepolygon'
18
29
 
19
- ```javascript
20
- var simplepolygon = require('simplepolygon')
30
+ // feature = {type: "Feature", geometry: {type: "Polygon", coordinates: [[[0,0], [2,0], [0,2], [2,2], [0,0]]]}}
21
31
 
22
- var poly = {
23
- "type": "Feature",
24
- "geometry": {
25
- "type": "Polygon",
26
- "coordinates": [[[0,0],[2,0],[0,2],[2,2],[0,0]]]
27
- }
28
- };
29
- var result = simplepolygon(poly)
32
+ const result = simplepolygon(feature);
30
33
  ```
31
34
 
32
- The **input** feature is a GeoJSON polygon which may be non-conform the [Simple Features standard](https://en.wikipedia.org/wiki/Simple_Features) in the sense that it's inner and outer rings may cross-intersect or self-intersect, that the outer ring must not contain the optional inner rings and that the winding number must not be positive for the outer and negative for the inner rings.
35
+ Where `feature` is a GeoJSON Feature of Polygon *geometry*, who's coordinates are an Array of Array of Positions, where each position has at least two coordinates. Only the first two coordinates (typically *X* and *Y* or *lon* and *lat*) are taken into account.
33
36
 
34
- The **output** is a FeatureCollection containing the simple, non-self-intersecting one-ring polygon features that the complex polygon is composed of. These simple polygons have properties such as their parent polygon, winding number and net winding number.
37
+ Importantly, this feature may be *un-conform* the [Simple Features standard](https://en.wikipedia.org/wiki/Simple_Features) in the sense that it's inner and outer rings may cross-intersect or self-intersect, that the outer ring must not contain the optional inner rings and that the winding number must not be positive for the outer and negative for the inner rings.
35
38
 
36
- In the above example, the output will be a FeatureCollection of two polygons, one with coordinates `[[[0,0],[2,0],[1,1],[0,0]]]`, parent -1, winding 1 and net winding 1, and one with coordinates `[[[1,1],[0,2],[2,2],[1,1]]]`, parent -1, winding -1 and net winding -1.
39
+ `result` is a FeatureCollection containing the simple, non-self-intersecting one-ring polygon features that the complex polygon is composed of. These simple polygons have *properties* such as their parent polygon, winding number and net winding number.
40
+
41
+ In the above example, the output will be a FeatureCollection of two polygons, one with coordinates `[[[0,0], [2,0], [1,1], [0,0]]]`, parent -1, winding 1 and net winding 1, and one with coordinates `[[[1,1], [0,2], [2,2], [1,1]]]`, parent -1, winding -1 and net winding -1.
37
42
 
38
43
  Another example input and output is shown below.
39
44
  ![](/example.png?raw=true)
@@ -61,7 +66,7 @@ This algorithm walks from intersections to intersection over (rings and) edges i
61
66
  - At a ring vertex, one pseudo-vertex (ring-pseudo-vertex) and one intersection (ring-intersection) is present
62
67
  - A pseudo-vertex has an incoming and outgoing (crossing) edge
63
68
  - The following objects are stored and passed by the index in the list between brackets: intersections (`isectList`) and pseudo-vertices (`pseudoVtxListByRingAndEdge`)
64
- - The algorithm checks if the input has no non-unique vertices. This is mainly to prevent self-intersecting input polygons such as `[[0,0],[2,0],[1,1],[0,2],[1,3],[2,2],[1,1],[0,0]]`, whose self-intersections would not be detected. As such, many polygons which are non-simple, by the OGC definition, for other reasons then self-intersection, will not be allowed. An exception includes polygons with spikes or cuts such as `[[0, 0], [2, 0], [0, 2], [4, 2], [2, 2], [0, 0]]`, who are currently allowed and treated correctly, but make the output non-simple (by OGC definition). This could be prevented by checking for vertices on other edges.
69
+ - The algorithm checks if the input has no non-unique vertices. This is mainly to prevent self-intersecting input polygons such as `[[0,0], [2,0], [1,1], [0,2], [1,3], [2,2], [1,1], [0,0]]`, whose self-intersections would not be detected. As such, many polygons which are non-simple, by the OGC definition, for other reasons than self-intersection, will not be allowed. An exception includes polygons with spikes or cuts such as `[[0, 0], [2, 0], [0, 2], [4, 2], [2, 2], [0, 0]]`, who are currently allowed and treated correctly, but make the output non-simple (by OGC definition). This could be prevented by checking for vertices on other edges.
65
70
  - The resulting component polygons are one-ring and simple (in the sense that their ring does not contain self-intersections) and two component simple polygons are either disjoint, touching in one or multiple vertices, or one fully encloses the other
66
71
  - This algorithm takes GeoJSON as input, be was developed for a euclidean (and not geodesic) setting. If used in a geodesic setting, the most important consideration to make is the computation of intersection points (which is practice is only an issue of the line segments are relatively long). Further we also note that winding numbers for area's larger than half of the globe are sometimes treated specially. All other concepts of this algorithm (convex angles, direction, ...) can be ported to a geodesic setting without problems.
67
72
  - Since v1.1.1, spatial indexes are used in the underlying computation of edge intersections and throughout the algorithm, to dramatically speed up the computations in case of large polygons
@@ -76,3 +81,9 @@ This code differs from the algorithm and nomenclature of the article it is inspi
76
81
  - Some variables are named differently: `edges` is called `LineSegments` in the article, `ringAndEdgeOut` is `l`, `PseudoVtx` is `nVtx`, `Isect` is `intersection`, `nxtIsectAlongEdgeIn` is `index`, `ringAndEdge1` and `ringAndEdge2` are `origin1` and `origin2`, `pseudoVtxListByRingAndEdge` is `polygonEdgeArray`, `isectList` is `intersectionList` and `isectQueue` is `intersectioQueue`
77
82
  - `pseudoVtxListByRingAndEdge` contains the ring vertex at its end as the last item, and not the ring vertex at its start as the first item
78
83
  - `winding` is not implemented as a property of an intersection, but as its own queue
84
+
85
+ ## Changelog
86
+
87
+ ### 2.0.0 - 2025-12-22
88
+
89
+ - This is now an ESM module written in TypeScript
@@ -0,0 +1,67 @@
1
+ import type { Feature, FeatureCollection, Polygon, Position } from 'geojson';
2
+ /**
3
+ * Takes a complex (i.e. self-intersecting) geojson polygon, and breaks it down into its composite simple, non-self-intersecting one-ring polygons.
4
+ *
5
+ * @module simplepolygon
6
+ * @param {Feature} feature Input polygon. This polygon may be unconform the {@link https://en.wikipedia.org/wiki/Simple_Features|Simple Features standard} in the sense that it's inner and outer rings may cross-intersect or self-intersect, that the outer ring must not contain the optional inner rings and that the winding number must not be positive for the outer and negative for the inner rings.
7
+ * @return {FeatureCollection} Feature collection containing the simple, non-self-intersecting one-ring polygon features that the complex polygon is composed of. These simple polygons have properties such as their parent polygon, winding number and net winding number.
8
+ *
9
+ * @example
10
+ * const poly = {
11
+ * "type": "Feature",
12
+ * "geometry": {
13
+ * "type": "Polygon",
14
+ * "coordinates": [[[0,0],[2,0],[0,2],[2,2],[0,0]]]
15
+ * }
16
+ * };
17
+ *
18
+ * const result = simplepolygon(poly);
19
+ *
20
+ * // =result
21
+ * // which will be a featureCollection of two polygons, one with coordinates [[[0,0],[2,0],[1,1],[0,0]]], parent -1, winding 1 and net winding 1, and one with coordinates [[[1,1],[0,2],[2,2],[1,1]]], parent -1, winding -1 and net winding -1
22
+ */
23
+ export default function simplepolygon(feature: Feature<Polygon>): FeatureCollection<Polygon>;
24
+ /**
25
+ * (Ring- or intersection-) Pseudo-vertices.
26
+ *
27
+ * @export
28
+ * @class PseudoVtx
29
+ * @typedef {PseudoVtx}
30
+ */
31
+ export declare class PseudoVtx {
32
+ coord: Position;
33
+ param: number;
34
+ ringAndEdgeIn: [number, number];
35
+ ringAndEdgeOut: [number, number];
36
+ nxtIsectAlongEdgeIn?: number;
37
+ /**
38
+ * Creates an instance of PseudoVtx.
39
+ *
40
+ * @constructor
41
+ * @param {Position} coord [x,y] of this pseudo-vertex
42
+ * @param {number} param fractional distance of this intersection on incomming edge
43
+ * @param {[number, number]} ringAndEdgeIn [ring index, edge index] of incomming edge
44
+ * @param {[number, number]} ringAndEdgeOut [ring index, edge index] of outgoing edge
45
+ */
46
+ constructor(coord: Position, param: number, ringAndEdgeIn: [number, number], ringAndEdgeOut: [number, number]);
47
+ }
48
+ /**
49
+ * Intersection.
50
+ *
51
+ * There are two intersection-pseudo-vertices per self-intersection and one ring-pseudo-vertex per ring-vertex-intersection. Their labels 1 and 2 are not assigned a particular meaning but are permanent once given.
52
+ *
53
+ * @export
54
+ * @class Intersection
55
+ * @typedef {Isect}
56
+ */
57
+ export declare class Isect {
58
+ coord: Position;
59
+ ringAndEdge1: [number, number];
60
+ ringAndEdge2: [number, number];
61
+ ringAndEdge1Walkable: boolean;
62
+ ringAndEdge2Walkable: boolean;
63
+ nxtIsectAlongRingAndEdge1?: number;
64
+ nxtIsectAlongRingAndEdge2?: number;
65
+ constructor(coord: Position, ringAndEdge1: [number, number], ringAndEdge2: [number, number], ringAndEdge1Walkable: boolean, ringAndEdge2Walkable: boolean);
66
+ }
67
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAW5E;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,OAAO,UAAU,aAAa,CACnC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GACxB,iBAAiB,CAAC,OAAO,CAAC,CA4e5B;AAED;;;;;;GAMG;AACH,qBAAa,SAAS;IACpB,KAAK,EAAE,QAAQ,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;IACb,aAAa,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAE5B;;;;;;;;OAQG;gBAED,KAAK,EAAE,QAAQ,EACf,KAAK,EAAE,MAAM,EACb,aAAa,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/B,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC;CAOnC;AAED;;;;;;;;GAQG;AACH,qBAAa,KAAK;IAChB,KAAK,EAAE,QAAQ,CAAA;IACf,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,oBAAoB,EAAE,OAAO,CAAA;IAC7B,oBAAoB,EAAE,OAAO,CAAA;IAC7B,yBAAyB,CAAC,EAAE,MAAM,CAAA;IAClC,yBAAyB,CAAC,EAAE,MAAM,CAAA;gBAGhC,KAAK,EAAE,QAAQ,EACf,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,oBAAoB,EAAE,OAAO,EAC7B,oBAAoB,EAAE,OAAO;CAQhC"}
package/dist/index.js ADDED
@@ -0,0 +1,566 @@
1
+ import gpsi, { equalArrays } from 'geojson-polygon-self-intersections';
2
+ import { point, polygon, featureCollection } from '@turf/helpers';
3
+ import booleanPointInPolygon from '@turf/boolean-point-in-polygon';
4
+ import area from '@turf/area';
5
+ import RBush from 'rbush';
6
+ /**
7
+ * Takes a complex (i.e. self-intersecting) geojson polygon, and breaks it down into its composite simple, non-self-intersecting one-ring polygons.
8
+ *
9
+ * @module simplepolygon
10
+ * @param {Feature} feature Input polygon. This polygon may be unconform the {@link https://en.wikipedia.org/wiki/Simple_Features|Simple Features standard} in the sense that it's inner and outer rings may cross-intersect or self-intersect, that the outer ring must not contain the optional inner rings and that the winding number must not be positive for the outer and negative for the inner rings.
11
+ * @return {FeatureCollection} Feature collection containing the simple, non-self-intersecting one-ring polygon features that the complex polygon is composed of. These simple polygons have properties such as their parent polygon, winding number and net winding number.
12
+ *
13
+ * @example
14
+ * const poly = {
15
+ * "type": "Feature",
16
+ * "geometry": {
17
+ * "type": "Polygon",
18
+ * "coordinates": [[[0,0],[2,0],[0,2],[2,2],[0,0]]]
19
+ * }
20
+ * };
21
+ *
22
+ * const result = simplepolygon(poly);
23
+ *
24
+ * // =result
25
+ * // which will be a featureCollection of two polygons, one with coordinates [[[0,0],[2,0],[1,1],[0,0]]], parent -1, winding 1 and net winding 1, and one with coordinates [[[1,1],[0,2],[2,2],[1,1]]], parent -1, winding -1 and net winding -1
26
+ */
27
+ export default function simplepolygon(feature) {
28
+ // Check input
29
+ if (feature.type != 'Feature')
30
+ throw new Error('The input must a geojson object of type Feature');
31
+ if (feature.geometry === undefined || feature.geometry == null)
32
+ throw new Error('The input must a geojson object with a non-empty geometry');
33
+ if (feature.geometry.type != 'Polygon')
34
+ throw new Error('The input must be a geojson Polygon');
35
+ // Process input
36
+ const numRings = feature.geometry.coordinates.length;
37
+ const vertices = [];
38
+ for (let i = 0; i < numRings; i++) {
39
+ const ring = feature.geometry.coordinates[i];
40
+ if (!equalArrays(ring[0], ring[ring.length - 1])) {
41
+ ring.push(ring[0]); // Close input ring if it is not
42
+ }
43
+ vertices.push.apply(vertices, ring.slice(0, ring.length - 1));
44
+ }
45
+ if (vertices.length != new Set(vertices).size)
46
+ throw new Error('The input polygon may not have duplicate vertices (except for the first and last vertex of each ring)');
47
+ const numvertices = vertices.length; // number of input ring vertices, with the last closing vertices not counted
48
+ logger('Processing input');
49
+ // Compute self-intersections
50
+ const selfIsectsData = gpsi(feature.geometry.coordinates, {
51
+ callbackFunction: ({ isect, ring0, edge0, start0, end0, frac0, ring1, edge1, start1, end1, frac1, unique }) => {
52
+ return [
53
+ isect,
54
+ ring0,
55
+ edge0,
56
+ start0,
57
+ end0,
58
+ frac0,
59
+ ring1,
60
+ edge1,
61
+ start1,
62
+ end1,
63
+ frac1,
64
+ unique
65
+ ];
66
+ }
67
+ });
68
+ const numSelfIsect = selfIsectsData.length;
69
+ logger('Computing self-intersections');
70
+ // If no self-intersections are found, the input rings are the output rings. Hence, we must only compute their winding numbers, net winding numbers and (since ohers rings could lie outside the first ring) parents.
71
+ if (numSelfIsect == 0) {
72
+ const outputFeatureArray = [];
73
+ for (let i = 0; i < numRings; i++) {
74
+ outputFeatureArray.push(polygon([feature.geometry.coordinates[i]], {
75
+ parent: -1,
76
+ winding: windingOfRing(feature.geometry.coordinates[i])
77
+ }));
78
+ }
79
+ const output = featureCollection(outputFeatureArray);
80
+ determineParents(output);
81
+ setNetWinding(output);
82
+ logger('No self-intersections found. Input rings are output rings. Computed winding numbers, net winding numbers and parents');
83
+ logger('Finishing without self-intersections');
84
+ return output;
85
+ }
86
+ // If self-intersections are found, we will compute the output rings with the help of two intermediate variables
87
+ // First, we build the pseudo vertex list and intersection list
88
+ // The Pseudo vertex list is an array with for each ring an array with for each edge an array containing the pseudo-vertices (as made by their constructor) that have this ring and edge as ringAndEdgeIn, sorted for each edge by their fractional distance on this edge. It's length hence equals numRings.
89
+ const pseudoVtxListByRingAndEdge = [];
90
+ // The intersection list is an array containing intersections (as made by their constructor). First all numvertices ring-vertex-intersections, then all self-intersections (intra- and inter-ring). The order of the latter is not important but is permanent once given.
91
+ const isectList = [];
92
+ // Adding ring-pseudo-vertices to pseudoVtxListByRingAndEdge and ring-vertex-intersections to isectList
93
+ for (let i = 0; i < numRings; i++) {
94
+ pseudoVtxListByRingAndEdge.push([]);
95
+ for (let j = 0; j < feature.geometry.coordinates[i].length - 1; j++) {
96
+ // Each edge will feature one ring-pseudo-vertex in its array, on the last position. i.e. edge j features the ring-pseudo-vertex of the ring vertex j+1, which has ringAndEdgeIn = [i,j], on the last position.
97
+ pseudoVtxListByRingAndEdge[i].push([
98
+ new PseudoVtx(feature.geometry.coordinates[i][mod(j + 1, feature.geometry.coordinates[i].length - 1)], 1, [i, j], [i, mod(j + 1, feature.geometry.coordinates[i].length - 1)])
99
+ ]);
100
+ // The first numvertices elements in isectList correspond to the ring-vertex-intersections
101
+ isectList.push(new Isect(feature.geometry.coordinates[i][j], [i, mod(j - 1, feature.geometry.coordinates[i].length - 1)], [i, j], false, true));
102
+ }
103
+ }
104
+ // Adding intersection-pseudo-vertices to pseudoVtxListByRingAndEdge and self-intersections to isectList
105
+ for (let i = 0; i < numSelfIsect; i++) {
106
+ // Adding intersection-pseudo-vertices made using selfIsectsData to pseudoVtxListByRingAndEdge's array corresponding to the incomming ring and edge
107
+ pseudoVtxListByRingAndEdge[selfIsectsData[i][1]][selfIsectsData[i][2]].push(new PseudoVtx(selfIsectsData[i][0], selfIsectsData[i][5], [selfIsectsData[i][1], selfIsectsData[i][2]], [selfIsectsData[i][6], selfIsectsData[i][7]]));
108
+ // selfIsectsData contains double mentions of each intersection, but we only want to add them once to isectList
109
+ if (selfIsectsData[i][11])
110
+ isectList.push(new Isect(selfIsectsData[i][0], [selfIsectsData[i][1], selfIsectsData[i][2]], [selfIsectsData[i][6], selfIsectsData[i][7]], true, true));
111
+ }
112
+ const numIsect = isectList.length;
113
+ // Sort edge arrays of pseudoVtxListByRingAndEdge by the fractional distance 'param'
114
+ for (let i = 0; i < pseudoVtxListByRingAndEdge.length; i++) {
115
+ for (let j = 0; j < pseudoVtxListByRingAndEdge[i].length; j++) {
116
+ pseudoVtxListByRingAndEdge[i][j].sort(function (a, b) {
117
+ return a.param < b.param ? -1 : 1;
118
+ });
119
+ }
120
+ }
121
+ logger('Setting up pseudoVtxListByRingAndEdge and isectList');
122
+ // Make a spatial index of intersections, in preperation for the following two steps
123
+ const allIsectsAsIsectRbushTreeItem = [];
124
+ for (let i = 0; i < numIsect; i++) {
125
+ allIsectsAsIsectRbushTreeItem.push({
126
+ minX: isectList[i].coord[0],
127
+ minY: isectList[i].coord[1],
128
+ maxX: isectList[i].coord[0],
129
+ maxY: isectList[i].coord[1],
130
+ index: i
131
+ }); // could pass isect: isectList[i], but not necessary
132
+ }
133
+ const isectRbushTree = new RBush();
134
+ isectRbushTree.load(allIsectsAsIsectRbushTreeItem);
135
+ // Now we will teach each intersection in isectList which is the next intersection along both it's [ring, edge]'s, in two steps.
136
+ // First, we find the next intersection for each pseudo-vertex in pseudoVtxListByRingAndEdge:
137
+ // For each pseudovertex in pseudoVtxListByRingAndEdge (3 loops) look at the next pseudovertex on that edge and find the corresponding intersection by comparing coordinates
138
+ for (let i = 0; i < pseudoVtxListByRingAndEdge.length; i++) {
139
+ for (let j = 0; j < pseudoVtxListByRingAndEdge[i].length; j++) {
140
+ for (let k = 0; k < pseudoVtxListByRingAndEdge[i][j].length; k++) {
141
+ let coordToFind;
142
+ if (k == pseudoVtxListByRingAndEdge[i][j].length - 1) {
143
+ // If it's the last pseudoVertex on that edge, then the next pseudoVertex is the first one on the next edge of that ring.
144
+ coordToFind =
145
+ pseudoVtxListByRingAndEdge[i][mod(j + 1, feature.geometry.coordinates[i].length - 1)][0].coord;
146
+ }
147
+ else {
148
+ coordToFind = pseudoVtxListByRingAndEdge[i][j][k + 1].coord;
149
+ }
150
+ const IsectRbushTreeItemFound = isectRbushTree.search({
151
+ minX: coordToFind[0],
152
+ minY: coordToFind[1],
153
+ maxX: coordToFind[0],
154
+ maxY: coordToFind[1]
155
+ })[0]; // We can take [0] of the result, because there is only one isect correponding to a pseudo-vertex
156
+ pseudoVtxListByRingAndEdge[i][j][k].nxtIsectAlongEdgeIn =
157
+ IsectRbushTreeItemFound.index;
158
+ }
159
+ }
160
+ }
161
+ logger('Computing nextIsect for pseudoVtxListByRingAndEdge');
162
+ // Second, we port this knowledge of the next intersection over to the intersections in isectList, by finding the intersection corresponding to each pseudo-vertex and copying the pseudo-vertex' knownledge of the next-intersection over to the intersection
163
+ for (let i = 0; i < pseudoVtxListByRingAndEdge.length; i++) {
164
+ for (let j = 0; j < pseudoVtxListByRingAndEdge[i].length; j++) {
165
+ for (let k = 0; k < pseudoVtxListByRingAndEdge[i][j].length; k++) {
166
+ const coordToFind = pseudoVtxListByRingAndEdge[i][j][k].coord;
167
+ const IsectRbushTreeItemFound = isectRbushTree.search({
168
+ minX: coordToFind[0],
169
+ minY: coordToFind[1],
170
+ maxX: coordToFind[0],
171
+ maxY: coordToFind[1]
172
+ })[0]; // We can take [0] of the result, because there is only one isect correponding to a pseudo-vertex
173
+ const l = IsectRbushTreeItemFound.index;
174
+ if (l < numvertices) {
175
+ // Special treatment at ring-vertices: we correct the misnaming that happened in the previous block, since ringAndEdgeOut = ringAndEdge2 for ring vertices.
176
+ isectList[l].nxtIsectAlongRingAndEdge2 =
177
+ pseudoVtxListByRingAndEdge[i][j][k].nxtIsectAlongEdgeIn;
178
+ }
179
+ else {
180
+ // Port the knowledge of the next intersection from the pseudo-vertices to the intersections, depending on how the edges are labeled in the pseudo-vertex and intersection.
181
+ if (equalArrays(isectList[l].ringAndEdge1, pseudoVtxListByRingAndEdge[i][j][k].ringAndEdgeIn)) {
182
+ isectList[l].nxtIsectAlongRingAndEdge1 =
183
+ pseudoVtxListByRingAndEdge[i][j][k].nxtIsectAlongEdgeIn;
184
+ }
185
+ else {
186
+ isectList[l].nxtIsectAlongRingAndEdge2 =
187
+ pseudoVtxListByRingAndEdge[i][j][k].nxtIsectAlongEdgeIn;
188
+ }
189
+ }
190
+ }
191
+ }
192
+ }
193
+ // This explains why, eventhough when we will walk away from an intersection, we will walk way from the corresponding pseudo-vertex along edgeOut, pseudo-vertices have the property 'nxtIsectAlongEdgeIn' in stead of some propery 'nxtPseudoVtxAlongEdgeOut'. This is because this property (which is easy to find out) is used in the above for nxtIsectAlongRingAndEdge1 and nxtIsectAlongRingAndEdge2!
194
+ logger('Porting nextIsect to isectList');
195
+ // Before we start walking over the intersections to build the output rings, we prepare a queue that stores information on intersections we still have to deal with, and put at least one intersection in it.
196
+ // This queue will contain information on intersections where we can start walking from once the current walk is finished, and its parent output ring (the smallest output ring it lies within, -1 if no parent or parent unknown yet) and its winding number (which we can already determine).
197
+ const queue = [];
198
+ // For each output ring, add the ring-vertex-intersection with the smalles x-value (i.e. the left-most) as a start intersection. By choosing such an extremal intersections, we are sure to start at an intersection that is a convex vertex of its output ring. By adding them all to the queue, we are sure that no rings will be forgotten. If due to ring-intersections such an intersection will be encountered while walking, it will be removed from the queue.
199
+ let i = 0;
200
+ for (let j = 0; j < numRings; j++) {
201
+ let leftIsect = i;
202
+ for (let k = 0; k < feature.geometry.coordinates[j].length - 1; k++) {
203
+ if (isectList[i].coord[0] < isectList[leftIsect].coord[0]) {
204
+ leftIsect = i;
205
+ }
206
+ i++;
207
+ }
208
+ // Compute winding at this left-most ring-vertex-intersection. We thus this by using our knowledge that this extremal vertex must be a convex vertex.
209
+ // We first find the intersection before and after it, and then use them to determine the winding number of the corresponding output ring, since we know that an extremal vertex of a simple, non-self-intersecting ring is always convex, so the only reason it would not be is because the winding number we use to compute it is wrong
210
+ const isectAfterLeftIsect = isectList[leftIsect].nxtIsectAlongRingAndEdge2;
211
+ if (isectAfterLeftIsect == undefined) {
212
+ throw new Error('Next intersection not defined');
213
+ }
214
+ let isectBeforeLeftIsect = 0;
215
+ for (let k = 0; k < isectList.length; k++) {
216
+ if (isectList[k].nxtIsectAlongRingAndEdge1 == leftIsect ||
217
+ isectList[k].nxtIsectAlongRingAndEdge2 == leftIsect) {
218
+ isectBeforeLeftIsect = k;
219
+ break;
220
+ }
221
+ }
222
+ const windingAtIsect = isConvex([
223
+ isectList[isectBeforeLeftIsect].coord,
224
+ isectList[leftIsect].coord,
225
+ isectList[isectAfterLeftIsect].coord
226
+ ], true)
227
+ ? 1
228
+ : -1;
229
+ queue.push({ isect: leftIsect, parent: -1, winding: windingAtIsect });
230
+ }
231
+ // Sort the queue by the same criterion used to find the leftIsect: the left-most leftIsect must be last in the queue, such that it will be popped first, such that we will work from out to in regarding input rings. This assumtion is used when predicting the winding number and parent of a new queue member.
232
+ queue.sort(function (a, b) {
233
+ return isectList[a.isect].coord > isectList[b.isect].coord ? -1 : 1;
234
+ });
235
+ logger('Initial state of the queue: ' + JSON.stringify(queue));
236
+ logger('Setting up queue');
237
+ // Initialise output
238
+ const outputFeatureArray = [];
239
+ // While the queue is not empty, take the last object (i.e. its intersection) out and start making an output ring by walking in the direction that has not been walked away over yet.
240
+ while (queue.length > 0) {
241
+ // Get the last object out of the queue
242
+ const popped = queue.pop();
243
+ const startIsect = popped.isect;
244
+ const currentOutputRingParent = popped.parent;
245
+ const currentOutputRingWinding = popped.winding;
246
+ // Make new output ring and add vertex from starting intersection
247
+ const currentOutputRing = outputFeatureArray.length;
248
+ const currentOutputRingCoords = [isectList[startIsect].coord];
249
+ logger('# Starting output ring number ' +
250
+ outputFeatureArray.length +
251
+ ' with winding ' +
252
+ currentOutputRingWinding +
253
+ ' from intersection ' +
254
+ startIsect);
255
+ if (startIsect < numvertices) {
256
+ logger('This is a ring-vertex-intersections, which means this output ring does not touch existing output rings');
257
+ }
258
+ // Set up the variables used while walking over intersections: 'currentIsect', 'nxtIsect' and 'walkingRingAndEdge'
259
+ let currentIsect = startIsect;
260
+ let walkingRingAndEdge;
261
+ let nxtIsect;
262
+ if (isectList[startIsect].ringAndEdge1Walkable) {
263
+ walkingRingAndEdge = isectList[startIsect].ringAndEdge1;
264
+ if (isectList[startIsect].nxtIsectAlongRingAndEdge1 != undefined) {
265
+ nxtIsect = isectList[startIsect].nxtIsectAlongRingAndEdge1;
266
+ }
267
+ else {
268
+ throw new Error('Next intersection not defined');
269
+ }
270
+ }
271
+ else {
272
+ walkingRingAndEdge = isectList[startIsect].ringAndEdge2;
273
+ if (isectList[startIsect].nxtIsectAlongRingAndEdge2 != undefined) {
274
+ nxtIsect = isectList[startIsect].nxtIsectAlongRingAndEdge2;
275
+ }
276
+ else {
277
+ throw new Error('Next intersection not defined');
278
+ }
279
+ }
280
+ // While we have not arrived back at the same intersection, keep walking
281
+ while (!equalArrays(isectList[startIsect].coord, isectList[nxtIsect].coord)) {
282
+ logger('Walking from intersection ' +
283
+ currentIsect +
284
+ ' to ' +
285
+ nxtIsect +
286
+ ' over ring ' +
287
+ walkingRingAndEdge[0] +
288
+ ' and edge ' +
289
+ walkingRingAndEdge[1]);
290
+ currentOutputRingCoords.push(isectList[nxtIsect].coord);
291
+ logger('Adding intersection ' + nxtIsect + ' to current output ring');
292
+ // If the next intersection is queued, we can remove it, because we will go there now.
293
+ let nxtIsectInQueue;
294
+ for (let i = 0; i < queue.length; i++) {
295
+ if (queue[i].isect == nxtIsect) {
296
+ nxtIsectInQueue = i;
297
+ break;
298
+ }
299
+ }
300
+ if (nxtIsectInQueue != undefined) {
301
+ logger('Removing intersection ' + nxtIsect + ' from queue');
302
+ queue.splice(nxtIsectInQueue, 1);
303
+ }
304
+ // Arriving at this new intersection, we know which will be our next walking ring and edge (if we came from 1 we will walk away from 2 and vice versa),
305
+ // So we can set it as our new walking ring and intersection and remember that we (will) have walked over it
306
+ // If we have never walked away from this new intersection along the other ring and edge then we will soon do, add the intersection (and the parent wand winding number) to the queue
307
+ // (We can predict the winding number and parent as follows: if the edge is convex, the other output ring started from there will have the alternate winding and lie outside of the current one, and thus have the same parent ring as the current ring. Otherwise, it will have the same winding number and lie inside of the current ring. We are, however, only sure of this of an output ring started from there does not enclose the current ring. This is why the initial queue's intersections must be sorted such that outer ones come out first.)
308
+ // We then update the other two walking variables.
309
+ if (equalArrays(walkingRingAndEdge, isectList[nxtIsect].ringAndEdge1)) {
310
+ walkingRingAndEdge = isectList[nxtIsect].ringAndEdge2;
311
+ isectList[nxtIsect].ringAndEdge2Walkable = false;
312
+ if (isectList[nxtIsect].ringAndEdge1Walkable) {
313
+ logger('Adding intersection ' + nxtIsect + ' to queue');
314
+ let pushing;
315
+ const nxtIsectAlongRingAndEdge2 = isectList[nxtIsect].nxtIsectAlongRingAndEdge2;
316
+ if (nxtIsectAlongRingAndEdge2 == undefined) {
317
+ throw new Error('Next intersection not defined');
318
+ }
319
+ if (isConvex([
320
+ isectList[currentIsect].coord,
321
+ isectList[nxtIsect].coord,
322
+ isectList[nxtIsectAlongRingAndEdge2].coord
323
+ ], currentOutputRingWinding == 1)) {
324
+ pushing = {
325
+ isect: nxtIsect,
326
+ parent: currentOutputRingParent,
327
+ winding: -currentOutputRingWinding
328
+ };
329
+ }
330
+ else {
331
+ pushing = {
332
+ isect: nxtIsect,
333
+ parent: currentOutputRing,
334
+ winding: currentOutputRingWinding
335
+ };
336
+ }
337
+ queue.push(pushing);
338
+ }
339
+ currentIsect = nxtIsect;
340
+ const nextIsectCandidate = isectList[nxtIsect].nxtIsectAlongRingAndEdge2;
341
+ if (nextIsectCandidate != undefined) {
342
+ nxtIsect = nextIsectCandidate;
343
+ }
344
+ else {
345
+ throw new Error('Next intersection not defined');
346
+ }
347
+ }
348
+ else {
349
+ walkingRingAndEdge = isectList[nxtIsect].ringAndEdge1;
350
+ isectList[nxtIsect].ringAndEdge1Walkable = false;
351
+ if (isectList[nxtIsect].ringAndEdge2Walkable) {
352
+ logger('Adding intersection ' + nxtIsect + ' to queue');
353
+ let pushing;
354
+ const nxtIsectAlongRingAndEdge1 = isectList[nxtIsect].nxtIsectAlongRingAndEdge1;
355
+ if (nxtIsectAlongRingAndEdge1 == undefined) {
356
+ throw new Error('Next intersection not defined');
357
+ }
358
+ if (isConvex([
359
+ isectList[currentIsect].coord,
360
+ isectList[nxtIsect].coord,
361
+ isectList[nxtIsectAlongRingAndEdge1].coord
362
+ ], currentOutputRingWinding == 1)) {
363
+ pushing = {
364
+ isect: nxtIsect,
365
+ parent: currentOutputRingParent,
366
+ winding: -currentOutputRingWinding
367
+ };
368
+ }
369
+ else {
370
+ pushing = {
371
+ isect: nxtIsect,
372
+ parent: currentOutputRing,
373
+ winding: currentOutputRingWinding
374
+ };
375
+ }
376
+ queue.push(pushing);
377
+ }
378
+ currentIsect = nxtIsect;
379
+ const nextIsectCandidate = isectList[nxtIsect].nxtIsectAlongRingAndEdge1;
380
+ if (nextIsectCandidate != undefined) {
381
+ nxtIsect = nextIsectCandidate;
382
+ }
383
+ else {
384
+ throw new Error('Next intersection not defined');
385
+ }
386
+ }
387
+ logger('Current state of the queue: ' + JSON.stringify(queue));
388
+ }
389
+ logger('Walking from intersection ' +
390
+ currentIsect +
391
+ ' to ' +
392
+ nxtIsect +
393
+ ' over ring ' +
394
+ walkingRingAndEdge[0] +
395
+ ' and edge ' +
396
+ walkingRingAndEdge[1] +
397
+ ' and closing ring');
398
+ // Close output ring
399
+ currentOutputRingCoords.push(isectList[nxtIsect].coord);
400
+ // Push output ring to output
401
+ outputFeatureArray.push(polygon([currentOutputRingCoords], {
402
+ index: currentOutputRing,
403
+ parent: currentOutputRingParent,
404
+ winding: currentOutputRingWinding,
405
+ netWinding: undefined
406
+ }));
407
+ }
408
+ const output = featureCollection(outputFeatureArray);
409
+ logger('Walking');
410
+ determineParents(output);
411
+ logger('Determining parents');
412
+ setNetWinding(output);
413
+ logger('Setting winding number');
414
+ logger('# Total of ' + output.features.length + ' rings');
415
+ return output;
416
+ }
417
+ /**
418
+ * (Ring- or intersection-) Pseudo-vertices.
419
+ *
420
+ * @export
421
+ * @class PseudoVtx
422
+ * @typedef {PseudoVtx}
423
+ */
424
+ export class PseudoVtx {
425
+ coord;
426
+ param;
427
+ ringAndEdgeIn;
428
+ ringAndEdgeOut;
429
+ nxtIsectAlongEdgeIn;
430
+ /**
431
+ * Creates an instance of PseudoVtx.
432
+ *
433
+ * @constructor
434
+ * @param {Position} coord [x,y] of this pseudo-vertex
435
+ * @param {number} param fractional distance of this intersection on incomming edge
436
+ * @param {[number, number]} ringAndEdgeIn [ring index, edge index] of incomming edge
437
+ * @param {[number, number]} ringAndEdgeOut [ring index, edge index] of outgoing edge
438
+ */
439
+ constructor(coord, param, ringAndEdgeIn, ringAndEdgeOut) {
440
+ this.coord = coord;
441
+ this.param = param;
442
+ this.ringAndEdgeIn = ringAndEdgeIn;
443
+ this.ringAndEdgeOut = ringAndEdgeOut;
444
+ }
445
+ }
446
+ /**
447
+ * Intersection.
448
+ *
449
+ * There are two intersection-pseudo-vertices per self-intersection and one ring-pseudo-vertex per ring-vertex-intersection. Their labels 1 and 2 are not assigned a particular meaning but are permanent once given.
450
+ *
451
+ * @export
452
+ * @class Intersection
453
+ * @typedef {Isect}
454
+ */
455
+ export class Isect {
456
+ coord;
457
+ ringAndEdge1;
458
+ ringAndEdge2;
459
+ ringAndEdge1Walkable;
460
+ ringAndEdge2Walkable;
461
+ nxtIsectAlongRingAndEdge1;
462
+ nxtIsectAlongRingAndEdge2;
463
+ constructor(coord, ringAndEdge1, ringAndEdge2, ringAndEdge1Walkable, ringAndEdge2Walkable) {
464
+ this.coord = coord;
465
+ this.ringAndEdge1 = ringAndEdge1;
466
+ this.ringAndEdge2 = ringAndEdge2;
467
+ this.ringAndEdge1Walkable = ringAndEdge1Walkable;
468
+ this.ringAndEdge2Walkable = ringAndEdge2Walkable;
469
+ }
470
+ }
471
+ // Function to determine if three consecutive points of a simple, non-self-intersecting ring make up a convex vertex, assuming the ring is right- or lefthanded
472
+ function isConvex(pts, righthanded) {
473
+ if (typeof righthanded === 'undefined')
474
+ righthanded = true;
475
+ if (pts.length != 3)
476
+ throw new Error('This function requires an array of three points [x,y]');
477
+ const d = (pts[1][0] - pts[0][0]) * (pts[2][1] - pts[0][1]) -
478
+ (pts[1][1] - pts[0][1]) * (pts[2][0] - pts[0][0]);
479
+ return d >= 0 == righthanded;
480
+ }
481
+ // Function to compute winding of simple, non-self-intersecting ring
482
+ function windingOfRing(ring) {
483
+ // Compute the winding number based on the vertex with the smallest x-value, it precessor and successor. An extremal vertex of a simple, non-self-intersecting ring is always convex, so the only reason it is not is because the winding number we use to compute it is wrong
484
+ let leftVtx = 0;
485
+ let winding;
486
+ for (let i = 0; i < ring.length - 1; i++) {
487
+ if (ring[i][0] < ring[leftVtx][0])
488
+ leftVtx = i;
489
+ }
490
+ if (isConvex([
491
+ ring[mod(leftVtx - 1, ring.length - 1)],
492
+ ring[leftVtx],
493
+ ring[mod(leftVtx + 1, ring.length - 1)]
494
+ ], true)) {
495
+ winding = 1;
496
+ }
497
+ else {
498
+ winding = -1;
499
+ }
500
+ return winding;
501
+ }
502
+ // Fix Javascript modulo for negative number. From http://stackoverflow.com/questions/4467539/javascript-modulo-not-behaving
503
+ function mod(n, m) {
504
+ return ((n % m) + m) % m;
505
+ }
506
+ function determineParents(output) {
507
+ const featuresWithoutParent = [];
508
+ for (let i = 0; i < output.features.length; i++) {
509
+ logger('Output ring ' +
510
+ i +
511
+ ' has parent ' +
512
+ output.features[i].properties?.parent);
513
+ if (output.features[i].properties?.parent == -1)
514
+ featuresWithoutParent.push(i);
515
+ }
516
+ logger('The following output ring(s) have no parent: ' + featuresWithoutParent);
517
+ if (featuresWithoutParent.length > 1) {
518
+ for (let i = 0; i < featuresWithoutParent.length; i++) {
519
+ let parent = -1;
520
+ const parentArea = Infinity;
521
+ for (let j = 0; j < output.features.length; j++) {
522
+ if (featuresWithoutParent[i] == j)
523
+ continue;
524
+ if (booleanPointInPolygon(point(output.features[featuresWithoutParent[i]].geometry
525
+ .coordinates[0][0]), output.features[j], { ignoreBoundary: true })) {
526
+ if (area(output.features[j]) < parentArea) {
527
+ parent = j;
528
+ logger('Ring ' +
529
+ featuresWithoutParent[i] +
530
+ ' lies inside output ring ' +
531
+ j);
532
+ }
533
+ }
534
+ }
535
+ const properties = output.features[featuresWithoutParent[i]].properties;
536
+ if (properties)
537
+ properties.parent = parent;
538
+ logger('Ring ' + featuresWithoutParent[i] + ' is assigned parent ' + parent);
539
+ }
540
+ }
541
+ }
542
+ function setNetWinding(output) {
543
+ for (let i = 0; i < output.features.length; i++) {
544
+ const properties = output.features[i].properties;
545
+ if (properties?.parent == -1) {
546
+ const netWinding = properties.winding;
547
+ properties.netWinding = netWinding;
548
+ setNetWindingOfChildren(output, i, netWinding);
549
+ }
550
+ }
551
+ }
552
+ function setNetWindingOfChildren(output, parent, parentNetWinding) {
553
+ for (let i = 0; i < output.features.length; i++) {
554
+ const properties = output.features[i].properties;
555
+ if (properties?.parent == parent) {
556
+ const netWinding = parentNetWinding + properties.winding;
557
+ properties.netWinding = netWinding;
558
+ setNetWindingOfChildren(output, i, netWinding);
559
+ }
560
+ }
561
+ }
562
+ function logger(message) {
563
+ if (process.env.DEBUG === 'true') {
564
+ console.log(message);
565
+ }
566
+ }
package/package.json CHANGED
@@ -1,18 +1,36 @@
1
1
  {
2
2
  "name": "simplepolygon",
3
- "version": "1.2.4",
4
- "main": "index.js",
3
+ "version": "2.0.0",
4
+ "author": {
5
+ "name": "Manuel Claeys Bouuaert",
6
+ "email": "manuel.claeys.b@gmail.com",
7
+ "url": "https://manuelclaeysbouuaert.be"
8
+ },
9
+ "license": "MIT",
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "type": "module",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ }
18
+ },
5
19
  "files": [
6
- "index.js"
20
+ "dist"
7
21
  ],
8
- "scripts": {
9
- "test": "node test.js",
10
- "bench": "node bench.js"
22
+ "publishConfig": {
23
+ "access": "public"
11
24
  },
12
25
  "repository": {
13
26
  "type": "git",
14
27
  "url": "git+https://github.com/mclaeysb/simplepolygon.git"
15
28
  },
29
+ "homepage": "https://github.com/mclaeysb/simplepolygon#readme",
30
+ "bugs": {
31
+ "url": "https://github.com/mclaeysb/simplepolygon/issues"
32
+ },
33
+ "description": "Takes a complex (i.e. self-intersecting) geojson polygon, and breaks it down into its composite simple, non-self-intersecting one-ring polygons.",
16
34
  "keywords": [
17
35
  "polygon",
18
36
  "complex polygon",
@@ -20,28 +38,38 @@
20
38
  "self-intersection",
21
39
  "winding number"
22
40
  ],
23
- "author": {
24
- "name": "Manuel Claeys Bouuaert"
25
- },
26
- "license": "MIT",
27
- "bugs": {
28
- "url": "https://github.com/mclaeysb/simplepolygon/issues"
41
+ "scripts": {
42
+ "watch": "tsc --watch",
43
+ "build": "tsc",
44
+ "test": "NODE_ENV=test vitest run",
45
+ "bench": "vitest bench",
46
+ "lint": "prettier --check src test && eslint src test --ext .js,.ts",
47
+ "format": "prettier --write src test",
48
+ "types": "tsc --noEmit"
29
49
  },
30
50
  "dependencies": {
31
- "@turf/area": "^4.5.0",
32
- "@turf/helpers": "^4.5.0",
33
- "@turf/inside": "^4.5.2",
34
- "@turf/within": "^4.5.0",
35
- "debug": "^2.6.3",
36
- "geojson-polygon-self-intersections": "^1.2.1",
37
- "rbush": "^2.0.1"
51
+ "@turf/area": "7.3.1",
52
+ "@turf/boolean-point-in-polygon": "7.3.1",
53
+ "@turf/helpers": "7.3.1",
54
+ "@turf/inside": "5.0.0",
55
+ "@turf/within": "4.5.0",
56
+ "geojson": "0.5.0",
57
+ "geojson-polygon-self-intersections": "2.0.0",
58
+ "rbush": "4.0.1"
38
59
  },
39
60
  "devDependencies": {
40
- "@turf/meta": "^4.0.1",
41
- "benchmark": "^2.1.4",
42
- "load-json-file": "^2.0.0",
43
- "tape": "^4.6.3",
44
- "write-json-file": "^2.0.0"
45
- },
46
- "homepage": "https://github.com/mclaeysb/simplepolygon#readme"
61
+ "@eslint/eslintrc": "3.3.3",
62
+ "@eslint/js": "9.39.2",
63
+ "@turf/meta": "7.3.1",
64
+ "@types/geojson": "7946.0.16",
65
+ "@types/node": "25.0.3",
66
+ "@types/rbush": "4.0.0",
67
+ "eslint": "9.39.2",
68
+ "eslint-config-prettier": "10.1.8",
69
+ "globals": "16.5.0",
70
+ "prettier": "3.7.4",
71
+ "typescript": "5.9.3",
72
+ "typescript-eslint": "8.50.0",
73
+ "vitest": "4.0.16"
74
+ }
47
75
  }
package/index.js DELETED
@@ -1,431 +0,0 @@
1
- var isects = require('geojson-polygon-self-intersections');
2
- var helpers = require('@turf/helpers');
3
- var inside = require('@turf/inside');
4
- var area = require('@turf/area');
5
- var rbush = require('rbush');
6
- var debug = require('debug')('simplepolygon');
7
- var debugAll = require('debug')('simplepolygon:all');
8
-
9
- /**
10
- * Takes a complex (i.e. self-intersecting) geojson polygon, and breaks it down into its composite simple, non-self-intersecting one-ring polygons.
11
- *
12
- * @module simplepolygon
13
- * @param {Feature} feature Input polygon. This polygon may be unconform the {@link https://en.wikipedia.org/wiki/Simple_Features|Simple Features standard} in the sense that it's inner and outer rings may cross-intersect or self-intersect, that the outer ring must not contain the optional inner rings and that the winding number must not be positive for the outer and negative for the inner rings.
14
- * @return {FeatureCollection} Feature collection containing the simple, non-self-intersecting one-ring polygon features that the complex polygon is composed of. These simple polygons have properties such as their parent polygon, winding number and net winding number.
15
- *
16
- * @example
17
- * var poly = {
18
- * "type": "Feature",
19
- * "geometry": {
20
- * "type": "Polygon",
21
- * "coordinates": [[[0,0],[2,0],[0,2],[2,2],[0,0]]]
22
- * }
23
- * };
24
- *
25
- * var result = simplepolygon(poly);
26
- *
27
- * // =result
28
- * // which will be a featureCollection of two polygons, one with coordinates [[[0,0],[2,0],[1,1],[0,0]]], parent -1, winding 1 and net winding 1, and one with coordinates [[[1,1],[0,2],[2,2],[1,1]]], parent -1, winding -1 and net winding -1
29
- */
30
-
31
- module.exports = function(feature) {
32
- // Check input
33
- if (feature.type != "Feature") throw new Error("The input must a geojson object of type Feature");
34
- if ((feature.geometry === undefined) || (feature.geometry == null)) throw new Error("The input must a geojson object with a non-empty geometry");
35
- if (feature.geometry.type != "Polygon") throw new Error("The input must be a geojson Polygon");
36
-
37
- // Process input
38
- var numRings = feature.geometry.coordinates.length;
39
- var vertices = [];
40
- for (var i = 0; i < numRings; i++) {
41
- var ring = feature.geometry.coordinates[i];
42
- if (!equalArrays(ring[0],ring[ring.length-1])) {
43
- ring.push(ring[0]) // Close input ring if it is not
44
- }
45
- vertices.push.apply(vertices,ring.slice(0,ring.length-1));
46
- }
47
- if (!isUnique(vertices)) throw new Error("The input polygon may not have duplicate vertices (except for the first and last vertex of each ring)");
48
- var numvertices = vertices.length; // number of input ring vertices, with the last closing vertices not counted
49
- debug("Processing input");
50
-
51
- // Compute self-intersections
52
- var selfIsectsData = isects(feature, function filterFn(isect, ring0, edge0, start0, end0, frac0, ring1, edge1, start1, end1, frac1, unique){
53
- return [isect, ring0, edge0, start0, end0, frac0, ring1, edge1, start1, end1, frac1, unique];
54
- });
55
- var numSelfIsect = selfIsectsData.length;
56
- debug("Computing self-intersections");
57
-
58
- // If no self-intersections are found, the input rings are the output rings. Hence, we must only compute their winding numbers, net winding numbers and (since ohers rings could lie outside the first ring) parents.
59
- if (numSelfIsect == 0) {
60
- var outputFeatureArray = [];
61
- for(var i = 0; i < numRings; i++) {
62
- outputFeatureArray.push(helpers.polygon([feature.geometry.coordinates[i]],{parent: -1, winding: windingOfRing(feature.geometry.coordinates[i])}));
63
- }
64
- var output = helpers.featureCollection(outputFeatureArray)
65
- determineParents();
66
- setNetWinding();
67
- debugAll("No self-intersections found. Input rings are output rings. Computed winding numbers, net winding numbers and parents");
68
- debug("Finishing without self-intersections");
69
- return output;
70
- }
71
-
72
- // If self-intersections are found, we will compute the output rings with the help of two intermediate variables
73
- // First, we build the pseudo vertex list and intersection list
74
- // The Pseudo vertex list is an array with for each ring an array with for each edge an array containing the pseudo-vertices (as made by their constructor) that have this ring and edge as ringAndEdgeIn, sorted for each edge by their fractional distance on this edge. It's length hence equals numRings.
75
- var pseudoVtxListByRingAndEdge = [];
76
- // The intersection list is an array containing intersections (as made by their constructor). First all numvertices ring-vertex-intersections, then all self-intersections (intra- and inter-ring). The order of the latter is not important but is permanent once given.
77
- var isectList = [];
78
- // Adding ring-pseudo-vertices to pseudoVtxListByRingAndEdge and ring-vertex-intersections to isectList
79
- for (var i = 0; i < numRings; i++) {
80
- pseudoVtxListByRingAndEdge.push([]);
81
- for (var j = 0; j < feature.geometry.coordinates[i].length-1; j++) {
82
- // Each edge will feature one ring-pseudo-vertex in its array, on the last position. i.e. edge j features the ring-pseudo-vertex of the ring vertex j+1, which has ringAndEdgeIn = [i,j], on the last position.
83
- pseudoVtxListByRingAndEdge[i].push([new PseudoVtx(feature.geometry.coordinates[i][(j+1).modulo(feature.geometry.coordinates[i].length-1)], 1, [i, j], [i, (j+1).modulo(feature.geometry.coordinates[i].length-1)], undefined)]);
84
- // The first numvertices elements in isectList correspond to the ring-vertex-intersections
85
- isectList.push(new Isect(feature.geometry.coordinates[i][j], [i, (j-1).modulo(feature.geometry.coordinates[i].length-1)], [i, j], undefined, undefined, false, true));
86
- }
87
- }
88
- // Adding intersection-pseudo-vertices to pseudoVtxListByRingAndEdge and self-intersections to isectList
89
- for (var i = 0; i < numSelfIsect; i++) {
90
- // Adding intersection-pseudo-vertices made using selfIsectsData to pseudoVtxListByRingAndEdge's array corresponding to the incomming ring and edge
91
- pseudoVtxListByRingAndEdge[selfIsectsData[i][1]][selfIsectsData[i][2]].push(new PseudoVtx(selfIsectsData[i][0], selfIsectsData[i][5], [selfIsectsData[i][1], selfIsectsData[i][2]], [selfIsectsData[i][6], selfIsectsData[i][7]], undefined));
92
- // selfIsectsData contains double mentions of each intersection, but we only want to add them once to isectList
93
- if (selfIsectsData[i][11]) isectList.push(new Isect(selfIsectsData[i][0], [selfIsectsData[i][1], selfIsectsData[i][2]], [selfIsectsData[i][6], selfIsectsData[i][7]], undefined, undefined, true, true));
94
- }
95
- var numIsect = isectList.length;
96
- // Sort edge arrays of pseudoVtxListByRingAndEdge by the fractional distance 'param'
97
- for (var i = 0; i < pseudoVtxListByRingAndEdge.length; i++) {
98
- for (var j = 0; j < pseudoVtxListByRingAndEdge[i].length; j++) {
99
- pseudoVtxListByRingAndEdge[i][j].sort(function(a, b){ return (a.param < b.param) ? -1 : 1 ; } );
100
- }
101
- }
102
- debug("Setting up pseudoVtxListByRingAndEdge and isectList");
103
-
104
- // Make a spatial index of intersections, in preperation for the following two steps
105
- var allIsectsAsIsectRbushTreeItem = [];
106
- for (var i = 0; i < numIsect; i++) {
107
- allIsectsAsIsectRbushTreeItem.push({minX: isectList[i].coord[0], minY: isectList[i].coord[1], maxX: isectList[i].coord[0], maxY: isectList[i].coord[1], index: i}); // could pass isect: isectList[i], but not necessary
108
- }
109
- var isectRbushTree = rbush();
110
- isectRbushTree.load(allIsectsAsIsectRbushTreeItem);
111
-
112
- // Now we will teach each intersection in isectList which is the next intersection along both it's [ring, edge]'s, in two steps.
113
- // First, we find the next intersection for each pseudo-vertex in pseudoVtxListByRingAndEdge:
114
- // For each pseudovertex in pseudoVtxListByRingAndEdge (3 loops) look at the next pseudovertex on that edge and find the corresponding intersection by comparing coordinates
115
- for (var i = 0; i < pseudoVtxListByRingAndEdge.length; i++){
116
- for (var j = 0; j < pseudoVtxListByRingAndEdge[i].length; j++){
117
- for (var k = 0; k < pseudoVtxListByRingAndEdge[i][j].length; k++){
118
- var coordToFind;
119
- if (k == pseudoVtxListByRingAndEdge[i][j].length-1) { // If it's the last pseudoVertex on that edge, then the next pseudoVertex is the first one on the next edge of that ring.
120
- coordToFind = pseudoVtxListByRingAndEdge[i][(j+1).modulo(feature.geometry.coordinates[i].length-1)][0].coord;
121
- } else {
122
- coordToFind = pseudoVtxListByRingAndEdge[i][j][k+1].coord;
123
- }
124
- var IsectRbushTreeItemFound = isectRbushTree.search({minX: coordToFind[0], minY: coordToFind[1], maxX: coordToFind[0], maxY: coordToFind[1]})[0]; // We can take [0] of the result, because there is only one isect correponding to a pseudo-vertex
125
- pseudoVtxListByRingAndEdge[i][j][k].nxtIsectAlongEdgeIn = IsectRbushTreeItemFound.index;
126
- }
127
- }
128
- }
129
- debug("Computing nextIsect for pseudoVtxListByRingAndEdge");
130
-
131
- // Second, we port this knowledge of the next intersection over to the intersections in isectList, by finding the intersection corresponding to each pseudo-vertex and copying the pseudo-vertex' knownledge of the next-intersection over to the intersection
132
- for (var i = 0; i < pseudoVtxListByRingAndEdge.length; i++){
133
- for (var j = 0; j < pseudoVtxListByRingAndEdge[i].length; j++){
134
- for (var k = 0; k < pseudoVtxListByRingAndEdge[i][j].length; k++){
135
- var coordToFind = pseudoVtxListByRingAndEdge[i][j][k].coord;
136
- var IsectRbushTreeItemFound = isectRbushTree.search({minX: coordToFind[0], minY: coordToFind[1], maxX: coordToFind[0], maxY: coordToFind[1]})[0]; // We can take [0] of the result, because there is only one isect correponding to a pseudo-vertex
137
- var l = IsectRbushTreeItemFound.index;
138
- if (l < numvertices) { // Special treatment at ring-vertices: we correct the misnaming that happened in the previous block, since ringAndEdgeOut = ringAndEdge2 for ring vertices.
139
- isectList[l].nxtIsectAlongRingAndEdge2 = pseudoVtxListByRingAndEdge[i][j][k].nxtIsectAlongEdgeIn;
140
- } else { // Port the knowledge of the next intersection from the pseudo-vertices to the intersections, depending on how the edges are labeled in the pseudo-vertex and intersection.
141
- if (equalArrays(isectList[l].ringAndEdge1, pseudoVtxListByRingAndEdge[i][j][k].ringAndEdgeIn)) {
142
- isectList[l].nxtIsectAlongRingAndEdge1 = pseudoVtxListByRingAndEdge[i][j][k].nxtIsectAlongEdgeIn;
143
- } else {
144
- isectList[l].nxtIsectAlongRingAndEdge2 = pseudoVtxListByRingAndEdge[i][j][k].nxtIsectAlongEdgeIn;
145
- }
146
- }
147
- }
148
- }
149
- }
150
- // This explains why, eventhough when we will walk away from an intersection, we will walk way from the corresponding pseudo-vertex along edgeOut, pseudo-vertices have the property 'nxtIsectAlongEdgeIn' in stead of some propery 'nxtPseudoVtxAlongEdgeOut'. This is because this property (which is easy to find out) is used in the above for nxtIsectAlongRingAndEdge1 and nxtIsectAlongRingAndEdge2!
151
- debug("Porting nextIsect to isectList");
152
-
153
- // Before we start walking over the intersections to build the output rings, we prepare a queue that stores information on intersections we still have to deal with, and put at least one intersection in it.
154
- // This queue will contain information on intersections where we can start walking from once the current walk is finished, and its parent output ring (the smallest output ring it lies within, -1 if no parent or parent unknown yet) and its winding number (which we can already determine).
155
- var queue = []
156
- // For each output ring, add the ring-vertex-intersection with the smalles x-value (i.e. the left-most) as a start intersection. By choosing such an extremal intersections, we are sure to start at an intersection that is a convex vertex of its output ring. By adding them all to the queue, we are sure that no rings will be forgotten. If due to ring-intersections such an intersection will be encountered while walking, it will be removed from the queue.
157
- var i = 0;
158
- for (var j = 0; j < numRings; j++) {
159
- var leftIsect = i;
160
- for (var k = 0; k < feature.geometry.coordinates[j].length-1; k++) {
161
- if (isectList[i].coord[0] < isectList[leftIsect].coord[0]) {
162
- leftIsect = i;
163
- }
164
- i++;
165
- }
166
- // Compute winding at this left-most ring-vertex-intersection. We thus this by using our knowledge that this extremal vertex must be a convex vertex.
167
- // We first find the intersection before and after it, and then use them to determine the winding number of the corresponding output ring, since we know that an extremal vertex of a simple, non-self-intersecting ring is always convex, so the only reason it would not be is because the winding number we use to compute it is wrong
168
- var isectAfterLeftIsect = isectList[leftIsect].nxtIsectAlongRingAndEdge2;
169
- for (var k = 0; k < isectList.length; k++) {
170
- if ((isectList[k].nxtIsectAlongRingAndEdge1 == leftIsect) || (isectList[k].nxtIsectAlongRingAndEdge2 == leftIsect)) {
171
- var isectBeforeLeftIsect = k;
172
- break
173
- }
174
- }
175
- var windingAtIsect = isConvex([isectList[isectBeforeLeftIsect].coord,isectList[leftIsect].coord,isectList[isectAfterLeftIsect].coord],true) ? 1 : -1;
176
-
177
- queue.push({isect: leftIsect, parent: -1, winding: windingAtIsect})
178
- }
179
- // Sort the queue by the same criterion used to find the leftIsect: the left-most leftIsect must be last in the queue, such that it will be popped first, such that we will work from out to in regarding input rings. This assumtion is used when predicting the winding number and parent of a new queue member.
180
- queue.sort(function(a, b){ return (isectList[a.isect].coord > isectList[b.isect].coord) ? -1 : 1 });
181
- debugAll("Initial state of the queue: " + JSON.stringify(queue));
182
- debug("Setting up queue");
183
-
184
- // Initialise output
185
- var outputFeatureArray = [];
186
-
187
- // While the queue is not empty, take the last object (i.e. its intersection) out and start making an output ring by walking in the direction that has not been walked away over yet.
188
- while (queue.length>0) {
189
- // Get the last object out of the queue
190
- var popped = queue.pop();
191
- var startIsect = popped.isect;
192
- var currentOutputRingParent = popped.parent;
193
- var currentOutputRingWinding = popped.winding;
194
- // Make new output ring and add vertex from starting intersection
195
- var currentOutputRing = outputFeatureArray.length;
196
- var currentOutputRingCoords = [isectList[startIsect].coord];
197
- debugAll("# Starting output ring number " + outputFeatureArray.length + " with winding " + currentOutputRingWinding + " from intersection " + startIsect);
198
- if (startIsect < numvertices) debugAll("This is a ring-vertex-intersections, which means this output ring does not touch existing output rings");
199
- // Set up the variables used while walking over intersections: 'currentIsect', 'nxtIsect' and 'walkingRingAndEdge'
200
- var currentIsect = startIsect;
201
- if (isectList[startIsect].ringAndEdge1Walkable) {
202
- var walkingRingAndEdge = isectList[startIsect].ringAndEdge1;
203
- var nxtIsect = isectList[startIsect].nxtIsectAlongRingAndEdge1;
204
- } else {
205
- var walkingRingAndEdge = isectList[startIsect].ringAndEdge2;
206
- var nxtIsect = isectList[startIsect].nxtIsectAlongRingAndEdge2;
207
- }
208
- // While we have not arrived back at the same intersection, keep walking
209
- while (!equalArrays(isectList[startIsect].coord,isectList[nxtIsect].coord)){
210
- debugAll("Walking from intersection " + currentIsect + " to " + nxtIsect + " over ring " + walkingRingAndEdge[0] + " and edge " + walkingRingAndEdge[1]);
211
- currentOutputRingCoords.push(isectList[nxtIsect].coord);
212
- debugAll("Adding intersection " + nxtIsect + " to current output ring");
213
- // If the next intersection is queued, we can remove it, because we will go there now.
214
- var nxtIsectInQueue = undefined;
215
- for(var i = 0; i < queue.length; i++) { if (queue[i].isect == nxtIsect) {nxtIsectInQueue = i; break; } }
216
- if (nxtIsectInQueue != undefined) {
217
- debugAll("Removing intersection " + nxtIsect + " from queue");
218
- queue.splice(nxtIsectInQueue,1);
219
- }
220
- // Arriving at this new intersection, we know which will be our next walking ring and edge (if we came from 1 we will walk away from 2 and vice versa),
221
- // So we can set it as our new walking ring and intersection and remember that we (will) have walked over it
222
- // If we have never walked away from this new intersection along the other ring and edge then we will soon do, add the intersection (and the parent wand winding number) to the queue
223
- // (We can predict the winding number and parent as follows: if the edge is convex, the other output ring started from there will have the alternate winding and lie outside of the current one, and thus have the same parent ring as the current ring. Otherwise, it will have the same winding number and lie inside of the current ring. We are, however, only sure of this of an output ring started from there does not enclose the current ring. This is why the initial queue's intersections must be sorted such that outer ones come out first.)
224
- // We then update the other two walking variables.
225
- if (equalArrays(walkingRingAndEdge,isectList[nxtIsect].ringAndEdge1)) {
226
- walkingRingAndEdge = isectList[nxtIsect].ringAndEdge2;
227
- isectList[nxtIsect].ringAndEdge2Walkable = false;
228
- if (isectList[nxtIsect].ringAndEdge1Walkable) {
229
- debugAll("Adding intersection " + nxtIsect + " to queue");
230
- var pushing = {isect: nxtIsect};
231
- if (isConvex([isectList[currentIsect].coord, isectList[nxtIsect].coord, isectList[isectList[nxtIsect].nxtIsectAlongRingAndEdge2].coord],currentOutputRingWinding == 1)) {
232
- pushing.parent = currentOutputRingParent;
233
- pushing.winding = -currentOutputRingWinding;
234
- } else {
235
- pushing.parent = currentOutputRing;
236
- pushing.winding = currentOutputRingWinding;
237
- }
238
- queue.push(pushing);
239
- }
240
- currentIsect = nxtIsect;
241
- nxtIsect = isectList[nxtIsect].nxtIsectAlongRingAndEdge2;
242
- } else {
243
- walkingRingAndEdge = isectList[nxtIsect].ringAndEdge1;
244
- isectList[nxtIsect].ringAndEdge1Walkable = false;
245
- if (isectList[nxtIsect].ringAndEdge2Walkable) {
246
- debugAll("Adding intersection " + nxtIsect + " to queue");
247
- var pushing = {isect: nxtIsect};
248
- if (isConvex([isectList[currentIsect].coord, isectList[nxtIsect].coord, isectList[isectList[nxtIsect].nxtIsectAlongRingAndEdge1].coord],currentOutputRingWinding == 1)) {
249
- pushing.parent = currentOutputRingParent;
250
- pushing.winding = -currentOutputRingWinding;
251
- } else {
252
- pushing.parent = currentOutputRing;
253
- pushing.winding = currentOutputRingWinding;
254
- }
255
- queue.push(pushing);
256
- }
257
- currentIsect = nxtIsect;
258
- nxtIsect = isectList[nxtIsect].nxtIsectAlongRingAndEdge1;
259
- }
260
- debugAll("Current state of the queue: " + JSON.stringify(queue));
261
- }
262
- debugAll("Walking from intersection " + currentIsect + " to " + nxtIsect + " over ring " + walkingRingAndEdge[0] + " and edge " + walkingRingAndEdge[1] + " and closing ring");
263
- // Close output ring
264
- currentOutputRingCoords.push(isectList[nxtIsect].coord);
265
- // Push output ring to output
266
- outputFeatureArray.push(helpers.polygon([currentOutputRingCoords],{index: currentOutputRing, parent: currentOutputRingParent, winding: currentOutputRingWinding, netWinding: undefined}));
267
- }
268
-
269
- var output = helpers.featureCollection(outputFeatureArray);
270
- debug("Walking");
271
-
272
- determineParents();
273
- debug("Determining parents");
274
-
275
- setNetWinding();
276
- debug("Setting winding number");
277
-
278
- // These functions are also used if no intersections are found
279
- function determineParents() {
280
- var featuresWithoutParent = [];
281
- for (var i = 0; i < output.features.length; i++) {
282
- debugAll("Output ring " + i + " has parent " + output.features[i].properties.parent);
283
- if (output.features[i].properties.parent == -1) featuresWithoutParent.push(i);
284
- }
285
- debugAll("The following output ring(s) have no parent: " + featuresWithoutParent);
286
- if (featuresWithoutParent.length > 1) {
287
- for (var i = 0; i < featuresWithoutParent.length; i++) {
288
- var parent = -1;
289
- var parentArea = Infinity;
290
- for (var j = 0; j < output.features.length; j++) {
291
- if (featuresWithoutParent[i] == j) continue
292
- if (inside(helpers.point(output.features[featuresWithoutParent[i]].geometry.coordinates[0][0]), output.features[j], true)) {
293
- if (area(output.features[j]) < parentArea) {
294
- parent = j;
295
- debugAll("Ring "+featuresWithoutParent[i]+" lies inside output ring "+j);
296
- }
297
- }
298
- }
299
- output.features[featuresWithoutParent[i]].properties.parent = parent;
300
- debugAll("Ring "+featuresWithoutParent[i]+" is assigned parent "+parent);
301
- }
302
- }
303
- }
304
-
305
- function setNetWinding() {
306
- for (var i = 0; i < output.features.length; i++) {
307
- if (output.features[i].properties.parent == -1) {
308
- var netWinding = output.features[i].properties.winding
309
- output.features[i].properties.netWinding = netWinding;
310
- setNetWindingOfChildren(i,netWinding)
311
- }
312
- }
313
- }
314
-
315
- function setNetWindingOfChildren(parent,ParentNetWinding){
316
- for (var i = 0; i < output.features.length; i++) {
317
- if (output.features[i].properties.parent == parent){
318
- var netWinding = ParentNetWinding + output.features[i].properties.winding
319
- output.features[i].properties.netWinding = netWinding;
320
- setNetWindingOfChildren(i,netWinding)
321
- }
322
- }
323
- }
324
-
325
- debugAll("# Total of " + output.features.length + " rings");
326
-
327
- return output;
328
- }
329
-
330
-
331
-
332
- // Constructor for (ring- or intersection-) pseudo-vertices.
333
- var PseudoVtx = function (coord, param, ringAndEdgeIn, ringAndEdgeOut, nxtIsectAlongEdgeIn) {
334
- this.coord = coord; // [x,y] of this pseudo-vertex
335
- this.param = param; // fractional distance of this intersection on incomming edge
336
- this.ringAndEdgeIn = ringAndEdgeIn; // [ring index, edge index] of incomming edge
337
- this.ringAndEdgeOut = ringAndEdgeOut; // [ring index, edge index] of outgoing edge
338
- this.nxtIsectAlongEdgeIn = nxtIsectAlongEdgeIn; // The next intersection when following the incomming edge (so not when following ringAndEdgeOut!)
339
- }
340
-
341
- // Constructor for an intersection. There are two intersection-pseudo-vertices per self-intersection and one ring-pseudo-vertex per ring-vertex-intersection. Their labels 1 and 2 are not assigned a particular meaning but are permanent once given.
342
- var Isect = function (coord, ringAndEdge1, ringAndEdge2, nxtIsectAlongRingAndEdge1, nxtIsectAlongRingAndEdge2, ringAndEdge1Walkable, ringAndEdge2Walkable) {
343
- this.coord = coord; // [x,y] of this intersection
344
- this.ringAndEdge1 = ringAndEdge1; // first edge of this intersection
345
- this.ringAndEdge2 = ringAndEdge2; // second edge of this intersection
346
- this.nxtIsectAlongRingAndEdge1 = nxtIsectAlongRingAndEdge1; // the next intersection when following ringAndEdge1
347
- this.nxtIsectAlongRingAndEdge2 = nxtIsectAlongRingAndEdge2; // the next intersection when following ringAndEdge2
348
- this.ringAndEdge1Walkable = ringAndEdge1Walkable; // May we (still) walk away from this intersection over ringAndEdge1?
349
- this.ringAndEdge2Walkable = ringAndEdge2Walkable; // May we (still) walk away from this intersection over ringAndEdge2?
350
- }
351
-
352
- // Function to determine if three consecutive points of a simple, non-self-intersecting ring make up a convex vertex, assuming the ring is right- or lefthanded
353
- function isConvex(pts, righthanded){
354
- // 'pts' is an [x,y] pair
355
- // 'righthanded' is a boolean
356
- if (typeof(righthanded) === 'undefined') righthanded = true;
357
- if (pts.length != 3) throw new Error("This function requires an array of three points [x,y]");
358
- var d = (pts[1][0] - pts[0][0]) * (pts[2][1] - pts[0][1]) - (pts[1][1] - pts[0][1]) * (pts[2][0] - pts[0][0]);
359
- return (d >= 0) == righthanded;
360
- }
361
-
362
- // Function to compute winding of simple, non-self-intersecting ring
363
- function windingOfRing(ring){
364
- // 'ring' is an array of [x,y] pairs with the last equal to the first
365
- // Compute the winding number based on the vertex with the smallest x-value, it precessor and successor. An extremal vertex of a simple, non-self-intersecting ring is always convex, so the only reason it is not is because the winding number we use to compute it is wrong
366
- var leftVtx = 0;
367
- for (var i = 0; i < ring.length-1; i++) { if (ring[i][0] < ring[leftVtx][0]) leftVtx = i; }
368
- if (isConvex([ring[(leftVtx-1).modulo(ring.length-1)],ring[leftVtx],ring[(leftVtx+1).modulo(ring.length-1)]],true)) {
369
- var winding = 1;
370
- } else {
371
- var winding = -1;
372
- }
373
- return winding
374
- }
375
-
376
- // Function to compare Arrays of numbers. From http://stackoverflow.com/questions/7837456/how-to-compare-arrays-in-javascript
377
- function equalArrays(array1, array2) {
378
- // if the other array is a falsy value, return
379
- if (!array1 || !array2)
380
- return false;
381
-
382
- // compare lengths - can save a lot of time
383
- if (array1.length != array2.length)
384
- return false;
385
-
386
- for (var i = 0, l=array1.length; i < l; i++) {
387
- // Check if we have nested arrays
388
- if (array1[i] instanceof Array && array2[i] instanceof Array) {
389
- // recurse into the nested arrays
390
- if (!equalArrays(array1[i],array2[i]))
391
- return false;
392
- }
393
- else if (array1[i] != array2[i]) {
394
- // Warning - two different object instances will never be equal: {x:20} != {x:20}
395
- return false;
396
- }
397
- }
398
- return true;
399
- }
400
-
401
- // Fix Javascript modulo for negative number. From http://stackoverflow.com/questions/4467539/javascript-modulo-not-behaving
402
- Number.prototype.modulo = function(n) {
403
- return ((this % n) + n) % n;
404
- }
405
-
406
- // Function to get array with only unique elements. From http://stackoverflow.com/questions/1960473/unique-values-in-an-array
407
- function getUnique(array) {
408
- var u = {}, a = [];
409
- for(var i = 0, l = array.length; i < l; ++i){
410
- if(u.hasOwnProperty(array[i])) {
411
- continue;
412
- }
413
- a.push(array[i]);
414
- u[array[i]] = 1;
415
- }
416
- return a;
417
- }
418
-
419
- // Function to check if array is unique (i.e. all unique elements, i.e. no duplicate elements)
420
- function isUnique(array) {
421
- var u = {}, a = [];
422
- var isUnique = 1;
423
- for(var i = 0, l = array.length; i < l; ++i){
424
- if(u.hasOwnProperty(array[i])) {
425
- isUnique = 0;
426
- break;
427
- }
428
- u[array[i]] = 1;
429
- }
430
- return isUnique;
431
- }