This commit is contained in:
233
www/map-advanced-zones.js
Normal file
233
www/map-advanced-zones.js
Normal file
@@ -0,0 +1,233 @@
|
||||
(() => {
|
||||
/**
|
||||
* 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) {
|
||||
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 = Number(z.line_width) || 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,
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user