UNPKG

@seasketch/geoprocessing

Version:

Geoprocessing and reporting framework for SeaSketch 2.0

205 lines • 9.77 kB
import { genSampleSketchCollection, keyBy, toSketchArray, isSketchCollection, groupBy, isPolygonFeatureArray, } from "../helpers/index.js"; import { clip } from "./clip.js"; import { createMetric, firstMatchingMetric } from "../metrics/index.js"; import { overlapFeatures } from "./overlapFeatures.js"; import { overlapArea } from "./overlapArea.js"; import { featureCollection, flatten } from "@turf/turf"; import cloneDeep from "lodash/cloneDeep.js"; import { rasterMetrics } from "./rasterMetrics.js"; /** * Generate overlap group metrics using rasterMetrics operation */ export async function overlapRasterGroupMetrics(options) { return overlapGroupMetrics({ ...options, operation: async (metricId, features, sc) => { if (isPolygonFeatureArray(features)) throw new Error(`Expected raster`); const overallGroupMetrics = await rasterMetrics(features, { metricId: metricId, feature: sc, }); return firstMatchingMetric(overallGroupMetrics, (m) => !!m.extra?.isCollection).value; }, }); } /** * Generate overlap group metrics using overlapFeatures operation */ export async function overlapFeaturesGroupMetrics(options) { return overlapGroupMetrics({ ...options, operation: async (metricId, features, sc) => { if (!isPolygonFeatureArray(features)) throw new Error(`Expected feature array`); const overallGroupMetrics = await overlapFeatures(metricId, features, sc, { includeChildMetrics: false, }); return overallGroupMetrics[0].value; }, }); } /** * Generate overlap group metrics using overlapArea operation * @deprecated - use overlapFeaturesGroupMetrics instead */ export async function overlapAreaGroupMetrics(options) { return overlapGroupMetrics({ ...options, featuresByClass: { [options.classId]: [] }, operation: async (metricId, features, sc) => { // Calculate just the overall area sum for group const overallGroupMetrics = await overlapArea(metricId, sc, options.outerArea, { includePercMetric: false, includeChildMetrics: false, }); return overallGroupMetrics[0].value; }, }); } /** * Given overlap metrics stratified by class and sketch, returns new metrics also stratified by group * Assumes a sketch is member of only one group, determined by caller-provided metricToGroup * For each group+class, calculates area of overlap between sketches in group and featuresByClass (with overlap between group sketches removed first) * Types of metrics returned: * sketch metrics: copy of caller-provided sketch metrics with addition of group ID * overall metric for each group+class: takes sketches in group, subtracts overlap between them and overlap with higher group sketches, and runs operation * If a group has no sketches in it, then no group metrics will be included for that group, and group+class metric will be 0 */ export async function overlapGroupMetrics(options) { const { metricId, groupIds, sketch, metricToGroup, metrics, featuresByClass, operation, onlyPresentGroups = false, } = options; const classes = Object.keys(featuresByClass); // Filter to individual sketch metrics and clone as base for group metrics const sketchMetrics = isSketchCollection(sketch) ? cloneDeep(metrics).filter((sm) => sm.sketchId !== sketch.properties.id) : cloneDeep(metrics).filter((sm) => sm.sketchId === sketch.properties.id); // Lookup and add group const groupSketchMetrics = sketchMetrics.map((m) => ({ ...m, groupId: metricToGroup(m), })); // For each group const groupMetricsPromises = groupIds.reduce((groupMetricsPromisesSoFar, curGroup) => { // For each class const groupMetricsPromise = classes.reduce((classMetricsPromisesSoFar, curClass) => { // async iife to wrap in new promise const curClassMetricsPromise = (async () => { // Filter to cur class metrics const curGroupSketchMetrics = groupSketchMetrics.filter((sm) => sm.classId === curClass && sm.metricId === metricId); // Optionally, skip this group if not present in metrics const curClassGroupIds = onlyPresentGroups ? Object.keys(groupBy(curGroupSketchMetrics, (m) => m.groupId)) : groupIds; if (onlyPresentGroups && !curClassGroupIds.includes(curGroup)) { return []; } const classGroupMetrics = await getClassGroupMetrics({ sketch, groupSketchMetrics: curGroupSketchMetrics, groups: groupIds, groupId: curGroup, metricId, features: featuresByClass[curClass], operation, }); return classGroupMetrics.map((metric) => ({ ...metric, classId: curClass, })); })(); return [...classMetricsPromisesSoFar, curClassMetricsPromise]; }, []); return [...groupMetricsPromisesSoFar, ...groupMetricsPromise]; }, []); // Await and unroll result const groupMetrics = (await Promise.all(groupMetricsPromises)).flat(); return groupMetrics; } /** * Given groupId, returns area of overlap between features and sketches in the group * Assumes that groupSketchMetrics and features are pre-filtered to a single class */ const getClassGroupMetrics = async (options) => { const { sketch, groupSketchMetrics, groups, groupId, metricId, features, operation, } = options; const sketches = toSketchArray(sketch); const sketchMap = keyBy(sketches, (item) => item.properties.id); // Filter to group. May result in empty list const curGroupSketchMetrics = groupSketchMetrics.filter((m) => m.groupId === groupId); const results = curGroupSketchMetrics; // If collection account for overlap if (isSketchCollection(sketch)) { // Get IDs of all sketches (non-collection) with current group and collection, from metrics const curGroupSketches = curGroupSketchMetrics .filter((gm) => gm.groupId === groupId) .map((gm) => sketchMap[gm.sketchId]) // sketchMap will be undefined for collection metrics .filter((gm) => !!gm); // so remove undefined // Get sketch metrics from higher groups (lower index value) and convert to sketches const higherGroupSketchMetrics = groups.reduce((otherSoFar, otherGroupName) => { // Append if lower index than current group const groupIndex = groups.indexOf(groupId); const otherIndex = groups.indexOf(otherGroupName); const otherGroupMetrics = groupSketchMetrics.filter((gm) => gm.groupId === otherGroupName); return otherIndex < groupIndex ? otherSoFar.concat(otherGroupMetrics) : otherSoFar; }, []); const higherGroupSketches = Object.values(keyBy(higherGroupSketchMetrics, (m) => m.sketchId)).map((ogm) => sketchMap[ogm.sketchId]); let groupValue = 0; if (curGroupSketches.length > 1 || higherGroupSketches.length > 0) { groupValue = await getReducedGroupAreaOverlap({ metricId, groupSketches: curGroupSketches, higherGroupSketches: higherGroupSketches, features, operation, }); } else { groupValue = curGroupSketchMetrics.reduce((sumSoFar, sm) => sm.value + sumSoFar, 0); } results.push(createMetric({ groupId: groupId, metricId: metricId, sketchId: sketch.properties.id, value: groupValue, extra: { sketchName: sketch.properties.name, isCollection: true, }, })); } // If no single sketch metrics for group, add a zero for group if (curGroupSketchMetrics.length === 0) { results.push(createMetric({ groupId: groupId, metricId: metricId, sketchId: sketch.properties.id, value: 0, extra: { sketchName: sketch.properties.name, }, })); } return results; }; /** * Calculates area of overlap between groupSketches and features * Removes overlap with higherGroupSketches first * If either sketch array is empty it will do the right thing */ const getReducedGroupAreaOverlap = async (options) => { const { metricId, groupSketches, higherGroupSketches, features, operation } = options; // Given current group sketches, subtract area of sketches in higher groups const otherOverlap = groupSketches .map((groupSketch) => clip(featureCollection([groupSketch, ...higherGroupSketches]), "difference")) .reduce((rem, diff) => (diff ? rem.concat(diff) : rem), []); const otherRemSketches = genSampleSketchCollection(featureCollection(flatten(featureCollection(otherOverlap)).features)).features; const finalFC = featureCollection(higherGroupSketches.length > 0 ? otherRemSketches : groupSketches); const finalSC = genSampleSketchCollection(finalFC); // Calc just the one overall metric for this group+class if (finalSC.features.length === 0) return 0; const overallValue = await operation(metricId, features, finalSC); return overallValue; }; //# sourceMappingURL=overlapGroupMetrics.js.map