234 lines
6.8 KiB
JavaScript
234 lines
6.8 KiB
JavaScript
(() => {
|
|
/**
|
|
* 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,
|
|
};
|
|
})();
|