-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
91 additions
and
42 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Polygon, { gid: number }>; | ||
type EezLandUnion = Feature<Polygon, { gid: number; UNION: string }>; | ||
|
||
// Defined at module level for potential caching/reuse by serverless process | ||
const SubdividedOsmLandSource = new VectorDataSource<OsmLandFeature>( | ||
"https://d3p1dsef9f0gjr.cloudfront.net/", | ||
); | ||
const SubdividedEezLandUnionSource = new VectorDataSource<EezLandUnion>( | ||
"https://d3muy0hbwp5qkl.cloudfront.net", | ||
); | ||
|
||
export async function clipLand(feature: Feature<Polygon | MultiPolygon>) { | ||
const landFeatures = await SubdividedOsmLandSource.fetchUnion( | ||
bbox(feature), | ||
"gid", | ||
); | ||
if (landFeatures.features.length === 0) return feature; | ||
const combined = combine(landFeatures).features[0] as Feature<MultiPolygon>; | ||
return combined ? clip(fc([feature, combined]), "difference") : feature; | ||
} | ||
|
||
export async function clipOutsideEez( | ||
feature: Feature<Polygon | MultiPolygon>, | ||
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<MultiPolygon>; | ||
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<Feature> { | ||
// 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<Polygon | MultiPolygon>[] = 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<Polygon>; | ||
} 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, | ||
}); |