(() => { /** * Planner hooks for Forbidden / Preferred / Unpreferred drive zones (MiR §4.2.6.6). * Vector overlay only — consumed by future global planner / path preview. */ const TYPES = { forbidden: "forbidden", preferred: "preferred", unpreferred: "unpreferred", }; const PLANNER_TYPES = new Set([TYPES.forbidden, TYPES.preferred, TYPES.unpreferred]); /** Relative traversal cost (forbidden = impassable). */ const COST = { forbidden: Infinity, unpreferred: 4, preferred: 0.35, neutral: 1, }; function pointInPolygon(px, py, points) { if (!points?.length) return false; let inside = false; for (let i = 0, j = points.length - 1; i < points.length; j = i++) { const xi = points[i].x; const yi = points[i].y; const xj = points[j].x; const yj = points[j].y; const intersect = yi > py !== yj > py && px < ((xj - xi) * (py - yi)) / (yj - yi) + xi; if (intersect) inside = !inside; } return inside; } function distPointToSegment(px, py, x1, y1, x2, y2) { const dx = x2 - x1; const dy = y2 - y1; const lenSq = dx * dx + dy * dy; if (lenSq === 0) return Math.hypot(px - x1, py - y1); let t = ((px - x1) * dx + (py - y1) * dy) / lenSq; t = Math.max(0, Math.min(1, t)); return Math.hypot(px - (x1 + t * dx), py - (y1 + t * dy)); } function pointNearPolyline(px, py, points, halfWidth, tolerance = 4) { if (!points?.length || points.length < 2) return false; const limit = halfWidth + tolerance; for (let j = 0; j < points.length - 1; j++) { const p1 = points[j]; const p2 = points[j + 1]; if (distPointToSegment(px, py, p1.x, p1.y, p2.x, p2.y) <= limit) return true; } return false; } function isLineGeometry(z) { return z?.geometry === "line"; } function isPlannerZoneType(type) { return PLANNER_TYPES.has(type); } function filterPlannerZones(zones) { return (Array.isArray(zones) ? zones : []).filter((z) => isPlannerZoneType(z?.type)); } /** Topmost planner zones containing image pixel (newest wins for overlaps). */ function zonesAtPoint(zones, px, py, mapMeta = null) { const list = Array.isArray(zones) ? zones : []; const hits = []; const linePx = (z) => window.MapObjects?.zoneLineWidthPx(z, mapMeta) ?? 8; for (let i = list.length - 1; i >= 0; i--) { const z = list[i]; if (!isPlannerZoneType(z?.type) || !z.points?.length) continue; if (isLineGeometry(z)) { const half = linePx(z) / 2; if (pointNearPolyline(px, py, z.points, half)) hits.push(z); } else if (pointInPolygon(px, py, z.points)) hits.push(z); } return hits; } function pixelCost(zones, px, py, mapMeta = null) { const hits = zonesAtPoint(zones, px, py, mapMeta); if (hits.some((z) => z.type === TYPES.forbidden)) return COST.forbidden; if (hits.some((z) => z.type === TYPES.unpreferred)) return COST.unpreferred; if (hits.some((z) => z.type === TYPES.preferred)) return COST.preferred; return COST.neutral; } function classifyPoint(zones, px, py, mapMeta = null) { const hits = zonesAtPoint(zones, px, py, mapMeta); return { forbidden: hits.some((z) => z.type === TYPES.forbidden), preferred: hits.some((z) => z.type === TYPES.preferred), unpreferred: hits.some((z) => z.type === TYPES.unpreferred), zones: hits, cost: pixelCost(zones, px, py, mapMeta), }; } function isPathBlocked(zones, pathPoints, mapMeta = null) { const pts = Array.isArray(pathPoints) ? pathPoints : []; return pts.some((p) => pixelCost(zones, p.x, p.y, mapMeta) === COST.forbidden); } function pathCost(zones, pathPoints, mapMeta = null) { const pts = Array.isArray(pathPoints) ? pathPoints : []; let sum = 0; for (const p of pts) { const c = pixelCost(zones, p.x, p.y, mapMeta); if (!Number.isFinite(c)) return Infinity; sum += c; } return sum; } window.MapPlannerZones = { TYPES, PLANNER_TYPES, COST, isPlannerZoneType, filterPlannerZones, zonesAtPoint, pixelCost, classifyPoint, isPathBlocked, pathCost, pointInPolygon, }; })();