699 lines
23 KiB
JavaScript
699 lines
23 KiB
JavaScript
(() => {
|
|
const TYPES = {
|
|
wall: "wall",
|
|
floor: "floor",
|
|
position: "position",
|
|
forbidden: "forbidden",
|
|
preferred: "preferred",
|
|
unpreferred: "unpreferred",
|
|
speed: "speed",
|
|
sound: "sound",
|
|
directional: "directional",
|
|
directional_line: "directional_line",
|
|
planner: "planner",
|
|
io: "io",
|
|
};
|
|
|
|
const FLOOR_PLAN_TYPES = new Set([TYPES.wall, TYPES.floor]);
|
|
const POLYGON_TYPES = new Set([
|
|
TYPES.floor,
|
|
TYPES.forbidden,
|
|
TYPES.preferred,
|
|
TYPES.unpreferred,
|
|
TYPES.speed,
|
|
TYPES.sound,
|
|
TYPES.directional,
|
|
TYPES.planner,
|
|
TYPES.io,
|
|
]);
|
|
const POINT_SHAPE_TYPES = new Set([TYPES.wall, TYPES.directional_line, ...POLYGON_TYPES]);
|
|
|
|
function newId() {
|
|
return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
}
|
|
|
|
function normalizePoint(p) {
|
|
const x = Number(p?.x);
|
|
const y = Number(p?.y);
|
|
if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
|
|
return { x, y };
|
|
}
|
|
|
|
function minPoints(type) {
|
|
if (type === TYPES.wall || type === TYPES.directional_line) return 2;
|
|
if (POLYGON_TYPES.has(type)) return 3;
|
|
return 0;
|
|
}
|
|
|
|
function isFloorPlanType(type) {
|
|
return FLOOR_PLAN_TYPES.has(type);
|
|
}
|
|
|
|
function isPlannerZoneType(type) {
|
|
return window.MapPlannerZones?.isPlannerZoneType(type) || false;
|
|
}
|
|
|
|
function isBehaviorZoneType(type) {
|
|
return window.MapBehaviorZones?.isBehaviorZoneType(type) || false;
|
|
}
|
|
|
|
function isAdvancedZoneType(type) {
|
|
return window.MapAdvancedZones?.isAdvancedZoneType(type) || false;
|
|
}
|
|
|
|
function isDirectionalLineType(type) {
|
|
return type === TYPES.directional_line;
|
|
}
|
|
|
|
function clampSpeedMps(value) {
|
|
return window.MapBehaviorZones?.clampSpeed(value) ?? 0.8;
|
|
}
|
|
|
|
function isPolygonType(type) {
|
|
return POLYGON_TYPES.has(type);
|
|
}
|
|
|
|
function isPolylineType(type) {
|
|
return type === TYPES.wall || type === TYPES.directional_line;
|
|
}
|
|
|
|
function isPointShapeType(type) {
|
|
return POINT_SHAPE_TYPES.has(type);
|
|
}
|
|
|
|
function isOverlayObjectType(type) {
|
|
return (
|
|
type === TYPES.position ||
|
|
isPlannerZoneType(type) ||
|
|
isBehaviorZoneType(type) ||
|
|
isAdvancedZoneType(type)
|
|
);
|
|
}
|
|
|
|
function isValidPolygon(z) {
|
|
if (!z || !isPolygonType(z.type)) return false;
|
|
const points = Array.isArray(z.points) ? z.points.map(normalizePoint).filter(Boolean) : [];
|
|
return points.length >= minPoints(z.type);
|
|
}
|
|
|
|
function isValidWall(z) {
|
|
if (!z || z.type !== TYPES.wall) return false;
|
|
const points = Array.isArray(z.points) ? z.points.map(normalizePoint).filter(Boolean) : [];
|
|
return points.length >= 2;
|
|
}
|
|
|
|
function isValidPosition(z) {
|
|
return (
|
|
z &&
|
|
z.type === TYPES.position &&
|
|
Number.isFinite(Number(z.x)) &&
|
|
Number.isFinite(Number(z.y)) &&
|
|
Number.isFinite(Number(z.yaw))
|
|
);
|
|
}
|
|
|
|
function isValidSpeedZone(z) {
|
|
if (!isValidPolygon(z) || z.type !== TYPES.speed) return false;
|
|
return Number.isFinite(Number(z.speed_mps));
|
|
}
|
|
|
|
function isValidSoundZone(z) {
|
|
return isValidPolygon(z) && z.type === TYPES.sound && typeof z.sound_id === "string";
|
|
}
|
|
|
|
function isValidDirectionalShape(z) {
|
|
return isValidPolygon(z) && z.type === TYPES.directional && Number.isFinite(Number(z.direction_deg));
|
|
}
|
|
|
|
function isValidDirectionalLine(z) {
|
|
if (!z || z.type !== TYPES.directional_line) return false;
|
|
const points = Array.isArray(z.points) ? z.points.map(normalizePoint).filter(Boolean) : [];
|
|
return points.length >= 2;
|
|
}
|
|
|
|
function isValidPlannerZone(z) {
|
|
return isValidPolygon(z) && z.type === TYPES.planner;
|
|
}
|
|
|
|
function isValidIoZone(z) {
|
|
return isValidPolygon(z) && z.type === TYPES.io && typeof z.io_module === "string";
|
|
}
|
|
|
|
function isValidZone(z) {
|
|
if (z?.type === TYPES.position) return isValidPosition(z);
|
|
if (z?.type === TYPES.wall) return isValidWall(z);
|
|
if (z?.type === TYPES.speed) return isValidSpeedZone(z);
|
|
if (z?.type === TYPES.sound) return isValidSoundZone(z);
|
|
if (z?.type === TYPES.directional) return isValidDirectionalShape(z);
|
|
if (z?.type === TYPES.directional_line) return isValidDirectionalLine(z);
|
|
if (z?.type === TYPES.planner) return isValidPlannerZone(z);
|
|
if (z?.type === TYPES.io) return isValidIoZone(z);
|
|
if (isPolygonType(z?.type)) return isValidPolygon(z);
|
|
return false;
|
|
}
|
|
|
|
/** Parse all map objects from API / database payload. */
|
|
function parseZones(raw) {
|
|
if (!Array.isArray(raw)) return [];
|
|
return raw
|
|
.map((z) => {
|
|
if (!z) return null;
|
|
if (z.type === TYPES.position) {
|
|
return {
|
|
id: typeof z.id === "string" && z.id ? z.id : newId(),
|
|
type: TYPES.position,
|
|
name: typeof z.name === "string" ? z.name : "",
|
|
x: Number(z.x),
|
|
y: Number(z.y),
|
|
yaw: Number(z.yaw),
|
|
};
|
|
}
|
|
if (isPointShapeType(z.type)) {
|
|
const base = {
|
|
id: typeof z.id === "string" && z.id ? z.id : newId(),
|
|
type: z.type,
|
|
points: (Array.isArray(z.points) ? z.points : []).map(normalizePoint).filter(Boolean),
|
|
};
|
|
if (z.type === TYPES.speed) {
|
|
base.speed_mps = clampSpeedMps(z.speed_mps);
|
|
}
|
|
if (z.type === TYPES.sound) {
|
|
base.sound_id = typeof z.sound_id === "string" ? z.sound_id : "";
|
|
}
|
|
if (z.type === TYPES.directional) {
|
|
base.direction_deg = window.MapAdvancedZones?.normalizeDirectionDeg(z.direction_deg) ?? 0;
|
|
}
|
|
if (z.type === TYPES.directional_line) {
|
|
base.reversed = !!z.reversed;
|
|
base.line_width = Number.isFinite(Number(z.line_width)) ? Number(z.line_width) : 8;
|
|
}
|
|
if (z.type === TYPES.planner) {
|
|
Object.assign(
|
|
base,
|
|
window.MapAdvancedZones?.normalizePlannerSettings(z) || {},
|
|
);
|
|
}
|
|
if (z.type === TYPES.io) {
|
|
const io = window.MapAdvancedZones?.normalizeIoSettings(z) || {};
|
|
base.io_module = io.io_module || "";
|
|
base.plc_register = io.plc_register;
|
|
base.plc_value = io.plc_value;
|
|
base.plc_mode = io.plc_mode;
|
|
}
|
|
return base;
|
|
}
|
|
return null;
|
|
})
|
|
.filter(isValidZone);
|
|
}
|
|
|
|
function createZone(type, points, extra = {}) {
|
|
const pts = points.map(normalizePoint).filter(Boolean);
|
|
if (pts.length < minPoints(type)) return null;
|
|
const zone = { id: newId(), type, points: pts };
|
|
if (type === TYPES.speed) zone.speed_mps = clampSpeedMps(extra.speed_mps);
|
|
if (type === TYPES.sound) zone.sound_id = typeof extra.sound_id === "string" ? extra.sound_id : "";
|
|
if (type === TYPES.directional) {
|
|
zone.direction_deg = window.MapAdvancedZones?.normalizeDirectionDeg(extra.direction_deg) ?? 0;
|
|
}
|
|
if (type === TYPES.directional_line) {
|
|
zone.reversed = !!extra.reversed;
|
|
zone.line_width = Number.isFinite(Number(extra.line_width)) ? Number(extra.line_width) : 8;
|
|
}
|
|
if (type === TYPES.planner) {
|
|
Object.assign(zone, window.MapAdvancedZones?.normalizePlannerSettings(extra) || {});
|
|
}
|
|
if (type === TYPES.io) {
|
|
const io = window.MapAdvancedZones?.normalizeIoSettings(extra) || {};
|
|
zone.io_module = io.io_module || "";
|
|
zone.plc_register = io.plc_register;
|
|
zone.plc_value = io.plc_value;
|
|
zone.plc_mode = io.plc_mode;
|
|
}
|
|
return zone;
|
|
}
|
|
|
|
function createPosition(worldX, worldY, yaw, name = "") {
|
|
if (!Number.isFinite(worldX) || !Number.isFinite(worldY) || !Number.isFinite(yaw)) return null;
|
|
return {
|
|
id: newId(),
|
|
type: TYPES.position,
|
|
name: name || "",
|
|
x: worldX,
|
|
y: worldY,
|
|
yaw,
|
|
};
|
|
}
|
|
|
|
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 pointInPolygon(px, py, points) {
|
|
return window.MapPlannerZones?.pointInPolygon(px, py, points) || false;
|
|
}
|
|
|
|
function dist(a, b) {
|
|
return Math.hypot(a.x - b.x, a.y - b.y);
|
|
}
|
|
|
|
function positionPixel(z, mapMeta, imgW, imgH) {
|
|
const geo = window.MapGeo;
|
|
if (!geo || !z) return null;
|
|
return geo.worldToPixel(mapMeta, imgW, imgH, z.x, z.y);
|
|
}
|
|
|
|
function hitTestPlannerZone(zones, px, py) {
|
|
for (let i = zones.length - 1; i >= 0; i--) {
|
|
const z = zones[i];
|
|
if (isPlannerZoneType(z.type) && pointInPolygon(px, py, z.points)) return z;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function hitTestBehaviorZone(zones, px, py) {
|
|
for (let i = zones.length - 1; i >= 0; i--) {
|
|
const z = zones[i];
|
|
if (isBehaviorZoneType(z.type) && pointInPolygon(px, py, z.points)) return z;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function hitTestAdvancedZone(zones, px, py, tolerance = 8) {
|
|
for (let i = zones.length - 1; i >= 0; i--) {
|
|
const z = zones[i];
|
|
if (!isAdvancedZoneType(z.type)) continue;
|
|
if (z.type === TYPES.directional_line) {
|
|
for (let j = 0; j < z.points.length - 1; j++) {
|
|
const p1 = z.points[j];
|
|
const p2 = z.points[j + 1];
|
|
const width = Number(z.line_width) || 8;
|
|
if (distPointToSegment(px, py, p1.x, p1.y, p2.x, p2.y) <= width / 2 + tolerance) return z;
|
|
}
|
|
continue;
|
|
}
|
|
if (z.points?.length && pointInPolygon(px, py, z.points)) return z;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** Find topmost floor-plan shape at image pixel. */
|
|
function hitTest(zones, px, py, tolerance = 8) {
|
|
for (let i = zones.length - 1; i >= 0; i--) {
|
|
const z = zones[i];
|
|
if (z.type === TYPES.floor && pointInPolygon(px, py, z.points)) return z;
|
|
if (z.type === TYPES.wall) {
|
|
for (let j = 0; j < z.points.length - 1; j++) {
|
|
const p1 = z.points[j];
|
|
const p2 = z.points[j + 1];
|
|
if (distPointToSegment(px, py, p1.x, p1.y, p2.x, p2.y) <= tolerance) return z;
|
|
}
|
|
}
|
|
if (z.type === TYPES.directional_line) {
|
|
for (let j = 0; j < z.points.length - 1; j++) {
|
|
const p1 = z.points[j];
|
|
const p2 = z.points[j + 1];
|
|
const width = Number(z.line_width) || 8;
|
|
if (distPointToSegment(px, py, p1.x, p1.y, p2.x, p2.y) <= width / 2 + tolerance) return z;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function hitTestPosition(zones, px, py, mapMeta, imgW, imgH, tolerance = 14) {
|
|
for (let i = zones.length - 1; i >= 0; i--) {
|
|
const z = zones[i];
|
|
if (z.type !== TYPES.position) continue;
|
|
const pt = positionPixel(z, mapMeta, imgW, imgH);
|
|
if (pt && dist({ x: px, y: py }, pt) <= tolerance) return z;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function hitTestAny(zones, px, py, mapMeta, imgW, imgH, tolerance = 8) {
|
|
const pos = hitTestPosition(zones, px, py, mapMeta, imgW, imgH, tolerance + 4);
|
|
if (pos) return pos;
|
|
const behavior = hitTestBehaviorZone(zones, px, py);
|
|
if (behavior) return behavior;
|
|
const advanced = hitTestAdvancedZone(zones, px, py, tolerance);
|
|
if (advanced) return advanced;
|
|
const planner = hitTestPlannerZone(zones, px, py);
|
|
if (planner) return planner;
|
|
return hitTest(zones, px, py, tolerance);
|
|
}
|
|
|
|
/** Hit vertex handle on selected or any point-based shape. */
|
|
function hitTestVertex(zones, px, py, selectedId, tolerance = 10) {
|
|
const tryZone = (z) => {
|
|
if (!isPointShapeType(z.type)) return null;
|
|
for (let i = 0; i < z.points.length; i++) {
|
|
if (dist({ x: px, y: py }, z.points[i]) <= tolerance) {
|
|
return { zoneId: z.id, pointIndex: i };
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
if (selectedId) {
|
|
const sel = zones.find((z) => z.id === selectedId);
|
|
const hit = sel ? tryZone(sel) : null;
|
|
if (hit) return hit;
|
|
}
|
|
for (let i = zones.length - 1; i >= 0; i--) {
|
|
const hit = tryZone(zones[i]);
|
|
if (hit) return hit;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function pointsAttr(points) {
|
|
return points.map((p) => `${p.x},${p.y}`).join(" ");
|
|
}
|
|
|
|
function clearSvg(svgEl) {
|
|
while (svgEl.firstChild) svgEl.removeChild(svgEl.firstChild);
|
|
}
|
|
|
|
function appendPolyline(parent, points, className) {
|
|
if (points.length < 2) return null;
|
|
const el = document.createElementNS("http://www.w3.org/2000/svg", "polyline");
|
|
el.setAttribute("class", className);
|
|
el.setAttribute("points", pointsAttr(points));
|
|
el.setAttribute("fill", "none");
|
|
parent.appendChild(el);
|
|
return el;
|
|
}
|
|
|
|
function appendPolygon(parent, points, className) {
|
|
if (points.length < 3) return null;
|
|
const el = document.createElementNS("http://www.w3.org/2000/svg", "polygon");
|
|
el.setAttribute("class", className);
|
|
el.setAttribute("points", pointsAttr(points));
|
|
parent.appendChild(el);
|
|
return el;
|
|
}
|
|
|
|
function appendLine(parent, p1, p2, className) {
|
|
const el = document.createElementNS("http://www.w3.org/2000/svg", "line");
|
|
el.setAttribute("class", className);
|
|
el.setAttribute("x1", String(p1.x));
|
|
el.setAttribute("y1", String(p1.y));
|
|
el.setAttribute("x2", String(p2.x));
|
|
el.setAttribute("y2", String(p2.y));
|
|
parent.appendChild(el);
|
|
return el;
|
|
}
|
|
|
|
function appendVertex(parent, p, className, r = 5, dataAttrs = {}) {
|
|
const el = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
|
el.setAttribute("class", className);
|
|
el.setAttribute("cx", String(p.x));
|
|
el.setAttribute("cy", String(p.y));
|
|
el.setAttribute("r", String(r));
|
|
Object.entries(dataAttrs).forEach(([k, v]) => el.setAttribute(k, v));
|
|
parent.appendChild(el);
|
|
return el;
|
|
}
|
|
|
|
function appendPositionMarker(parent, px, py, yawDeg, selected) {
|
|
const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
|
g.setAttribute("class", selected ? "mapObjPosition mapObjPosition--selected" : "mapObjPosition");
|
|
g.setAttribute("transform", `translate(${px},${py}) rotate(${yawDeg})`);
|
|
|
|
const shaft = document.createElementNS("http://www.w3.org/2000/svg", "line");
|
|
shaft.setAttribute("class", "mapObjPositionShaft");
|
|
shaft.setAttribute("x1", "0");
|
|
shaft.setAttribute("y1", "0");
|
|
shaft.setAttribute("x2", "22");
|
|
shaft.setAttribute("y2", "0");
|
|
g.appendChild(shaft);
|
|
|
|
const head = document.createElementNS("http://www.w3.org/2000/svg", "polygon");
|
|
head.setAttribute("class", "mapObjPositionHead");
|
|
head.setAttribute("points", "22,0 14,-5 14,5");
|
|
g.appendChild(head);
|
|
|
|
const dot = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
|
dot.setAttribute("class", "mapObjPositionDot");
|
|
dot.setAttribute("cx", "0");
|
|
dot.setAttribute("cy", "0");
|
|
dot.setAttribute("r", "4");
|
|
g.appendChild(dot);
|
|
|
|
parent.appendChild(g);
|
|
return g;
|
|
}
|
|
|
|
function appendSelectionRect(parent, rect) {
|
|
if (!rect) return null;
|
|
const x = Math.min(rect.x0, rect.x1);
|
|
const y = Math.min(rect.y0, rect.y1);
|
|
const w = Math.abs(rect.x1 - rect.x0);
|
|
const h = Math.abs(rect.y1 - rect.y0);
|
|
if (w < 1 && h < 1) return null;
|
|
const el = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
el.setAttribute("class", "mapObjSelectionRect");
|
|
el.setAttribute("x", String(x));
|
|
el.setAttribute("y", String(y));
|
|
el.setAttribute("width", String(w));
|
|
el.setAttribute("height", String(h));
|
|
parent.appendChild(el);
|
|
return el;
|
|
}
|
|
|
|
function polygonCentroid(points) {
|
|
if (!points?.length) return null;
|
|
let x = 0;
|
|
let y = 0;
|
|
for (const p of points) {
|
|
x += p.x;
|
|
y += p.y;
|
|
}
|
|
return { x: x / points.length, y: y / points.length };
|
|
}
|
|
|
|
function lineMidpoint(points) {
|
|
if (!points?.length) return null;
|
|
if (points.length === 1) return { ...points[0] };
|
|
const p0 = points[0];
|
|
const p1 = points[points.length - 1];
|
|
return { x: (p0.x + p1.x) / 2, y: (p0.y + p1.y) / 2 };
|
|
}
|
|
|
|
function appendDirectionArrow(parent, cx, cy, directionDeg, selected = false) {
|
|
const adv = window.MapAdvancedZones;
|
|
const deg = adv?.normalizeDirectionDeg(directionDeg) ?? 0;
|
|
const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
|
g.setAttribute(
|
|
"class",
|
|
selected ? "mapObjDirectionArrow mapObjDirectionArrow--selected" : "mapObjDirectionArrow",
|
|
);
|
|
g.setAttribute("transform", `translate(${cx},${cy}) rotate(${-deg})`);
|
|
|
|
const shaft = document.createElementNS("http://www.w3.org/2000/svg", "line");
|
|
shaft.setAttribute("class", "mapObjDirectionShaft");
|
|
shaft.setAttribute("x1", "-14");
|
|
shaft.setAttribute("y1", "0");
|
|
shaft.setAttribute("x2", "14");
|
|
shaft.setAttribute("y2", "0");
|
|
g.appendChild(shaft);
|
|
|
|
const head = document.createElementNS("http://www.w3.org/2000/svg", "polygon");
|
|
head.setAttribute("class", "mapObjDirectionHead");
|
|
head.setAttribute("points", "14,0 6,-5 6,5");
|
|
g.appendChild(head);
|
|
|
|
parent.appendChild(g);
|
|
return g;
|
|
}
|
|
|
|
function appendPolylineStyled(parent, points, className, strokeWidth = null) {
|
|
const el = appendPolyline(parent, points, className);
|
|
if (el && strokeWidth != null) el.setAttribute("stroke-width", String(strokeWidth));
|
|
return el;
|
|
}
|
|
|
|
function polygonClass(type, selected, draft = false) {
|
|
const base = {
|
|
[TYPES.floor]: "mapObjFloor",
|
|
[TYPES.forbidden]: "mapObjForbidden",
|
|
[TYPES.preferred]: "mapObjPreferred",
|
|
[TYPES.unpreferred]: "mapObjUnpreferred",
|
|
[TYPES.speed]: "mapObjSpeed",
|
|
[TYPES.sound]: "mapObjSound",
|
|
[TYPES.directional]: "mapObjDirectional",
|
|
[TYPES.planner]: "mapObjPlannerSettings",
|
|
[TYPES.io]: "mapObjIo",
|
|
}[type];
|
|
if (!base) return "";
|
|
if (draft) return `${base} ${base}--draft`;
|
|
if (selected) return `${base} ${base}--selected`;
|
|
return base;
|
|
}
|
|
|
|
function filterVisible(zones, visibility) {
|
|
const vis = visibility || {};
|
|
return zones.filter((z) => {
|
|
if (z.type === TYPES.wall) return vis.walls !== false;
|
|
if (z.type === TYPES.floor) return vis.floors !== false;
|
|
if (z.type === TYPES.position) return vis.positions !== false;
|
|
if (z.type === TYPES.forbidden) return vis.forbidden !== false;
|
|
if (z.type === TYPES.preferred) return vis.preferred !== false;
|
|
if (z.type === TYPES.unpreferred) return vis.unpreferred !== false;
|
|
if (z.type === TYPES.speed) return vis.speed !== false;
|
|
if (z.type === TYPES.sound) return vis.sound !== false;
|
|
if (z.type === TYPES.directional || z.type === TYPES.directional_line) {
|
|
return vis.directional !== false;
|
|
}
|
|
if (z.type === TYPES.planner) return vis.planner !== false;
|
|
if (z.type === TYPES.io) return vis.io !== false;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Render map objects on SVG overlay.
|
|
* @param {SVGSVGElement} svgEl
|
|
* @param {object[]} zones
|
|
* @param {object} opts
|
|
*/
|
|
function render(svgEl, zones, opts = {}) {
|
|
if (!svgEl) return;
|
|
clearSvg(svgEl);
|
|
|
|
const mapMeta = opts.mapMeta || {};
|
|
const imgW = opts.imageWidth || 0;
|
|
const imgH = opts.imageHeight || 0;
|
|
const list = filterVisible(zones, opts.visibility);
|
|
|
|
list.forEach((z) => {
|
|
const selected = z.id === opts.selectedId;
|
|
if (z.type === TYPES.wall) {
|
|
appendPolyline(svgEl, z.points, selected ? "mapObjWall mapObjWall--selected" : "mapObjWall");
|
|
} else if (z.type === TYPES.directional_line) {
|
|
const cls = selected
|
|
? "mapObjDirectionalLine mapObjDirectionalLine--selected"
|
|
: "mapObjDirectionalLine";
|
|
appendPolylineStyled(svgEl, z.points, cls, z.line_width || 8);
|
|
const mid = lineMidpoint(z.points);
|
|
if (mid) {
|
|
appendDirectionArrow(
|
|
svgEl,
|
|
mid.x,
|
|
mid.y,
|
|
window.MapAdvancedZones?.zoneDirectionDeg(z) ?? 0,
|
|
selected,
|
|
);
|
|
}
|
|
} else if (isPolygonType(z.type)) {
|
|
appendPolygon(svgEl, z.points, polygonClass(z.type, selected));
|
|
if (z.type === TYPES.directional) {
|
|
const c = polygonCentroid(z.points);
|
|
if (c) appendDirectionArrow(svgEl, c.x, c.y, z.direction_deg ?? 0, selected);
|
|
}
|
|
} else if (z.type === TYPES.position) {
|
|
const pt = positionPixel(z, mapMeta, imgW, imgH);
|
|
if (pt) {
|
|
const yawDeg = (-Number(z.yaw) * 180) / Math.PI;
|
|
appendPositionMarker(svgEl, pt.x, pt.y, yawDeg, selected);
|
|
}
|
|
}
|
|
});
|
|
|
|
if (opts.selectedId && opts.showVertices !== false) {
|
|
const sel = list.find((z) => z.id === opts.selectedId);
|
|
if (sel && isPointShapeType(sel.type)) {
|
|
sel.points.forEach((p, i) => {
|
|
appendVertex(svgEl, p, "mapObjVertex mapObjVertex--handle", 6, {
|
|
"data-vertex-index": String(i),
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
const draft = opts.draft;
|
|
if (draft?.kind === "shape" && draft.type && Array.isArray(draft.points) && draft.points.length) {
|
|
const pts = draft.points;
|
|
const hover = draft.hover;
|
|
if (draft.type === TYPES.wall || draft.type === TYPES.directional_line) {
|
|
const draftCls =
|
|
draft.type === TYPES.directional_line
|
|
? "mapObjDirectionalLine mapObjDirectionalLine--draft"
|
|
: "mapObjWall mapObjWall--draft";
|
|
appendPolylineStyled(svgEl, pts, draftCls, draft.type === TYPES.directional_line ? 8 : null);
|
|
if (hover && pts.length) appendLine(svgEl, pts[pts.length - 1], hover, "mapObjDraftLine");
|
|
} else if (isPolygonType(draft.type)) {
|
|
if (pts.length >= 3) appendPolygon(svgEl, pts, polygonClass(draft.type, false, true));
|
|
else if (pts.length === 2) appendPolyline(svgEl, pts, "mapObjDraftLine");
|
|
if (hover && pts.length) appendLine(svgEl, pts[pts.length - 1], hover, "mapObjDraftLine");
|
|
appendVertex(svgEl, pts[0], "mapObjCloseHint", 6);
|
|
}
|
|
pts.forEach((p) => appendVertex(svgEl, p, "mapObjVertex"));
|
|
}
|
|
|
|
if (draft?.kind === "position" && draft.px != null && draft.py != null) {
|
|
const yawDeg = (-Number(draft.yaw || 0) * 180) / Math.PI;
|
|
appendPositionMarker(svgEl, draft.px, draft.py, yawDeg, true);
|
|
}
|
|
|
|
appendSelectionRect(svgEl, opts.selectionRect);
|
|
}
|
|
|
|
function constrainAxis(from, to) {
|
|
const dx = Math.abs(to.x - from.x);
|
|
const dy = Math.abs(to.y - from.y);
|
|
if (dx >= dy) return { x: to.x, y: from.y };
|
|
return { x: from.x, y: to.y };
|
|
}
|
|
|
|
function nearPoint(a, b, tolerance = 12) {
|
|
return dist(a, b) <= tolerance;
|
|
}
|
|
|
|
function yawFromPoints(origin, target) {
|
|
return Math.atan2(-(target.y - origin.y), target.x - origin.x);
|
|
}
|
|
|
|
window.MapObjects = {
|
|
TYPES,
|
|
FLOOR_PLAN_TYPES,
|
|
POLYGON_TYPES,
|
|
POINT_SHAPE_TYPES,
|
|
newId,
|
|
parseZones,
|
|
createZone,
|
|
createPosition,
|
|
minPoints,
|
|
isFloorPlanType,
|
|
isPlannerZoneType,
|
|
isBehaviorZoneType,
|
|
isAdvancedZoneType,
|
|
isDirectionalLineType,
|
|
clampSpeedMps,
|
|
isPolygonType,
|
|
isPolylineType,
|
|
isPointShapeType,
|
|
isOverlayObjectType,
|
|
isValidZone,
|
|
hitTest,
|
|
hitTestPlannerZone,
|
|
hitTestBehaviorZone,
|
|
hitTestAdvancedZone,
|
|
hitTestPosition,
|
|
hitTestAny,
|
|
hitTestVertex,
|
|
positionPixel,
|
|
render,
|
|
filterVisible,
|
|
constrainAxis,
|
|
nearPoint,
|
|
yawFromPoints,
|
|
};
|
|
})();
|