From 6e6cce48b8047b0cb330e37ee930263f1612a199 Mon Sep 17 00:00:00 2001 From: Abby Vath Meyer Date: Tue, 4 Feb 2025 19:35:09 +0000 Subject: [PATCH] Uce original clipToOceanEez --- src/functions/clipToOceanEez.ts | 133 ++++++++++++++++++++++---------- 1 file changed, 91 insertions(+), 42 deletions(-) diff --git a/src/functions/clipToOceanEez.ts b/src/functions/clipToOceanEez.ts index c34ce38..b390ebe 100644 --- a/src/functions/clipToOceanEez.ts +++ b/src/functions/clipToOceanEez.ts @@ -1,60 +1,109 @@ import { + ValidationError, + PreprocessingHandler, + VectorDataSource, + isPolygonFeature, Feature, - FeatureClipOperation, - MultiPolygon, Polygon, - PreprocessingHandler, - Sketch, - clipToPolygonFeatures, - ensureValidPolygon, - isVectorDatasource, - loadFgb, + MultiPolygon, + clip, } from "@seasketch/geoprocessing"; -import project from "../../project/projectClient.js"; -import { bbox } from "@turf/turf"; +import area from "@turf/area"; +import bbox from "@turf/bbox"; +import { featureCollection as fc } from "@turf/helpers"; +import combine from "@turf/combine"; +import flatten from "@turf/flatten"; +import kinks from "@turf/kinks"; + +const MAX_SKETCH_SIZE = 1000000 * 1000 ** 2; + +type OsmLandFeature = Feature; +type EezLandUnion = Feature; + +// Defined at module level for potential caching/reuse by serverless process +const SubdividedOsmLandSource = new VectorDataSource( + "https://d3p1dsef9f0gjr.cloudfront.net/", +); +const SubdividedEezLandUnionSource = new VectorDataSource( + "https://d3muy0hbwp5qkl.cloudfront.net", +); + +export async function clipLand(feature: Feature) { + const landFeatures = await SubdividedOsmLandSource.fetchUnion( + bbox(feature), + "gid", + ); + if (landFeatures.features.length === 0) return feature; + const combined = combine(landFeatures).features[0] as Feature; + return combined ? clip(fc([feature, combined]), "difference") : feature; +} + +export async function clipOutsideEez( + feature: Feature, + eezFilterByNames: string[] = [], +) { + let eezFeatures = await SubdividedEezLandUnionSource.fetch(bbox(feature)); + // Optionally filter down to a single country/union EEZ boundary + if (eezFilterByNames.length > 0) { + eezFeatures = eezFeatures.filter((e) => + eezFilterByNames.includes(e.properties.UNION), + ); + } + if (eezFeatures.length === 0) return feature; + const combined = combine(fc(eezFeatures)) + .features[0] as Feature; + return clip(fc([feature, combined]), "intersection"); +} /** - * Preprocessor takes a Polygon feature/sketch and returns the portion that - * is in the ocean (not on land) and within one or more EEZ boundaries. + * Takes a Polygon feature and returns the portion that is in the ocean and within an EEZ boundary + * If results in multiple polygons then returns the largest */ export async function clipToOceanEez( - feature: Feature | Sketch, + feature: Feature, + eezFilterByNames?: string[], ): Promise { - // throws if not valid with specific message - ensureValidPolygon(feature, { - minSize: 1, - enforceMinSize: false, - maxSize: 500_000 * 1000 ** 2, // Default 500,000 KM - enforceMaxSize: false, - }); - - const featureBox = bbox(feature); - - const ds = project.getDatasourceById("nearshore_dissolved"); - if (!isVectorDatasource(ds)) - throw new Error(`Expected vector datasource for ${ds.datasourceId}`); - const url = project.getDatasourceUrl(ds); - - // Keep portion of sketch within EEZ - const features: Feature[] = await loadFgb( - url, - featureBox, - ); + if (!isPolygonFeature(feature)) { + throw new ValidationError("Input must be a polygon"); + } + + if (area(feature) > MAX_SKETCH_SIZE) { + throw new ValidationError( + "Please limit sketches to under 1,000,000 square km", + ); + } + + const kinkPoints = kinks(feature); + if (kinkPoints.features.length > 0) { + throw new ValidationError("Your sketch polygon crosses itself."); + } - const keepInsideEez: FeatureClipOperation = { - operation: "intersection", - clipFeatures: features, - }; + let clipped = await clipLand(feature); + if (clipped) clipped = await clipOutsideEez(clipped, eezFilterByNames); - return clipToPolygonFeatures(feature, [keepInsideEez], { - ensurePolygon: true, - }); + if (!clipped || area(clipped) === 0) { + throw new ValidationError("Sketch is outside of project boundaries"); + } else { + if (clipped.geometry.type === "MultiPolygon") { + const flattened = flatten(clipped); + let biggest = [0, 0]; + for (var i = 0; i < flattened.features.length; i++) { + const a = area(flattened.features[i]); + if (a > biggest[0]) { + biggest = [a, i]; + } + } + return flattened.features[biggest[1]] as Feature; + } else { + return clipped; + } + } } export default new PreprocessingHandler(clipToOceanEez, { title: "clipToOceanEez", - description: "Clips sketches to state waters", + description: + "Erases portion of sketch overlapping with land or extending into ocean outsize EEZ boundary", timeout: 40, requiresProperties: [], - memory: 4096, });