Skip to content

Commit 60d721e

Browse files
committed
export intersectInChunks, intersectInChunksArea, intersectSum. Move them over to clip toolbox, build up docs
1 parent a2a4dd9 commit 60d721e

31 files changed

+663
-634
lines changed

packages/geoprocessing/scripts/base/datasources/precalcVectorDatasource.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ import {
1010
FeatureCollection,
1111
MultiPolygon,
1212
ProjectClientBase,
13-
clipMultiMerge,
1413
createMetric,
1514
datasourceConfig,
1615
genZodErrorMessage,
1716
} from "../../../src/index.js";
1817
import { getFeatures } from "../../../src/dataproviders/index.js";
1918
import { area, truncate, featureCollection } from "@turf/turf";
2019
import { getGeographyFeatures } from "../geographies/helpers.js";
20+
import { clipMultiMerge } from "../../../src/toolbox/clip.js";
2121

2222
/**
2323
* Creates precalc metrics for a datasource and geography

packages/geoprocessing/scripts/global/datasources/mr-eez-land-union-precalc.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import fs from "fs-extra";
22
import { area, bbox, featureCollection as fc } from "@turf/turf";
33
import { $ } from "zx";
44
import { getFeatures } from "../../../src/dataproviders/index.js";
5-
import { chunk, clip, roundDecimal } from "../../../src/helpers/index.js";
5+
import { chunk, roundDecimal } from "../../../src/helpers/index.js";
66
import project from "./ProjectClientGlobal.js";
7+
import { clip } from "../../../src/toolbox/clip.js";
78

89
const outfile = "./mr-eez-land-union-precalc.json";
910
const infile = "/mnt/c/data/EEZ_land_union_v3_202003/EEZ_Land_v3_202030.shp";

packages/geoprocessing/src/helpers/clip.ts

-107
This file was deleted.

packages/geoprocessing/src/helpers/index.ts

-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
export * from "./bboxOverlap.js";
22
export * from "./chunk.js";
3-
export * from "./clip.js";
43
export * from "./feature.js";
54
export * from "./geo.js";
65
export * from "./groupBy.js";

packages/geoprocessing/src/toolbox/area.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { Sketch, SketchCollection, Polygon, Metric } from "../types/index.js";
2-
import { isSketchCollection, clip } from "../helpers/index.js";
2+
import { isSketchCollection } from "../helpers/index.js";
33
import { createMetric } from "../metrics/index.js";
44
import { featureCollection, featureEach, area as turfArea } from "@turf/turf";
5+
import { clip } from "./clip.js";
56

67
/**
78
* Calculates the area of each sketch and collection.
+194
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import polygonClipping from "polygon-clipping";
2+
import {
3+
multiPolygon,
4+
polygon,
5+
geomEach,
6+
getGeom,
7+
featureCollection,
8+
area,
9+
} from "@turf/turf";
10+
import {
11+
Feature,
12+
MultiPolygon,
13+
Polygon,
14+
FeatureCollection,
15+
Position,
16+
GeoJsonProperties,
17+
} from "../types/geojson.js";
18+
import { ValidationError } from "../types/index.js";
19+
import { chunk } from "../helpers/chunk.js";
20+
21+
/**
22+
* Performs one of 4 different clip operations on features
23+
* @param features - FeatureCollection of Polygons or MultiPolygons. First feature is the subject, the rest are the clippers
24+
* @param operation - one of "union", "intersection", "xor", "difference"
25+
* @param options - optional properties to set on the resulting feature
26+
* @returns clipped Feature of Polygon or MultiPolygon
27+
*/
28+
export function clip<
29+
P extends GeoJsonProperties | undefined = GeoJsonProperties,
30+
>(
31+
features: FeatureCollection<Polygon | MultiPolygon>,
32+
operation: "union" | "intersection" | "xor" | "difference",
33+
options: {
34+
properties?: P;
35+
} = {},
36+
): Feature<Polygon | MultiPolygon> | null {
37+
if (!features || !features.features || features.features.length === 0)
38+
throw new ValidationError("Missing or empty features for clip");
39+
const coords: (Position[][] | Position[][][])[] = [];
40+
geomEach(features, (geom) => {
41+
coords.push(geom.coordinates);
42+
});
43+
//@ts-expect-error type mismatch
44+
const clipped = polygonClipping[operation](coords[0], ...coords.slice(1));
45+
46+
if (clipped.length === 0) return null;
47+
if (clipped.length === 1)
48+
return polygon(clipped[0], options.properties) as Feature<
49+
Polygon | MultiPolygon
50+
>;
51+
return multiPolygon(clipped, options.properties) as Feature<
52+
Polygon | MultiPolygon
53+
>;
54+
}
55+
56+
/**
57+
* Performs clip after first merging features2 coords into a single multipolygon.
58+
* Avoids errors in underlying clipping library when too many features in features2
59+
* @param feature1 polygon or multipolygon to clip
60+
* @param features2 collection of polygons or multipolygons to clip feature1 against
61+
* @param operation one of "union", "intersection", "xor", "difference"
62+
* @param options.properties properties to set on the resulting feature
63+
* @returns polygon or multipolygon feature result from clip operation, if no overlap then returns null
64+
* @todo - migrate back to using turf now that it supports multiple features as second argument
65+
*/
66+
export function clipMultiMerge<
67+
P extends GeoJsonProperties | undefined = GeoJsonProperties,
68+
>(
69+
feature1: Feature<Polygon | MultiPolygon>,
70+
features2: FeatureCollection<Polygon | MultiPolygon>,
71+
operation: "union" | "intersection" | "xor" | "difference",
72+
options: {
73+
properties?: P;
74+
} = {},
75+
): Feature<Polygon | MultiPolygon> | null {
76+
if (
77+
!feature1 ||
78+
!features2 ||
79+
!features2.features ||
80+
features2.features.length === 0
81+
)
82+
throw new ValidationError("Missing or empty features for clip");
83+
84+
const geom1 = getGeom(feature1);
85+
// Combine into one multipoly coordinate array
86+
const coords2 = (() => {
87+
return features2.features.reduce<MultiPolygon["coordinates"]>(
88+
(acc, poly) => {
89+
if (poly.geometry.type === "Polygon") {
90+
return [...acc, poly.geometry.coordinates];
91+
} else {
92+
return [...acc, ...poly.geometry.coordinates];
93+
}
94+
},
95+
[],
96+
);
97+
})();
98+
const result = polygonClipping[operation](
99+
geom1.coordinates as any,
100+
coords2 as any,
101+
);
102+
if (result.length === 0) return null;
103+
if (result.length === 1)
104+
return polygon(result[0], options.properties) as Feature<
105+
Polygon | MultiPolygon
106+
>;
107+
return multiPolygon(result, options.properties) as Feature<
108+
Polygon | MultiPolygon
109+
>;
110+
}
111+
112+
/**
113+
* Calculates area overlap between a feature A and a feature array B.
114+
* Intersection is done in chunks on featuresB to avoid errors due to too many features
115+
* @param featureA single feature to intersect with featuresB
116+
* @param featuresB array of features
117+
* @param chunkSize Size of array to split featuresB into, avoids intersect failure due to large array)
118+
* @returns area of intersection of featureA with featuresB
119+
*/
120+
export const intersectInChunksArea = (
121+
featureA: Feature<Polygon | MultiPolygon>,
122+
featuresB: Feature<Polygon | MultiPolygon>[],
123+
chunkSize: number,
124+
) => {
125+
// intersect and get area of remainder
126+
const featureArea = intersectInChunks(featureA, featuresB, chunkSize).reduce(
127+
(sumSoFar, rem) => (rem ? area(rem) + sumSoFar : sumSoFar),
128+
0,
129+
);
130+
return { value: featureArea, indices: [] };
131+
};
132+
133+
/**
134+
* Calculates area overlap between a feature A and a feature array B.
135+
* Intersection is done in chunks on featuresB to avoid errors due to too many features
136+
* @param featureA single feature to intersect with featuresB
137+
* @param featuresB array of features
138+
* @param chunkSize Size of array to split featuresB into, avoids intersect failure due to large array)
139+
* @returns intersection of featureA with featuresB
140+
*/
141+
export const intersectInChunks = (
142+
featureA: Feature<Polygon | MultiPolygon>,
143+
featuresB: Feature<Polygon | MultiPolygon>[],
144+
chunkSize: number,
145+
) => {
146+
// chunk to avoid blowing up intersect
147+
const chunks = chunk(featuresB, chunkSize || 5000);
148+
// intersect chunks and flatten into single result
149+
return chunks.flatMap((curChunk) => {
150+
// MultiMerge will merge curChunk FC into a single multipolygon before intersection
151+
const rem = clipMultiMerge(
152+
featureA,
153+
featureCollection(curChunk),
154+
"intersection",
155+
);
156+
return rem || [];
157+
});
158+
};
159+
160+
/**
161+
* Sums the value of intersecting features. No support for partial, counts the whole feature
162+
* @param featureA single feature to intersect with featuresB
163+
* @param featuresB array of features
164+
* @param sumProperty Property in featuresB with value to sum, if not defined each feature will count as 1
165+
* @returns Sum of features/feature property which overlap with the sketch, and a list of
166+
* indices for features that overlap with the sketch to be used in calculating total sum of
167+
* the sketch collection
168+
*/
169+
export const intersectSum = (
170+
featureA: Feature<Polygon | MultiPolygon>,
171+
featuresB: Feature<Polygon | MultiPolygon>[],
172+
sumProperty?: string,
173+
) => {
174+
const indices: number[] = [];
175+
// intersect and get sum of remainder
176+
const sketchValue = featuresB
177+
.map((curFeature, index) => {
178+
const rem = clip(
179+
featureCollection([featureA, curFeature]),
180+
"intersection",
181+
);
182+
183+
if (!rem) return { count: 0 };
184+
185+
indices.push(index);
186+
187+
if (!sumProperty) return { count: 1 };
188+
else if (curFeature.properties![sumProperty] >= 0)
189+
return { count: curFeature.properties![sumProperty] };
190+
else return { count: 1 };
191+
})
192+
.reduce((sumSoFar, { count }) => sumSoFar + count, 0);
193+
return { value: sketchValue, indices: indices };
194+
};

packages/geoprocessing/src/toolbox/genPreprocessor.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import {
2-
clip,
32
isPolygonFeature,
43
isPolygonFeatureArray,
54
numberFormat,
65
} from "../helpers/index.js";
7-
import { clipMultiMerge } from "../helpers/index.js";
6+
import { clipMultiMerge } from "./clip.js";
87
import {
98
ValidationError,
109
Feature,
@@ -30,6 +29,7 @@ import {
3029
} from "../datasources/helpers.js";
3130
import { ProjectClientInterface } from "../project/ProjectClientBase.js";
3231
import { getFeatures } from "../dataproviders/getFeatures.js";
32+
import { clip } from "./clip.js";
3333

3434
/**
3535
* Returns true if feature is valid and meets requirements set by options.

packages/geoprocessing/src/toolbox/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from "./area.js";
2+
export * from "./clip.js";
23
export * from "./width.js";
34
export * from "./overlapArea.js";
45
export * from "./overlapFeatures.js";

0 commit comments

Comments
 (0)