UNPKG

@seasketch/geoprocessing

Version:

Geoprocessing and reporting framework for SeaSketch 2.0

279 lines • 11.5 kB
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