/** * Canonicalize a provider-neutral GeoJSON linear ring before it reaches a * Cesium geometry worker. GeoJSON closes a ring by repeating its first * coordinate, while GroundPolylineGeometry with `loop: true` expects an open * sequence. Some source snapshots contain both adjacent duplicates and more * than one closing coordinate; forwarding either shape can create a * zero-length segment and stop Cesium's render loop. * * Invalid coordinates invalidate the complete ring. Silently retaining a * partial polygon would display a boundary the provider never published. * * @param {Array<[number, number]>} ring * @returns {Array<[number, number]>} */ export function normalizeHGeoZoneRing(ring) { if (!Array.isArray(ring)) return []; /** @type {Array<[number, number]>} */ const normalized = []; for (const coordinate of ring) { if ( !Array.isArray(coordinate) || coordinate.length < 2 || !Number.isFinite(coordinate[0]) || !Number.isFinite(coordinate[1]) || coordinate[0] < -180 || coordinate[0] > 180 || coordinate[1] < -90 || coordinate[1] > 90 ) return []; const next = /** @type {[number, number]} */ ([coordinate[0], coordinate[1]]); if (!sameCoordinate(normalized.at(-1), next)) normalized.push(next); } while (normalized.length > 1 && sameCoordinate(normalized[0], normalized.at(-1))) { normalized.pop(); } const distinct = new Set(normalized.map(([longitude, latitude]) => `${longitude}:${latitude}`)); return distinct.size >= 3 ? normalized : []; } /** * @param {[number, number] | undefined} left * @param {[number, number] | undefined} right */ function sameCoordinate(left, right) { return Boolean(left && right && left[0] === right[0] && left[1] === right[1]); }