Source: utils/geojsonGeometryShape.mjs

/**
 * Minimal RFC 7946 coordinate-structure checks for GeoJSON Geometry `coordinates`.
 * Aligned with RFC 7946 Geometry object `coordinates` definitions.
 * Validates nesting (Polygon vs MultiPolygon, etc.), not ring orientation, bbox, or right-hand rule.
 * @see https://datatracker.ietf.org/doc/html/rfc7946#section-3.1
 */

/**
 * @param {unknown} pos
 * @returns {boolean}
 */
function isPosition(pos) {
    if (!Array.isArray(pos) || pos.length < 2) return false;
    const [x, y, z] = pos;
    if (typeof x !== 'number' || typeof y !== 'number') return false;
    if (pos.length === 2) return true;
    if (pos.length === 3) return typeof z === 'number';
    return false;
}

/**
 * Linear ring: closed loop; RFC requires >= 4 positions.
 * @param {unknown} ring
 * @returns {boolean}
 */
function isLinearRing(ring) {
    if (!Array.isArray(ring) || ring.length < 4) return false;
    return ring.every(isPosition);
}

/**
 * @param {unknown} coords
 * @returns {boolean}
 */
function isPolygonCoordinates(coords) {
    if (!Array.isArray(coords)) return false;
    return coords.every(isLinearRing);
}

/**
 * @param {unknown} coords
 * @returns {boolean}
 */
function isMultiPolygonCoordinates(coords) {
    if (!Array.isArray(coords)) return false;
    return coords.every((polygon) => isPolygonCoordinates(polygon));
}

/**
 * @param {unknown} coords
 * @returns {boolean}
 */
function isLineStringCoordinates(coords) {
    if (!Array.isArray(coords) || coords.length < 2) return false;
    return coords.every(isPosition);
}

/**
 * @param {unknown} coords
 * @returns {boolean}
 */
function isMultiLineStringCoordinates(coords) {
    if (!Array.isArray(coords)) return false;
    return coords.every((line) => isLineStringCoordinates(line));
}

/**
 * @param {unknown} coords
 * @returns {boolean}
 */
function isMultiPointCoordinates(coords) {
    if (!Array.isArray(coords)) return false;
    return coords.every(isPosition);
}

/**
 * Returns whether `coordinates` matches the RFC 7946 array shape for `type`.
 * @param {string|null|undefined} type - GeoJSON geometry type
 * @param {unknown} coordinates
 * @returns {boolean}
 */
export function coordinatesMatchGeometryType(type, coordinates) {
    if (type == null || type === 'GeometryCollection') return true;
    const t = String(type);
    switch (t) {
        case 'Point':
            return isPosition(coordinates);
        case 'MultiPoint':
            return isMultiPointCoordinates(coordinates);
        case 'LineString':
            return isLineStringCoordinates(coordinates);
        case 'MultiLineString':
            return isMultiLineStringCoordinates(coordinates);
        case 'Polygon':
            return isPolygonCoordinates(coordinates);
        case 'MultiPolygon':
            return isMultiPolygonCoordinates(coordinates);
        default:
            return true;
    }
}

/**
 * Structural check only: `coordinates` array depth vs `type`. Skips `GeometryCollection` and missing/empty coordinates.
 * @param {Object} geom - GeoJSON-like object with optional `type`, `coordinates`, `geometries`
 * @returns {Object} `{ ok: boolean }` and optional `message` (string) when `ok` is false
 */
export function validateGeoJSONGeometry(geom) {
    if (!geom || geom.type === 'GeometryCollection') return { ok: true };
    const type = geom.type;
    const coordinates = geom.coordinates;
    if (type == null) return { ok: true };
    if (coordinates === undefined || coordinates === null) return { ok: true };
    if (Array.isArray(coordinates) && coordinates.length === 0) return { ok: true };

    if (coordinatesMatchGeometryType(type, coordinates)) return { ok: true };
    return {
        ok: false,
        message: `GeoJSON coordinates nesting does not match RFC 7946 for geometry type "${type}".`,
    };
}