104 lines
4.0 KiB
JavaScript
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;
|