export type LngLat = { lng: number; lat: number } function normalizeLngLat(a: number, b: number): LngLat | null { if (!Number.isFinite(a) || !Number.isFinite(b)) return null const looksLikeLngLat = Math.abs(a) <= 180 && Math.abs(b) <= 90 const looksLikeLatLng = Math.abs(a) <= 90 && Math.abs(b) <= 180 if (looksLikeLngLat) return { lng: a, lat: b } if (looksLikeLatLng) return { lng: b, lat: a } return null } function parsePointLike(v: any): LngLat | null { if (!v) return null if (Array.isArray(v) && v.length >= 2) { return normalizeLngLat(Number(v[0]), Number(v[1])) } if (typeof v === 'object') { const a = v.lng ?? v.lon ?? v.longitude ?? v.x const b = v.lat ?? v.latitude ?? v.y if (a !== undefined && b !== undefined) { return normalizeLngLat(Number(a), Number(b)) } } if (typeof v === 'string') { return parseLngLatFromText(v) } return null } export function parseLngLatFromText(raw: string | undefined): LngLat | null { const text = (raw || '').trim() if (!text) return null const parts = text.split(/[,\s]+/).filter(Boolean) if (parts.length < 2) return null const a = parts[0] const b = parts[1] if (!a || !b) return null return normalizeLngLat(parseFloat(a), parseFloat(b)) } /** * Parse fence "points" into a polygon point list. */ export function parseFencePoints(pointsRaw: string | undefined): LngLat[] { const text = (pointsRaw || '').trim() if (!text) return [] if (text.startsWith('[') || text.startsWith('{')) { try { const parsed = JSON.parse(text) if (Array.isArray(parsed)) { const list = parsed.map(parsePointLike).filter(Boolean) as LngLat[] if (list.length) return list if (Array.isArray(parsed[0])) { const inner = (parsed[0] as any[]).map(parsePointLike).filter(Boolean) as LngLat[] if (inner.length) return inner } } } catch (_e) { // fall through } } const segments = text.split(/[;|\n\r]+/).map(s => s.trim()).filter(Boolean) if (segments.length > 1) { const list = segments.map(seg => { const nums = seg.match(/-?\d+(\.\d+)?/g) || [] if (nums.length < 2) return null const a = nums[0] const b = nums[1] if (!a || !b) return null return normalizeLngLat(parseFloat(a), parseFloat(b)) }).filter(Boolean) as LngLat[] if (list.length) return list } const nums = text.match(/-?\d+(\.\d+)?/g) || [] if (nums.length >= 6 && nums.length % 2 === 0) { const list: LngLat[] = [] for (let i = 0; i < nums.length; i += 2) { const a = nums[i] const b = nums[i + 1] if (!a || !b) continue const p = normalizeLngLat(parseFloat(a), parseFloat(b)) if (p) list.push(p) } if (list.length) return list } return [] } function pointOnSegment(p: LngLat, a: LngLat, b: LngLat, eps = 1e-9): boolean { const cross = (b.lat - a.lat) * (p.lng - a.lng) - (b.lng - a.lng) * (p.lat - a.lat) if (Math.abs(cross) > eps) return false const dot = (p.lng - a.lng) * (b.lng - a.lng) + (p.lat - a.lat) * (b.lat - a.lat) if (dot < -eps) return false const lenSq = (b.lng - a.lng) ** 2 + (b.lat - a.lat) ** 2 return dot <= lenSq + eps } export function pointInPolygon(p: LngLat, polygon: LngLat[]): boolean { if (!polygon || polygon.length < 3) return false for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { const a = polygon[j] const b = polygon[i] if (pointOnSegment(p, a, b)) return true } let inside = false const x = p.lng const y = p.lat for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { const xi = polygon[i].lng const yi = polygon[i].lat const xj = polygon[j].lng const yj = polygon[j].lat const intersect = (yi > y) !== (yj > y) && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi if (intersect) inside = !inside } return inside } export function pointInAnyPolygon(p: LngLat, polygons: LngLat[][]): boolean { for (const poly of polygons) { if (pointInPolygon(p, poly)) return true } return false }