(() => { /** * Runtime hooks for Directional / Planner settings / I/O map zones (MiR ยง4.2.6.5, .10, .11). */ const TYPES = { directional: "directional", directional_line: "directional_line", planner: "planner", io: "io", }; const ADVANCED_TYPES = new Set([ TYPES.directional, TYPES.directional_line, TYPES.planner, TYPES.io, ]); const DIRECTION_DEGREES = [0, 45, 90, 135, 180, 225, 270, 315]; const DEFAULT_PLANNER = { no_localization: false, look_ahead: false, path_deviation: 0.5, path_timeout: 30, ignore_obstacles: false, }; const DEFAULT_IO = { io_module: "", plc_register: null, plc_value: null, plc_mode: "set", }; function pointInPolygon(px, py, points) { return window.MapPlannerZones?.pointInPolygon(px, py, points) || false; } function normalizeDirectionDeg(value) { const n = Number(value); if (!Number.isFinite(n)) return 0; let deg = ((Math.round(n / 45) * 45) % 360 + 360) % 360; if (!DIRECTION_DEGREES.includes(deg)) { deg = DIRECTION_DEGREES.reduce((best, d) => Math.abs(d - deg) < Math.abs(best - deg) ? d : best, ); } return deg; } function clampPathDeviation(value) { const n = Number(value); if (!Number.isFinite(n)) return DEFAULT_PLANNER.path_deviation; return Math.min(3, Math.max(0, n)); } function clampPathTimeout(value) { const n = Number(value); if (!Number.isFinite(n) || n < 0) return DEFAULT_PLANNER.path_timeout; return n; } function normalizePlannerSettings(raw = {}) { return { no_localization: !!raw.no_localization, look_ahead: !!raw.look_ahead, path_deviation: clampPathDeviation(raw.path_deviation), path_timeout: clampPathTimeout(raw.path_timeout), ignore_obstacles: !!raw.ignore_obstacles, }; } function normalizeIoSettings(raw = {}) { const mode = raw.plc_mode; return { io_module: typeof raw.io_module === "string" ? raw.io_module : "", plc_register: raw.plc_register == null || raw.plc_register === "" ? null : Number(raw.plc_register), plc_value: raw.plc_value == null || raw.plc_value === "" ? null : Number(raw.plc_value), plc_mode: mode === "add" || mode === "subtract" ? mode : "set", }; } function isAdvancedZoneType(type) { return ADVANCED_TYPES.has(type); } function isDirectionalType(type) { return type === TYPES.directional || type === TYPES.directional_line; } function filterAdvancedZones(zones) { return (Array.isArray(zones) ? zones : []).filter((z) => isAdvancedZoneType(z?.type)); } function pointNearPolyline(px, py, points, halfWidth = 8) { if (!points?.length || points.length < 2) return false; for (let i = 0; i < points.length - 1; i++) { const p1 = points[i]; const p2 = points[i + 1]; const dx = p2.x - p1.x; const dy = p2.y - p1.y; const lenSq = dx * dx + dy * dy; let dist; if (lenSq === 0) dist = Math.hypot(px - p1.x, py - p1.y); else { let t = ((px - p1.x) * dx + (py - p1.y) * dy) / lenSq; t = Math.max(0, Math.min(1, t)); dist = Math.hypot(px - (p1.x + t * dx), py - (p1.y + t * dy)); } if (dist <= halfWidth) return true; } return false; } function zonesAtPoint(zones, px, py, mapMeta = null) { const list = Array.isArray(zones) ? zones : []; const hits = []; for (let i = list.length - 1; i >= 0; i--) { const z = list[i]; if (!isAdvancedZoneType(z?.type) || !z.points?.length) continue; if (z.type === TYPES.directional_line) { const width = window.MapObjects?.zoneLineWidthPx(z, mapMeta) ?? 8; if (pointNearPolyline(px, py, z.points, width / 2 + 4)) hits.push(z); continue; } if (pointInPolygon(px, py, z.points)) hits.push(z); } return hits; } function lineDirectionDeg(zone) { if (!zone?.points?.length || zone.points.length < 2) return 0; const p0 = zone.points[0]; const p1 = zone.points[zone.points.length - 1]; let deg = (Math.atan2(-(p1.y - p0.y), p1.x - p0.x) * 180) / Math.PI; if (zone.reversed) deg = (deg + 180) % 360; return normalizeDirectionDeg(deg); } function zoneDirectionDeg(zone) { if (!zone) return 0; if (zone.type === TYPES.directional_line) return lineDirectionDeg(zone); if (zone.type === TYPES.directional) return normalizeDirectionDeg(zone.direction_deg); return 0; } /** Heading radians (map image coords) allowed if dot >= 0 with zone arrow. */ function isHeadingAllowed(zone, headingRad) { const arrowRad = (-zoneDirectionDeg(zone) * Math.PI) / 180; const hx = Math.cos(headingRad); const hy = Math.sin(headingRad); const ax = Math.cos(arrowRad); const ay = Math.sin(arrowRad); return hx * ax + hy * ay >= 0; } function getDirectionalConstraint(zones, px, py, headingRad) { const hits = zonesAtPoint(zones, px, py).filter(isDirectionalType); const zone = hits[0]; if (!zone) return null; return { zone, direction_deg: zoneDirectionDeg(zone), allowed: isHeadingAllowed(zone, headingRad), }; } function getPlannerSettings(zones, px, py) { const zone = zonesAtPoint(zones, px, py).find((z) => z.type === TYPES.planner); if (!zone) return null; return { zone, settings: normalizePlannerSettings(zone), }; } function getIoActivation(zones, px, py) { const zone = zonesAtPoint(zones, px, py).find((z) => z.type === TYPES.io); if (!zone) return null; return { zone, settings: normalizeIoSettings(zone), }; } function classifyPoint(zones, px, py, headingRad = null) { const hits = zonesAtPoint(zones, px, py); const directional = hits.find(isDirectionalType); const planner = hits.find((z) => z.type === TYPES.planner); const io = hits.find((z) => z.type === TYPES.io); const out = { directional: directional || null, direction_deg: directional ? zoneDirectionDeg(directional) : null, planner: planner || null, planner_settings: planner ? normalizePlannerSettings(planner) : null, io: io || null, io_settings: io ? normalizeIoSettings(io) : null, zones: hits, }; if (directional && headingRad != null) { out.heading_allowed = isHeadingAllowed(directional, headingRad); } return out; } window.MapAdvancedZones = { TYPES, ADVANCED_TYPES, DIRECTION_DEGREES, DEFAULT_PLANNER, DEFAULT_IO, normalizeDirectionDeg, normalizePlannerSettings, normalizeIoSettings, clampPathDeviation, clampPathTimeout, isAdvancedZoneType, isDirectionalType, filterAdvancedZones, zonesAtPoint, zoneDirectionDeg, isHeadingAllowed, getDirectionalConstraint, getPlannerSettings, getIoActivation, classifyPoint, }; })();