Files
NODEDC_PLATFORM/packages/external-provider-contract/src/geometry.mjs
T

104 lines
4.0 KiB
JavaScript

const GEOMETRY_TYPES = new Set(["Point", "LineString", "Polygon", "MultiPolygon"]);
const MAX_GEOMETRY_BYTES = 192 * 1024;
const MAX_GEOMETRY_VERTICES = 10_000;
/**
* Validate the bounded GeoJSON geometry subset accepted by provider-neutral
* Data Products. GeometryCollections and arbitrary feature properties are
* deliberately excluded: a fact owns exactly one geometry and its attributes
* stay under the fact field policy.
*/
export function validateGeoJsonGeometry(value, path, errors, { allowedTypes = GEOMETRY_TYPES } = {}) {
if (!isPlainObject(value) || !allowedTypes.has(value.type) || !Array.isArray(value.coordinates)) {
errors.push(`${path}_must_be_bounded_geojson_geometry`);
return;
}
rejectUnknownKeys(value, new Set(["type", "coordinates"]), path, errors);
if (serializedByteLength(value) > MAX_GEOMETRY_BYTES) {
errors.push(`${path}_size_exceeded`);
return;
}
const vertices = [];
if (value.type === "Point") {
validatePosition(value.coordinates, path, errors, vertices);
} else if (value.type === "LineString") {
validateLineString(value.coordinates, path, errors, vertices);
} else if (value.type === "Polygon") {
validatePolygon(value.coordinates, path, errors, vertices);
} else if (value.type === "MultiPolygon") {
if (!value.coordinates.length) errors.push(`${path}.coordinates_must_be_nonempty`);
value.coordinates.forEach((polygon, index) => validatePolygon(polygon, `${path}.coordinates[${index}]`, errors, vertices));
}
if (vertices.length > MAX_GEOMETRY_VERTICES) errors.push(`${path}_vertex_limit_exceeded`);
}
export function isBoundedGeoJsonGeometry(value, allowedTypes = GEOMETRY_TYPES) {
const errors = [];
validateGeoJsonGeometry(value, "geometry", errors, { allowedTypes });
return errors.length === 0;
}
function validatePolygon(value, path, errors, vertices) {
if (!Array.isArray(value) || !value.length) {
errors.push(`${path}_polygon_rings_required`);
return;
}
value.forEach((ring, index) => {
const ringPath = `${path}[${index}]`;
if (!Array.isArray(ring) || ring.length < 4) {
errors.push(`${ringPath}_linear_ring_requires_four_positions`);
return;
}
ring.forEach((position, positionIndex) => validatePosition(position, `${ringPath}[${positionIndex}]`, errors, vertices));
if (!samePosition(ring[0], ring.at(-1))) errors.push(`${ringPath}_linear_ring_must_be_closed`);
});
}
function validateLineString(value, path, errors, vertices) {
if (!Array.isArray(value) || value.length < 2) {
errors.push(`${path}_line_string_requires_two_positions`);
return;
}
value.forEach((position, index) => validatePosition(position, `${path}[${index}]`, errors, vertices));
}
function validatePosition(value, path, errors, vertices) {
if (!Array.isArray(value) || value.length !== 2 || !value.every(Number.isFinite)) {
errors.push(`${path}_position_invalid`);
return;
}
const [longitude, latitude] = value;
if (longitude < -180 || longitude > 180) errors.push(`${path}.longitude_out_of_range`);
if (latitude < -90 || latitude > 90) errors.push(`${path}.latitude_out_of_range`);
vertices.push(value);
}
function samePosition(left, right) {
return Array.isArray(left) && Array.isArray(right)
&& left.length === 2 && right.length === 2
&& Object.is(left[0], right[0]) && Object.is(left[1], right[1]);
}
function rejectUnknownKeys(value, allowed, path, errors) {
for (const key of Object.keys(value)) {
if (!allowed.has(key)) errors.push(`${path}.${key}_not_allowed`);
}
}
function serializedByteLength(value) {
try {
return Buffer.byteLength(JSON.stringify(value));
} catch {
return Number.POSITIVE_INFINITY;
}
}
function isPlainObject(value) {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
export const DATA_PRODUCT_GEOMETRY_TYPES = Object.freeze([...GEOMETRY_TYPES]);
export const DATA_PRODUCT_MAX_GEOMETRY_BYTES = MAX_GEOMETRY_BYTES;
export const DATA_PRODUCT_MAX_GEOMETRY_VERTICES = MAX_GEOMETRY_VERTICES;