(() => {
  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]);
  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)}`;
  }

  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 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);
  }

  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;
    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) : [];
    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 (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, mapMeta = null) {
    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;
            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(
              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;
          }
          if (isPlannerZoneType(z.type) && z.geometry === GEOMETRY_LINE) {
            base.geometry = GEOMETRY_LINE;
            assignLineWidthCm(base, readLineWidthCmFromRaw(z, mapMeta), z.type);
          }
          return base;
        }
        return null;
      })
      .filter(isValidZone);
  }

  function createZone(type, points, extra = {}) {
    const pts = points.map(normalizePoint).filter(Boolean);
    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) {
      zone.direction_deg = window.MapAdvancedZones?.normalizeDirectionDeg(extra.direction_deg) ?? 0;
    }
    if (type === TYPES.directional_line) {
      zone.reversed = !!extra.reversed;
      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) || {});
    }
    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, tolerance = 8, mapMeta = null) {
    for (let i = zones.length - 1; i >= 0; i--) {
      const z = zones[i];
      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;
  }

  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, mapMeta = null) {
    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 = zoneLineWidthPx(z, mapMeta);
          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, 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) <= 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 = zoneLineWidthPx(z, mapMeta);
          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, mapMeta);
    if (advanced) return advanced;
    const planner = hitTestPlannerZone(zones, px, py, tolerance, mapMeta);
    if (planner) return planner;
    return hitTest(zones, px, py, tolerance, mapMeta);
  }

  /** 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.directional_line]: "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 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) => {
      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) {
        appendPolylineStyled(
          svgEl,
          z.points,
          selected ? "mapObjWall mapObjWall--selected" : "mapObjWall",
          zoneLineWidthPx(z, mapMeta),
        );
      } else if (z.type === TYPES.directional_line) {
        const cls = zonePolygonClass(TYPES.directional_line, selected);
        appendPolylineStyled(svgEl, z.points, cls, zoneLineWidthPx(z, mapMeta));
        const mid = lineMidpoint(z.points);
        if (mid) {
          appendDirectionArrow(
            svgEl,
            mid.x,
            mid.y,
            window.MapAdvancedZones?.zoneDirectionDeg(z) ?? 0,
            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, 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);
        }
      } 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 (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, 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);
      }
      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,
    minPointsForDraw,
    supportsDrawLine,
    supportsDrawShape,
    defaultDrawTool,
    defaultLineWidthCm,
    minLineWidthCm,
    zoneLineWidthCm,
    zoneLineWidthPx,
    typeUsesLineWidthCm,
    resolveZoneTypeForDraw,
    isLineGeometry,
    isDraftPolyline,
    GEOMETRY_LINE,
    isFloorPlanType,
    isPlannerZoneType,
    isBehaviorZoneType,
    isAdvancedZoneType,
    isDirectionalLineType,
    clampSpeedMps,
    isPolygonType,
    isPolylineType,
    isPointShapeType,
    isOverlayObjectType,
    isValidZone,
    hitTest,
    hitTestPlannerZone,
    hitTestBehaviorZone,
    hitTestAdvancedZone,
    hitTestPosition,
    hitTestAny,
    hitTestVertex,
    positionPixel,
    render,
    filterVisible,
    constrainAxis,
    nearPoint,
    yawFromPoints,
  };
})();
