This commit is contained in:
@@ -27,6 +27,21 @@
|
||||
TYPES.io,
|
||||
]);
|
||||
const POINT_SHAPE_TYPES = new Set([TYPES.wall, TYPES.directional_line, ...POLYGON_TYPES]);
|
||||
const DUAL_DRAW_TYPES = new Set([
|
||||
TYPES.forbidden,
|
||||
TYPES.preferred,
|
||||
TYPES.unpreferred,
|
||||
TYPES.directional,
|
||||
]);
|
||||
const LINE_ONLY_OBJECT_TYPES = new Set([TYPES.wall]);
|
||||
const SHAPE_ONLY_OBJECT_TYPES = new Set([
|
||||
TYPES.floor,
|
||||
TYPES.speed,
|
||||
TYPES.sound,
|
||||
TYPES.planner,
|
||||
TYPES.io,
|
||||
]);
|
||||
const GEOMETRY_LINE = "line";
|
||||
|
||||
function newId() {
|
||||
return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
||||
@@ -45,6 +60,91 @@
|
||||
return 0;
|
||||
}
|
||||
|
||||
function supportsDrawLine(type) {
|
||||
if (!type) return false;
|
||||
return LINE_ONLY_OBJECT_TYPES.has(type) || DUAL_DRAW_TYPES.has(type) || type === TYPES.directional_line;
|
||||
}
|
||||
|
||||
function supportsDrawShape(type) {
|
||||
if (!type) return false;
|
||||
return SHAPE_ONLY_OBJECT_TYPES.has(type) || DUAL_DRAW_TYPES.has(type);
|
||||
}
|
||||
|
||||
function defaultDrawTool(type) {
|
||||
if (type === TYPES.wall) return "drawLine";
|
||||
if (supportsDrawShape(type) && !supportsDrawLine(type)) return "drawShape";
|
||||
if (supportsDrawLine(type) && !supportsDrawShape(type)) return "drawLine";
|
||||
return "drawShape";
|
||||
}
|
||||
|
||||
function defaultLineWidthCm(type) {
|
||||
if (type === TYPES.wall) return 15;
|
||||
return 40;
|
||||
}
|
||||
|
||||
function minLineWidthCm(type) {
|
||||
if (type === TYPES.wall) return 5;
|
||||
return 10;
|
||||
}
|
||||
|
||||
function typeUsesLineWidthCm(type, geometry = null) {
|
||||
if (type === TYPES.wall || type === TYPES.directional_line) return true;
|
||||
return isPlannerZoneType(type) && geometry === GEOMETRY_LINE;
|
||||
}
|
||||
|
||||
function zoneLineWidthCm(zone, mapMeta) {
|
||||
if (Number.isFinite(Number(zone?.line_width_cm))) return Number(zone.line_width_cm);
|
||||
if (Number.isFinite(Number(zone?.line_width))) {
|
||||
return window.MapGeo?.pixelsToCm(Number(zone.line_width), mapMeta) ?? defaultLineWidthCm(zone?.type);
|
||||
}
|
||||
return defaultLineWidthCm(zone?.type);
|
||||
}
|
||||
|
||||
function zoneLineWidthPx(zone, mapMeta) {
|
||||
const geo = window.MapGeo;
|
||||
const cm = zoneLineWidthCm(zone, mapMeta);
|
||||
const px = geo?.cmToPixels(cm, mapMeta);
|
||||
if (Number.isFinite(px) && px > 0) return px;
|
||||
return zone?.type === TYPES.wall ? 3 : 8;
|
||||
}
|
||||
|
||||
function readLineWidthCmFromRaw(z, mapMeta) {
|
||||
if (Number.isFinite(Number(z?.line_width_cm))) return Number(z.line_width_cm);
|
||||
if (Number.isFinite(Number(z?.line_width))) {
|
||||
return window.MapGeo?.pixelsToCm(Number(z.line_width), mapMeta) ?? defaultLineWidthCm(z?.type);
|
||||
}
|
||||
return defaultLineWidthCm(z?.type);
|
||||
}
|
||||
|
||||
function assignLineWidthCm(zone, cm, type) {
|
||||
const min = minLineWidthCm(type);
|
||||
zone.line_width_cm = Math.max(min, Number(cm) || defaultLineWidthCm(type));
|
||||
}
|
||||
|
||||
function resolveZoneTypeForDraw(objectType, drawMode) {
|
||||
if (objectType === TYPES.directional && drawMode === "line") return TYPES.directional_line;
|
||||
return objectType;
|
||||
}
|
||||
|
||||
function minPointsForDraw(type, drawMode) {
|
||||
const zoneType = resolveZoneTypeForDraw(type, drawMode || "shape");
|
||||
if (drawMode === "line" || isPolylineType(zoneType)) return 2;
|
||||
return minPoints(zoneType);
|
||||
}
|
||||
|
||||
function isLineGeometry(z) {
|
||||
if (!z) return false;
|
||||
if (z.type === TYPES.wall || z.type === TYPES.directional_line) return true;
|
||||
return isPlannerZoneType(z.type) && z.geometry === GEOMETRY_LINE;
|
||||
}
|
||||
|
||||
function isDraftPolyline(draft) {
|
||||
if (!draft || draft.kind !== "shape") return false;
|
||||
if (draft.drawMode === "line") return true;
|
||||
if (draft.drawMode === "shape") return false;
|
||||
return isPolylineType(draft.type);
|
||||
}
|
||||
|
||||
function isFloorPlanType(type) {
|
||||
return FLOOR_PLAN_TYPES.has(type);
|
||||
}
|
||||
@@ -92,10 +192,17 @@
|
||||
|
||||
function isValidPolygon(z) {
|
||||
if (!z || !isPolygonType(z.type)) return false;
|
||||
if (isLineGeometry(z)) return false;
|
||||
const points = Array.isArray(z.points) ? z.points.map(normalizePoint).filter(Boolean) : [];
|
||||
return points.length >= minPoints(z.type);
|
||||
}
|
||||
|
||||
function isValidPlannerLineZone(z) {
|
||||
if (!z || !isPlannerZoneType(z.type) || z.geometry !== GEOMETRY_LINE) return false;
|
||||
const points = Array.isArray(z.points) ? z.points.map(normalizePoint).filter(Boolean) : [];
|
||||
return points.length >= 2;
|
||||
}
|
||||
|
||||
function isValidWall(z) {
|
||||
if (!z || z.type !== TYPES.wall) return false;
|
||||
const points = Array.isArray(z.points) ? z.points.map(normalizePoint).filter(Boolean) : [];
|
||||
@@ -148,12 +255,13 @@
|
||||
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 (isPlannerZoneType(z?.type) && z.geometry === GEOMETRY_LINE) return isValidPlannerLineZone(z);
|
||||
if (isPolygonType(z?.type)) return isValidPolygon(z);
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Parse all map objects from API / database payload. */
|
||||
function parseZones(raw) {
|
||||
function parseZones(raw, mapMeta = null) {
|
||||
if (!Array.isArray(raw)) return [];
|
||||
return raw
|
||||
.map((z) => {
|
||||
@@ -185,7 +293,10 @@
|
||||
}
|
||||
if (z.type === TYPES.directional_line) {
|
||||
base.reversed = !!z.reversed;
|
||||
base.line_width = Number.isFinite(Number(z.line_width)) ? Number(z.line_width) : 8;
|
||||
assignLineWidthCm(base, readLineWidthCmFromRaw(z, mapMeta), TYPES.directional_line);
|
||||
}
|
||||
if (z.type === TYPES.wall) {
|
||||
assignLineWidthCm(base, readLineWidthCmFromRaw(z, mapMeta), TYPES.wall);
|
||||
}
|
||||
if (z.type === TYPES.planner) {
|
||||
Object.assign(
|
||||
@@ -200,6 +311,10 @@
|
||||
base.plc_value = io.plc_value;
|
||||
base.plc_mode = io.plc_mode;
|
||||
}
|
||||
if (isPlannerZoneType(z.type) && z.geometry === GEOMETRY_LINE) {
|
||||
base.geometry = GEOMETRY_LINE;
|
||||
assignLineWidthCm(base, readLineWidthCmFromRaw(z, mapMeta), z.type);
|
||||
}
|
||||
return base;
|
||||
}
|
||||
return null;
|
||||
@@ -209,8 +324,14 @@
|
||||
|
||||
function createZone(type, points, extra = {}) {
|
||||
const pts = points.map(normalizePoint).filter(Boolean);
|
||||
if (pts.length < minPoints(type)) return null;
|
||||
const lineGeom = isPlannerZoneType(type) && extra.geometry === GEOMETRY_LINE;
|
||||
const min = lineGeom ? 2 : minPoints(type);
|
||||
if (pts.length < min) return null;
|
||||
const zone = { id: newId(), type, points: pts };
|
||||
if (lineGeom) {
|
||||
zone.geometry = GEOMETRY_LINE;
|
||||
assignLineWidthCm(zone, extra.line_width_cm, type);
|
||||
}
|
||||
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) {
|
||||
@@ -218,7 +339,10 @@
|
||||
}
|
||||
if (type === TYPES.directional_line) {
|
||||
zone.reversed = !!extra.reversed;
|
||||
zone.line_width = Number.isFinite(Number(extra.line_width)) ? Number(extra.line_width) : 8;
|
||||
assignLineWidthCm(zone, extra.line_width_cm, type);
|
||||
}
|
||||
if (type === TYPES.wall) {
|
||||
assignLineWidthCm(zone, extra.line_width_cm, type);
|
||||
}
|
||||
if (type === TYPES.planner) {
|
||||
Object.assign(zone, window.MapAdvancedZones?.normalizePlannerSettings(extra) || {});
|
||||
@@ -269,10 +393,20 @@
|
||||
return geo.worldToPixel(mapMeta, imgW, imgH, z.x, z.y);
|
||||
}
|
||||
|
||||
function hitTestPlannerZone(zones, px, py) {
|
||||
function hitTestPlannerZone(zones, px, py, tolerance = 8, mapMeta = null) {
|
||||
for (let i = zones.length - 1; i >= 0; i--) {
|
||||
const z = zones[i];
|
||||
if (isPlannerZoneType(z.type) && pointInPolygon(px, py, z.points)) return z;
|
||||
if (!isPlannerZoneType(z.type) || !z.points?.length) continue;
|
||||
if (isLineGeometry(z)) {
|
||||
const half = zoneLineWidthPx(z, mapMeta) / 2;
|
||||
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) <= half + tolerance) return z;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (pointInPolygon(px, py, z.points)) return z;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -285,7 +419,7 @@
|
||||
return null;
|
||||
}
|
||||
|
||||
function hitTestAdvancedZone(zones, px, py, tolerance = 8) {
|
||||
function hitTestAdvancedZone(zones, px, py, tolerance = 8, mapMeta = null) {
|
||||
for (let i = zones.length - 1; i >= 0; i--) {
|
||||
const z = zones[i];
|
||||
if (!isAdvancedZoneType(z.type)) continue;
|
||||
@@ -293,7 +427,7 @@
|
||||
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;
|
||||
const width = zoneLineWidthPx(z, mapMeta);
|
||||
if (distPointToSegment(px, py, p1.x, p1.y, p2.x, p2.y) <= width / 2 + tolerance) return z;
|
||||
}
|
||||
continue;
|
||||
@@ -304,22 +438,23 @@
|
||||
}
|
||||
|
||||
/** Find topmost floor-plan shape at image pixel. */
|
||||
function hitTest(zones, px, py, tolerance = 8) {
|
||||
function hitTest(zones, px, py, tolerance = 8, mapMeta = null) {
|
||||
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) {
|
||||
const half = zoneLineWidthPx(z, mapMeta) / 2;
|
||||
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 (distPointToSegment(px, py, p1.x, p1.y, p2.x, p2.y) <= half + 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;
|
||||
const width = zoneLineWidthPx(z, mapMeta);
|
||||
if (distPointToSegment(px, py, p1.x, p1.y, p2.x, p2.y) <= width / 2 + tolerance) return z;
|
||||
}
|
||||
}
|
||||
@@ -342,11 +477,11 @@
|
||||
if (pos) return pos;
|
||||
const behavior = hitTestBehaviorZone(zones, px, py);
|
||||
if (behavior) return behavior;
|
||||
const advanced = hitTestAdvancedZone(zones, px, py, tolerance);
|
||||
const advanced = hitTestAdvancedZone(zones, px, py, tolerance, mapMeta);
|
||||
if (advanced) return advanced;
|
||||
const planner = hitTestPlannerZone(zones, px, py);
|
||||
const planner = hitTestPlannerZone(zones, px, py, tolerance, mapMeta);
|
||||
if (planner) return planner;
|
||||
return hitTest(zones, px, py, tolerance);
|
||||
return hitTest(zones, px, py, tolerance, mapMeta);
|
||||
}
|
||||
|
||||
/** Hit vertex handle on selected or any point-based shape. */
|
||||
@@ -528,6 +663,7 @@
|
||||
[TYPES.speed]: "mapObjSpeed",
|
||||
[TYPES.sound]: "mapObjSound",
|
||||
[TYPES.directional]: "mapObjDirectional",
|
||||
[TYPES.directional_line]: "mapObjDirectional",
|
||||
[TYPES.planner]: "mapObjPlannerSettings",
|
||||
[TYPES.io]: "mapObjIo",
|
||||
}[type];
|
||||
@@ -537,6 +673,10 @@
|
||||
return base;
|
||||
}
|
||||
|
||||
function zonePolygonClass(type, selected, draft = false) {
|
||||
return polygonClass(type === TYPES.directional_line ? TYPES.directional : type, selected, draft);
|
||||
}
|
||||
|
||||
function filterVisible(zones, visibility) {
|
||||
const vis = visibility || {};
|
||||
return zones.filter((z) => {
|
||||
@@ -575,12 +715,15 @@
|
||||
list.forEach((z) => {
|
||||
const selected = z.id === opts.selectedId;
|
||||
if (z.type === TYPES.wall) {
|
||||
appendPolyline(svgEl, z.points, selected ? "mapObjWall mapObjWall--selected" : "mapObjWall");
|
||||
appendPolylineStyled(
|
||||
svgEl,
|
||||
z.points,
|
||||
selected ? "mapObjWall mapObjWall--selected" : "mapObjWall",
|
||||
zoneLineWidthPx(z, mapMeta),
|
||||
);
|
||||
} else if (z.type === TYPES.directional_line) {
|
||||
const cls = selected
|
||||
? "mapObjDirectionalLine mapObjDirectionalLine--selected"
|
||||
: "mapObjDirectionalLine";
|
||||
appendPolylineStyled(svgEl, z.points, cls, z.line_width || 8);
|
||||
const cls = zonePolygonClass(TYPES.directional_line, selected);
|
||||
appendPolylineStyled(svgEl, z.points, cls, zoneLineWidthPx(z, mapMeta));
|
||||
const mid = lineMidpoint(z.points);
|
||||
if (mid) {
|
||||
appendDirectionArrow(
|
||||
@@ -591,8 +734,15 @@
|
||||
selected,
|
||||
);
|
||||
}
|
||||
} else if (isPlannerZoneType(z.type) && isLineGeometry(z)) {
|
||||
appendPolylineStyled(
|
||||
svgEl,
|
||||
z.points,
|
||||
zonePolygonClass(z.type, selected),
|
||||
zoneLineWidthPx(z, mapMeta),
|
||||
);
|
||||
} else if (isPolygonType(z.type)) {
|
||||
appendPolygon(svgEl, z.points, polygonClass(z.type, selected));
|
||||
appendPolygon(svgEl, z.points, zonePolygonClass(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);
|
||||
@@ -621,15 +771,22 @@
|
||||
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 (isDraftPolyline(draft)) {
|
||||
const draftZone = { type: draft.type, line_width_cm: draft.line_width_cm };
|
||||
const strokeW = zoneLineWidthPx(draftZone, mapMeta);
|
||||
let draftCls = "mapObjWall mapObjWall--draft";
|
||||
const zoneType = resolveZoneTypeForDraw(draft.type, draft.drawMode || "line");
|
||||
if (zoneType === TYPES.directional_line || isPlannerZoneType(draft.type)) {
|
||||
draftCls = zonePolygonClass(
|
||||
zoneType === TYPES.directional_line ? TYPES.directional_line : draft.type,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
}
|
||||
appendPolylineStyled(svgEl, pts, draftCls, strokeW);
|
||||
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));
|
||||
if (pts.length >= 3) appendPolygon(svgEl, pts, zonePolygonClass(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);
|
||||
@@ -670,6 +827,19 @@
|
||||
createZone,
|
||||
createPosition,
|
||||
minPoints,
|
||||
minPointsForDraw,
|
||||
supportsDrawLine,
|
||||
supportsDrawShape,
|
||||
defaultDrawTool,
|
||||
defaultLineWidthCm,
|
||||
minLineWidthCm,
|
||||
zoneLineWidthCm,
|
||||
zoneLineWidthPx,
|
||||
typeUsesLineWidthCm,
|
||||
resolveZoneTypeForDraw,
|
||||
isLineGeometry,
|
||||
isDraftPolyline,
|
||||
GEOMETRY_LINE,
|
||||
isFloorPlanType,
|
||||
isPlannerZoneType,
|
||||
isBehaviorZoneType,
|
||||
|
||||
Reference in New Issue
Block a user