@seasketch/geoprocessing
Version:
Geoprocessing and reporting framework for SeaSketch 2.0
279 lines • 11.5 kB
JavaScript
import { isSketchCollection, toSketchArray } from "../helpers/index.js";
import { createMetric } from "../metrics/index.js";
import { featureCollection, featureEach, area as turfArea, simplify, } from "@turf/turf";
import { ValidationError } from "../types/index.js";
import { clip } from "./clip.js";
/**
* Assuming sketches are within some outer boundary with size outerArea,
* calculates metric for both the area of each sketch and the percentage of outerArea they take up.
* If sketch is a collection, will return metrics for each child sketch as well as the collection
* collection level metric will calculated by unioning child sketches to remove overlap.
* If collection level calculation produces an "Unable to complete output ring" error, it
* will fallback to simplify the sketch with simplifyTolerance (default to .0000001 if not passed) and try again.
* @deprecated use overlapFeatures (numerators) + precalculated metrics (denominators) + toPercentMetric
*/
export async function overlapArea(
/** Metric identifier */
metricId,
/** single sketch or collection. */
sketch,
/** area of outer boundary (e.g. planning area) */
outerArea, options = {}) {
if (!sketch)
throw new ValidationError("Missing sketch");
const { includePercMetric = true, includeChildMetrics = true } = options;
const percMetricId = `${metricId}Perc`;
const collectionExtra = {};
// Remove overlap
const combinedSketchArea = (() => {
let sketches = toSketchArray(sketch);
try {
// Simplify if enabled
if (options?.simplifyTolerance)
sketches = simplify(featureCollection(sketches), {
tolerance: options.simplifyTolerance,
highQuality: true,
}).features;
const combinedSketch = clip(featureCollection(sketches), "union");
return combinedSketch ? turfArea(combinedSketch) : 0;
}
catch (error) {
if (error instanceof Error &&
error.message.includes("Unable to complete output ring")) {
// Fallback to simplify
const tolerance = options?.simplifyTolerance || 0.000_001;
const combinedSketch = clip(simplify(featureCollection(sketches), {
tolerance,
highQuality: true,
}), "union");
collectionExtra.simplifyTolerance = tolerance;
return combinedSketch ? turfArea(combinedSketch) : 0;
}
else {
// Return zero to return something and flag error
collectionExtra.error = true;
console.error(error);
console.log("Returning zero area with error flagged");
return 0;
}
}
})();
const sketchMetrics = [];
if (sketch) {
featureEach(sketch, (curSketch) => {
if (!curSketch || !curSketch.properties) {
console.log("Warning: feature or its properties are undefined, skipped");
}
else if (curSketch.geometry) {
const sketchArea = turfArea(curSketch);
sketchMetrics.push(createMetric({
metricId,
sketchId: curSketch.properties.id,
value: sketchArea,
extra: {
sketchName: curSketch.properties.name,
},
}));
if (includePercMetric) {
sketchMetrics.push(createMetric({
metricId: percMetricId,
sketchId: curSketch.properties.id,
value: sketchArea / outerArea,
extra: {
sketchName: curSketch.properties.name,
},
}));
}
}
else {
console.log(`Warning: feature is missing geometry, zeroed: sketchId:${curSketch.properties.id}, name:${curSketch.properties.name}`);
sketchMetrics.push(createMetric({
metricId,
sketchId: curSketch.properties.id,
value: 0,
extra: {
sketchName: curSketch.properties.name,
},
}));
if (includePercMetric) {
sketchMetrics.push(createMetric({
metricId: percMetricId,
sketchId: curSketch.properties.id,
value: 0,
extra: {
sketchName: curSketch.properties.name,
},
}));
}
}
});
}
const collMetrics = [];
if (isSketchCollection(sketch)) {
collMetrics.push(createMetric({
metricId,
sketchId: sketch.properties.id,
value: combinedSketchArea,
extra: {
sketchName: sketch.properties.name,
isCollection: true,
},
}));
if (includePercMetric) {
collMetrics.push(createMetric({
metricId: percMetricId,
sketchId: sketch.properties.id,
value: combinedSketchArea / outerArea,
extra: {
sketchName: sketch.properties.name,
isCollection: true,
},
}));
}
}
return [...(includeChildMetrics ? sketchMetrics : []), ...collMetrics];
}
/**
* Returns area stats for sketch input after performing overlay operation against a subarea feature.
* Includes both area overlap and percent area overlap metrics, because calculating percent later would be too complicated
* For sketch collections, dissolve is used when calculating total sketch area to prevent double counting
* @deprecated - using geographies will clip your datasources and you can just use overlapFeatures. This will be removed in a future version
*/
export async function overlapSubarea(
/** Metric identifier */
metricId,
/** Single sketch or collection */
sketch,
/** subarea feature */
subareaFeature, options) {
if (!sketch)
throw new ValidationError("Missing sketch");
const percMetricId = `${metricId}Perc`;
const operation = options?.operation || "intersection";
const collectionExtra = {};
const subareaArea = options?.outerArea && operation === "intersection"
? options?.outerArea
: subareaFeature
? turfArea(subareaFeature)
: 0;
const sketches = toSketchArray(sketch);
if (operation === "difference" && !options?.outerArea)
throw new ValidationError("Missing outerArea which is required when operation is difference");
// Run op and keep null remainders for reporting purposes
const subsketches = (() => {
return sketches.map((sketch) => subareaFeature
? clip(featureCollection([sketch, subareaFeature]), operation)
: null);
})();
// calculate area of all subsketches
const subsketchArea = (() => {
// Remove null
let allSubsketches = subsketches.reduce((subsketches, subsketch) => subsketch ? [...subsketches, subsketch] : subsketches, []);
// Remove overlap
try {
// Simplify if enabled
if (options?.simplifyTolerance)
allSubsketches = simplify(featureCollection(allSubsketches), {
tolerance: options.simplifyTolerance,
highQuality: true,
}).features;
const combinedSketch = allSubsketches.length > 0
? clip(featureCollection(allSubsketches), "union")
: featureCollection(allSubsketches);
return allSubsketches && combinedSketch ? turfArea(combinedSketch) : 0;
}
catch (error) {
if (error instanceof Error &&
error.message.includes("Unable to complete output ring")) {
// Fallback to simplify
const tolerance = options?.simplifyTolerance || 0.000_001;
const combinedSketch = clip(simplify(featureCollection(allSubsketches), {
tolerance,
highQuality: true,
}), "union");
collectionExtra.simplifyTolerance = tolerance;
return combinedSketch ? turfArea(combinedSketch) : 0;
}
else {
// Return zero to return something and flag error
collectionExtra.error = true;
console.error(error);
console.log("Returning zero area with error flagged");
return 0;
}
}
})();
// Choose inner or outer subarea for calculating percentage
const operationArea = (() => {
return operation === "difference" && options?.outerArea
? options?.outerArea - subareaArea
: subareaArea;
})();
const metrics = [];
if (subsketches) {
for (const [index, feat] of subsketches.entries()) {
const origSketch = sketches[index];
if (feat) {
const subsketchArea = turfArea(feat);
metrics.push(createMetric({
metricId,
sketchId: origSketch.properties.id,
value: subsketchArea,
extra: {
sketchName: origSketch.properties.name,
},
}));
metrics.push(createMetric({
metricId: percMetricId,
sketchId: origSketch.properties.id,
value: subsketchArea === 0 ? 0 : subsketchArea / operationArea,
extra: {
sketchName: origSketch.properties.name,
},
}));
}
else {
metrics.push(createMetric({
metricId,
sketchId: origSketch.properties.id,
value: 0,
extra: {
sketchName: origSketch.properties.name,
},
}));
metrics.push(createMetric({
metricId: percMetricId,
sketchId: origSketch.properties.id,
value: 0,
extra: {
sketchName: origSketch.properties.name,
},
}));
}
}
}
if (isSketchCollection(sketch)) {
metrics.push(createMetric({
metricId,
sketchId: sketch.properties.id,
value: subsketchArea,
extra: {
...collectionExtra,
sketchName: sketch.properties.name,
isCollection: true,
},
}));
metrics.push(createMetric({
metricId: percMetricId,
sketchId: sketch.properties.id,
value: subsketchArea === 0 ? 0 : subsketchArea / operationArea,
extra: {
...collectionExtra,
sketchName: sketch.properties.name,
isCollection: true,
},
}));
}
return metrics;
}
//# sourceMappingURL=overlapArea.js.map