feat(platform): add replaceable geozone data layer
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user