94 lines
3.5 KiB
TypeScript
94 lines
3.5 KiB
TypeScript
import { BadRequestException } from '@nestjs/common';
|
|
import { AssetGeometryType } from '../database/entities';
|
|
|
|
export interface GeoJsonGeometry {
|
|
type: AssetGeometryType;
|
|
coordinates: unknown[];
|
|
}
|
|
|
|
type Position = [number, number];
|
|
|
|
function invalid(message: string): never {
|
|
throw new BadRequestException({
|
|
code: 'INVALID_ASSET_GEOMETRY',
|
|
message,
|
|
});
|
|
}
|
|
|
|
function position(value: unknown, label: string): Position {
|
|
if (!Array.isArray(value) || value.length < 2) {
|
|
return invalid(`${label} debe contener longitud y latitud`);
|
|
}
|
|
const longitude = Number(value[0]);
|
|
const latitude = Number(value[1]);
|
|
if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) {
|
|
return invalid(`${label} contiene coordenadas no numéricas`);
|
|
}
|
|
if (longitude < -180 || longitude > 180 || latitude < -90 || latitude > 90) {
|
|
return invalid(`${label} está fuera del rango geográfico válido`);
|
|
}
|
|
return [longitude, latitude];
|
|
}
|
|
|
|
function positions(value: unknown, minimum: number, label: string): Position[] {
|
|
if (!Array.isArray(value) || value.length < minimum) {
|
|
return invalid(`${label} necesita al menos ${minimum} vértices`);
|
|
}
|
|
if (value.length > 10_000) return invalid(`${label} supera el máximo de 10000 vértices`);
|
|
return value.map((item, index) => position(item, `${label} · vértice ${index + 1}`));
|
|
}
|
|
|
|
function samePosition(first: Position, last: Position): boolean {
|
|
return first[0] === last[0] && first[1] === last[1];
|
|
}
|
|
|
|
export function validateGeoJsonGeometry(input: GeoJsonGeometry): GeoJsonGeometry {
|
|
if (!Object.values(AssetGeometryType).includes(input.type)) {
|
|
return invalid('El tipo de geometría no está permitido');
|
|
}
|
|
|
|
if (input.type === AssetGeometryType.POINT) {
|
|
return { type: input.type, coordinates: position(input.coordinates, 'El punto') };
|
|
}
|
|
|
|
if (input.type === AssetGeometryType.LINESTRING) {
|
|
const line = positions(input.coordinates, 2, 'La línea');
|
|
if (new Set(line.map((item) => item.join(','))).size < 2) {
|
|
return invalid('La línea necesita al menos dos posiciones diferentes');
|
|
}
|
|
return { type: input.type, coordinates: line };
|
|
}
|
|
|
|
if (!Array.isArray(input.coordinates) || input.coordinates.length < 1) {
|
|
return invalid('El polígono necesita al menos un anillo');
|
|
}
|
|
if (input.coordinates.length > 20) return invalid('El polígono supera el máximo de 20 anillos');
|
|
const rings = input.coordinates.map((ring, ringIndex) => {
|
|
const normalized = positions(ring, 4, `Anillo ${ringIndex + 1}`);
|
|
if (!samePosition(normalized[0]!, normalized[normalized.length - 1]!)) {
|
|
return invalid(`El anillo ${ringIndex + 1} debe estar cerrado`);
|
|
}
|
|
if (new Set(normalized.slice(0, -1).map((item) => item.join(','))).size < 3) {
|
|
return invalid(`El anillo ${ringIndex + 1} necesita tres posiciones diferentes`);
|
|
}
|
|
return normalized;
|
|
});
|
|
return { type: input.type, coordinates: rings };
|
|
}
|
|
|
|
export function parseBoundingBox(value?: string): [number, number, number, number] | null {
|
|
if (!value) return null;
|
|
const numbers = value.split(',').map(Number);
|
|
if (numbers.length !== 4 || numbers.some((item) => !Number.isFinite(item))) {
|
|
return invalid('El área visible del mapa no es válida');
|
|
}
|
|
const [west, south, east, north] = numbers as [number, number, number, number];
|
|
if (
|
|
west < -180 || east > 180 || south < -90 || north > 90 ||
|
|
west >= east || south >= north
|
|
) {
|
|
return invalid('El área visible del mapa está fuera de rango');
|
|
}
|
|
return [west, south, east, north];
|
|
}
|