PageSourceSearch

https://app.marinesensitivity.org/species/maplibregl-binding-0.5.0.9000/maplibregl.js

js marinesensitivity.org collected 2026-10-06 21:46:09 UTC 239,221 bytes, 6,443 lines download raw bytes

1// Attach cluster-click + cursor handlers to a "-clusters" layer.
2// Auto-branches on source capability:
3//   - live-clustered GeoJSON  -> await source.getClusterExpansionZoom()
4//   - precomputed vector tiles -> easeTo with zoom + 2
5// Idempotent: handler refs are stored per layer id so remove_layer can
6// tear them down, and re-calling attach on the same layer is a no-op.
7function _mapglAttachClusterHandlers(map, layerId, sourceId) {
8  if (!map._mapglClusterHandlers) map._mapglClusterHandlers = {};
9  if (map._mapglClusterHandlers[layerId]) return;
10
11  const click = async (e) => {
12    const features = map.queryRenderedFeatures(e.point, {
13      layers: [layerId],
14    });
15    if (!features.length) return;
16    const feature = features[0];
17    const source = map.getSource(sourceId);
18
19    if (source && typeof source.getClusterExpansionZoom === "function") {
20      // Native live-clustered GeoJSON source (MapLibre v5: Promise API).
21      try {
22        const clusterId = feature.properties.cluster_id;
23        const zoom = await source.getClusterExpansionZoom(clusterId);
24        map.easeTo({ center: feature.geometry.coordinates, zoom });
25      } catch (err) {
26        /* swallow: nothing to do if expansion zoom fails */
27      }
28    } else {
29      // Pre-clustered vector tiles: no source-side cluster tree, so
30      // step the zoom up by one. Tiles re-cluster at every level, so
31      // +2 would traverse an intermediate level mid-ease and flash a
32      // second regrouping — +1 gives a clean single break-apart.
33      map.easeTo({
34        center: feature.geometry.coordinates,
35        zoom: Math.min(map.getZoom() + 1, 20),
36      });
37    }
38  };
39  const mouseenter = () => {
40    map.getCanvas().style.cursor = "pointer";
41  };
42  const mouseleave = () => {
43    map.getCanvas().style.cursor = "";
44  };
45
46  map.on("click", layerId, click);
47  map.on("mouseenter", layerId, mouseenter);
48  map.on("mouseleave", layerId, mouseleave);
49  map._mapglClusterHandlers[layerId] = { click, mouseenter, mouseleave };
50}
51
52function _mapglDetachClusterHandlers(map, layerId) {
53  if (!map._mapglClusterHandlers || !map._mapglClusterHandlers[layerId]) return;
54  const h = map._mapglClusterHandlers[layerId];
55  map.off("click", layerId, h.click);
56  map.off("mouseenter", layerId, h.mouseenter);
57  map.off("mouseleave", layerId, h.mouseleave);
58  delete map._mapglClusterHandlers[layerId];
59}
60
61function _mapglPostDrawnFeatures(syncUrl, mapglId, features) {
62  if (!syncUrl || !mapglId || typeof fetch !== "function") return;
63
64  fetch(syncUrl, {
65    method: "POST",
66    body: JSON.stringify(features),
67  }).catch((e) => {
68    if (typeof console !== "undefined" && console.debug) {
69      console.debug("mapgl draw sync failed", e);
70    }
71  });
72}
73
74function _mapglSyncDrawnFeatures(options) {
75  const drawControl = options.drawControl;
76  if (!drawControl) return;
77
78  const drawnFeatures = drawControl.getAll();
79  const widget = options.widget;
80
81  if (HTMLWidgets.shinyMode && typeof Shiny !== "undefined") {
82    Shiny.setInputValue(options.inputId + "_drawn_features", drawnFeatures, {
83      priority: "event",
84    });
85  } else if (options.syncUrl && options.mapglId) {
86    if (options.debounce && widget) {
87      clearTimeout(widget._mapglDrawSyncTimer);
88      widget._mapglDrawSyncTimer = setTimeout(() => {
89        _mapglPostDrawnFeatures(
90          options.syncUrl,
91          options.mapglId,
92          drawnFeatures,
93        );
94      }, 150);
95    } else {
96      _mapglPostDrawnFeatures(options.syncUrl, options.mapglId, drawnFeatures);
97    }
98  }
99
100  if (widget) {
101    widget.drawFeatures = drawnFeatures;
102  }
103}
104
105function _mapglHasDrawAttributes(attributes) {
106  return Array.isArray(attributes) && attributes.length > 0;
107}
108
109function _mapglAttributeDefaultValue(field) {
110  if (Object.prototype.hasOwnProperty.call(field, "default")) {
111    return field.default;
112  }
113  return undefined;
114}
115
116function _mapglEnsureDrawAttributeStyles() {
117  if (document.querySelector("#mapgl-draw-attribute-editor-styles")) return;
118
119  const style = document.createElement("style");
120  style.id = "mapgl-draw-attribute-editor-styles";
121  style.textContent = `
122    .mapgl-draw-attribute-editor {
123      position: absolute;
124      z-index: 2;
125      width: 260px;
126      max-width: calc(100% - 20px);
127      background: #ffffff;
128      border-radius: 4px;
129      box-shadow: 0 1px 4px rgba(0,0,0,0.28);
130      color: #222;
131      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
132      font-size: 12px;
133      display: none;
134      overflow: hidden;
135    }
136    .mapgl-draw-attribute-editor.mapgl-draw-attribute-editor-visible {
137      display: block;
138    }
139    .mapgl-draw-attribute-editor-header {
140      padding: 9px 10px;
141      border-bottom: 1px solid #e2e2e2;
142      font-size: 12px;
143      font-weight: 700;
144      letter-spacing: 0.02em;
145      text-transform: uppercase;
146    }
147    .mapgl-draw-attribute-editor-body {
148      padding: 10px;
149    }
150    .mapgl-draw-attribute-field {
151      margin-bottom: 10px;
152    }
153    .mapgl-draw-attribute-field label {
154      display: block;
155      margin-bottom: 4px;
156      font-weight: 600;
157    }
158    .mapgl-draw-attribute-field input,
159    .mapgl-draw-attribute-field select,
160    .mapgl-draw-attribute-field textarea {
161      box-sizing: border-box;
162      width: 100%;
163      border: 1px solid #cfcfcf;
164      border-radius: 3px;
165      padding: 6px 7px;
166      font: inherit;
167    }
168    .mapgl-draw-attribute-field input[type="checkbox"] {
169      width: auto;
170      margin-right: 6px;
171    }
172    .mapgl-draw-attribute-checkbox-label {
173      display: flex !important;
174      align-items: center;
175      gap: 4px;
176      margin-bottom: 0 !important;
177    }
178    .mapgl-draw-attribute-actions {
179      display: flex;
180      align-items: center;
181      justify-content: space-between;
182      gap: 8px;
183      padding-top: 2px;
184    }
185    .mapgl-draw-attribute-save {
186      border: 0;
187      border-radius: 3px;
188      background: #1f2937;
189      color: #fff;
190      cursor: pointer;
191      font: inherit;
192      font-weight: 600;
193      padding: 6px 10px;
194    }
195    .mapgl-draw-attribute-message {
196      color: #666;
197      font-size: 11px;
198    }
199  `;
200  document.head.appendChild(style);
201}
202
203function _mapglEditorPositionStyle(position) {
204  const style = {};
205  const vertical = position && position.indexOf("bottom") !== -1 ? "bottom" : "top";
206  const horizontal = position && position.indexOf("right") !== -1 ? "left" : "right";
207  style[vertical] = "10px";
208  style[horizontal] = "10px";
209  return style;
210}
211
212function _mapglApplyStyle(element, style) {
213  Object.keys(style).forEach((key) => {
214    element.style[key] = style[key];
215  });
216}
217
218function formatDmsCoordinate(value, axis, precision) {
219  const direction =
220    axis === "lng" ? (value < 0 ? "W" : "E") : value < 0 ? "S" : "N";
221  const absolute = Math.abs(value);
222  let degrees = Math.floor(absolute);
223  const minutesFloat = (absolute - degrees) * 60;
224  let minutes = Math.floor(minutesFloat);
225  let seconds = (minutesFloat - minutes) * 60;
226
227  const factor = Math.pow(10, precision);
228  seconds = Math.round(seconds * factor) / factor;
229  if (seconds >= 60) {
230    seconds = 0;
231    minutes += 1;
232  }
233  if (minutes >= 60) {
234    minutes = 0;
235    degrees += 1;
236  }
237
238  const secondsText = seconds
239    .toFixed(precision)
240    .padStart(precision > 0 ? precision + 3 : 2, "0");
241  return `${degrees}\u00b0${String(minutes).padStart(2, "0")}'${secondsText}"${direction}`;
242}
243
244function formatCoordinates(lngLat, format, precision) {
245  if (format === "dms") {
246    return `${formatDmsCoordinate(lngLat.lng, "lng", precision)}, ${formatDmsCoordinate(lngLat.lat, "lat", precision)}`;
247  }
248  return `${lngLat.lng.toFixed(precision)}, ${lngLat.lat.toFixed(precision)}`;
249}
250
251function createCoordinatesControl(options) {
252  const controlOptions = options || {};
253  const format = controlOptions.format || "decimal";
254  const precisionValue = Number(controlOptions.precision);
255  const precision = Number.isFinite(precisionValue)
256    ? Math.min(20, Math.max(0, Math.floor(precisionValue)))
257    : format === "dms" ? 1 : 5;
258  const emptyText = controlOptions.empty_text || "Move cursor over map";
259  const labelText = controlOptions.label || "";
260  const wrapLongitude = controlOptions.wrap !== false;
261
262  const container = document.createElement("div");
263  container.className = "mapgl-coordinates-control maplibregl-ctrl";
264  container.style.cssText = `
265    background: #ffffff;
266    padding: 8px 10px;
267    border-radius: 4px;
268    box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
269    color: #222;
270    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
271    font-size: 12px;
272    line-height: 1.35;
273    min-width: 142px;
274    max-width: 240px;
275    border: 1px solid rgba(0,0,0,0.1);
276    pointer-events: none;
277  `;
278
279  if (labelText) {
280    const label = document.createElement("div");
281    label.className = "mapgl-coordinates-label";
282    label.textContent = labelText;
283    label.style.cssText = `
284      margin-bottom: 2px;
285      color: #666;
286      font-size: 11px;
287      font-weight: 600;
288      letter-spacing: 0.02em;
289      text-transform: uppercase;
290    `;
291    container.appendChild(label);
292  }
293
294  const value = document.createElement("div");
295  value.className = "mapgl-coordinates-value";
296  value.textContent = emptyText;
297  value.style.cssText = `
298    color: #111;
299    font-variant-numeric: tabular-nums;
300    white-space: nowrap;
301  `;
302  container.appendChild(value);
303
304  let mapRef = null;
305  const update = (event) => {
306    if (!event || !event.lngLat) return;
307    const lngLat =
308      wrapLongitude && typeof event.lngLat.wrap === "function"
309        ? event.lngLat.wrap()
310        : event.lngLat;
311    value.textContent = formatCoordinates(lngLat, format, precision);
312  };
313  const clear = () => {
314    value.textContent = emptyText;
315  };
316
317  return {
318    onAdd: function (map) {
319      mapRef = map;
320      mapRef.on("mousemove", update);
321      mapRef.getCanvas().addEventListener("mouseleave", clear);
322      return container;
323    },
324    onRemove: function () {
325      if (mapRef) {
326        mapRef.off("mousemove", update);
327        mapRef.getCanvas().removeEventListener("mouseleave", clear);
328      }
329      if (container.parentNode) {
330        container.parentNode.removeChild(container);
331      }
332      mapRef = null;
333    },
334  };
335}
336
337function _mapglCurrentFieldValue(feature, field) {
338  const properties = (feature && feature.properties) || {};
339  if (Object.prototype.hasOwnProperty.call(properties, field.name)) {
340    return properties[field.name];
341  }
342  const defaultValue = _mapglAttributeDefaultValue(field);
343  if (defaultValue !== undefined) {
344    return defaultValue;
345  }
346  return field.type === "checkbox" ? false : "";
347}
348
349function _mapglApplyAttributeDefaults(drawControl, feature, attributes) {
350  const featureId = feature && feature.id;
351  if (!featureId) return;
352
353  const properties = feature.properties || {};
354  attributes.forEach((field) => {
355    if (Object.prototype.hasOwnProperty.call(properties, field.name)) return;
356    const defaultValue = _mapglAttributeDefaultValue(field);
357    if (defaultValue !== undefined) {
358      drawControl.setFeatureProperty(featureId, field.name, defaultValue);
359    }
360  });
361}
362
363function _mapglCreateAttributeInput(field, value) {
364  let input;
365
366  if (field.type === "textarea") {
367    input = document.createElement("textarea");
368    input.rows = 3;
369    input.value = value == null ? "" : value;
370  } else if (field.type === "select") {
371    input = document.createElement("select");
372    const emptyOption = document.createElement("option");
373    emptyOption.value = "";
374    emptyOption.textContent = "";
375    input.appendChild(emptyOption);
376    (field.choices || []).forEach((choice, index) => {
377      const option = document.createElement("option");
378      option.value = String(index);
379      option.textContent = choice.label == null ? String(choice.value) : choice.label;
380      option.dataset.mapglValue = JSON.stringify(choice.value);
381      if (value === choice.value || String(value) === String(choice.value)) {
382        option.selected = true;
383      }
384      input.appendChild(option);
385    });
386  } else if (field.type === "number") {
387    input = document.createElement("input");
388    input.type = "number";
389    input.value = value == null ? "" : value;
390    ["min", "max", "step"].forEach((key) => {
391      if (Object.prototype.hasOwnProperty.call(field, key)) {
392        input.setAttribute(key, field[key]);
393      }
394    });
395  } else if (field.type === "checkbox") {
396    input = document.createElement("input");
397    input.type = "checkbox";
398    input.checked = value === true || value === "true" || value === 1;
399  } else {
400    input = document.createElement("input");
401    input.type = "text";
402    input.value = value == null ? "" : value;
403  }
404
405  if (field.placeholder && field.type !== "select" && field.type !== "checkbox") {
406    input.placeholder = field.placeholder;
407  }
408  if (field.required) {
409    input.required = true;
410  }
411  input.dataset.fieldName = field.name;
412  input.dataset.fieldType = field.type;
413  return input;
414}
415
416function _mapglInputValue(input, field) {
417  if (field.type === "checkbox") {
418    return input.checked;
419  }
420  if (field.type === "number") {
421    return input.value === "" ? null : Number(input.value);
422  }
423  if (field.type === "select") {
424    if (input.value === "") return null;
425    const selected = input.options[input.selectedIndex];
426    return selected && selected.dataset.mapglValue
427      ? JSON.parse(selected.dataset.mapglValue)
428      : input.value;
429  }
430  return input.value;
431}
432
433function initializeDrawAttributeEditor(map, drawControl, options) {
434  const attributes = options.attributes || [];
435  if (!_mapglHasDrawAttributes(attributes)) return null;
436
437  _mapglEnsureDrawAttributeStyles();
438
439  const container = document.createElement("div");
440  container.className = "mapgl-draw-attribute-editor";
441  _mapglApplyStyle(container, _mapglEditorPositionStyle(options.position));
442
443  const header = document.createElement("div");
444  header.className = "mapgl-draw-attribute-editor-header";
445  header.textContent = "Feature attributes";
446
447  const body = document.createElement("form");
448  body.className = "mapgl-draw-attribute-editor-body";
449
450  container.appendChild(header);
451  container.appendChild(body);
452  map.getContainer().appendChild(container);
453
454  let selectedFeatureId = null;
455
456  const hide = () => {
457    selectedFeatureId = null;
458    container.classList.remove("mapgl-draw-attribute-editor-visible");
459    body.innerHTML = "";
460  };
461
462  const show = (feature) => {
463    selectedFeatureId = feature && feature.id;
464    if (!selectedFeatureId) {
465      hide();
466      return;
467    }
468
469    const currentFeature = drawControl.get(selectedFeatureId) || feature;
470    body.innerHTML = "";
471
472    attributes.forEach((field) => {
473      const value = _mapglCurrentFieldValue(currentFeature, field);
474      const wrapper = document.createElement("div");
475      wrapper.className = "mapgl-draw-attribute-field";
476      const input = _mapglCreateAttributeInput(field, value);
477
478      if (field.type === "checkbox") {
479        const label = document.createElement("label");
480        label.className = "mapgl-draw-attribute-checkbox-label";
481        label.appendChild(input);
482        label.appendChild(document.createTextNode(field.label || field.name));
483        wrapper.appendChild(label);
484      } else {
485        const label = document.createElement("label");
486        label.textContent = field.label || field.name;
487        wrapper.appendChild(label);
488        wrapper.appendChild(input);
489      }
490
491      body.appendChild(wrapper);
492    });
493
494    const actions = document.createElement("div");
495    actions.className = "mapgl-draw-attribute-actions";
496
497    const save = document.createElement("button");
498    save.type = "submit";
499    save.className = "mapgl-draw-attribute-save";
500    save.textContent = "Save";
501
502    const message = document.createElement("span");
503    message.className = "mapgl-draw-attribute-message";
504
505    actions.appendChild(save);
506    actions.appendChild(message);
507    body.appendChild(actions);
508    container.classList.add("mapgl-draw-attribute-editor-visible");
509  };
510
511  body.addEventListener("submit", (e) => {
512    e.preventDefault();
513    if (!selectedFeatureId) return;
514    if (typeof body.reportValidity === "function" && !body.reportValidity()) {
515      return;
516    }
517
518    attributes.forEach((field) => {
519      const input = Array.from(
520        body.querySelectorAll("[data-field-name]"),
521      ).find((element) => element.dataset.fieldName === field.name);
522      if (!input) return;
523      drawControl.setFeatureProperty(
524        selectedFeatureId,
525        field.name,
526        _mapglInputValue(input, field),
527      );
528    });
529
530    const message = body.querySelector(".mapgl-draw-attribute-message");
531    if (message) {
532      message.textContent = "Saved";
533      setTimeout(() => {
534        message.textContent = "";
535      }, 1200);
536    }
537
538    if (typeof options.onChange === "function") {
539      options.onChange(false);
540    }
541  });
542
543  map.on("draw.create", (e) => {
544    if (e.features) {
545      e.features.forEach((feature) => {
546        _mapglApplyAttributeDefaults(drawControl, feature, attributes);
547      });
548    }
549    if (e.features && e.features.length === 1) {
550      show(e.features[0]);
551    }
552  });
553
554  map.on("draw.selectionchange", (e) => {
555    if (e.features && e.features.length === 1) {
556      show(e.features[0]);
557    } else {
558      hide();
559    }
560  });
561
562  map.on("draw.delete", hide);
563
564  return {
565    hide: hide,
566    show: show,
567  };
568}
569
570// Shared layer-state and filter-composition helpers. See the twin block
571// in mapboxgl.js for the design notes.
572function _mapglEnsureLayerState(map) {
573  if (!window._mapglLayerState) window._mapglLayerState = {};
574  const mapId = map.getContainer().id;
575  const s =
576    window._mapglLayerState[mapId] ||
577    (window._mapglLayerState[mapId] = {});
578  if (!s.filters) s.filters = {};
579  if (!s.paintProperties) s.paintProperties = {};
580  if (!s.layoutProperties) s.layoutProperties = {};
581  if (!s.tooltips) s.tooltips = {};
582  if (!s.popups) s.popups = {};
583  // Parallel maps holding the (optional) tooltip/popup style spec for proxy
584  // set_tooltip()/set_popup() so theming survives a style reload.
585  if (!s.tooltipStyles) s.tooltipStyles = {};
586  if (!s.popupStyles) s.popupStyles = {};
587  if (!s.legends) s.legends = {};
588  if (!s.interactiveFilters) s.interactiveFilters = {};
589  if (!s.filterStack) s.filterStack = {};
590  return s;
591}
592
593function _mapglComposeAndApplyFilter(map, layerId) {
594  const state = _mapglEnsureLayerState(map);
595  const stack = state.filterStack[layerId] || {};
596  const active = [stack.base, stack.user, stack.legend, stack.slider].filter(
597    (f) => f != null,
598  );
599  const composed =
600    active.length === 0 ? null : active.length === 1 ? active[0] : ["all", ...active];
601  if (map.getLayer && map.getLayer(layerId)) {
602    map.setFilter(layerId, composed);
603  }
604  state.filters[layerId] = composed;
605}
606
607window._mapglEnsureLayerState = _mapglEnsureLayerState;
608window._mapglComposeFilter = _mapglComposeAndApplyFilter;
609
610// Measurement functionality
611function createMeasurementBox(map) {
612  const box = document.createElement("div");
613  box.id = `measurement-box-${map._container.id}`;
614  box.className = "mapgl-measurement-box";
615  box.style.cssText = `
616    position: absolute;
617    bottom: 45px;
618    left: 10px;
619    background: white;
620    padding: 10px 15px;
621    border-radius: 4px;
622    box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
623    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
624    font-size: 12px;
625    line-height: 1.4;
626    z-index: 1;
627    display: none;
628    min-width: 120px;
629    max-width: 200px;
630    border: 1px solid rgba(0,0,0,0.1);
631  `;
632
633  box.innerHTML = `
634    <div style="font-weight: 600; margin-bottom: 5px; color: #333; font-size: 11px; text-transform: uppercase;">
635      Measurement
636    </div>
637    <div class="measurement-content">
638      <div id="measurement-primary" style="font-size: 14px; font-weight: 500; color: #000; margin-bottom: 2px;"></div>
639      <div id="measurement-secondary" style="font-size: 11px; color: #666;"></div>
640    </div>
641  `;
642
643  map.getContainer().appendChild(box);
644  return box;
645}
646
647function calculateDrawingMeasurements(mode, state, coords) {
648  try {
649    if (mode === "draw_line_string" && coords && coords.length >= 2) {
650      const line = turf.lineString(coords);
651      const distance = turf.length(line, { units: "kilometers" });
652      return { type: "distance", value: distance };
653    } else if (
654      (mode === "draw_polygon" || mode === "draw_freehand") &&
655      coords &&
656      coords.length >= 3
657    ) {
658      // Ensure polygon is closed by adding first point at end if needed
659      const closedCoords = [...coords];
660      if (
661        closedCoords[0][0] !== closedCoords[closedCoords.length - 1][0] ||
662        closedCoords[0][1] !== closedCoords[closedCoords.length - 1][1]
663      ) {
664        closedCoords.push(closedCoords[0]);
665      }
666
667      try {
668        const polygon = turf.polygon([closedCoords]);
669        const area = turf.area(polygon) / 1000000; // Convert to km²
670        const perimeter = turf.length(turf.polygonToLine(polygon), {
671          units: "kilometers",
672        });
673        return { type: "area", value: area, perimeter: perimeter };
674      } catch (error) {
675        return null;
676      }
677    } else if (mode === "draw_rectangle" && coords && coords.length >= 4) {
678      const polygon = turf.polygon([coords]);
679      const area = turf.area(polygon) / 1000000; // Convert to km²
680      const perimeter = turf.length(turf.polygonToLine(polygon), {
681        units: "kilometers",
682      });
683      return { type: "area", value: area, perimeter: perimeter };
684    } else if (mode === "draw_radius" && coords && coords.length >= 2) {
685      const center = turf.point(coords[0]);
686      const edge = turf.point(coords[1]);
687      const radius = turf.distance(center, edge, { units: "kilometers" });
688      const area = Math.PI * radius * radius; // πr²
689      return { type: "radius", value: radius, area: area };
690    }
691  } catch (e) {
692    return null;
693  }
694
695  return null;
696}
697
698function formatMeasurements(measurements, units) {
699  if (!measurements) return { primary: "", secondary: "" };
700
701  const formatDistance = function (km) {
702    let result = [];
703
704    if (units === "metric" || units === "both") {
705      if (km < 1) {
706        result.push(`${(km * 1000).toFixed(0)} m`);
707      } else {
708        result.push(`${km.toFixed(2)} km`);
709      }
710    }
711
712    if (units === "imperial" || units === "both") {
713      const miles = km * 0.621371;
714      if (miles < 0.1) {
715        result.push(`${(miles * 5280).toFixed(0)} ft`);
716      } else {
717        result.push(`${miles.toFixed(2)} mi`);
718      }
719    }
720
721    return result;
722  };
723
724  const formatArea = function (sqKm) {
725    let result = [];
726
727    if (units === "metric" || units === "both") {
728      if (sqKm < 0.01) {
729        result.push(`${(sqKm * 1000000).toFixed(0)} m²`);
730      } else if (sqKm < 1) {
731        result.push(`${(sqKm * 100).toFixed(2)} ha`);
732      } else {
733        result.push(`${sqKm.toFixed(2)} km²`);
734      }
735    }
736
737    if (units === "imperial" || units === "both") {
738      const sqMiles = sqKm * 0.386102;
739      if (sqMiles < 0.001) {
740        result.push(`${(sqMiles * 640).toFixed(2)} acres`);
741      } else {
742        result.push(`${sqMiles.toFixed(3)} mi²`);
743      }
744    }
745
746    return result;
747  };
748
749  if (measurements.type === "distance") {
750    const formatted = formatDistance(measurements.value);
751    return {
752      primary: formatted[0] || "",
753      secondary: formatted[1] || "",
754    };
755  } else if (measurements.type === "area") {
756    const areaFormatted = formatArea(measurements.value);
757    const perimeterFormatted = formatDistance(measurements.perimeter);
758
759    if (units === "both") {
760      return {
761        primary: areaFormatted[0] || "",
762        secondary: `${areaFormatted[1] || ""} • ${perimeterFormatted[0] || ""}`,
763      };
764    } else {
765      return {
766        primary: areaFormatted[0] || "",
767        secondary: `Perimeter: ${perimeterFormatted[0] || ""}`,
768      };
769    }
770  } else if (measurements.type === "radius") {
771    const distFormatted = formatDistance(measurements.value);
772    const areaFormatted = formatArea(measurements.area);
773
774    return {
775      primary: `Radius: ${distFormatted[0] || ""}`,
776      secondary:
777        units === "both"
778          ? `${distFormatted[1] || ""} • ${areaFormatted[0] || ""}`
779          : `Area: ${areaFormatted[0] || ""}`,
780    };
781  }
782
783  return { primary: "", secondary: "" };
784}
785
786function updateMeasurementDisplay(box, measurements, units) {
787  const primary = box.querySelector("#measurement-primary");
788  const secondary = box.querySelector("#measurement-secondary");
789
790  const formatted = formatMeasurements(measurements, units);
791
792  if (formatted.primary) {
793    primary.textContent = formatted.primary;
794    secondary.textContent = formatted.secondary;
795    box.style.display = "block";
796  } else {
797    box.style.display = "none";
798  }
799}
800
801function initializeMeasurements(map, draw, units) {
802  const measurementBox = createMeasurementBox(map);
803  const DRAWING_MODES = [
804    "draw_line_string",
805    "draw_polygon",
806    "draw_rectangle",
807    "draw_radius",
808    "draw_freehand",
809  ];
810
811  // Store original handlers
812  const originalHandlers = {};
813
814  DRAWING_MODES.forEach((mode) => {
815    const modeObj = MapboxDraw.modes[mode];
816    if (!modeObj) return;
817
818    // Wrap onClick for polygon mode (better for click-based drawing)
819    if (modeObj.onClick && mode === "draw_polygon") {
820      originalHandlers[mode + "_onClick"] = modeObj.onClick;
821      modeObj.onClick = function (state, e) {
822        const result = originalHandlers[mode + "_onClick"].call(this, state, e);
823
824        // For polygon mode, show measurements after each click
825        if (
826          state.polygon &&
827          state.polygon.coordinates &&
828          state.polygon.coordinates[0].length >= 3
829        ) {
830          const coords = state.polygon.coordinates[0];
831          const measurements = calculateDrawingMeasurements(
832            mode,
833            state,
834            coords,
835          );
836          updateMeasurementDisplay(measurementBox, measurements, units);
837        }
838
839        return result;
840      };
841    }
842
843    // Wrap onMouseMove for real-time updates (lines, rectangles, radius)
844    if (modeObj.onMouseMove && mode !== "draw_polygon") {
845      originalHandlers[mode + "_onMouseMove"] = modeObj.onMouseMove;
846      modeObj.onMouseMove = function (state, e) {
847        originalHandlers[mode + "_onMouseMove"].call(this, state, e);
848
849        let coords = null;
850        if (state.line && state.line.coordinates) {
851          coords = [...state.line.coordinates, [e.lngLat.lng, e.lngLat.lat]];
852        } else if (state.rectangle && state.rectangle.coordinates) {
853          coords = state.rectangle.coordinates[0];
854        }
855
856        if (coords) {
857          const measurements = calculateDrawingMeasurements(
858            mode,
859            state,
860            coords,
861          );
862          updateMeasurementDisplay(measurementBox, measurements, units);
863        }
864      };
865    }
866
867    // Wrap onDrag for freehand mode
868    if (modeObj.onDrag) {
869      originalHandlers[mode + "_onDrag"] = modeObj.onDrag;
870      modeObj.onDrag = function (state, e) {
871        const result = originalHandlers[mode + "_onDrag"].call(this, state, e);
872
873        if (state.polygon && state.polygon.coordinates) {
874          const coords = state.polygon.coordinates[0];
875          if (coords.length >= 3) {
876            const measurements = calculateDrawingMeasurements(
877              mode,
878              state,
879              coords,
880            );
881            updateMeasurementDisplay(measurementBox, measurements, units);
882          }
883        }
884
885        return result;
886      };
887    }
888  });
889
890  // Hide measurement box when drawing stops and handle button states
891  map.on("draw.modechange", (e) => {
892    if (e.mode === "simple_select") {
893      measurementBox.style.display = "none";
894      // Reset button states when switching to select mode
895      const drawControlGroup = map
896        .getContainer()
897        .querySelector(".maplibregl-ctrl-group");
898      if (drawControlGroup) {
899        drawControlGroup
900          .querySelectorAll("button")
901          .forEach((btn) => btn.classList.remove("active"));
902      }
903    }
904  });
905
906  map.on("draw.create", () => {
907    measurementBox.style.display = "none";
908    // Reset button states when drawing is completed
909    const drawControlGroup = map
910      .getContainer()
911      .querySelector(".maplibregl-ctrl-group");
912    if (drawControlGroup) {
913      drawControlGroup
914        .querySelectorAll("button")
915        .forEach((btn) => btn.classList.remove("active"));
916    }
917  });
918
919  map.on("draw.delete", () => {
920    measurementBox.style.display = "none";
921  });
922
923  // Special handling for freehand mode using data update events
924  map.on("draw.update", (e) => {
925    const currentMode = draw.getMode();
926
927    if (currentMode === "draw_freehand" && e.features && e.features[0]) {
928      const feature = e.features[0];
929      if (
930        feature.geometry.type === "Polygon" &&
931        feature.geometry.coordinates[0].length >= 3
932      ) {
933        const coords = feature.geometry.coordinates[0];
934        const measurements = calculateDrawingMeasurements(
935          "draw_freehand",
936          {},
937          coords,
938        );
939        updateMeasurementDisplay(measurementBox, measurements, units);
940      }
941    }
942
943    // Also handle editing mode - when features are being edited
944    if (currentMode === "direct_select" && e.features && e.features[0]) {
945      const feature = e.features[0];
946      if (
947        feature.geometry.type === "Polygon" &&
948        feature.geometry.coordinates[0].length >= 3
949      ) {
950        const coords = feature.geometry.coordinates[0];
951        const measurements = calculateDrawingMeasurements(
952          "draw_polygon",
953          {},
954          coords,
955        );
956        updateMeasurementDisplay(measurementBox, measurements, units);
957      } else if (
958        feature.geometry.type === "LineString" &&
959        feature.geometry.coordinates.length >= 2
960      ) {
961        const coords = feature.geometry.coordinates;
962        const measurements = calculateDrawingMeasurements(
963          "draw_line_string",
964          {},
965          coords,
966        );
967        updateMeasurementDisplay(measurementBox, measurements, units);
968      }
969    }
970  });
971
972  // Show measurements when selecting features for editing
973  map.on("draw.selectionchange", (e) => {
974    if (e.features && e.features.length > 0) {
975      const feature = e.features[0];
976      if (
977        feature.geometry.type === "Polygon" &&
978        feature.geometry.coordinates[0].length >= 3
979      ) {
980        const coords = feature.geometry.coordinates[0];
981        const measurements = calculateDrawingMeasurements(
982          "draw_polygon",
983          {},
984          coords,
985        );
986        updateMeasurementDisplay(measurementBox, measurements, units);
987      } else if (
988        feature.geometry.type === "LineString" &&
989        feature.geometry.coordinates.length >= 2
990      ) {
991        const coords = feature.geometry.coordinates;
992        const measurements = calculateDrawingMeasurements(
993          "draw_line_string",
994          {},
995          coords,
996        );
997        updateMeasurementDisplay(measurementBox, measurements, units);
998      }
999    } else {
1000      // No features selected, hide measurement box
1001      measurementBox.style.display = "none";
1002    }
1003  });
1004}
1005
1006function evaluateExpression(expression, properties) {
1007  if (!Array.isArray(expression)) {
1008    return expression;
1009  }
1010
1011  const operator = expression[0];
1012
1013  switch (operator) {
1014    case "get":
1015      return properties[expression[1]];
1016    case "concat":
1017      return expression
1018        .slice(1)
1019        .map((item) => evaluateExpression(item, properties))
1020        .join("");
1021    case "to-string":
1022      return String(evaluateExpression(expression[1], properties));
1023    case "to-number":
1024      return Number(evaluateExpression(expression[1], properties));
1025    case "number-format":
1026      const value = evaluateExpression(expression[1], properties);
1027      const options = expression[2] || {};
1028
1029      // Handle locale option
1030      const locale = options.locale || "en-US";
1031
1032      // Build Intl.NumberFormat options
1033      const formatOptions = {};
1034
1035      // Style options
1036      if (options.style) formatOptions.style = options.style; // 'decimal', 'currency', 'percent', 'unit'
1037      if (options.currency) formatOptions.currency = options.currency;
1038      if (options.unit) formatOptions.unit = options.unit;
1039
1040      // Digit options
1041      if (options.hasOwnProperty("min-fraction-digits")) {
1042        formatOptions.minimumFractionDigits = options["min-fraction-digits"];
1043      }
1044      if (options.hasOwnProperty("max-fraction-digits")) {
1045        formatOptions.maximumFractionDigits = options["max-fraction-digits"];
1046      }
1047      if (options.hasOwnProperty("min-integer-digits")) {
1048        formatOptions.minimumIntegerDigits = options["min-integer-digits"];
1049      }
1050
1051      // Notation options
1052      if (options.notation) formatOptions.notation = options.notation; // 'standard', 'scientific', 'engineering', 'compact'
1053      if (options.compactDisplay)
1054        formatOptions.compactDisplay = options.compactDisplay; // 'short', 'long'
1055
1056      // Grouping
1057      if (options.hasOwnProperty("useGrouping")) {
1058        formatOptions.useGrouping = options.useGrouping;
1059      }
1060
1061      return new Intl.NumberFormat(locale, formatOptions).format(value);
1062    default:
1063      // For literals and other simple values
1064      return expression;
1065  }
1066}
1067
1068// Escape a value for safe insertion as tooltip/popup text. Used by the brace
1069// template renderer so substituted values cannot inject markup.
1070function escapeTemplateValue(value) {
1071  return String(value)
1072    .replace(/&/g, "&amp;")
1073    .replace(/</g, "&lt;")
1074    .replace(/>/g, "&gt;")
1075    .replace(/"/g, "&quot;")
1076    .replace(/'/g, "&#39;");
1077}
1078
1079// Render a {brace} template against a properties object. Each {key} (dotted
1080// paths like {origin.id} supported) is replaced by the HTML-escaped value;
1081// literal text/markup in the template is emitted as-is. Shared, package-wide
1082// tooltip/popup syntax. NOTE: duplicated verbatim in mapboxgl.js,
1083// mapboxgl_compare.js, maplibregl_compare.js, and flowmap.js — keep in sync.
1084function renderTemplate(template, properties) {
1085  if (typeof template !== "string") {
1086    return template;
1087  }
1088  return template.replace(/\{([^}]+)\}/g, function (match, path) {
1089    const value = path
1090      .trim()
1091      .split(".")
1092      .reduce(function (acc, key) {
1093        return acc == null ? undefined : acc[key];
1094      }, properties);
1095    return value == null ? "" : escapeTemplateValue(value);
1096  });
1097}
1098
1099// Resolve a tooltip/popup content spec against a feature's properties:
1100//   array  -> Mapbox-style expression (concat/number_format/get) via evaluateExpression
1101//   "{..}" -> brace template via renderTemplate
1102//   string -> a column name (direct property lookup)
1103function resolveTooltipContent(spec, properties) {
1104  if (Array.isArray(spec)) {
1105    return evaluateExpression(spec, properties);
1106  }
1107  if (typeof spec === "string" && spec.indexOf("{") !== -1) {
1108    return renderTemplate(spec, properties);
1109  }
1110  return properties[spec];
1111}
1112
1113// Convert "#rgb"/"#rrggbb" + alpha to rgba(); pass other color strings through.
1114function tooltipHexToRgba(color, alpha) {
1115  if (typeof color !== "string" || color.charAt(0) !== "#") {
1116    return color;
1117  }
1118  let h = color.slice(1);
1119  if (h.length === 3) {
1120    h = h
1121      .split("")
1122      .map(function (c) {
1123        return c + c;
1124      })
1125      .join("");
1126  }
1127  const r = parseInt(h.slice(0, 2), 16);
1128  const g = parseInt(h.slice(2, 4), 16);
1129  const b = parseInt(h.slice(4, 6), 16);
1130  return "rgba(" + r + ", " + g + ", " + b + ", " + alpha + ")";
1131}
1132
1133// Build (and cache) a scoped CSS class from a tooltip/popup style spec. Returns
1134// null for an empty/absent spec (native appearance). One <style> per unique
1135// spec, shared via a window-level cache. NOTE: duplicated across the bindings
1136// and flowmap.js — keep in sync.
1137function tooltipStyleToClass(spec) {
1138  if (!spec || typeof spec !== "object") {
1139    return null;
1140  }
1141  if (!window._mapglTooltipStyleClasses) {
1142    window._mapglTooltipStyleClasses = {};
1143  }
1144  const cache = window._mapglTooltipStyleClasses;
1145  const key = JSON.stringify(spec);
1146  if (cache[key]) {
1147    return cache[key];
1148  }
1149  const cls = "mapgl-tooltip-style-" + (Object.keys(cache).length + 1);
1150
1151  let bg = spec.background_color;
1152  if (bg != null && spec.background_opacity != null) {
1153    bg = tooltipHexToRgba(bg, spec.background_opacity);
1154  }
1155
1156  const content = [];
1157  if (bg != null) content.push("background:" + bg + ";");
1158  if (spec.text_color != null) content.push("color:" + spec.text_color + ";");
1159  if (spec.border_color != null || spec.border_width != null) {
1160    const bw = spec.border_width == null ? 1 : spec.border_width;
1161    const bc = spec.border_color == null ? "transparent" : spec.border_color;
1162    content.push("border:" + bw + "px solid " + bc + ";");
1163  }
1164  if (spec.border_radius != null) {
1165    content.push("border-radius:" + spec.border_radius + "px;");
1166  }
1167  if (spec.font_family != null) {
1168    content.push("font-family:" + spec.font_family + ";");
1169  }
1170  if (spec.font_size != null) {
1171    content.push("font-size:" + spec.font_size + "px;");
1172  }
1173  if (spec.font_weight != null) {
1174    content.push("font-weight:" + spec.font_weight + ";");
1175  }
1176  if (spec.padding != null) content.push("padding:" + spec.padding + "px;");
1177  if (spec.max_width != null) {
1178    const mw =
1179      typeof spec.max_width === "number"
1180        ? spec.max_width + "px"
1181        : spec.max_width;
1182    content.push("max-width:" + mw + ";");
1183  }
1184  if (spec.shadow) {
1185    const ss = spec.shadow_size == null ? 8 : spec.shadow_size;
1186    const sc = spec.shadow_color == null ? "rgba(0, 0, 0, 0.2)" : spec.shadow_color;
1187    content.push("box-shadow:0 2px " + ss + "px " + sc + ";");
1188  }
1189
1190  let css =
1191    "." +
1192    cls +
1193    " .mapboxgl-popup-content, ." +
1194    cls +
1195    " .maplibregl-popup-content {" +
1196    content.join("") +
1197    "}";
1198  if (bg != null) {
1199    css +=
1200      "." +
1201      cls +
1202      " .mapboxgl-popup-tip, ." +
1203      cls +
1204      " .maplibregl-popup-tip {border-top-color:" +
1205      bg +
1206      ";border-bottom-color:" +
1207      bg +
1208      ";}";
1209  }
1210
1211  const styleEl = document.createElement("style");
1212  styleEl.textContent = css;
1213  document.head.appendChild(styleEl);
1214  cache[key] = cls;
1215  return cls;
1216}
1217
1218// Apply a style spec's generated class to a popup. A tooltip's Popup is built
1219// lazily on addTo(), so its container does not exist at creation time and
1220// neither addClassName() nor (in Mapbox GL v3) a later options.className write
1221// reliably lands. Instead we (re)apply the class on every "open" event, when
1222// the container is guaranteed to exist — works for both Mapbox and MapLibre —
1223// and swap out any previously applied generated class so nothing accumulates.
1224function applyPopupClass(popup, spec) {
1225  if (!popup) {
1226    return;
1227  }
1228  const cls = tooltipStyleToClass(spec);
1229  const prev = popup._mapglStyleClass;
1230  if (popup._container && prev && prev !== cls) {
1231    popup._container.classList.remove(prev);
1232  }
1233  popup._mapglStyleClass = cls || null;
1234  if (popup._container && cls) {
1235    popup._container.classList.add(cls);
1236  }
1237  if (!popup._mapglClassHook && typeof popup.on === "function") {
1238    popup._mapglClassHook = true;
1239    popup.on("open", function () {
1240      if (popup._container && popup._mapglStyleClass) {
1241        popup._container.classList.add(popup._mapglStyleClass);
1242      }
1243    });
1244  }
1245}
1246
1247function onMouseMoveTooltip(e, map, tooltipPopup, tooltipProperty, layerId) {
1248  if (e.features.length > 0) {
1249    // Query all features at this point to determine z-order
1250    // Features are returned in top-to-bottom rendering order
1251    const allFeatures = map.queryRenderedFeatures(e.point);
1252
1253    // Find the topmost layer that has a tooltip
1254    let topmostLayerWithTooltip = null;
1255    for (let i = 0; i < allFeatures.length; i++) {
1256      const feature = allFeatures[i];
1257      const featureLayerId = feature.layer.id;
1258
1259      // Check if this layer has a tooltip handler registered
1260      if (window._mapboxHandlers && window._mapboxHandlers[featureLayerId]) {
1261        topmostLayerWithTooltip = featureLayerId;
1262        break;
1263      }
1264    }
1265
1266    // Only show tooltip if this is the topmost layer with a tooltip
1267    if (topmostLayerWithTooltip === layerId) {
1268      map.getCanvas().style.cursor = "pointer";
1269
1270      // Clear any existing active tooltip first to prevent stacking
1271      if (window._activeTooltip && window._activeTooltip !== tooltipPopup) {
1272        window._activeTooltip.remove();
1273      }
1274
1275      // tooltipProperty may be a column name, a {brace} template, or a
1276      // concat()/number_format() expression — resolveTooltipContent handles all.
1277      const description = resolveTooltipContent(
1278        tooltipProperty,
1279        e.features[0].properties,
1280      );
1281
1282      tooltipPopup.setLngLat(e.lngLat).setHTML(description).addTo(map);
1283
1284      // Store reference to currently active tooltip
1285      window._activeTooltip = tooltipPopup;
1286    } else {
1287      // This layer is not topmost, hide tooltip if it was showing
1288      tooltipPopup.remove();
1289      if (window._activeTooltip === tooltipPopup) {
1290        delete window._activeTooltip;
1291      }
1292    }
1293  } else {
1294    tooltipPopup.remove();
1295    // If this was the active tooltip, clear the reference
1296    if (window._activeTooltip === tooltipPopup) {
1297      delete window._activeTooltip;
1298    }
1299  }
1300}
1301
1302function onMouseLeaveTooltip(map, tooltipPopup) {
1303  map.getCanvas().style.cursor = "";
1304  tooltipPopup.remove();
1305  if (window._activeTooltip === tooltipPopup) {
1306    delete window._activeTooltip;
1307  }
1308}
1309
1310function onClickPopup(e, map, popupProperty, layerId, popupStyle) {
1311  if (e.features.length > 0) {
1312    // Query all features at this point to determine z-order
1313    const allFeatures = map.queryRenderedFeatures(e.point);
1314
1315    // Find the topmost layer that has a popup
1316    let topmostLayerWithPopup = null;
1317    for (let i = 0; i < allFeatures.length; i++) {
1318      const feature = allFeatures[i];
1319      const featureLayerId = feature.layer.id;
1320
1321      // Check if this layer has a popup handler registered
1322      if (
1323        window._mapboxClickHandlers &&
1324        window._mapboxClickHandlers[featureLayerId]
1325      ) {
1326        topmostLayerWithPopup = featureLayerId;
1327        break;
1328      }
1329    }
1330
1331    // Only show popup if this is the topmost layer with a popup
1332    if (topmostLayerWithPopup === layerId) {
1333      // popupProperty may be a column name, a {brace} template, or a
1334      // concat()/number_format() expression.
1335      const description = resolveTooltipContent(
1336        popupProperty,
1337        e.features[0].properties,
1338      );
1339
1340      // Remove any existing popup for this layer
1341      if (window._mapboxPopups && window._mapboxPopups[layerId]) {
1342        window._mapboxPopups[layerId].remove();
1343      }
1344
1345      // Create and show the popup
1346      const popup = new maplibregl.Popup({ maxWidth: "400px" })
1347        .setLngLat(e.lngLat)
1348        .setHTML(description)
1349        .addTo(map);
1350      applyPopupClass(popup, popupStyle);
1351
1352      // Store reference to this popup
1353      if (!window._mapboxPopups) {
1354        window._mapboxPopups = {};
1355      }
1356      window._mapboxPopups[layerId] = popup;
1357
1358      // Remove reference when popup is closed
1359      popup.on("close", function () {
1360        if (window._mapboxPopups[layerId] === popup) {
1361          delete window._mapboxPopups[layerId];
1362        }
1363      });
1364    }
1365  }
1366}
1367
1368// Helper function to generate draw styles based on parameters
1369function generateDrawStyles(styling) {
1370  if (!styling) return null;
1371
1372  return [
1373    // Point styles
1374    {
1375      id: "gl-draw-point-active",
1376      type: "circle",
1377      filter: [
1378        "all",
1379        ["==", "$type", "Point"],
1380        ["==", "meta", "feature"],
1381        ["==", "active", "true"],
1382      ],
1383      paint: {
1384        "circle-radius": styling.vertex_radius + 2,
1385        "circle-color": styling.active_color,
1386      },
1387    },
1388    {
1389      id: "gl-draw-point",
1390      type: "circle",
1391      filter: [
1392        "all",
1393        ["==", "$type", "Point"],
1394        ["==", "meta", "feature"],
1395        ["==", "active", "false"],
1396      ],
1397      paint: {
1398        "circle-radius": styling.vertex_radius,
1399        "circle-color": styling.point_color,
1400      },
1401    },
1402    // Line styles
1403    {
1404      id: "gl-draw-line",
1405      type: "line",
1406      filter: ["all", ["==", "$type", "LineString"]],
1407      layout: {
1408        "line-cap": "round",
1409        "line-join": "round",
1410      },
1411      paint: {
1412        "line-color": [
1413          "case",
1414          ["==", ["get", "active"], "true"],
1415          styling.active_color,
1416          styling.line_color,
1417        ],
1418        "line-width": styling.line_width,
1419      },
1420    },
1421    // Polygon fill
1422    {
1423      id: "gl-draw-polygon-fill",
1424      type: "fill",
1425      filter: ["all", ["==", "$type", "Polygon"]],
1426      paint: {
1427        "fill-color": [
1428          "case",
1429          ["==", ["get", "active"], "true"],
1430          styling.active_color,
1431          styling.fill_color,
1432        ],
1433        "fill-outline-color": [
1434          "case",
1435          ["==", ["get", "active"], "true"],
1436          styling.active_color,
1437          styling.fill_color,
1438        ],
1439        "fill-opacity": styling.fill_opacity,
1440      },
1441    },
1442    // Polygon outline
1443    {
1444      id: "gl-draw-polygon-stroke",
1445      type: "line",
1446      filter: ["all", ["==", "$type", "Polygon"]],
1447      layout: {
1448        "line-cap": "round",
1449        "line-join": "round",
1450      },
1451      paint: {
1452        "line-color": [
1453          "case",
1454          ["==", ["get", "active"], "true"],
1455          styling.active_color,
1456          styling.line_color,
1457        ],
1458        "line-width": styling.line_width,
1459      },
1460    },
1461    // Midpoints
1462    {
1463      id: "gl-draw-polygon-midpoint",
1464      type: "circle",
1465      filter: ["all", ["==", "$type", "Point"], ["==", "meta", "midpoint"]],
1466      paint: {
1467        "circle-radius": 3,
1468        "circle-color": styling.active_color,
1469      },
1470    },
1471    // Vertex point halos
1472    {
1473      id: "gl-draw-vertex-halo-active",
1474      type: "circle",
1475      filter: ["all", ["==", "meta", "vertex"], ["==", "$type", "Point"]],
1476      paint: {
1477        "circle-radius": [
1478          "case",
1479          ["==", ["get", "active"], "true"],
1480          styling.vertex_radius + 4,
1481          styling.vertex_radius + 2,
1482        ],
1483        "circle-color": "#FFF",
1484      },
1485    },
1486    // Vertex points
1487    {
1488      id: "gl-draw-vertex-active",
1489      type: "circle",
1490      filter: ["all", ["==", "meta", "vertex"], ["==", "$type", "Point"]],
1491      paint: {
1492        "circle-radius": [
1493          "case",
1494          ["==", ["get", "active"], "true"],
1495          styling.vertex_radius + 2,
1496          styling.vertex_radius,
1497        ],
1498        "circle-color": styling.active_color,
1499      },
1500    },
1501  ];
1502}
1503
1504function addBezierDrawStyles(styles, styling) {
1505  const activeColor = styling && styling.active_color ? styling.active_color : "#fbb03b";
1506  const baseStyles = styles || (MapboxDraw.lib && MapboxDraw.lib.theme) || [];
1507  const bezierStyles = [
1508    {
1509      id: "gl-draw-bezier-handle-line-active",
1510      type: "line",
1511      filter: [
1512        "all",
1513        ["==", "$type", "LineString"],
1514        ["==", "meta2", "handle-line"],
1515      ],
1516      layout: {
1517        "line-cap": "round",
1518        "line-join": "round",
1519      },
1520      paint: {
1521        "line-color": activeColor,
1522        "line-width": 1,
1523      },
1524    },
1525    {
1526      id: "gl-draw-bezier-handle-stroke-inactive",
1527      type: "circle",
1528      filter: [
1529        "all",
1530        ["==", "meta", "vertex"],
1531        ["==", "meta2", "handle"],
1532        ["==", "$type", "Point"],
1533        ["!=", "mode", "static"],
1534      ],
1535      paint: {
1536        "circle-radius": 5,
1537        "circle-color": "#fff",
1538      },
1539    },
1540    {
1541      id: "gl-draw-bezier-handle-inactive",
1542      type: "circle",
1543      filter: [
1544        "all",
1545        ["==", "meta", "vertex"],
1546        ["==", "meta2", "handle"],
1547        ["==", "$type", "Point"],
1548        ["!=", "mode", "static"],
1549      ],
1550      paint: {
1551        "circle-radius": 4,
1552        "circle-color": activeColor,
1553      },
1554    },
1555  ];
1556
1557  return baseStyles.concat(bezierStyles);
1558}
1559
1560function sanitizeBezierDisplayFeature(feature) {
1561  if (!feature || !feature.properties) return feature;
1562
1563  const sanitized = {
1564    type: feature.type,
1565    properties: {},
1566    geometry: feature.geometry,
1567  };
1568
1569  if (Object.prototype.hasOwnProperty.call(feature, "id")) {
1570    sanitized.id = feature.id;
1571  }
1572
1573  Object.keys(feature.properties).forEach((key) => {
1574    if (
1575      key === "bezierGroup" ||
1576      key === "user_bezierGroup" ||
1577      key === "bezierCurve"
1578    ) {
1579      return;
1580    }
1581    sanitized.properties[key] = feature.properties[key];
1582  });
1583
1584  return sanitized;
1585}
1586
1587function wrapBezierDisplayModeForMapLibre(mode) {
1588  if (!mode || typeof mode.toDisplayFeatures !== "function") return mode;
1589
1590  const wrappedMode = Object.assign({}, mode);
1591  const originalToDisplayFeatures = mode.toDisplayFeatures;
1592  wrappedMode.toDisplayFeatures = function (state, geojson, display) {
1593    return originalToDisplayFeatures.call(
1594      this,
1595      state,
1596      geojson,
1597      function (feature) {
1598        display(sanitizeBezierDisplayFeature(feature));
1599      },
1600    );
1601  };
1602
1603  return wrappedMode;
1604}
1605
1606function wrapBezierToolbarMode(mode) {
1607  if (!mode || typeof mode.onSetup !== "function") return mode;
1608
1609  const wrappedMode = Object.assign({}, mode);
1610  const originalOnSetup = mode.onSetup;
1611  wrappedMode.onSetup = function (options) {
1612    const activateUIButton = this.activateUIButton;
1613    this.activateUIButton = function () {};
1614
1615    try {
1616      return originalOnSetup.call(this, options);
1617    } finally {
1618      this.activateUIButton = activateUIButton;
1619    }
1620  };
1621
1622  return wrappedMode;
1623}
1624
1625function enableBezierDrawMode(drawOptions, styling) {
1626  if (!window.MapglBezierDraw) {
1627    console.warn("Bezier draw mode dependency is not available.");
1628    return drawOptions;
1629  }
1630
1631  drawOptions.modes = Object.assign({}, drawOptions.modes || MapboxDraw.modes, {
1632    simple_select: wrapBezierDisplayModeForMapLibre(
1633      window.MapglBezierDraw.SimpleSelectModeBezierOverride,
1634    ),
1635    direct_select: wrapBezierDisplayModeForMapLibre(
1636      window.MapglBezierDraw.DirectModeBezierOverride,
1637    ),
1638    draw_bezier_curve: wrapBezierDisplayModeForMapLibre(
1639      wrapBezierToolbarMode(window.MapglBezierDraw.DrawBezierCurve),
1640    ),
1641  });
1642  drawOptions.userProperties = true;
1643  drawOptions.styles = addBezierDrawStyles(drawOptions.styles, styling);
1644
1645  return drawOptions;
1646}
1647
1648function addBezierButtonStyles() {
1649  if (document.querySelector("#mapgl-bezier-styles")) return;
1650
1651  const style = document.createElement("style");
1652  style.id = "mapgl-bezier-styles";
1653  style.textContent = `
1654    .mapbox-gl-draw_bezier {
1655      background: transparent;
1656      background-image: url('data:image/svg+xml;utf8,%3Csvg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"%3E%3Cpath d="M3 15 C7 3, 13 17, 17 5" fill="none" stroke="%23000000" stroke-width="2" stroke-linecap="round"/%3E%3Ccircle cx="3" cy="15" r="1.5" fill="%23000000"/%3E%3Ccircle cx="17" cy="5" r="1.5" fill="%23000000"/%3E%3C/svg%3E') !important;
1657      background-repeat: no-repeat !important;
1658      background-position: center !important;
1659    }
1660    .mapbox-gl-draw_bezier_polygon {
1661      background: transparent;
1662      background-image: url('data:image/svg+xml;utf8,%3Csvg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"%3E%3Cpath d="M4 14 C5 4, 15 4, 16 14 C12 17, 8 17, 4 14 Z" fill="none" stroke="%23000000" stroke-width="2" stroke-linejoin="round"/%3E%3C/svg%3E') !important;
1663      background-repeat: no-repeat !important;
1664      background-position: center !important;
1665    }
1666    .mapbox-gl-draw_bezier:hover,
1667    .mapbox-gl-draw_bezier_polygon:hover,
1668    .mapbox-gl-draw_bezier.active,
1669    .mapbox-gl-draw_bezier_polygon.active {
1670      background-color: rgba(0, 0, 0, 0.05);
1671    }
1672  `;
1673  document.head.appendChild(style);
1674}
1675
1676function setActiveBezierButton(drawControlGroup, className) {
1677  if (!drawControlGroup || !className) return;
1678
1679  drawControlGroup
1680    .querySelectorAll(".active")
1681    .forEach((btn) => btn.classList.remove("active"));
1682
1683  const button = drawControlGroup.querySelector("." + className);
1684  if (button) button.classList.add("active");
1685}
1686
1687function clearActiveBezierButton(drawControlGroup) {
1688  if (!drawControlGroup) return;
1689
1690  drawControlGroup._mapglActiveBezierButton = null;
1691  drawControlGroup
1692    .querySelectorAll(
1693      ".mapbox-gl-draw_bezier.active, .mapbox-gl-draw_bezier_polygon.active",
1694    )
1695    .forEach((btn) => btn.classList.remove("active"));
1696}
1697
1698function addBezierButtons(map, drawControl, options) {
1699  if (!drawControl || !(options.bezier || options.bezierPolygon)) return;
1700
1701  addBezierButtonStyles();
1702
1703  setTimeout(() => {
1704    const drawControlGroup = map
1705      .getContainer()
1706      .querySelector(options.controlGroupSelector);
1707    if (!drawControlGroup) return;
1708
1709    const trashBtn = drawControlGroup.querySelector(".mapbox-gl-draw_trash");
1710    const iconClass = options.iconClass || "";
1711
1712    if (!drawControlGroup._mapglBezierNativeClickHandler) {
1713      drawControlGroup._mapglBezierNativeClickHandler = (e) => {
1714        if (
1715          e.target.closest(
1716            ".mapbox-gl-draw_bezier, .mapbox-gl-draw_bezier_polygon",
1717          )
1718        ) {
1719          return;
1720        }
1721        clearActiveBezierButton(drawControlGroup);
1722      };
1723      drawControlGroup.addEventListener(
1724        "click",
1725        drawControlGroup._mapglBezierNativeClickHandler,
1726        true,
1727      );
1728    }
1729
1730    const addButton = (className, title, modeOptions) => {
1731      if (drawControlGroup.querySelector("." + className)) return;
1732
1733      const button = document.createElement("button");
1734      button.className = `${className} ${iconClass}`.trim();
1735      button.title = title;
1736      button.type = "button";
1737      button.addEventListener("click", () => {
1738        drawControlGroup._mapglActiveBezierButton = className;
1739        drawControl.changeMode("draw_bezier_curve", modeOptions || {});
1740        setActiveBezierButton(drawControlGroup, className);
1741        requestAnimationFrame(() => {
1742          if (drawControlGroup._mapglActiveBezierButton === className) {
1743            setActiveBezierButton(drawControlGroup, className);
1744          }
1745        });
1746      });
1747
1748      if (trashBtn) {
1749        drawControlGroup.insertBefore(button, trashBtn);
1750      } else {
1751        drawControlGroup.appendChild(button);
1752      }
1753    };
1754
1755    if (options.bezier) {
1756      addButton("mapbox-gl-draw_bezier", "Bezier curve tool", {});
1757    }
1758    if (options.bezierPolygon) {
1759      addButton("mapbox-gl-draw_bezier_polygon", "Bezier polygon tool", {
1760        isPolygon: true,
1761      });
1762    }
1763
1764    map.on("draw.modechange", (e) => {
1765      if (e && e.mode === "draw_bezier_curve") {
1766        setActiveBezierButton(
1767          drawControlGroup,
1768          drawControlGroup._mapglActiveBezierButton,
1769        );
1770      } else {
1771        clearActiveBezierButton(drawControlGroup);
1772      }
1773    });
1774  }, 100);
1775}
1776
1777// Helper function to add features from a source to draw
1778function addSourceFeaturesToDraw(draw, sourceId, map) {
1779  const mapId = map.getContainer().id;
1780
1781  // First try to get data from our registry (more reliable across MapLibre/Mapbox)
1782  const storedData = getStoredSourceData(mapId, sourceId);
1783  if (storedData) {
1784    draw.add(storedData);
1785    return;
1786  }
1787
1788  // Fallback to internal _data property (for backwards compatibility with Mapbox)
1789  const source = map.getSource(sourceId);
1790  if (source && source._data) {
1791    draw.add(source._data);
1792  } else {
1793    console.warn("Source not found or has no data:", sourceId);
1794  }
1795}
1796
1797// Global registries for tracking user-added images and pending operations during style changes
1798if (!window._mapgl) {
1799  window._mapgl = {
1800    // Registry of user-added images per map: { mapId: { imageId: { url, options } } }
1801    userImages: {},
1802    // Queue of pending operations during style loading: { mapId: [] }
1803    pendingOperations: {},
1804    // Track which maps are currently loading a style: { mapId: true/false }
1805    styleLoading: {},
1806    // Registry of GeoJSON source data per map: { mapId: { sourceId: geojsonData } }
1807    sourceData: {}
1808  };
1809}
1810
1811// Helper to store GeoJSON source data for later retrieval
1812function storeSourceData(mapId, sourceId, data) {
1813  if (!window._mapgl.sourceData[mapId]) {
1814    window._mapgl.sourceData[mapId] = {};
1815  }
1816  window._mapgl.sourceData[mapId][sourceId] = data;
1817}
1818
1819// Helper to get stored GeoJSON source data
1820function getStoredSourceData(mapId, sourceId) {
1821  if (window._mapgl.sourceData[mapId]) {
1822    return window._mapgl.sourceData[mapId][sourceId];
1823  }
1824  return null;
1825}
1826
1827// Helper to queue an operation or execute it immediately
1828function queueOrExecute(mapId, operation) {
1829  if (window._mapgl.styleLoading[mapId]) {
1830    if (!window._mapgl.pendingOperations[mapId]) {
1831      window._mapgl.pendingOperations[mapId] = [];
1832    }
1833    window._mapgl.pendingOperations[mapId].push(operation);
1834    console.log("[MapGL Debug] Queued operation during style load for map:", mapId);
1835  } else {
1836    operation();
1837  }
1838}
1839
1840// Helper to execute all pending operations for a map
1841function executePendingOperations(mapId) {
1842  const pending = window._mapgl.pendingOperations[mapId];
1843  if (pending && pending.length > 0) {
1844    console.log("[MapGL Debug] Executing", pending.length, "pending operations for map:", mapId);
1845    pending.forEach(function(op) {
1846      try {
1847        op();
1848      } catch (err) {
1849        console.error("[MapGL Debug] Error executing pending operation:", err);
1850      }
1851    });
1852    window._mapgl.pendingOperations[mapId] = [];
1853  }
1854}
1855
1856// Helper to register a user image
1857function registerUserImage(mapId, imageId, url, options) {
1858  if (!window._mapgl.userImages[mapId]) {
1859    window._mapgl.userImages[mapId] = {};
1860  }
1861  window._mapgl.userImages[mapId][imageId] = { url: url, options: options };
1862  console.log("[MapGL Debug] Registered user image:", imageId, "for map:", mapId);
1863}
1864
1865// Helper to re-add all user images after style change
1866async function reAddUserImages(map, mapId) {
1867  const images = window._mapgl.userImages[mapId];
1868  if (!images) return;
1869
1870  for (const imageId in images) {
1871    const imageInfo = images[imageId];
1872    try {
1873      const image = await map.loadImage(imageInfo.url);
1874      if (!map.hasImage(imageId)) {
1875        map.addImage(imageId, image.data, imageInfo.options);
1876        console.log("[MapGL Debug] Re-added user image after style change:", imageId);
1877      }
1878    } catch (error) {
1879      console.error("[MapGL Debug] Error re-adding image:", imageId, error);
1880    }
1881  }
1882}
1883
1884// Screenshot capture functionality
1885// Screenshot functions are now in screenshot.js (shared module)
1886
1887HTMLWidgets.widget({
1888  name: "maplibregl",
1889
1890  type: "output",
1891
1892  factory: function (el, width, height) {
1893    let map;
1894    let draw;
1895
1896    return {
1897      renderValue: function (x) {
1898        if (typeof maplibregl === "undefined") {
1899          console.error("Maplibre GL JS is not loaded.");
1900          return;
1901        }
1902
1903        // Clean up existing map if present to prevent control stacking on re-render
1904        if (map) {
1905          // Remove all controls from the old map
1906          if (map.controls && map.controls.length > 0) {
1907            map.controls.forEach(function (controlObj) {
1908              try {
1909                if (controlObj.control) {
1910                  map.removeControl(controlObj.control);
1911                }
1912              } catch (e) {
1913                // Control may already be removed
1914              }
1915            });
1916          }
1917          // Remove the old map
1918          try {
1919            map.remove();
1920          } catch (e) {
1921            // Map may already be removed
1922          }
1923          map = null;
1924        }
1925
1926        let protocol = new pmtiles.Protocol({ metadata: true });
1927        maplibregl.addProtocol("pmtiles", protocol.tile);
1928
1929        map = new maplibregl.Map({
1930          container: el.id,
1931          style: x.style,
1932          center: x.center,
1933          zoom: x.zoom,
1934          bearing: x.bearing,
1935          pitch: x.pitch,
1936          ...x.additional_params,
1937        });
1938
1939        map.controls = [];
1940        map._initialStyleLoaded = false;
1941
1942        map.on("style.load", async function () {
1943          // Store basemap layer IDs before user layers are added
1944          if (!map._basemapLayerIds) {
1945            map._basemapLayerIds = new Set(
1946              map.getStyle().layers.map(l => l.id)
1947            );
1948          }
1949
1950          if (x.projection) {
1951            map.setProjection({ type: x.projection });
1952          }
1953
1954          map.resize();
1955
1956          if (HTMLWidgets.shinyMode) {
1957            map.on("load", function () {
1958              var bounds = map.getBounds();
1959              var center = map.getCenter();
1960              var zoom = map.getZoom();
1961
1962              Shiny.setInputValue(el.id + "_zoom", zoom);
1963              Shiny.setInputValue(el.id + "_center", {
1964                lng: center.lng,
1965                lat: center.lat,
1966              });
1967              Shiny.setInputValue(el.id + "_bbox", {
1968                xmin: bounds.getWest(),
1969                ymin: bounds.getSouth(),
1970                xmax: bounds.getEast(),
1971                ymax: bounds.getNorth(),
1972              });
1973            });
1974
1975            map.on("moveend", function (e) {
1976              var map = e.target;
1977              var bounds = map.getBounds();
1978              var center = map.getCenter();
1979              var zoom = map.getZoom();
1980
1981              Shiny.onInputChange(el.id + "_zoom", zoom);
1982              Shiny.onInputChange(el.id + "_center", {
1983                lng: center.lng,
1984                lat: center.lat,
1985              });
1986              Shiny.onInputChange(el.id + "_bbox", {
1987                xmin: bounds.getWest(),
1988                ymin: bounds.getSouth(),
1989                xmax: bounds.getEast(),
1990                ymax: bounds.getNorth(),
1991              });
1992            });
1993          }
1994
1995          // Set config properties if provided
1996          if (x.config_properties) {
1997            x.config_properties.forEach(function (config) {
1998              map.setConfigProperty(
1999                config.importId,
2000                config.configName,
2001                config.value,
2002              );
2003            });
2004          }
2005
2006          if (x.markers) {
2007            if (!window.maplibreglMarkers) {
2008              window.maplibreglMarkers = [];
2009            }
2010            x.markers.forEach(function (marker) {
2011              const markerOptions = {
2012                color: marker.color,
2013                rotation: marker.rotation,
2014                draggable: marker.options.draggable || false,
2015                ...marker.options,
2016              };
2017              const mapMarker = new maplibregl.Marker(markerOptions)
2018                .setLngLat([marker.lng, marker.lat])
2019                .addTo(map);
2020
2021              if (marker.popup) {
2022                mapMarker.setPopup(
2023                  new maplibregl.Popup({
2024                    offset: 25,
2025                    maxWidth: "400px",
2026                  }).setHTML(marker.popup),
2027                );
2028              }
2029
2030              if (HTMLWidgets.shinyMode) {
2031                const markerId = marker.id;
2032                if (markerId) {
2033                  const lngLat = mapMarker.getLngLat();
2034                  Shiny.setInputValue(el.id + "_marker_" + markerId, {
2035                    id: markerId,
2036                    lng: lngLat.lng,
2037                    lat: lngLat.lat,
2038                  });
2039
2040                  mapMarker.on("dragend", function () {
2041                    const lngLat = mapMarker.getLngLat();
2042                    Shiny.setInputValue(el.id + "_marker_" + markerId, {
2043                      id: markerId,
2044                      lng: lngLat.lng,
2045                      lat: lngLat.lat,
2046                    });
2047                  });
2048                }
2049              }
2050
2051              window.maplibreglMarkers.push(mapMarker);
2052            });
2053          }
2054
2055          // Add sources if provided
2056          if (x.sources) {
2057            for (const source of x.sources) {
2058              if (source.type === "vector") {
2059                const sourceOptions = {
2060                  type: "vector",
2061                };
2062                // Add url or tiles
2063                if (source.url) {
2064                  sourceOptions.url = source.url;
2065                  // Auto-detect MLT encoding for PMTiles sources
2066                  if (
2067                    source.url.startsWith("pmtiles://") &&
2068                    typeof pmtiles !== "undefined" &&
2069                    !source.encoding
2070                  ) {
2071                    try {
2072                      const rawUrl = source.url.replace("pmtiles://", "");
2073                      const p = new pmtiles.PMTiles(rawUrl);
2074                      const header = await p.getHeader();
2075                      if (header.tileType === pmtiles.TileType.Mlt) {
2076                        sourceOptions.encoding = "mlt";
2077                      }
2078                    } catch (e) {
2079                      console.warn("Failed to detect PMTiles tile type:", e);
2080                    }
2081                  }
2082                }
2083                if (source.tiles) {
2084                  sourceOptions.tiles = source.tiles;
2085                }
2086                // Add promoteId if provided
2087                if (source.promoteId) {
2088                  sourceOptions.promoteId = source.promoteId;
2089                }
2090                // Add any other additional options
2091                for (const [key, value] of Object.entries(source)) {
2092                  if (
2093                    !["id", "type", "url", "tiles", "promoteId"].includes(key)
2094                  ) {
2095                    sourceOptions[key] = value;
2096                  }
2097                }
2098                map.addSource(source.id, sourceOptions);
2099              } else if (source.type === "geojson") {
2100                const geojsonData = source.data;
2101                const sourceOptions = {
2102                  type: "geojson",
2103                  data: geojsonData,
2104                  generateId: source.generateId,
2105                };
2106
2107                // Add additional options
2108                for (const [key, value] of Object.entries(source)) {
2109                  if (!["id", "type", "data", "generateId"].includes(key)) {
2110                    sourceOptions[key] = value;
2111                  }
2112                }
2113
2114                map.addSource(source.id, sourceOptions);
2115                // Store GeoJSON data for later retrieval (e.g., for draw control)
2116                storeSourceData(el.id, source.id, geojsonData);
2117              } else if (source.type === "raster") {
2118                if (source.url) {
2119                  map.addSource(source.id, {
2120                    type: "raster",
2121                    url: source.url,
2122                    tileSize: source.tileSize,
2123                    maxzoom: source.maxzoom,
2124                  });
2125                } else if (source.tiles) {
2126                  map.addSource(source.id, {
2127                    type: "raster",
2128                    tiles: source.tiles,
2129                    tileSize: source.tileSize,
2130                    maxzoom: source.maxzoom,
2131                  });
2132                }
2133              } else if (source.type === "raster-dem") {
2134                map.addSource(source.id, {
2135                  type: "raster-dem",
2136                  url: source.url,
2137                  tileSize: source.tileSize,
2138                  maxzoom: source.maxzoom,
2139                });
2140              } else if (source.type === "image") {
2141                map.addSource(source.id, {
2142                  type: "image",
2143                  url: source.url,
2144                  coordinates: source.coordinates,
2145                });
2146              } else if (source.type === "video") {
2147                map.addSource(source.id, {
2148                  type: "video",
2149                  urls: source.urls,
2150                  coordinates: source.coordinates,
2151                });
2152              }
2153            }
2154          }
2155
2156          function add_my_layers(layer) {
2157            try {
2158              const layerConfig = {
2159                id: layer.id,
2160                type: layer.type,
2161                source: layer.source,
2162                layout: layer.layout || {},
2163                paint: layer.paint || {},
2164              };
2165
2166              // Check if source is an object and set generateId if source type is 'geojson'
2167              if (
2168                typeof layer.source === "object" &&
2169                layer.source.type === "geojson"
2170              ) {
2171                layerConfig.source.generateId = true;
2172              } else if (typeof layer.source === "string") {
2173                // Handle string source if needed
2174                layerConfig.source = layer.source;
2175              }
2176
2177              if (layer.source_layer) {
2178                layerConfig["source-layer"] = layer.source_layer;
2179              }
2180
2181              if (layer.slot) {
2182                layerConfig["slot"] = layer.slot;
2183              }
2184
2185              if (layer.minzoom) {
2186                layerConfig["minzoom"] = layer.minzoom;
2187              }
2188
2189              if (layer.maxzoom) {
2190                layerConfig["maxzoom"] = layer.maxzoom;
2191              }
2192
2193              if (layer.filter) {
2194                layerConfig["filter"] = layer.filter;
2195              }
2196
2197              if (layer.before_id) {
2198                map.addLayer(layerConfig, layer.before_id);
2199              } else {
2200                map.addLayer(layerConfig);
2201              }
2202
2203              // Seed the filter registry with this layer's initial filter
2204              // so later filter sources compose with it rather than clobber it.
2205              (function () {
2206                const _s = _mapglEnsureLayerState(map);
2207                _s.filterStack[layer.id] = _s.filterStack[layer.id] || {};
2208                _s.filterStack[layer.id].base = layer.filter || null;
2209                _s.filters[layer.id] = layer.filter || null;
2210              })();
2211
2212              // Add popups or tooltips if provided
2213              if (layer.popup) {
2214                // Initialize popup tracking if it doesn't exist
2215                if (!window._mapboxPopups) {
2216                  window._mapboxPopups = {};
2217                }
2218
2219                // Create click handler for this layer
2220                const clickHandler = function (e) {
2221                  onClickPopup(e, map, layer.popup, layer.id, layer.popup_style);
2222                };
2223
2224                // Store these handler references so we can remove them later if needed
2225                if (!window._mapboxClickHandlers) {
2226                  window._mapboxClickHandlers = {};
2227                }
2228                window._mapboxClickHandlers[layer.id] = clickHandler;
2229
2230                // Add the click handler
2231                map.on("click", layer.id, clickHandler);
2232
2233                // Change cursor to pointer when hovering over the layer
2234                map.on("mouseenter", layer.id, function () {
2235                  map.getCanvas().style.cursor = "pointer";
2236                });
2237
2238                // Change cursor back to default when leaving the layer
2239                map.on("mouseleave", layer.id, function () {
2240                  map.getCanvas().style.cursor = "";
2241                });
2242              }
2243
2244              if (layer.tooltip) {
2245                const tooltip = new maplibregl.Popup({
2246                  closeButton: false,
2247                  closeOnClick: false,
2248                  maxWidth: "400px",
2249                });
2250                applyPopupClass(tooltip, layer.tooltip_style);
2251
2252                // Create a reference to the mousemove handler function.
2253                // We need to pass 'e', 'map', 'tooltip', 'layer.tooltip', and 'layer.id' to onMouseMoveTooltip.
2254                const mouseMoveHandler = function (e) {
2255                  onMouseMoveTooltip(e, map, tooltip, layer.tooltip, layer.id);
2256                };
2257
2258                // Create a reference to the mouseleave handler function.
2259                // We need to pass 'map' and 'tooltip' to onMouseLeaveTooltip.
2260                const mouseLeaveHandler = function () {
2261                  onMouseLeaveTooltip(map, tooltip);
2262                };
2263
2264                // Attach the named handler references, not anonymous functions.
2265                map.on("mousemove", layer.id, mouseMoveHandler);
2266                map.on("mouseleave", layer.id, mouseLeaveHandler);
2267
2268                // Store these handler references so you can remove them later if needed
2269                if (!window._mapboxHandlers) {
2270                  window._mapboxHandlers = {};
2271                }
2272                window._mapboxHandlers[layer.id] = {
2273                  mousemove: mouseMoveHandler,
2274                  mouseleave: mouseLeaveHandler,
2275                };
2276              }
2277
2278              // Add hover effect if provided
2279              if (layer.hover_options) {
2280                const jsHoverOptions = {};
2281                for (const [key, value] of Object.entries(
2282                  layer.hover_options,
2283                )) {
2284                  const jsKey = key.replace(/_/g, "-");
2285                  jsHoverOptions[jsKey] = value;
2286                }
2287
2288                let hoveredFeatureId = null;
2289
2290                map.on("mousemove", layer.id, function (e) {
2291                  if (e.features.length > 0) {
2292                    // Check if the feature has an id
2293                    const featureId = e.features[0].id;
2294
2295                    // Only proceed if the feature has an id
2296                    if (featureId !== undefined && featureId !== null) {
2297                      if (hoveredFeatureId !== null) {
2298                        const featureState = {
2299                          source:
2300                            typeof layer.source === "string"
2301                              ? layer.source
2302                              : layer.id,
2303                          id: hoveredFeatureId,
2304                        };
2305                        if (layer.source_layer) {
2306                          featureState.sourceLayer = layer.source_layer;
2307                        }
2308                        map.setFeatureState(featureState, { hover: false });
2309                      }
2310                      hoveredFeatureId = featureId;
2311                      const featureState = {
2312                        source:
2313                          typeof layer.source === "string"
2314                            ? layer.source
2315                            : layer.id,
2316                        id: hoveredFeatureId,
2317                      };
2318                      if (layer.source_layer) {
2319                        featureState.sourceLayer = layer.source_layer;
2320                      }
2321                      map.setFeatureState(featureState, { hover: true });
2322                    }
2323                  }
2324                });
2325
2326                map.on("mouseleave", layer.id, function () {
2327                  if (hoveredFeatureId !== null) {
2328                    const featureState = {
2329                      source:
2330                        typeof layer.source === "string"
2331                          ? layer.source
2332                          : layer.id,
2333                      id: hoveredFeatureId,
2334                    };
2335                    if (layer.source_layer) {
2336                      featureState.sourceLayer = layer.source_layer;
2337                    }
2338                    map.setFeatureState(featureState, { hover: false });
2339                  }
2340                  hoveredFeatureId = null;
2341                });
2342
2343                Object.keys(jsHoverOptions).forEach(function (key) {
2344                  const originalPaint =
2345                    map.getPaintProperty(layer.id, key) || layer.paint[key];
2346                  map.setPaintProperty(layer.id, key, [
2347                    "case",
2348                    ["boolean", ["feature-state", "hover"], false],
2349                    jsHoverOptions[key],
2350                    originalPaint,
2351                  ]);
2352                });
2353              }
2354            } catch (e) {
2355              console.error("Failed to add layer: ", layer, e);
2356            }
2357          }
2358          if (x.h3j_sources) {
2359            x.h3j_sources.forEach(async function (source) {
2360              await map.addH3JSource(source.id, {
2361                data: source.url,
2362              });
2363
2364              // A bit hacky?
2365              if (x.layers) {
2366                x.layers.forEach((layer) => add_my_layers(layer));
2367              }
2368            });
2369          }
2370          if (x.h3t_sources) {
2371            x.h3t_sources.forEach(function (source) {
2372              map.addH3TSource(source.id, {
2373                tiles: source.tiles,
2374                sourcelayer: source.sourcelayer,
2375                geometry_type: source.geometry_type,
2376                minzoom: source.minzoom,
2377                maxzoom: source.maxzoom,
2378                promoteId: source.promoteId,
2379                debug: source.debug,
2380              });
2381              if (x.layers) {
2382                x.layers.forEach((layer) => add_my_layers(layer));
2383              }
2384            });
2385          }
2386
2387          // Add layers if provided
2388          if (x.layers) {
2389            x.layers.forEach((layer) => add_my_layers(layer));
2390          }
2391
2392          if (window.MapGLFlowmapPlugin) {
2393            window.MapGLFlowmapPlugin.init(map, x, el, HTMLWidgets);
2394          }
2395
2396          // Apply setFilter if provided
2397          if (x.setFilter) {
2398            x.setFilter.forEach(function (filter) {
2399              map.setFilter(filter.layer, filter.filter);
2400            });
2401          }
2402
2403          // Apply moveLayer operations if provided
2404          if (x.moveLayer) {
2405            x.moveLayer.forEach(function (moveOp) {
2406              if (map.getLayer(moveOp.layer)) {
2407                if (moveOp.before) {
2408                  map.moveLayer(moveOp.layer, moveOp.before);
2409                } else {
2410                  map.moveLayer(moveOp.layer);
2411                }
2412              }
2413            });
2414          }
2415
2416          // Set terrain if provided
2417          if (x.terrain) {
2418            map.setTerrain({
2419              source: x.terrain.source,
2420              exaggeration: x.terrain.exaggeration,
2421            });
2422          }
2423
2424          // Set fog
2425          if (x.fog) {
2426            map.setFog(x.fog);
2427          }
2428
2429          if (x.fitBounds) {
2430            map.fitBounds(x.fitBounds.bounds, x.fitBounds.options);
2431          }
2432          if (x.flyTo) {
2433            map.flyTo(x.flyTo);
2434          }
2435          if (x.easeTo) {
2436            map.easeTo(x.easeTo);
2437          }
2438          if (x.setCenter) {
2439            map.setCenter(x.setCenter);
2440          }
2441          if (x.setZoom) {
2442            map.setZoom(x.setZoom);
2443          }
2444          if (x.jumpTo) {
2445            map.jumpTo(x.jumpTo);
2446          }
2447
2448          // Add controls only on initial style load to prevent duplication when set_style() is called
2449          if (!map._initialStyleLoaded) {
2450            // Add scale control if enabled
2451            if (x.scale_control) {
2452            const scaleControl = new maplibregl.ScaleControl({
2453              maxWidth: x.scale_control.maxWidth,
2454              unit: x.scale_control.unit,
2455            });
2456            map.addControl(scaleControl, x.scale_control.position);
2457            map.controls.push({ type: "scale", control: scaleControl });
2458          }
2459
2460          // Add globe control if enabled
2461          if (x.globe_control) {
2462            const globeControl = new maplibregl.GlobeControl();
2463            map.addControl(globeControl, x.globe_control.position);
2464            map.controls.push({ type: "globe", control: globeControl });
2465          }
2466
2467          // Add custom controls if any are defined
2468          if (x.custom_controls) {
2469            Object.keys(x.custom_controls).forEach(function (key) {
2470              const controlOptions = x.custom_controls[key];
2471              const customControlContainer = document.createElement("div");
2472
2473              if (controlOptions.className) {
2474                customControlContainer.className = controlOptions.className;
2475              } else {
2476                customControlContainer.className =
2477                  "maplibregl-ctrl maplibregl-ctrl-group";
2478              }
2479
2480              customControlContainer.innerHTML = controlOptions.html;
2481
2482              const customControl = {
2483                onAdd: function () {
2484                  return customControlContainer;
2485                },
2486                onRemove: function () {
2487                  if (customControlContainer.parentNode) {
2488                    customControlContainer.parentNode.removeChild(
2489                      customControlContainer,
2490                    );
2491                  }
2492                },
2493              };
2494
2495              map.addControl(
2496                customControl,
2497                controlOptions.position || "top-right",
2498              );
2499              map.controls.push({ type: key, control: customControl });
2500            });
2501          }
2502
2503          // Add globe minimap if enabled
2504          if (x.globe_minimap && x.globe_minimap.enabled) {
2505            const globeMinimapOptions = {
2506              globeSize: x.globe_minimap.globe_size,
2507              landColor: x.globe_minimap.land_color,
2508              waterColor: x.globe_minimap.water_color,
2509              markerColor: x.globe_minimap.marker_color,
2510              markerSize: x.globe_minimap.marker_size,
2511            };
2512            const globeMinimap = new GlobeMinimap(globeMinimapOptions);
2513            map.addControl(globeMinimap, x.globe_minimap.position);
2514            map.controls.push({ type: "globe_minimap", control: globeMinimap });
2515          }
2516
2517          if (x.setProjection) {
2518            x.setProjection.forEach(function (projectionConfig) {
2519              if (projectionConfig.projection) {
2520                const projection =
2521                  typeof projectionConfig.projection === "string"
2522                    ? { type: projectionConfig.projection }
2523                    : projectionConfig.projection;
2524                map.setProjection(projection);
2525              }
2526            });
2527          }
2528
2529          // Add geocoder control if enabled
2530          if (x.geocoder_control) {
2531            const provider = x.geocoder_control.provider || "osm";
2532            let geocoder;
2533
2534            if (provider === "maptiler") {
2535              // MapTiler geocoder
2536              const maptilerOptions = {
2537                apiKey: x.geocoder_control.api_key,
2538                maplibregl: maplibregl,
2539                ...x.geocoder_control,
2540              };
2541
2542              // Create MapTiler geocoder
2543              geocoder = new maplibreglMaptilerGeocoder.GeocodingControl(
2544                maptilerOptions,
2545              );
2546            } else {
2547              // OSM/Nominatim geocoder (default)
2548              const geocoderApi = {
2549                forwardGeocode: async (config) => {
2550                  const features = [];
2551                  try {
2552                    const request = `https://nominatim.openstreetmap.org/search?q=${
2553                      config.query
2554                    }&format=geojson&polygon_geojson=1&addressdetails=1`;
2555                    const response = await fetch(request);
2556                    const geojson = await response.json();
2557                    for (const feature of geojson.features) {
2558                      const center = [
2559                        feature.bbox[0] +
2560                          (feature.bbox[2] - feature.bbox[0]) / 2,
2561                        feature.bbox[1] +
2562                          (feature.bbox[3] - feature.bbox[1]) / 2,
2563                      ];
2564                      const point = {
2565                        type: "Feature",
2566                        geometry: {
2567                          type: "Point",
2568                          coordinates: center,
2569                        },
2570                        place_name: feature.properties.display_name,
2571                        properties: feature.properties,
2572                        text: feature.properties.display_name,
2573                        place_type: ["place"],
2574                        center,
2575                      };
2576                      features.push(point);
2577                    }
2578                  } catch (e) {
2579                    console.error(`Failed to forwardGeocode with error: ${e}`);
2580                  }
2581
2582                  return {
2583                    features,
2584                  };
2585                },
2586              };
2587              const geocoderOptions = {
2588                maplibregl: maplibregl,
2589                ...x.geocoder_control,
2590              };
2591
2592              // Set default values if not provided
2593              if (!geocoderOptions.placeholder)
2594                geocoderOptions.placeholder = "Search";
2595              if (typeof geocoderOptions.collapsed === "undefined")
2596                geocoderOptions.collapsed = false;
2597
2598              geocoder = new MaplibreGeocoder(geocoderApi, geocoderOptions);
2599            }
2600
2601            map.addControl(
2602              geocoder,
2603              x.geocoder_control.position || "top-right",
2604            );
2605            map.controls.push({ type: "geocoder", control: geocoder });
2606
2607            // Apply CSS fix for MapTiler geocoder to prevent cutoff
2608            if (provider === "maptiler") {
2609              setTimeout(() => {
2610                const controlContainer = document.querySelector(
2611                  ".maplibregl-ctrl-geocoder",
2612                );
2613                if (controlContainer) {
2614                  controlContainer.style.maxWidth = "300px";
2615                  controlContainer.style.width = "auto";
2616                }
2617              }, 100);
2618            }
2619
2620            // Handle geocoder results in Shiny mode
2621            if (HTMLWidgets.shinyMode) {
2622              if (provider === "maptiler") {
2623                // MapTiler uses different event names
2624                geocoder.on("pick", function (e) {
2625                  Shiny.setInputValue(el.id + "_geocoder", {
2626                    result: e,
2627                    time: new Date(),
2628                  });
2629                });
2630              } else {
2631                // OSM geocoder
2632                geocoder.on("results", function (e) {
2633                  Shiny.setInputValue(el.id + "_geocoder", {
2634                    result: e,
2635                    time: new Date(),
2636                  });
2637                });
2638              }
2639            }
2640          }
2641
2642          if (x.draw_control && x.draw_control.enabled) {
2643            MapboxDraw.constants.classes.CONTROL_BASE = "maplibregl-ctrl";
2644            MapboxDraw.constants.classes.CONTROL_PREFIX = "maplibregl-ctrl-";
2645            MapboxDraw.constants.classes.CONTROL_GROUP =
2646              "maplibregl-ctrl-group";
2647
2648            let drawOptions = x.draw_control.options || {};
2649
2650            // Generate styles if styling parameters provided
2651            if (x.draw_control.styling) {
2652              const generatedStyles = generateDrawStyles(
2653                x.draw_control.styling,
2654              );
2655              if (generatedStyles) {
2656                drawOptions.styles = generatedStyles;
2657              }
2658            }
2659
2660            if (x.draw_control.freehand) {
2661              drawOptions = Object.assign({}, drawOptions, {
2662                modes: Object.assign({}, MapboxDraw.modes, {
2663                  draw_polygon: Object.assign(
2664                    {},
2665                    MapboxDraw.modes.draw_freehand,
2666                    {
2667                      // Store the simplify_freehand option on the map object
2668                      onSetup: function (opts) {
2669                        const state =
2670                          MapboxDraw.modes.draw_freehand.onSetup.call(
2671                            this,
2672                            opts,
2673                          );
2674                        this.map.simplify_freehand =
2675                          x.draw_control.simplify_freehand;
2676                        return state;
2677                      },
2678                    },
2679                  ),
2680                }),
2681                // defaultMode: 'draw_polygon' # Don't set the default yet
2682              });
2683            }
2684
2685            // Add rectangle mode if enabled
2686            if (x.draw_control.rectangle) {
2687              if (!drawOptions.modes) {
2688                drawOptions.modes = Object.assign({}, MapboxDraw.modes);
2689              }
2690              drawOptions.modes.draw_rectangle =
2691                MapboxDraw.modes.draw_rectangle;
2692            }
2693
2694            // Add radius mode if enabled
2695            if (x.draw_control.radius) {
2696              if (!drawOptions.modes) {
2697                drawOptions.modes = Object.assign({}, MapboxDraw.modes);
2698              }
2699              drawOptions.modes.draw_radius = MapboxDraw.modes.draw_radius;
2700            }
2701
2702            // Fix MapLibre compatibility - ensure we always have custom styles
2703            if (!drawOptions.styles) {
2704              drawOptions.styles = generateDrawStyles({
2705                vertex_radius: 5,
2706                active_color: "#fbb03b",
2707                point_color: "#3bb2d0",
2708                line_color: "#3bb2d0",
2709                fill_color: "#3bb2d0",
2710                fill_opacity: 0.1,
2711                line_width: 2,
2712              });
2713            }
2714
2715            if (x.draw_control.bezier || x.draw_control.bezier_polygon) {
2716              drawOptions = enableBezierDrawMode(
2717                drawOptions,
2718                x.draw_control.styling,
2719              );
2720            }
2721
2722            if (_mapglHasDrawAttributes(x.draw_control.attributes)) {
2723              drawOptions = Object.assign({}, drawOptions, {
2724                userProperties: true,
2725              });
2726            }
2727
2728            draw = new MapboxDraw(drawOptions);
2729            map.addControl(draw, x.draw_control.position);
2730            map.controls.push({ type: "draw", control: draw });
2731
2732            // Add lasso icon CSS for freehand mode
2733            if (x.draw_control.freehand) {
2734              if (!document.querySelector("#mapgl-freehand-lasso-styles")) {
2735                const style = document.createElement("style");
2736                style.id = "mapgl-freehand-lasso-styles";
2737                style.textContent = `
2738                  .mapbox-gl-draw_polygon {
2739                    background-image: url('data:image/svg+xml;utf8,%3Csvg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"%3E%3Cpath d="M 7 3 C 5 2.5, 3.5 3, 3 5 C 2.5 7, 3 8.5, 4 9.5 C 5 10.5, 5.5 11.5, 5 13 C 4.5 14.5, 5 15.5, 6.5 16 C 8 16.5, 9.5 16, 11 15 C 12.5 14, 13.5 13, 14.5 11.5 C 15.5 10, 16 8.5, 15.5 7 C 15 5.5, 14 4.5, 12.5 3.5 C 11 2.5, 9 2.5, 7 3 Z" fill="none" stroke="%23000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/%3E%3C/svg%3E') !important;
2740                  }
2741                `;
2742                document.head.appendChild(style);
2743              }
2744
2745              // Update tooltip for freehand mode
2746              setTimeout(() => {
2747                const polygonButton = map
2748                  .getContainer()
2749                  .querySelector(".mapbox-gl-draw_polygon");
2750                if (polygonButton) {
2751                  polygonButton.title = "Freehand polygon tool (p)";
2752                }
2753              }, 100);
2754            }
2755
2756            // Add rectangle icon CSS if rectangle mode is enabled
2757            if (x.draw_control.rectangle) {
2758              if (!document.querySelector("#mapgl-rectangle-styles")) {
2759                const style = document.createElement("style");
2760                style.id = "mapgl-rectangle-styles";
2761                style.textContent = `
2762                  .mapbox-gl-draw_rectangle {
2763                    background: transparent;
2764                    border: none;
2765                    cursor: pointer;
2766                    display: block;
2767                    height: 30px;
2768                    width: 30px;
2769                    padding: 0;
2770                    outline: none;
2771                    background-image: url('data:image/svg+xml;utf8,%3Csvg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"%3E%3Crect x="4" y="5" width="12" height="10" fill="none" stroke="%23000000" stroke-width="2"/%3E%3C/svg%3E') !important;
2772                    background-repeat: no-repeat !important;
2773                    background-position: center !important;
2774                  }
2775                  .mapbox-gl-draw_rectangle:hover {
2776                    background-color: rgba(0, 0, 0, 0.05);
2777                  }
2778                  .mapbox-gl-draw_rectangle.active {
2779                    background-color: rgba(0, 0, 0, 0.05);
2780                  }
2781                `;
2782                document.head.appendChild(style);
2783              }
2784            }
2785
2786            // Add radius/circle icon CSS if radius mode is enabled
2787            if (x.draw_control.radius) {
2788              if (!document.querySelector("#mapgl-radius-styles")) {
2789                const style = document.createElement("style");
2790                style.id = "mapgl-radius-styles";
2791                style.textContent = `
2792                  .mapbox-gl-draw_radius {
2793                    background: transparent;
2794                    border: none;
2795                    cursor: pointer;
2796                    display: block;
2797                    height: 30px;
2798                    width: 30px;
2799                    padding: 0;
2800                    outline: none;
2801                    background-image: url('data:image/svg+xml;utf8,%3Csvg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"%3E%3Ccircle cx="10" cy="10" r="7" fill="none" stroke="%23000000" stroke-width="2"/%3E%3Ccircle cx="10" cy="10" r="1.5" fill="%23000000"/%3E%3C/svg%3E') !important;
2802                    background-repeat: no-repeat !important;
2803                    background-position: center !important;
2804                  }
2805                  .mapbox-gl-draw_radius:hover {
2806                    background-color: rgba(0, 0, 0, 0.05);
2807                  }
2808                  .mapbox-gl-draw_radius.active {
2809                    background-color: rgba(0, 0, 0, 0.05);
2810                  }
2811                `;
2812                document.head.appendChild(style);
2813              }
2814            }
2815
2816            addBezierButtons(map, draw, {
2817              bezier: x.draw_control.bezier,
2818              bezierPolygon: x.draw_control.bezier_polygon,
2819              controlGroupSelector:
2820                ".maplibregl-ctrl-group:has(.mapbox-gl-draw_polygon)",
2821            });
2822
2823            initializeDrawAttributeEditor(map, draw, {
2824              attributes: x.draw_control.attributes,
2825              position: x.draw_control.position,
2826              onChange: updateDrawnFeatures,
2827            });
2828
2829            // Add event listeners
2830            map.on("draw.create", () => updateDrawnFeatures(false));
2831            map.on("draw.delete", () => updateDrawnFeatures(false));
2832            map.on("draw.update", () => updateDrawnFeatures(true));
2833
2834            // Add measurement functionality if enabled
2835            if (x.draw_control.show_measurements) {
2836              initializeMeasurements(
2837                map,
2838                draw,
2839                x.draw_control.measurement_units,
2840              );
2841            }
2842
2843            // Add initial features if provided
2844            if (x.draw_control.source) {
2845              addSourceFeaturesToDraw(draw, x.draw_control.source, map);
2846              updateDrawnFeatures(false);
2847            }
2848
2849            // Process any queued features
2850            if (x.draw_features_queue) {
2851              x.draw_features_queue.forEach(function (data) {
2852                if (data.clear_existing) {
2853                  draw.deleteAll();
2854                }
2855                addSourceFeaturesToDraw(draw, data.source, map);
2856              });
2857              updateDrawnFeatures(false);
2858            }
2859
2860            // Add custom mode buttons
2861            setTimeout(() => {
2862              const drawControlGroup = map
2863                .getContainer()
2864                .querySelector(".maplibregl-ctrl-group");
2865
2866              if (drawControlGroup) {
2867                // Find the trash button to insert before it
2868                const trashBtn = drawControlGroup.querySelector(
2869                  ".mapbox-gl-draw_trash",
2870                );
2871
2872                if (x.draw_control.rectangle) {
2873                  const rectangleBtn = document.createElement("button");
2874                  rectangleBtn.className = "mapbox-gl-draw_rectangle";
2875                  rectangleBtn.title = "Rectangle tool";
2876                  rectangleBtn.type = "button";
2877                  rectangleBtn.onclick = function () {
2878                    draw.changeMode("draw_rectangle");
2879                    // Update active state
2880                    drawControlGroup
2881                      .querySelectorAll("button")
2882                      .forEach((btn) => btn.classList.remove("active"));
2883                    rectangleBtn.classList.add("active");
2884                  };
2885                  // Insert before trash button if it exists, otherwise append
2886                  if (trashBtn) {
2887                    drawControlGroup.insertBefore(rectangleBtn, trashBtn);
2888                  } else {
2889                    drawControlGroup.appendChild(rectangleBtn);
2890                  }
2891                }
2892
2893                if (x.draw_control.radius) {
2894                  const radiusBtn = document.createElement("button");
2895                  radiusBtn.className = "mapbox-gl-draw_radius";
2896                  radiusBtn.title = "Radius/Circle tool";
2897                  radiusBtn.type = "button";
2898                  radiusBtn.onclick = function () {
2899                    draw.changeMode("draw_radius");
2900                    // Update active state
2901                    drawControlGroup
2902                      .querySelectorAll("button")
2903                      .forEach((btn) => btn.classList.remove("active"));
2904                    radiusBtn.classList.add("active");
2905                  };
2906                  // Insert before trash button if it exists, otherwise append
2907                  if (trashBtn) {
2908                    drawControlGroup.insertBefore(radiusBtn, trashBtn);
2909                  } else {
2910                    drawControlGroup.appendChild(radiusBtn);
2911                  }
2912                }
2913              }
2914            }, 100);
2915
2916            // Apply orientation styling
2917            if (x.draw_control.orientation === "horizontal") {
2918              const drawBar = map
2919                .getContainer()
2920                .querySelector(".maplibregl-ctrl-group");
2921              if (drawBar) {
2922                drawBar.style.display = "flex";
2923                drawBar.style.flexDirection = "row";
2924              }
2925            }
2926
2927            // Add download button if requested
2928            if (x.draw_control.download_button) {
2929              // Add CSS for download button if not already added
2930              if (!document.querySelector("#mapgl-draw-download-styles")) {
2931                const style = document.createElement("style");
2932                style.id = "mapgl-draw-download-styles";
2933                style.textContent = `
2934                  .mapbox-gl-draw_download {
2935                    background: transparent;
2936                    border: none;
2937                    cursor: pointer;
2938                    display: block;
2939                    height: 30px;
2940                    width: 30px;
2941                    padding: 0;
2942                    outline: none;
2943                  }
2944                  .mapbox-gl-draw_download:hover {
2945                    background-color: rgba(0, 0, 0, 0.05);
2946                  }
2947                  .mapbox-gl-draw_download svg {
2948                    width: 20px;
2949                    height: 20px;
2950                    margin: 5px;
2951                    fill: #333;
2952                  }
2953                `;
2954                document.head.appendChild(style);
2955              }
2956
2957              // Small delay to ensure Draw control is fully rendered
2958              setTimeout(() => {
2959                // Find the Draw control button group
2960                const drawButtons = map
2961                  .getContainer()
2962                  .querySelector(
2963                    ".maplibregl-ctrl-group:has(.mapbox-gl-draw_polygon)",
2964                  );
2965
2966                if (drawButtons) {
2967                  // Create download button
2968                  const downloadBtn = document.createElement("button");
2969                  downloadBtn.className = "mapbox-gl-draw_download";
2970                  downloadBtn.title = "Download drawn features as GeoJSON";
2971
2972                  // Add SVG download icon
2973                  downloadBtn.innerHTML = `
2974                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
2975                      <path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>
2976                    </svg>
2977                  `;
2978
2979                  downloadBtn.addEventListener("click", () => {
2980                    // Get all drawn features
2981                    const data = draw.getAll();
2982
2983                    if (data.features.length === 0) {
2984                      alert(
2985                        "No features to download. Please draw something first!",
2986                      );
2987                      return;
2988                    }
2989
2990                    // Convert to string with nice formatting
2991                    const dataStr = JSON.stringify(data, null, 2);
2992
2993                    // Create blob and download
2994                    const blob = new Blob([dataStr], {
2995                      type: "application/json",
2996                    });
2997                    const url = URL.createObjectURL(blob);
2998
2999                    const a = document.createElement("a");
3000                    a.href = url;
3001                    a.download = `${x.draw_control.download_filename}.geojson`;
3002                    document.body.appendChild(a);
3003                    a.click();
3004                    document.body.removeChild(a);
3005                    URL.revokeObjectURL(url);
3006                  });
3007
3008                  // Append to the Draw control button group
3009                  drawButtons.appendChild(downloadBtn);
3010                }
3011              }, 100);
3012            }
3013          }
3014
3015          function updateDrawnFeatures(debounce) {
3016            _mapglSyncDrawnFeatures({
3017              inputId: el.id,
3018              drawControl: draw,
3019              widget: HTMLWidgets.find("#" + el.id),
3020              syncUrl: x.sync_url,
3021              mapglId: x.mapgl_id,
3022              debounce: debounce,
3023            });
3024          }
3025
3026          if (!x.add) {
3027            const existingLegends = el.querySelectorAll(".mapboxgl-legend");
3028            existingLegends.forEach((legend) => legend.remove());
3029          }
3030
3031          if (x.legend_html && x.legend_css) {
3032            const legendCss = document.createElement("style");
3033            legendCss.innerHTML = x.legend_css;
3034            document.head.appendChild(legendCss);
3035
3036            const legend = document.createElement("div");
3037            legend.innerHTML = x.legend_html;
3038            legend.classList.add("mapboxgl-legend");
3039            el.appendChild(legend);
3040          }
3041
3042          // Initialize legend interactivity if configured
3043          if (x.legend_interactivity && x.legend_interactivity.length > 0) {
3044            x.legend_interactivity.forEach(function(config) {
3045              if (typeof initializeLegendInteractivity === "function") {
3046                initializeLegendInteractivity(map, el.id, config);
3047              }
3048            });
3049          }
3050
3051          // Initialize draggable legends
3052          if (typeof initializeDraggableLegends === "function") {
3053            initializeDraggableLegends(el);
3054          }
3055
3056          // Add fullscreen control if enabled
3057          if (x.fullscreen_control && x.fullscreen_control.enabled) {
3058            const position = x.fullscreen_control.position || "top-right";
3059            const fullscreen = new maplibregl.FullscreenControl();
3060            map.addControl(fullscreen, position);
3061            map.controls.push({ type: "fullscreen", control: fullscreen });
3062          }
3063
3064          // Add slider control if configured
3065          if (
3066            x.slider_control &&
3067            typeof window.MapglSliderControl === "function"
3068          ) {
3069            const _tsCfg = x.slider_control;
3070            const _ts = new window.MapglSliderControl(_tsCfg);
3071            map.addControl(_ts, _tsCfg.position || "top-left");
3072            map.controls.push({ type: "slider", control: _ts });
3073          }
3074
3075          // Add geolocate control if enabled
3076          if (x.geolocate_control) {
3077            const geolocate = new maplibregl.GeolocateControl({
3078              positionOptions: x.geolocate_control.positionOptions,
3079              trackUserLocation: x.geolocate_control.trackUserLocation,
3080              showAccuracyCircle: x.geolocate_control.showAccuracyCircle,
3081              showUserLocation: x.geolocate_control.showUserLocation,
3082              showUserHeading: x.geolocate_control.showUserHeading,
3083              fitBoundsOptions: x.geolocate_control.fitBoundsOptions,
3084            });
3085            map.addControl(geolocate, x.geolocate_control.position);
3086            map.controls.push({ type: "geolocate", control: geolocate });
3087
3088            if (HTMLWidgets.shinyMode) {
3089              geolocate.on("geolocate", function (event) {
3090                Shiny.setInputValue(el.id + "_geolocate", {
3091                  coords: event.coords,
3092                  time: new Date(),
3093                });
3094              });
3095
3096              geolocate.on("trackuserlocationstart", function () {
3097                Shiny.setInputValue(el.id + "_geolocate_tracking", {
3098                  status: "start",
3099                  time: new Date(),
3100                });
3101              });
3102
3103              geolocate.on("trackuserlocationend", function () {
3104                Shiny.setInputValue(el.id + "_geolocate_tracking", {
3105                  status: "end",
3106                  time: new Date(),
3107                });
3108              });
3109
3110              geolocate.on("error", function (error) {
3111                if (error.error.code === 1) {
3112                  Shiny.setInputValue(el.id + "_geolocate_error", {
3113                    message: "Location permission denied",
3114                    time: new Date(),
3115                  });
3116                }
3117              });
3118            }
3119          }
3120
3121          // Add navigation control if enabled
3122          if (x.navigation_control) {
3123            const nav = new maplibregl.NavigationControl({
3124              showCompass: x.navigation_control.show_compass,
3125              showZoom: x.navigation_control.show_zoom,
3126              visualizePitch: x.navigation_control.visualize_pitch,
3127            });
3128            map.addControl(nav, x.navigation_control.position);
3129            map.controls.push({ type: "navigation", control: nav });
3130
3131            if (x.navigation_control.orientation === "horizontal") {
3132              const navBar = map
3133                .getContainer()
3134                .querySelector(
3135                  ".maplibregl-ctrl-group:not(.mapbox-gl-draw_ctrl-draw-btn)",
3136                );
3137              if (navBar) {
3138                navBar.style.display = "flex";
3139                navBar.style.flexDirection = "row";
3140              }
3141            }
3142          }
3143
3144          // Add reset control if enabled
3145          if (x.reset_control) {
3146            const resetControl = document.createElement("button");
3147            resetControl.className =
3148              "maplibregl-ctrl-icon maplibregl-ctrl-reset";
3149            resetControl.type = "button";
3150            resetControl.setAttribute("aria-label", "Reset");
3151            resetControl.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"></polyline><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path></svg>';
3152            resetControl.style.cssText = "display:flex;justify-content:center;align-items:center;cursor:pointer;";
3153
3154            const resetContainer = document.createElement("div");
3155            resetContainer.className = "maplibregl-ctrl maplibregl-ctrl-group";
3156            resetContainer.appendChild(resetControl);
3157
3158            // Initialize with empty object, will be populated after map loads
3159            let initialView = {};
3160
3161            // Capture the initial view after the map has loaded and all view operations are complete
3162            map.once("load", function () {
3163              initialView = {
3164                center: map.getCenter(),
3165                zoom: map.getZoom(),
3166                pitch: map.getPitch(),
3167                bearing: map.getBearing(),
3168                animate: x.reset_control.animate,
3169              };
3170
3171              if (x.reset_control.duration) {
3172                initialView.duration = x.reset_control.duration;
3173              }
3174            });
3175
3176            resetControl.onclick = function () {
3177              // Only reset if we have captured the initial view
3178              if (initialView.center) {
3179                map.easeTo(initialView);
3180              }
3181            };
3182
3183            const resetControlObj = {
3184              onAdd: function () {
3185                return resetContainer;
3186              },
3187              onRemove: function () {
3188                resetContainer.parentNode.removeChild(resetContainer);
3189              },
3190            };
3191
3192            map.addControl(resetControlObj, x.reset_control.position);
3193
3194            map.controls.push({ type: "reset", control: resetControlObj });
3195          }
3196
3197          // Add screenshot control if enabled
3198          if (x.screenshot_control) {
3199            const screenshotControlObj = createScreenshotControl(map, x.screenshot_control, true);
3200            map.addControl(screenshotControlObj, x.screenshot_control.position || "top-right");
3201            map.controls.push({ type: "screenshot", control: screenshotControlObj });
3202          }
3203
3204          // Add coordinates control if enabled
3205          if (x.coordinates_control) {
3206            const coordinatesControlObj = createCoordinatesControl(x.coordinates_control);
3207            map.addControl(
3208              coordinatesControlObj,
3209              x.coordinates_control.position || "bottom-right",
3210            );
3211            map.controls.push({ type: "coordinates", control: coordinatesControlObj });
3212          }
3213
3214          if (x.images && Array.isArray(x.images)) {
3215            const mapId = el.id;
3216            x.images.forEach(async function (imageInfo) {
3217              try {
3218                // Register the image for potential re-adding after style changes
3219                registerUserImage(mapId, imageInfo.id, imageInfo.url, imageInfo.options);
3220                const image = await map.loadImage(imageInfo.url);
3221                if (!map.hasImage(imageInfo.id)) {
3222                  map.addImage(imageInfo.id, image.data, imageInfo.options);
3223                }
3224              } catch (error) {
3225                console.error("Error loading image:", error);
3226              }
3227            });
3228          } else if (x.images) {
3229            console.error("x.images is not an array:", x.images);
3230          }
3231
3232          // Add the layers control if provided
3233          if (x.layers_control) {
3234            const layersControl = new MapglLayersControl(x.layers_control);
3235            map.addControl(
3236              layersControl,
3237              x.layers_control.position || "top-left",
3238            );
3239            map.controls.push({ type: "layers", control: layersControl });
3240          }
3241
3242            // Mark initial style as loaded to prevent control duplication on subsequent set_style() calls
3243            map._initialStyleLoaded = true;
3244          }
3245
3246          // If clusters are present, attach event handling. Helper
3247          // auto-branches on source capability (native vs precomputed).
3248          map.getStyle().layers.forEach((layer) => {
3249            if (layer.id.includes("-clusters")) {
3250              _mapglAttachClusterHandlers(map, layer.id, layer.source);
3251            }
3252          });
3253
3254          // Add click event listener in shinyMode
3255          if (HTMLWidgets.shinyMode) {
3256            map.on("click", function (e) {
3257              // Check if draw control is active and in a drawing mode
3258              let isDrawing = false;
3259              if (typeof draw !== "undefined" && draw) {
3260                const mode = draw.getMode();
3261                isDrawing =
3262                  mode === "draw_point" ||
3263                  mode === "draw_line_string" ||
3264                  mode === "draw_polygon";
3265              }
3266
3267              // Only process feature clicks if not actively drawing
3268              if (!isDrawing) {
3269                const features = map.queryRenderedFeatures(e.point);
3270
3271                if (features.length > 0) {
3272                  const feature = features[0];
3273                  Shiny.onInputChange(el.id + "_feature_click", {
3274                    id: feature.id,
3275                    properties: feature.properties,
3276                    layer: feature.layer.id,
3277                    lng: e.lngLat.lng,
3278                    lat: e.lngLat.lat,
3279                    time: new Date(),
3280                  });
3281                } else {
3282                  Shiny.onInputChange(el.id + "_feature_click", null);
3283                }
3284              }
3285
3286              // Event listener for the map (always fire this)
3287              Shiny.onInputChange(el.id + "_click", {
3288                lng: e.lngLat.lng,
3289                lat: e.lngLat.lat,
3290                time: new Date(),
3291              });
3292            });
3293
3294            // add hover listener for shinyMode if enabled
3295            if (x.hover_events && x.hover_events.enabled) {
3296              map.on("mousemove", function (e) {
3297                // Feature hover events
3298                if (x.hover_events.features) {
3299                  const options = x.hover_events.layer_id
3300                    ? {
3301                        layers: Array.isArray(x.hover_events.layer_id)
3302                          ? x.hover_events.layer_id
3303                          : x.hover_events.layer_id
3304                              .split(",")
3305                              .map((id) => id.trim()),
3306                      }
3307                    : undefined;
3308                  const features = map.queryRenderedFeatures(e.point, options);
3309
3310                  if (features.length > 0) {
3311                    const feature = features[0];
3312                    Shiny.onInputChange(el.id + "_feature_hover", {
3313                      id: feature.id,
3314                      properties: feature.properties,
3315                      layer: feature.layer.id,
3316                      lng: e.lngLat.lng,
3317                      lat: e.lngLat.lat,
3318                      time: new Date(),
3319                    });
3320                  } else {
3321                    Shiny.onInputChange(el.id + "_feature_hover", null);
3322                  }
3323                }
3324
3325                // Coordinate hover events
3326                if (x.hover_events.coordinates) {
3327                  Shiny.onInputChange(el.id + "_hover", {
3328                    lng: e.lngLat.lng,
3329                    lat: e.lngLat.lat,
3330                    time: new Date(),
3331                  });
3332                }
3333              });
3334            }
3335          }
3336
3337          // Process turf operations for static maps
3338          if (x.turf_operations) {
3339            processTurfOperationsOnLoad(map, x.turf_operations, el.id);
3340          }
3341
3342          el.map = map;
3343        });
3344        el.map = map;
3345      },
3346
3347      getMap: function () {
3348        return map; // Return the map instance
3349      },
3350
3351      getDraw: function () {
3352        return draw; // Return the draw instance
3353      },
3354
3355      getDrawnFeatures: function () {
3356        return (
3357          this.drawFeatures || {
3358            type: "FeatureCollection",
3359            features: [],
3360          }
3361        );
3362      },
3363
3364      resize: function (width, height) {
3365        if (map) {
3366          map.resize();
3367        }
3368      },
3369    };
3370  },
3371});
3372
3373if (HTMLWidgets.shinyMode) {
3374  Shiny.addCustomMessageHandler("maplibre-proxy", async function (data) {
3375    var widget = HTMLWidgets.find("#" + data.id);
3376    if (!widget) return;
3377    var map = widget.getMap();
3378    if (map) {
3379      var message = data.message;
3380
3381      // Initialize (or upgrade) layer state tracking via the shared helper.
3382      const mapId = map.getContainer().id;
3383      const layerState = _mapglEnsureLayerState(map);
3384
3385      // Helper function to update drawn features
3386      function updateDrawnFeatures(debounce) {
3387        var drawControl = widget.drawControl || widget.getDraw();
3388        _mapglSyncDrawnFeatures({
3389          inputId: data.id,
3390          drawControl: drawControl,
3391          widget: widget,
3392          debounce: debounce,
3393        });
3394      }
3395      if (message.type === "set_filter") {
3396        // Route through the filter registry so `user` composes with
3397        // base/legend/slider slots.
3398        layerState.filterStack[message.layer] =
3399          layerState.filterStack[message.layer] || {};
3400        layerState.filterStack[message.layer].user = message.filter || null;
3401        _mapglComposeAndApplyFilter(map, message.layer);
3402      } else if (message.type === "add_h3t_sources") {
3403        (message.h3t_sources || []).forEach(function (source) {
3404          try {
3405            map.addH3TSource(source.id, {
3406              tiles: source.tiles,
3407              sourcelayer: source.sourcelayer,
3408              geometry_type: source.geometry_type,
3409              minzoom: source.minzoom,
3410              maxzoom: source.maxzoom,
3411              promoteId: source.promoteId,
3412              debug: source.debug,
3413            });
3414          } catch (e) {
3415            console.error("addH3TSource failed for", source.id, e);
3416          }
3417        });
3418      } else if (message.type === "add_h3j_sources") {
3419        (message.h3j_sources || []).forEach(async function (source) {
3420          try {
3421            await map.addH3JSource(source.id, { data: source.url });
3422          } catch (e) {
3423            console.error("addH3JSource failed for", source.id, e);
3424          }
3425        });
3426      } else if (message.type === "add_source") {
3427        if (message.source.type === "vector") {
3428          const sourceConfig = {
3429            type: "vector",
3430          };
3431          // Add url or tiles
3432          if (message.source.url) {
3433            sourceConfig.url = message.source.url;
3434            // Auto-detect MLT encoding for PMTiles sources
3435            if (
3436              message.source.url.startsWith("pmtiles://") &&
3437              typeof pmtiles !== "undefined" &&
3438              !message.source.encoding
3439            ) {
3440              try {
3441                const rawUrl = message.source.url.replace("pmtiles://", "");
3442                const p = new pmtiles.PMTiles(rawUrl);
3443                const header = await p.getHeader();
3444                if (header.tileType === pmtiles.TileType.Mlt) {
3445                  sourceConfig.encoding = "mlt";
3446                }
3447              } catch (e) {
3448                console.warn("Failed to detect PMTiles tile type:", e);
3449              }
3450            }
3451          }
3452          if (message.source.tiles) {
3453            sourceConfig.tiles = message.source.tiles;
3454          }
3455          // Add promoteId if provided
3456          if (message.source.promoteId) {
3457            sourceConfig.promoteId = message.source.promoteId;
3458          }
3459          // Add any other properties from the source object
3460          Object.keys(message.source).forEach(function (key) {
3461            if (
3462              key !== "id" &&
3463              key !== "type" &&
3464              key !== "url" &&
3465              key !== "tiles" &&
3466              key !== "promoteId"
3467            ) {
3468              sourceConfig[key] = message.source[key];
3469            }
3470          });
3471          map.addSource(message.source.id, sourceConfig);
3472        } else if (message.source.type === "geojson") {
3473          const sourceConfig = {
3474            type: "geojson",
3475            data: message.source.data,
3476          };
3477          // Add any other properties
3478          Object.keys(message.source).forEach(function (key) {
3479            if (key !== "id" && key !== "type" && key !== "data") {
3480              sourceConfig[key] = message.source[key];
3481            }
3482          });
3483          map.addSource(message.source.id, sourceConfig);
3484          // Store GeoJSON data for later retrieval (e.g., for draw control)
3485          storeSourceData(mapId, message.source.id, message.source.data);
3486        } else {
3487          // For other source types, pass through remaining properties
3488          const sourceConfig = { type: message.source.type };
3489          Object.keys(message.source).forEach(function (key) {
3490            if (key !== "id") {
3491              sourceConfig[key] = message.source[key];
3492            }
3493          });
3494          map.addSource(message.source.id, sourceConfig);
3495        }
3496      } else if (message.type === "add_layer") {
3497        try {
3498          // Ensure paint and layout are objects, not null
3499          if (message.layer.paint === null) {
3500            message.layer.paint = {};
3501          }
3502          if (message.layer.layout === null) {
3503            message.layer.layout = {};
3504          }
3505          if (message.layer.before_id) {
3506            map.addLayer(message.layer, message.layer.before_id);
3507          } else {
3508            map.addLayer(message.layer);
3509          }
3510
3511          // Cluster click/cursor parity for layers added via proxy.
3512          if (
3513            message.layer.id &&
3514            message.layer.id.includes("-clusters")
3515          ) {
3516            _mapglAttachClusterHandlers(
3517              map,
3518              message.layer.id,
3519              message.layer.source,
3520            );
3521          }
3522
3523          // Seed the filter registry for this late-added layer. If an
3524          // active slider targets this layer id, compose its filter
3525          // in immediately.
3526          layerState.filterStack[message.layer.id] =
3527            layerState.filterStack[message.layer.id] || {};
3528          layerState.filterStack[message.layer.id].base =
3529            message.layer.filter || null;
3530          const _ts = map._mapglSliderControl;
3531          if (
3532            _ts &&
3533            _ts.targetsLayer &&
3534            _ts.targetsLayer(message.layer.id)
3535          ) {
3536            layerState.filterStack[message.layer.id].slider =
3537              _ts.currentFilter();
3538            if (typeof _ts.captureAndApplyPaintForLayer === "function") {
3539              _ts.captureAndApplyPaintForLayer(message.layer.id);
3540            }
3541          }
3542          _mapglComposeAndApplyFilter(map, message.layer.id);
3543
3544          // Add popups or tooltips if provided
3545          if (message.layer.popup) {
3546            // Initialize popup tracking if it doesn't exist
3547            if (!window._mapboxPopups) {
3548              window._mapboxPopups = {};
3549            }
3550
3551            // Create click handler for this layer
3552            const clickHandler = function (e) {
3553              onClickPopup(
3554                e,
3555                map,
3556                message.layer.popup,
3557                message.layer.id,
3558                message.layer.popup_style,
3559              );
3560            };
3561
3562            // Store these handler references so we can remove them later if needed
3563            if (!window._mapboxClickHandlers) {
3564              window._mapboxClickHandlers = {};
3565            }
3566            window._mapboxClickHandlers[message.layer.id] = clickHandler;
3567
3568            // Add the click handler
3569            map.on("click", message.layer.id, clickHandler);
3570
3571            // Change cursor to pointer when hovering over the layer
3572            map.on("mouseenter", message.layer.id, function () {
3573              map.getCanvas().style.cursor = "pointer";
3574            });
3575
3576            // Change cursor back to default when leaving the layer
3577            map.on("mouseleave", message.layer.id, function () {
3578              map.getCanvas().style.cursor = "";
3579            });
3580          }
3581
3582          if (message.layer.tooltip) {
3583            const tooltip = new maplibregl.Popup({
3584              closeButton: false,
3585              closeOnClick: false,
3586              maxWidth: "400px",
3587            });
3588            applyPopupClass(tooltip, message.layer.tooltip_style);
3589
3590            // Define named handler functions:
3591            const mouseMoveHandler = function (e) {
3592              onMouseMoveTooltip(
3593                e,
3594                map,
3595                tooltip,
3596                message.layer.tooltip,
3597                message.layer.id,
3598              );
3599            };
3600
3601            const mouseLeaveHandler = function () {
3602              onMouseLeaveTooltip(map, tooltip);
3603            };
3604
3605            // Attach handlers by reference:
3606            map.on("mousemove", message.layer.id, mouseMoveHandler);
3607            map.on("mouseleave", message.layer.id, mouseLeaveHandler);
3608
3609            // Store these handler references for later removal:
3610            if (!window._mapboxHandlers) {
3611              window._mapboxHandlers = {};
3612            }
3613            window._mapboxHandlers[message.layer.id] = {
3614              mousemove: mouseMoveHandler,
3615              mouseleave: mouseLeaveHandler,
3616            };
3617          }
3618
3619          // Add hover effect if provided
3620          if (message.layer.hover_options) {
3621            const jsHoverOptions = {};
3622            for (const [key, value] of Object.entries(
3623              message.layer.hover_options,
3624            )) {
3625              const jsKey = key.replace(/_/g, "-");
3626              jsHoverOptions[jsKey] = value;
3627            }
3628
3629            let hoveredFeatureId = null;
3630
3631            map.on("mousemove", message.layer.id, function (e) {
3632              if (e.features.length > 0) {
3633                // Check if the feature has an id
3634                const featureId = e.features[0].id;
3635
3636                // Only proceed if the feature has an id
3637                if (featureId !== undefined && featureId !== null) {
3638                  if (hoveredFeatureId !== null) {
3639                    const featureState = {
3640                      source:
3641                        typeof message.layer.source === "string"
3642                          ? message.layer.source
3643                          : message.layer.id,
3644                      id: hoveredFeatureId,
3645                    };
3646                    if (message.layer.source_layer) {
3647                      featureState.sourceLayer = message.layer.source_layer;
3648                    }
3649                    map.setFeatureState(featureState, { hover: false });
3650                  }
3651                  hoveredFeatureId = featureId;
3652                  const featureState = {
3653                    source:
3654                      typeof message.layer.source === "string"
3655                        ? message.layer.source
3656                        : message.layer.id,
3657                    id: hoveredFeatureId,
3658                  };
3659                  if (message.layer.source_layer) {
3660                    featureState.sourceLayer = message.layer.source_layer;
3661                  }
3662                  map.setFeatureState(featureState, { hover: true });
3663                }
3664              }
3665            });
3666
3667            map.on("mouseleave", message.layer.id, function () {
3668              if (hoveredFeatureId !== null) {
3669                const featureState = {
3670                  source:
3671                    typeof message.layer.source === "string"
3672                      ? message.layer.source
3673                      : message.layer.id,
3674                  id: hoveredFeatureId,
3675                };
3676                if (message.layer.source_layer) {
3677                  featureState.sourceLayer = message.layer.source_layer;
3678                }
3679                map.setFeatureState(featureState, { hover: false });
3680              }
3681              hoveredFeatureId = null;
3682            });
3683
3684            Object.keys(jsHoverOptions).forEach(function (key) {
3685              const originalPaint =
3686                map.getPaintProperty(message.layer.id, key) ||
3687                message.layer.paint[key];
3688              map.setPaintProperty(message.layer.id, key, [
3689                "case",
3690                ["boolean", ["feature-state", "hover"], false],
3691                jsHoverOptions[key],
3692                originalPaint,
3693              ]);
3694            });
3695          }
3696        } catch (e) {
3697          console.error("Failed to add layer via proxy: ", message.layer, e);
3698        }
3699      } else if (message.type === "remove_layer") {
3700        // If there's an active tooltip, remove it first
3701        if (window._activeTooltip) {
3702          window._activeTooltip.remove();
3703          delete window._activeTooltip;
3704        }
3705
3706        // If there's an active popup for this layer, remove it
3707        // Check both message.layer and message.layer.id as keys due to different message formats
3708        if (window._mapboxPopups) {
3709          // First check if we have a popup stored with message.layer key
3710          if (window._mapboxPopups[message.layer]) {
3711            window._mapboxPopups[message.layer].remove();
3712            delete window._mapboxPopups[message.layer];
3713          }
3714
3715          // Also check if we have a popup stored with message.layer.id key, which happens when added via add_layer
3716          if (
3717            message.layer &&
3718            message.layer.id &&
3719            window._mapboxPopups[message.layer.id]
3720          ) {
3721            window._mapboxPopups[message.layer.id].remove();
3722            delete window._mapboxPopups[message.layer.id];
3723          }
3724        }
3725
3726        // Cascade: if this layer is the parent of a cluster_options
3727        // shortcut (siblings `-clusters` and `-cluster-count` exist),
3728        // remove those too. The shared source drops with the parent
3729        // layer below.
3730        ["-clusters", "-cluster-count"].forEach((suffix) => {
3731          const siblingId = message.layer + suffix;
3732          if (map.getLayer(siblingId)) {
3733            _mapglDetachClusterHandlers(map, siblingId);
3734            map.removeLayer(siblingId);
3735          }
3736        });
3737
3738        if (map.getLayer(message.layer)) {
3739          // Remove tooltip handlers
3740          if (window._mapboxHandlers && window._mapboxHandlers[message.layer]) {
3741            const handlers = window._mapboxHandlers[message.layer];
3742            if (handlers.mousemove) {
3743              map.off("mousemove", message.layer, handlers.mousemove);
3744            }
3745            if (handlers.mouseleave) {
3746              map.off("mouseleave", message.layer, handlers.mouseleave);
3747            }
3748            // Clean up the reference
3749            delete window._mapboxHandlers[message.layer];
3750          }
3751
3752          // Remove click handlers for popups
3753          if (window._mapboxClickHandlers) {
3754            // First check for handlers stored with message.layer key
3755            if (window._mapboxClickHandlers[message.layer]) {
3756              map.off(
3757                "click",
3758                message.layer,
3759                window._mapboxClickHandlers[message.layer],
3760              );
3761              delete window._mapboxClickHandlers[message.layer];
3762            }
3763
3764            // Also check for handlers stored with message.layer.id key from add_layer
3765            if (
3766              message.layer &&
3767              message.layer.id &&
3768              window._mapboxClickHandlers[message.layer.id]
3769            ) {
3770              map.off(
3771                "click",
3772                message.layer,
3773                window._mapboxClickHandlers[message.layer.id],
3774              );
3775              delete window._mapboxClickHandlers[message.layer.id];
3776            }
3777          }
3778
3779          // Tear down cluster click/cursor handlers if any were attached.
3780          _mapglDetachClusterHandlers(map, message.layer);
3781
3782          // Remove the layer
3783          map.removeLayer(message.layer);
3784        }
3785        if (map.getSource(message.layer)) {
3786          map.removeSource(message.layer);
3787        }
3788
3789        // Clean up tracked layer state
3790        if (window._mapglLayerState && window._mapglLayerState[mapId]) {
3791          const _ls = window._mapglLayerState[mapId];
3792          delete _ls.filters[message.layer];
3793          delete _ls.paintProperties[message.layer];
3794          delete _ls.layoutProperties[message.layer];
3795          delete _ls.tooltips[message.layer];
3796          delete _ls.popups[message.layer];
3797          delete _ls.filterStack[message.layer];
3798          // Note: legends are not tied to specific layers, so we don't clear them here
3799        }
3800      } else if (message.type === "fit_bounds") {
3801        map.fitBounds(message.bounds, message.options);
3802      } else if (message.type === "fly_to") {
3803        map.flyTo(message.options);
3804      } else if (message.type === "ease_to") {
3805        map.easeTo(message.options);
3806      } else if (message.type === "set_center") {
3807        map.setCenter(message.center);
3808      } else if (message.type === "set_zoom") {
3809        map.setZoom(message.zoom);
3810      } else if (message.type === "jump_to") {
3811        map.jumpTo(message.options);
3812      } else if (message.type === "set_layout_property") {
3813        map.setLayoutProperty(message.layer, message.name, message.value);
3814        // Track layout property state for layer restoration
3815        if (!layerState.layoutProperties[message.layer]) {
3816          layerState.layoutProperties[message.layer] = {};
3817        }
3818        layerState.layoutProperties[message.layer][message.name] =
3819          message.value;
3820      } else if (message.type === "set_flowmap_filter") {
3821        if (window.MapGLFlowmapPlugin) {
3822          window.MapGLFlowmapPlugin.setFilter(map, message.id, message.filter);
3823        }
3824      } else if (message.type === "set_flowmap_settings") {
3825        if (window.MapGLFlowmapPlugin) {
3826          window.MapGLFlowmapPlugin.setSettings(
3827            map,
3828            message.id,
3829            message.settings,
3830          );
3831        }
3832      } else if (message.type === "set_paint_property") {
3833        const layerId = message.layer;
3834        const propertyName = message.name;
3835        const newValue = message.value;
3836
3837        // Check if the layer has hover options
3838        const layerStyle = map
3839          .getStyle()
3840          .layers.find((layer) => layer.id === layerId);
3841        const currentPaintProperty = map.getPaintProperty(
3842          layerId,
3843          propertyName,
3844        );
3845
3846        if (
3847          currentPaintProperty &&
3848          Array.isArray(currentPaintProperty) &&
3849          currentPaintProperty[0] === "case"
3850        ) {
3851          // This property has hover options, so we need to preserve them
3852          const hoverValue = currentPaintProperty[2];
3853          const newPaintProperty = [
3854            "case",
3855            ["boolean", ["feature-state", "hover"], false],
3856            hoverValue,
3857            newValue,
3858          ];
3859          map.setPaintProperty(layerId, propertyName, newPaintProperty);
3860        } else {
3861          // No hover options, just set the new value directly
3862          map.setPaintProperty(layerId, propertyName, newValue);
3863        }
3864        // Track paint property state for layer restoration
3865        if (!layerState.paintProperties[layerId]) {
3866          layerState.paintProperties[layerId] = {};
3867        }
3868        layerState.paintProperties[layerId][propertyName] = newValue;
3869      } else if (message.type === "query_rendered_features") {
3870        // Query rendered features
3871        let queryOptions = {};
3872        if (message.layers) {
3873          // Ensure layers is always an array
3874          queryOptions.layers = Array.isArray(message.layers)
3875            ? message.layers
3876            : [message.layers];
3877        }
3878        if (message.filter) queryOptions.filter = message.filter;
3879
3880        let features;
3881        if (message.geometry) {
3882          features = map.queryRenderedFeatures(message.geometry, queryOptions);
3883        } else {
3884          // No geometry specified - query entire viewport
3885          features = map.queryRenderedFeatures(queryOptions);
3886        }
3887
3888        // Deduplicate features by id or by properties if no id
3889        const uniqueFeatures = new Map();
3890        features.forEach(function (feature) {
3891          let key;
3892          if (feature.id !== undefined && feature.id !== null) {
3893            key = feature.id;
3894          } else {
3895            // Create a key from properties if no id available
3896            key = JSON.stringify(feature.properties);
3897          }
3898
3899          if (!uniqueFeatures.has(key)) {
3900            uniqueFeatures.set(key, feature);
3901          }
3902        });
3903
3904        // Convert to GeoJSON FeatureCollection
3905        const deduplicatedFeatures = Array.from(uniqueFeatures.values());
3906        const featureCollection = {
3907          type: "FeatureCollection",
3908          features: deduplicatedFeatures,
3909        };
3910
3911        Shiny.setInputValue(
3912          data.id + "_queried_features",
3913          JSON.stringify(featureCollection),
3914        );
3915      } else if (message.type === "add_legend") {
3916        // Extract legend ID from HTML to track it
3917        const legendIdMatch = message.html.match(/id="([^"]+)"/);
3918        const legendId = legendIdMatch ? legendIdMatch[1] : null;
3919
3920        if (!message.add) {
3921          const existingLegends = document.querySelectorAll(
3922            `#${data.id} .mapboxgl-legend`,
3923          );
3924          existingLegends.forEach((legend) => legend.remove());
3925
3926          // Clean up any existing legend styles that might have been added
3927          const legendStyles = document.querySelectorAll(
3928            `style[data-mapgl-legend-css="${data.id}"]`,
3929          );
3930          legendStyles.forEach((style) => style.remove());
3931
3932          // Clear legend state when replacing all legends
3933          layerState.legends = {};
3934        }
3935
3936        // Track legend state
3937        if (legendId) {
3938          layerState.legends[legendId] = {
3939            html: message.html,
3940            css: message.legend_css,
3941          };
3942        }
3943
3944        const legendCss = document.createElement("style");
3945        legendCss.innerHTML = message.legend_css;
3946        legendCss.setAttribute("data-mapgl-legend-css", data.id); // Mark this style for later cleanup
3947        document.head.appendChild(legendCss);
3948
3949        const legend = document.createElement("div");
3950        legend.innerHTML = message.html;
3951        legend.classList.add("mapboxgl-legend");
3952        document.getElementById(data.id).appendChild(legend);
3953
3954        // Initialize legend interactivity if configured
3955        if (message.interactivity && typeof initializeLegendInteractivity === "function") {
3956          initializeLegendInteractivity(map, data.id, message.interactivity);
3957        }
3958      } else if (message.type === "set_config_property") {
3959        map.setConfigProperty(
3960          message.importId,
3961          message.configName,
3962          message.value,
3963        );
3964      } else if (message.type === "set_style") {
3965        // Default preserve_layers to true if not specified
3966        const preserveLayers = message.preserve_layers !== false;
3967        const mapId = map.getContainer().id;
3968
3969        // Mark style as loading to queue any incoming operations
3970        window._mapgl.styleLoading[mapId] = true;
3971        console.log("[MapGL Debug] Style loading started for map:", mapId);
3972
3973        console.log(
3974          "[MapGL Debug] set_style called with preserve_layers:",
3975          preserveLayers,
3976        );
3977        console.log(
3978          "[MapGL Debug] message.preserve_layers:",
3979          message.preserve_layers,
3980        );
3981
3982        // If we should preserve layers and sources
3983        if (preserveLayers) {
3984          // Store the current style before changing it
3985          const currentStyle = map.getStyle();
3986          const userSourceIds = [];
3987          const userLayers = [];
3988
3989          console.log(
3990            "[MapGL Debug] Current style sources:",
3991            Object.keys(currentStyle.sources),
3992          );
3993          console.log(
3994            "[MapGL Debug] Current style layers:",
3995            currentStyle.layers.map((l) => l.id),
3996          );
3997
3998          // Store layer IDs we know were added by the user via R code
3999          // This is the most reliable way to identify user-added layers
4000          const knownUserLayerIds = [];
4001
4002          // For each layer in the current style, determine if it's a user-added layer
4003          currentStyle.layers.forEach(function (layer) {
4004            const layerId = layer.id;
4005
4006            // Critical: Check for nc_counties specifically since we know that's used in the test app
4007            if (layerId === "nc_counties") {
4008              console.log("[MapGL Debug] Found explicit test layer:", layerId);
4009              knownUserLayerIds.push(layerId);
4010              if (layer.source && !userSourceIds.includes(layer.source)) {
4011                console.log(
4012                  "[MapGL Debug] Found source from test layer:",
4013                  layer.source,
4014                );
4015                userSourceIds.push(layer.source);
4016              }
4017              return; // Skip other checks for this layer
4018            }
4019
4020            // These are common patterns for user-added layers from R code
4021            if (
4022              // Specific layer IDs from the R package
4023              layerId.endsWith("_counties") ||
4024              layerId.endsWith("_label") ||
4025              layerId.endsWith("_layer") ||
4026              // Look for hover handlers - only user-added layers have these
4027              (window._mapboxHandlers && window._mapboxHandlers[layerId]) ||
4028              // If the layer ID contains these strings, it's likely user-added
4029              layerId.includes("user") ||
4030              layerId.includes("custom") ||
4031              // If the paint property has a hover case, it's user-added
4032              (layer.paint &&
4033                Object.values(layer.paint).some(
4034                  (value) =>
4035                    Array.isArray(value) &&
4036                    value[0] === "case" &&
4037                    Array.isArray(value[1]) &&
4038                    value[1][1] &&
4039                    Array.isArray(value[1][1]) &&
4040                    value[1][1][0] === "feature-state" &&
4041                    value[1][1][1] === "hover",
4042                ))
4043            ) {
4044              console.log("[MapGL Debug] Found user layer:", layerId);
4045              knownUserLayerIds.push(layerId);
4046              // Only include its source if it's not a base map source
4047              if (layer.source && !userSourceIds.includes(layer.source)) {
4048                const layerSource = currentStyle.sources[layer.source];
4049                const isBaseMapSource =
4050                  layerSource &&
4051                  layerSource.type === "vector" &&
4052                  (layer.source === "composite" ||
4053                    layer.source === "mapbox" ||
4054                    layer.source.startsWith("mapbox-") ||
4055                    layer.source === "openmaptiles" ||
4056                    layer.source.startsWith("carto") ||
4057                    layer.source.startsWith("maptiler"));
4058
4059                if (!isBaseMapSource) {
4060                  console.log(
4061                    "[MapGL Debug] Found user source from layer:",
4062                    layer.source,
4063                  );
4064                  userSourceIds.push(layer.source);
4065                } else {
4066                  console.log(
4067                    "[MapGL Debug] Not adding base map source from layer:",
4068                    layer.source,
4069                  );
4070                }
4071              }
4072            }
4073          });
4074
4075          // For each source, determine if it's a user-added source
4076          for (const sourceId in currentStyle.sources) {
4077            const source = currentStyle.sources[sourceId];
4078
4079            console.log(
4080              "[MapGL Debug] Examining source:",
4081              sourceId,
4082              "type:",
4083              source.type,
4084            );
4085
4086            // Strategy 1: All GeoJSON sources are likely user-added
4087            if (source.type === "geojson") {
4088              console.log("[MapGL Debug] Found user GeoJSON source:", sourceId);
4089              if (!userSourceIds.includes(sourceId)) {
4090                userSourceIds.push(sourceId);
4091              }
4092            }
4093            // Strategy 2: Check for source data URL patterns typical of R-generated data
4094            else if (
4095              source.url &&
4096              typeof source.url === "string" &&
4097              (source.url.includes("data:application/json") ||
4098                source.url.includes("blob:"))
4099            ) {
4100              console.log(
4101                "[MapGL Debug] Found user source with data URL:",
4102                sourceId,
4103              );
4104              if (!userSourceIds.includes(sourceId)) {
4105                userSourceIds.push(sourceId);
4106              }
4107            }
4108            // Strategy 3: Standard filtering - exclude common base map sources
4109            else if (
4110              sourceId !== "composite" &&
4111              sourceId !== "mapbox" &&
4112              !sourceId.startsWith("mapbox-") &&
4113              sourceId !== "openmaptiles" && // Common in MapLibre styles
4114              !(sourceId.startsWith("carto") && sourceId !== "carto-source") && // Filter CARTO base sources but keep user ones
4115              !(
4116                sourceId.startsWith("maptiler") && !sourceId.includes("user")
4117              ) && // Filter MapTiler sources but keep user ones
4118              !sourceId.includes("terrain") && // Common terrain sources
4119              !sourceId.includes("hillshade") && // Common hillshade sources
4120              !(sourceId.includes("basemap") && !sourceId.includes("user")) && // Filter basemap sources but keep user ones
4121              sourceId !== "satellite" && // Filter MapTiler satellite source specifically
4122              sourceId !== "aerial" // Filter aerial imagery sources
4123            ) {
4124              console.log(
4125                "[MapGL Debug] Found user source via filtering:",
4126                sourceId,
4127              );
4128              if (!userSourceIds.includes(sourceId)) {
4129                userSourceIds.push(sourceId);
4130              }
4131            } else {
4132              console.log(
4133                "[MapGL Debug] Filtered out base map source:",
4134                sourceId,
4135              );
4136            }
4137
4138            // Store layer-specific handler references
4139            if (window._mapboxHandlers) {
4140              const handlers = window._mapboxHandlers;
4141              for (const layerId in handlers) {
4142                // Find layers associated with this source
4143                const layer = currentStyle.layers.find((l) => l.id === layerId);
4144                if (layer && layer.source === sourceId) {
4145                  layer._handlers = handlers[layerId];
4146                }
4147              }
4148            }
4149          }
4150
4151          // Identify layers using user-added sources or known user layer IDs
4152          // ONLY include layers that use genuinely user-added sources (not base map sources)
4153          currentStyle.layers.forEach(function (layer) {
4154            // Check if this layer uses a genuine user source (not filtered out base map sources)
4155            const usesUserSource = userSourceIds.includes(layer.source);
4156            const isKnownUserLayer = knownUserLayerIds.includes(layer.id);
4157
4158            // Additional check: exclude layers that use base map sources even if they were temporarily added to userSourceIds
4159            const layerSource = currentStyle.sources[layer.source];
4160            const isBaseMapSource =
4161              layerSource &&
4162              layerSource.type === "vector" &&
4163              (layer.source === "composite" ||
4164                layer.source === "mapbox" ||
4165                layer.source.startsWith("mapbox-") ||
4166                layer.source === "openmaptiles" ||
4167                layer.source.startsWith("carto") ||
4168                layer.source.startsWith("maptiler"));
4169
4170            if ((usesUserSource || isKnownUserLayer) && !isBaseMapSource) {
4171              // Capture current visibility state (may differ from layer definition)
4172              const currentVisibility = map.getLayoutProperty(layer.id, 'visibility');
4173              if (currentVisibility !== undefined) {
4174                if (!layer.layout) {
4175                  layer.layout = {};
4176                }
4177                layer.layout.visibility = currentVisibility;
4178              }
4179              userLayers.push(layer);
4180              console.log(
4181                "[MapGL Debug] Including user layer:",
4182                layer.id,
4183                "source:",
4184                layer.source,
4185              );
4186            } else if (isBaseMapSource) {
4187              console.log(
4188                "[MapGL Debug] Excluding base map layer:",
4189                layer.id,
4190                "source:",
4191                layer.source,
4192              );
4193            }
4194          });
4195
4196          // Log detected user sources and layers
4197          console.log("[MapGL Debug] Detected user sources:", userSourceIds);
4198          console.log(
4199            "[MapGL Debug] Detected user layers:",
4200            userLayers.map((l) => l.id),
4201          );
4202          console.log(
4203            "[MapGL Debug] Will preserve",
4204            userLayers.length,
4205            "user layers",
4206          );
4207
4208          // Store them for potential use outside the onStyleLoad event
4209          // This helps in case the event timing is different in MapLibre
4210          if (!window._mapglPreservedData) {
4211            window._mapglPreservedData = {};
4212          }
4213          window._mapglPreservedData[map.getContainer().id] = {
4214            sources: userSourceIds.map((id) => ({
4215              id,
4216              source: currentStyle.sources[id],
4217            })),
4218            layers: userLayers,
4219          };
4220
4221          // Set up event listener to re-add sources and layers after style loads
4222          const onStyleLoad = function () {
4223            console.log("[MapGL Debug] style.load event fired");
4224
4225            try {
4226              // Re-add user sources
4227              userSourceIds.forEach(function (sourceId) {
4228                try {
4229                  if (!map.getSource(sourceId)) {
4230                    const source = currentStyle.sources[sourceId];
4231                    console.log("[MapGL Debug] Re-adding source:", sourceId);
4232                    map.addSource(sourceId, source);
4233                  }
4234                } catch (err) {
4235                  console.error(
4236                    "[MapGL Debug] Error re-adding source:",
4237                    sourceId,
4238                    err,
4239                  );
4240                }
4241              });
4242
4243              // Re-add user layers
4244              userLayers.forEach(function (layer) {
4245                try {
4246                  if (!map.getLayer(layer.id)) {
4247                    console.log("[MapGL Debug] Re-adding layer:", layer.id);
4248                    map.addLayer(layer);
4249
4250                    // Explicitly set visibility if it was set to 'none'
4251                    if (layer.layout && layer.layout.visibility === 'none') {
4252                      map.setLayoutProperty(layer.id, 'visibility', 'none');
4253                      console.log(`[MapGL Debug] Explicitly set ${layer.id} visibility to none`);
4254                    }
4255
4256                    // Re-add event handlers for tooltips and hover effects
4257                    if (layer._handlers) {
4258                      const handlers = layer._handlers;
4259
4260                      if (handlers.mousemove) {
4261                        console.log(
4262                          "[MapGL Debug] Re-adding mousemove handler for:",
4263                          layer.id,
4264                        );
4265                        map.on("mousemove", layer.id, handlers.mousemove);
4266                      }
4267
4268                      if (handlers.mouseleave) {
4269                        console.log(
4270                          "[MapGL Debug] Re-adding mouseleave handler for:",
4271                          layer.id,
4272                        );
4273                        map.on("mouseleave", layer.id, handlers.mouseleave);
4274                      }
4275                    }
4276
4277                    // Check if we need to restore tooltip handlers
4278                    const layerId = layer.id;
4279                    if (layerId === "nc_counties" || layer.tooltip) {
4280                      console.log(
4281                        "[MapGL Debug] Restoring tooltip for:",
4282                        layerId,
4283                      );
4284
4285                      // Create a new tooltip popup
4286                      const tooltip = new maplibregl.Popup({
4287                        closeButton: false,
4288                        closeOnClick: false,
4289                        maxWidth: "400px",
4290                      });
4291                      applyPopupClass(tooltip, layer.tooltip_style);
4292
4293                      // Re-add tooltip handlers
4294                      const tooltipProperty = layer.tooltip || "NAME";
4295
4296                      const mouseMoveHandler = function (e) {
4297                        map.getCanvas().style.cursor = "pointer";
4298                        if (e.features.length > 0) {
4299                          // Use the shared resolver so {brace} templates and
4300                          // concat()/number_format() expressions keep working.
4301                          const description = resolveTooltipContent(
4302                            tooltipProperty,
4303                            e.features[0].properties,
4304                          );
4305                          tooltip
4306                            .setLngLat(e.lngLat)
4307                            .setHTML(description)
4308                            .addTo(map);
4309                        }
4310                      };
4311
4312                      const mouseLeaveHandler = function () {
4313                        map.getCanvas().style.cursor = "";
4314                        tooltip.remove();
4315                      };
4316
4317                      map.on("mousemove", layerId, mouseMoveHandler);
4318                      map.on("mouseleave", layerId, mouseLeaveHandler);
4319
4320                      // Store these handlers
4321                      if (!window._mapboxHandlers) {
4322                        window._mapboxHandlers = {};
4323                      }
4324                      window._mapboxHandlers[layerId] = {
4325                        mousemove: mouseMoveHandler,
4326                        mouseleave: mouseLeaveHandler,
4327                      };
4328                    }
4329
4330                    // Recreate hover states if needed
4331                    if (layer.paint) {
4332                      for (const key in layer.paint) {
4333                        const value = layer.paint[key];
4334                        if (
4335                          Array.isArray(value) &&
4336                          value[0] === "case" &&
4337                          Array.isArray(value[1]) &&
4338                          value[1][0] === "boolean" &&
4339                          value[1][1][0] === "feature-state" &&
4340                          value[1][1][1] === "hover"
4341                        ) {
4342                          // This is a hover-enabled paint property
4343                          map.setPaintProperty(layer.id, key, value);
4344                        }
4345                      }
4346                    }
4347                  }
4348                } catch (err) {
4349                  console.error(
4350                    "[MapGL Debug] Error re-adding layer:",
4351                    layer.id,
4352                    err,
4353                  );
4354                }
4355              });
4356            } catch (err) {
4357              console.error("[MapGL Debug] Error in style.load handler:", err);
4358            }
4359
4360            // Clear any active tooltips before restoration to prevent stacking
4361            if (window._activeTooltip) {
4362              window._activeTooltip.remove();
4363              delete window._activeTooltip;
4364            }
4365
4366            // Restore tracked layer modifications
4367            const mapId = map.getContainer().id;
4368            const savedLayerState =
4369              window._mapglLayerState && window._mapglLayerState[mapId];
4370            if (savedLayerState) {
4371              console.log(
4372                "[MapGL Debug] Restoring tracked layer modifications",
4373              );
4374
4375              // Restore filters
4376              for (const layerId in savedLayerState.filters) {
4377                if (map.getLayer(layerId)) {
4378                  console.log(
4379                    "[MapGL Debug] Restoring filter for layer:",
4380                    layerId,
4381                  );
4382                  map.setFilter(layerId, savedLayerState.filters[layerId]);
4383                }
4384              }
4385
4386              // Restore paint properties
4387              for (const layerId in savedLayerState.paintProperties) {
4388                if (map.getLayer(layerId)) {
4389                  const properties = savedLayerState.paintProperties[layerId];
4390                  for (const propertyName in properties) {
4391                    const savedValue = properties[propertyName];
4392
4393                    console.log(
4394                      "[MapGL Debug] Restoring paint property:",
4395                      layerId,
4396                      propertyName,
4397                      savedValue,
4398                    );
4399
4400                    // Check if layer has hover effects that need to be preserved
4401                    const currentValue = map.getPaintProperty(
4402                      layerId,
4403                      propertyName,
4404                    );
4405                    if (
4406                      currentValue &&
4407                      Array.isArray(currentValue) &&
4408                      currentValue[0] === "case"
4409                    ) {
4410                      // Preserve hover effects while updating base value
4411                      const hoverValue = currentValue[2];
4412                      const newPaintProperty = [
4413                        "case",
4414                        ["boolean", ["feature-state", "hover"], false],
4415                        hoverValue,
4416                        savedValue,
4417                      ];
4418                      map.setPaintProperty(
4419                        layerId,
4420                        propertyName,
4421                        newPaintProperty,
4422                      );
4423                    } else {
4424                      map.setPaintProperty(layerId, propertyName, savedValue);
4425                    }
4426                  }
4427                }
4428              }
4429
4430              // Restore layout properties
4431              for (const layerId in savedLayerState.layoutProperties) {
4432                if (map.getLayer(layerId)) {
4433                  const properties = savedLayerState.layoutProperties[layerId];
4434                  for (const propertyName in properties) {
4435                    console.log(
4436                      "[MapGL Debug] Restoring layout property:",
4437                      layerId,
4438                      propertyName,
4439                      properties[propertyName],
4440                    );
4441                    map.setLayoutProperty(
4442                      layerId,
4443                      propertyName,
4444                      properties[propertyName],
4445                    );
4446                  }
4447                }
4448              }
4449
4450              // Restore tooltips
4451              for (const layerId in savedLayerState.tooltips) {
4452                if (map.getLayer(layerId)) {
4453                  const tooltipProperty = savedLayerState.tooltips[layerId];
4454                  console.log(
4455                    "[MapGL Debug] Restoring tooltip:",
4456                    layerId,
4457                    tooltipProperty,
4458                  );
4459
4460                  // Remove existing tooltip handlers first
4461                  if (
4462                    window._mapboxHandlers &&
4463                    window._mapboxHandlers[layerId]
4464                  ) {
4465                    if (window._mapboxHandlers[layerId].mousemove) {
4466                      map.off(
4467                        "mousemove",
4468                        layerId,
4469                        window._mapboxHandlers[layerId].mousemove,
4470                      );
4471                    }
4472                    if (window._mapboxHandlers[layerId].mouseleave) {
4473                      map.off(
4474                        "mouseleave",
4475                        layerId,
4476                        window._mapboxHandlers[layerId].mouseleave,
4477                      );
4478                    }
4479                  }
4480
4481                  // Create new tooltip
4482                  const tooltip = new maplibregl.Popup({
4483                    closeButton: false,
4484                    closeOnClick: false,
4485                    maxWidth: "400px",
4486                  });
4487                  applyPopupClass(
4488                    tooltip,
4489                    (savedLayerState.tooltipStyles &&
4490                      savedLayerState.tooltipStyles[layerId]) ||
4491                      null,
4492                  );
4493
4494                  const mouseMoveHandler = function (e) {
4495                    onMouseMoveTooltip(
4496                      e,
4497                      map,
4498                      tooltip,
4499                      tooltipProperty,
4500                      layerId,
4501                    );
4502                  };
4503
4504                  const mouseLeaveHandler = function () {
4505                    onMouseLeaveTooltip(map, tooltip);
4506                  };
4507
4508                  map.on("mousemove", layerId, mouseMoveHandler);
4509                  map.on("mouseleave", layerId, mouseLeaveHandler);
4510
4511                  // Store handler references
4512                  if (!window._mapboxHandlers) {
4513                    window._mapboxHandlers = {};
4514                  }
4515                  window._mapboxHandlers[layerId] = {
4516                    mousemove: mouseMoveHandler,
4517                    mouseleave: mouseLeaveHandler,
4518                  };
4519                }
4520              }
4521
4522              // Restore popups
4523              for (const layerId in savedLayerState.popups) {
4524                if (map.getLayer(layerId)) {
4525                  const popupProperty = savedLayerState.popups[layerId];
4526                  console.log(
4527                    "[MapGL Debug] Restoring popup:",
4528                    layerId,
4529                    popupProperty,
4530                  );
4531
4532                  // Remove existing popup handlers first
4533                  if (
4534                    window._mapboxClickHandlers &&
4535                    window._mapboxClickHandlers[layerId]
4536                  ) {
4537                    map.off(
4538                      "click",
4539                      layerId,
4540                      window._mapboxClickHandlers[layerId],
4541                    );
4542                  }
4543
4544                  // Create new popup handler
4545                  const clickHandler = function (e) {
4546                    onClickPopup(
4547                      e,
4548                      map,
4549                      popupProperty,
4550                      layerId,
4551                      (savedLayerState.popupStyles &&
4552                        savedLayerState.popupStyles[layerId]) ||
4553                        null,
4554                    );
4555                  };
4556
4557                  map.on("click", layerId, clickHandler);
4558
4559                  // Add hover effects for cursor
4560                  map.on("mouseenter", layerId, function () {
4561                    map.getCanvas().style.cursor = "pointer";
4562                  });
4563                  map.on("mouseleave", layerId, function () {
4564                    map.getCanvas().style.cursor = "";
4565                  });
4566
4567                  // Store handler reference
4568                  if (!window._mapboxClickHandlers) {
4569                    window._mapboxClickHandlers = {};
4570                  }
4571                  window._mapboxClickHandlers[layerId] = clickHandler;
4572                }
4573              }
4574
4575              // Restore legends
4576              if (Object.keys(savedLayerState.legends).length > 0) {
4577                // Clear any existing legends first to prevent stacking
4578                const existingLegends = document.querySelectorAll(
4579                  `#${mapId} .mapboxgl-legend`,
4580                );
4581                existingLegends.forEach((legend) => legend.remove());
4582
4583                // Clear existing legend styles
4584                const legendStyles = document.querySelectorAll(
4585                  `style[data-mapgl-legend-css="${mapId}"]`,
4586                );
4587                legendStyles.forEach((style) => style.remove());
4588
4589                // Restore each legend
4590                for (const legendId in savedLayerState.legends) {
4591                  const legendData = savedLayerState.legends[legendId];
4592                  console.log("[MapGL Debug] Restoring legend:", legendId);
4593
4594                  // Add legend CSS
4595                  const legendCss = document.createElement("style");
4596                  legendCss.innerHTML = legendData.css;
4597                  legendCss.setAttribute("data-mapgl-legend-css", mapId);
4598                  document.head.appendChild(legendCss);
4599
4600                  // Add legend HTML
4601                  const legend = document.createElement("div");
4602                  legend.innerHTML = legendData.html;
4603                  legend.classList.add("mapboxgl-legend");
4604                  const mapContainer = document.getElementById(mapId);
4605                  if (mapContainer) {
4606                    mapContainer.appendChild(legend);
4607                  }
4608                }
4609              }
4610            }
4611
4612            // Re-add user images after style change
4613            reAddUserImages(map, mapId);
4614
4615            // Sync layers-control link states with the restored visibility
4616            (map._mapglLayersControls || []).forEach((c) =>
4617              c.syncVisibilityStates(),
4618            );
4619
4620            // Mark style loading as complete and execute any pending operations
4621            window._mapgl.styleLoading[mapId] = false;
4622            console.log("[MapGL Debug] Style loading completed for map:", mapId);
4623            executePendingOperations(mapId);
4624
4625          };
4626
4627          map.once("style.load", function() {
4628            // Wait for map to be fully idle before adding layers
4629            map.once("idle", onStyleLoad);
4630          });
4631
4632          // Add a backup mechanism specific to MapLibre
4633          // Some MapLibre styles or versions may have different event timing
4634          if (userLayers.length > 0) {
4635            // Set a timeout to check if layers were added after a reasonable delay
4636            setTimeout(function () {
4637              try {
4638                console.log("[MapGL Debug] Running backup layer check");
4639                const mapId = map.getContainer().id;
4640                const preserved =
4641                  window._mapglPreservedData &&
4642                  window._mapglPreservedData[mapId];
4643
4644                if (preserved) {
4645                  // Check if user layers were successfully restored
4646                  const firstLayerId = preserved.layers[0]?.id;
4647                  if (firstLayerId && !map.getLayer(firstLayerId)) {
4648                    console.log(
4649                      "[MapGL Debug] Backup restoration needed for layers",
4650                    );
4651
4652                    // Re-add sources first
4653                    preserved.sources.forEach(function (src) {
4654                      try {
4655                        if (!map.getSource(src.id)) {
4656                          console.log(
4657                            "[MapGL Debug] Backup: adding source",
4658                            src.id,
4659                          );
4660                          map.addSource(src.id, src.source);
4661                        }
4662                      } catch (err) {
4663                        console.error(
4664                          "[MapGL Debug] Backup: error adding source",
4665                          src.id,
4666                          err,
4667                        );
4668                      }
4669                    });
4670
4671                    // Then re-add layers
4672                    preserved.layers.forEach(function (layer) {
4673                      try {
4674                        if (!map.getLayer(layer.id)) {
4675                          console.log(
4676                            "[MapGL Debug] Backup: adding layer",
4677                            layer.id,
4678                          );
4679                          map.addLayer(layer);
4680
4681                          // Check for nc_counties layer to restore tooltip
4682                          if (layer.id === "nc_counties") {
4683                            console.log(
4684                              "[MapGL Debug] Backup: restoring tooltip for",
4685                              layer.id,
4686                            );
4687
4688                            // Create a new tooltip popup
4689                            const tooltip = new maplibregl.Popup({
4690                              closeButton: false,
4691                              closeOnClick: false,
4692                              maxWidth: "400px",
4693                            });
4694
4695                            // Re-add tooltip handlers
4696                            const tooltipProperty = "NAME";
4697
4698                            const mouseMoveHandler = function (e) {
4699                              map.getCanvas().style.cursor = "pointer";
4700                              if (e.features.length > 0) {
4701                                const description =
4702                                  e.features[0].properties[tooltipProperty];
4703                                tooltip
4704                                  .setLngLat(e.lngLat)
4705                                  .setHTML(description)
4706                                  .addTo(map);
4707                              }
4708                            };
4709
4710                            const mouseLeaveHandler = function () {
4711                              map.getCanvas().style.cursor = "";
4712                              tooltip.remove();
4713                            };
4714
4715                            map.on("mousemove", layer.id, mouseMoveHandler);
4716                            map.on("mouseleave", layer.id, mouseLeaveHandler);
4717
4718                            // Store these handlers
4719                            if (!window._mapboxHandlers) {
4720                              window._mapboxHandlers = {};
4721                            }
4722                            window._mapboxHandlers[layer.id] = {
4723                              mousemove: mouseMoveHandler,
4724                              mouseleave: mouseLeaveHandler,
4725                            };
4726                          }
4727
4728                          // Restore hover states
4729                          if (layer.paint) {
4730                            for (const key in layer.paint) {
4731                              const value = layer.paint[key];
4732                              if (
4733                                Array.isArray(value) &&
4734                                value[0] === "case" &&
4735                                Array.isArray(value[1]) &&
4736                                value[1][0] === "boolean" &&
4737                                value[1][1] &&
4738                                Array.isArray(value[1][1]) &&
4739                                value[1][1][0] === "feature-state" &&
4740                                value[1][1][1] === "hover"
4741                              ) {
4742                                // This is a hover-enabled paint property
4743                                console.log(
4744                                  "[MapGL Debug] Backup: restoring hover style for",
4745                                  layer.id,
4746                                  key,
4747                                );
4748                                map.setPaintProperty(layer.id, key, value);
4749                              }
4750                            }
4751                          }
4752                        }
4753                      } catch (err) {
4754                        console.error(
4755                          "[MapGL Debug] Backup: error adding layer",
4756                          layer.id,
4757                          err,
4758                        );
4759                      }
4760                    });
4761
4762                    // Restore tracked layer modifications in backup
4763                    const mapId = map.getContainer().id;
4764                    const savedLayerState =
4765                      window._mapglLayerState && window._mapglLayerState[mapId];
4766                    if (savedLayerState) {
4767                      console.log(
4768                        "[MapGL Debug] Backup: restoring tracked layer modifications",
4769                      );
4770
4771                      // Restore filters
4772                      for (const layerId in savedLayerState.filters) {
4773                        if (map.getLayer(layerId)) {
4774                          console.log(
4775                            "[MapGL Debug] Backup: restoring filter for layer:",
4776                            layerId,
4777                          );
4778                          map.setFilter(
4779                            layerId,
4780                            savedLayerState.filters[layerId],
4781                          );
4782                        }
4783                      }
4784
4785                      // Restore paint properties
4786                      for (const layerId in savedLayerState.paintProperties) {
4787                        if (map.getLayer(layerId)) {
4788                          const properties =
4789                            savedLayerState.paintProperties[layerId];
4790                          for (const propertyName in properties) {
4791                            const savedValue = properties[propertyName];
4792                            console.log(
4793                              "[MapGL Debug] Backup: restoring paint property:",
4794                              layerId,
4795                              propertyName,
4796                              savedValue,
4797                            );
4798
4799                            // Check if layer has hover effects that need to be preserved
4800                            const currentValue = map.getPaintProperty(
4801                              layerId,
4802                              propertyName,
4803                            );
4804                            if (
4805                              currentValue &&
4806                              Array.isArray(currentValue) &&
4807                              currentValue[0] === "case"
4808                            ) {
4809                              // Preserve hover effects while updating base value
4810                              const hoverValue = currentValue[2];
4811                              const newPaintProperty = [
4812                                "case",
4813                                ["boolean", ["feature-state", "hover"], false],
4814                                hoverValue,
4815                                savedValue,
4816                              ];
4817                              map.setPaintProperty(
4818                                layerId,
4819                                propertyName,
4820                                newPaintProperty,
4821                              );
4822                            } else {
4823                              map.setPaintProperty(
4824                                layerId,
4825                                propertyName,
4826                                savedValue,
4827                              );
4828                            }
4829                          }
4830                        }
4831                      }
4832
4833                      // Restore layout properties
4834                      for (const layerId in savedLayerState.layoutProperties) {
4835                        if (map.getLayer(layerId)) {
4836                          const properties =
4837                            savedLayerState.layoutProperties[layerId];
4838                          for (const propertyName in properties) {
4839                            console.log(
4840                              "[MapGL Debug] Backup: restoring layout property:",
4841                              layerId,
4842                              propertyName,
4843                              properties[propertyName],
4844                            );
4845                            map.setLayoutProperty(
4846                              layerId,
4847                              propertyName,
4848                              properties[propertyName],
4849                            );
4850                          }
4851                        }
4852                      }
4853
4854                      // Restore tooltips
4855                      for (const layerId in savedLayerState.tooltips) {
4856                        if (map.getLayer(layerId)) {
4857                          const tooltipProperty =
4858                            savedLayerState.tooltips[layerId];
4859                          console.log(
4860                            "[MapGL Debug] Backup: restoring tooltip:",
4861                            layerId,
4862                            tooltipProperty,
4863                          );
4864
4865                          if (
4866                            window._mapboxHandlers &&
4867                            window._mapboxHandlers[layerId]
4868                          ) {
4869                            if (window._mapboxHandlers[layerId].mousemove) {
4870                              map.off(
4871                                "mousemove",
4872                                layerId,
4873                                window._mapboxHandlers[layerId].mousemove,
4874                              );
4875                            }
4876                            if (window._mapboxHandlers[layerId].mouseleave) {
4877                              map.off(
4878                                "mouseleave",
4879                                layerId,
4880                                window._mapboxHandlers[layerId].mouseleave,
4881                              );
4882                            }
4883                          }
4884
4885                          const tooltip = new maplibregl.Popup({
4886                            closeButton: false,
4887                            closeOnClick: false,
4888                            maxWidth: "400px",
4889                          });
4890                          applyPopupClass(
4891                            tooltip,
4892                            (savedLayerState.tooltipStyles &&
4893                              savedLayerState.tooltipStyles[layerId]) ||
4894                              null,
4895                          );
4896
4897                          const mouseMoveHandler = function (e) {
4898                            onMouseMoveTooltip(
4899                              e,
4900                              map,
4901                              tooltip,
4902                              tooltipProperty,
4903                              layerId,
4904                            );
4905                          };
4906
4907                          const mouseLeaveHandler = function () {
4908                            onMouseLeaveTooltip(map, tooltip);
4909                          };
4910
4911                          map.on("mousemove", layerId, mouseMoveHandler);
4912                          map.on("mouseleave", layerId, mouseLeaveHandler);
4913
4914                          if (!window._mapboxHandlers) {
4915                            window._mapboxHandlers = {};
4916                          }
4917                          window._mapboxHandlers[layerId] = {
4918                            mousemove: mouseMoveHandler,
4919                            mouseleave: mouseLeaveHandler,
4920                          };
4921                        }
4922                      }
4923
4924                      // Restore popups
4925                      for (const layerId in savedLayerState.popups) {
4926                        if (map.getLayer(layerId)) {
4927                          const popupProperty = savedLayerState.popups[layerId];
4928                          console.log(
4929                            "[MapGL Debug] Backup: restoring popup:",
4930                            layerId,
4931                            popupProperty,
4932                          );
4933
4934                          if (
4935                            window._mapboxClickHandlers &&
4936                            window._mapboxClickHandlers[layerId]
4937                          ) {
4938                            map.off(
4939                              "click",
4940                              layerId,
4941                              window._mapboxClickHandlers[layerId],
4942                            );
4943                          }
4944
4945                          const clickHandler = function (e) {
4946                            onClickPopup(
4947                              e,
4948                              map,
4949                              popupProperty,
4950                              layerId,
4951                              (savedLayerState.popupStyles &&
4952                                savedLayerState.popupStyles[layerId]) ||
4953                                null,
4954                            );
4955                          };
4956
4957                          map.on("click", layerId, clickHandler);
4958                          map.on("mouseenter", layerId, function () {
4959                            map.getCanvas().style.cursor = "pointer";
4960                          });
4961                          map.on("mouseleave", layerId, function () {
4962                            map.getCanvas().style.cursor = "";
4963                          });
4964
4965                          if (!window._mapboxClickHandlers) {
4966                            window._mapboxClickHandlers = {};
4967                          }
4968                          window._mapboxClickHandlers[layerId] = clickHandler;
4969                        }
4970                      }
4971
4972                      // Restore legends
4973                      if (Object.keys(savedLayerState.legends).length > 0) {
4974                        // Clear any existing legends first to prevent stacking
4975                        const existingLegends = document.querySelectorAll(
4976                          `#${mapId} .mapboxgl-legend`,
4977                        );
4978                        existingLegends.forEach((legend) => legend.remove());
4979
4980                        // Clear existing legend styles
4981                        const legendStyles = document.querySelectorAll(
4982                          `style[data-mapgl-legend-css="${mapId}"]`,
4983                        );
4984                        legendStyles.forEach((style) => style.remove());
4985
4986                        // Restore each legend
4987                        for (const legendId in savedLayerState.legends) {
4988                          const legendData = savedLayerState.legends[legendId];
4989                          console.log(
4990                            "[MapGL Debug] Backup: restoring legend:",
4991                            legendId,
4992                          );
4993
4994                          // Add legend CSS
4995                          const legendCss = document.createElement("style");
4996                          legendCss.innerHTML = legendData.css;
4997                          legendCss.setAttribute(
4998                            "data-mapgl-legend-css",
4999                            mapId,
5000                          );
5001                          document.head.appendChild(legendCss);
5002
5003                          // Add legend HTML
5004                          const legend = document.createElement("div");
5005                          legend.innerHTML = legendData.html;
5006                          legend.classList.add("mapboxgl-legend");
5007                          const mapContainer = document.getElementById(mapId);
5008                          if (mapContainer) {
5009                            mapContainer.appendChild(legend);
5010                          }
5011                        }
5012                      }
5013                    }
5014                  } else {
5015                    console.log(
5016                      "[MapGL Debug] Backup check: layers already restored properly",
5017                    );
5018                  }
5019                }
5020              } catch (err) {
5021                console.error(
5022                  "[MapGL Debug] Error in backup restoration:",
5023                  err,
5024                );
5025              }
5026            }, 500); // 500ms delay - faster recovery
5027
5028            // Add a second backup with a bit more delay in case the first one fails
5029            setTimeout(function () {
5030              try {
5031                console.log("[MapGL Debug] Running second backup layer check");
5032                const mapId = map.getContainer().id;
5033                const preserved =
5034                  window._mapglPreservedData &&
5035                  window._mapglPreservedData[mapId];
5036
5037                if (preserved) {
5038                  // Check if user layers were successfully restored
5039                  const firstLayerId = preserved.layers[0]?.id;
5040                  if (firstLayerId && !map.getLayer(firstLayerId)) {
5041                    console.log(
5042                      "[MapGL Debug] Second backup restoration needed",
5043                    );
5044
5045                    // Re-add sources first
5046                    preserved.sources.forEach(function (src) {
5047                      try {
5048                        if (!map.getSource(src.id)) {
5049                          console.log(
5050                            "[MapGL Debug] Second backup: adding source",
5051                            src.id,
5052                          );
5053                          map.addSource(src.id, src.source);
5054                        }
5055                      } catch (err) {
5056                        console.error(
5057                          "[MapGL Debug] Second backup: error adding source",
5058                          src.id,
5059                          err,
5060                        );
5061                      }
5062                    });
5063
5064                    // Then re-add layers
5065                    preserved.layers.forEach(function (layer) {
5066                      try {
5067                        if (!map.getLayer(layer.id)) {
5068                          console.log(
5069                            "[MapGL Debug] Second backup: adding layer",
5070                            layer.id,
5071                          );
5072                          map.addLayer(layer);
5073
5074                          // Check for nc_counties layer to restore tooltip
5075                          if (layer.id === "nc_counties") {
5076                            console.log(
5077                              "[MapGL Debug] Second backup: restoring tooltip for",
5078                              layer.id,
5079                            );
5080
5081                            // Create a new tooltip popup
5082                            const tooltip = new maplibregl.Popup({
5083                              closeButton: false,
5084                              closeOnClick: false,
5085                              maxWidth: "400px",
5086                            });
5087
5088                            // Re-add tooltip handlers
5089                            const tooltipProperty = "NAME";
5090
5091                            const mouseMoveHandler = function (e) {
5092                              map.getCanvas().style.cursor = "pointer";
5093                              if (e.features.length > 0) {
5094                                const description =
5095                                  e.features[0].properties[tooltipProperty];
5096                                tooltip
5097                                  .setLngLat(e.lngLat)
5098                                  .setHTML(description)
5099                                  .addTo(map);
5100                              }
5101                            };
5102
5103                            const mouseLeaveHandler = function () {
5104                              map.getCanvas().style.cursor = "";
5105                              tooltip.remove();
5106                            };
5107
5108                            map.on("mousemove", layer.id, mouseMoveHandler);
5109                            map.on("mouseleave", layer.id, mouseLeaveHandler);
5110
5111                            // Store these handlers
5112                            if (!window._mapboxHandlers) {
5113                              window._mapboxHandlers = {};
5114                            }
5115                            window._mapboxHandlers[layer.id] = {
5116                              mousemove: mouseMoveHandler,
5117                              mouseleave: mouseLeaveHandler,
5118                            };
5119                          }
5120
5121                          // Restore hover states
5122                          if (layer.paint) {
5123                            for (const key in layer.paint) {
5124                              const value = layer.paint[key];
5125                              if (
5126                                Array.isArray(value) &&
5127                                value[0] === "case" &&
5128                                Array.isArray(value[1]) &&
5129                                value[1][0] === "boolean" &&
5130                                value[1][1] &&
5131                                Array.isArray(value[1][1]) &&
5132                                value[1][1][0] === "feature-state" &&
5133                                value[1][1][1] === "hover"
5134                              ) {
5135                                // This is a hover-enabled paint property
5136                                console.log(
5137                                  "[MapGL Debug] Second backup: restoring hover style for",
5138                                  layer.id,
5139                                  key,
5140                                );
5141                                map.setPaintProperty(layer.id, key, value);
5142                              }
5143                            }
5144                          }
5145                        }
5146                      } catch (err) {
5147                        console.error(
5148                          "[MapGL Debug] Second backup: error adding layer",
5149                          layer.id,
5150                          err,
5151                        );
5152                      }
5153                    });
5154
5155                    // Restore tracked layer modifications in second backup
5156                    const mapId = map.getContainer().id;
5157                    const savedLayerState =
5158                      window._mapglLayerState && window._mapglLayerState[mapId];
5159                    if (savedLayerState) {
5160                      console.log(
5161                        "[MapGL Debug] Second backup: restoring tracked layer modifications",
5162                      );
5163
5164                      // Restore filters
5165                      for (const layerId in savedLayerState.filters) {
5166                        if (map.getLayer(layerId)) {
5167                          console.log(
5168                            "[MapGL Debug] Second backup: restoring filter for layer:",
5169                            layerId,
5170                          );
5171                          map.setFilter(
5172                            layerId,
5173                            savedLayerState.filters[layerId],
5174                          );
5175                        }
5176                      }
5177
5178                      // Restore paint properties
5179                      for (const layerId in savedLayerState.paintProperties) {
5180                        if (map.getLayer(layerId)) {
5181                          const properties =
5182                            savedLayerState.paintProperties[layerId];
5183                          for (const propertyName in properties) {
5184                            const savedValue = properties[propertyName];
5185                            console.log(
5186                              "[MapGL Debug] Second backup: restoring paint property:",
5187                              layerId,
5188                              propertyName,
5189                              savedValue,
5190                            );
5191
5192                            // Check if layer has hover effects that need to be preserved
5193                            const currentValue = map.getPaintProperty(
5194                              layerId,
5195                              propertyName,
5196                            );
5197                            if (
5198                              currentValue &&
5199                              Array.isArray(currentValue) &&
5200                              currentValue[0] === "case"
5201                            ) {
5202                              // Preserve hover effects while updating base value
5203                              const hoverValue = currentValue[2];
5204                              const newPaintProperty = [
5205                                "case",
5206                                ["boolean", ["feature-state", "hover"], false],
5207                                hoverValue,
5208                                savedValue,
5209                              ];
5210                              map.setPaintProperty(
5211                                layerId,
5212                                propertyName,
5213                                newPaintProperty,
5214                              );
5215                            } else {
5216                              map.setPaintProperty(
5217                                layerId,
5218                                propertyName,
5219                                savedValue,
5220                              );
5221                            }
5222                          }
5223                        }
5224                      }
5225
5226                      // Restore layout properties
5227                      for (const layerId in savedLayerState.layoutProperties) {
5228                        if (map.getLayer(layerId)) {
5229                          const properties =
5230                            savedLayerState.layoutProperties[layerId];
5231                          for (const propertyName in properties) {
5232                            console.log(
5233                              "[MapGL Debug] Second backup: restoring layout property:",
5234                              layerId,
5235                              propertyName,
5236                              properties[propertyName],
5237                            );
5238                            map.setLayoutProperty(
5239                              layerId,
5240                              propertyName,
5241                              properties[propertyName],
5242                            );
5243                          }
5244                        }
5245                      }
5246
5247                      // Restore tooltips
5248                      for (const layerId in savedLayerState.tooltips) {
5249                        if (map.getLayer(layerId)) {
5250                          const tooltipProperty =
5251                            savedLayerState.tooltips[layerId];
5252                          console.log(
5253                            "[MapGL Debug] Second backup: restoring tooltip:",
5254                            layerId,
5255                            tooltipProperty,
5256                          );
5257
5258                          if (
5259                            window._mapboxHandlers &&
5260                            window._mapboxHandlers[layerId]
5261                          ) {
5262                            if (window._mapboxHandlers[layerId].mousemove) {
5263                              map.off(
5264                                "mousemove",
5265                                layerId,
5266                                window._mapboxHandlers[layerId].mousemove,
5267                              );
5268                            }
5269                            if (window._mapboxHandlers[layerId].mouseleave) {
5270                              map.off(
5271                                "mouseleave",
5272                                layerId,
5273                                window._mapboxHandlers[layerId].mouseleave,
5274                              );
5275                            }
5276                          }
5277
5278                          const tooltip = new maplibregl.Popup({
5279                            closeButton: false,
5280                            closeOnClick: false,
5281                            maxWidth: "400px",
5282                          });
5283                          applyPopupClass(
5284                            tooltip,
5285                            (savedLayerState.tooltipStyles &&
5286                              savedLayerState.tooltipStyles[layerId]) ||
5287                              null,
5288                          );
5289
5290                          const mouseMoveHandler = function (e) {
5291                            onMouseMoveTooltip(
5292                              e,
5293                              map,
5294                              tooltip,
5295                              tooltipProperty,
5296                              layerId,
5297                            );
5298                          };
5299
5300                          const mouseLeaveHandler = function () {
5301                            onMouseLeaveTooltip(map, tooltip);
5302                          };
5303
5304                          map.on("mousemove", layerId, mouseMoveHandler);
5305                          map.on("mouseleave", layerId, mouseLeaveHandler);
5306
5307                          if (!window._mapboxHandlers) {
5308                            window._mapboxHandlers = {};
5309                          }
5310                          window._mapboxHandlers[layerId] = {
5311                            mousemove: mouseMoveHandler,
5312                            mouseleave: mouseLeaveHandler,
5313                          };
5314                        }
5315                      }
5316
5317                      // Restore popups
5318                      for (const layerId in savedLayerState.popups) {
5319                        if (map.getLayer(layerId)) {
5320                          const popupProperty = savedLayerState.popups[layerId];
5321                          console.log(
5322                            "[MapGL Debug] Second backup: restoring popup:",
5323                            layerId,
5324                            popupProperty,
5325                          );
5326
5327                          if (
5328                            window._mapboxClickHandlers &&
5329                            window._mapboxClickHandlers[layerId]
5330                          ) {
5331                            map.off(
5332                              "click",
5333                              layerId,
5334                              window._mapboxClickHandlers[layerId],
5335                            );
5336                          }
5337
5338                          const clickHandler = function (e) {
5339                            onClickPopup(
5340                              e,
5341                              map,
5342                              popupProperty,
5343                              layerId,
5344                              (savedLayerState.popupStyles &&
5345                                savedLayerState.popupStyles[layerId]) ||
5346                                null,
5347                            );
5348                          };
5349
5350                          map.on("click", layerId, clickHandler);
5351                          map.on("mouseenter", layerId, function () {
5352                            map.getCanvas().style.cursor = "pointer";
5353                          });
5354                          map.on("mouseleave", layerId, function () {
5355                            map.getCanvas().style.cursor = "";
5356                          });
5357
5358                          if (!window._mapboxClickHandlers) {
5359                            window._mapboxClickHandlers = {};
5360                          }
5361                          window._mapboxClickHandlers[layerId] = clickHandler;
5362                        }
5363                      }
5364
5365                      // Restore legends
5366                      if (Object.keys(savedLayerState.legends).length > 0) {
5367                        // Clear any existing legends first to prevent stacking
5368                        const existingLegends = document.querySelectorAll(
5369                          `#${mapId} .mapboxgl-legend`,
5370                        );
5371                        existingLegends.forEach((legend) => legend.remove());
5372
5373                        // Clear existing legend styles
5374                        const legendStyles = document.querySelectorAll(
5375                          `style[data-mapgl-legend-css="${mapId}"]`,
5376                        );
5377                        legendStyles.forEach((style) => style.remove());
5378
5379                        // Restore each legend
5380                        for (const legendId in savedLayerState.legends) {
5381                          const legendData = savedLayerState.legends[legendId];
5382                          console.log(
5383                            "[MapGL Debug] Second backup: restoring legend:",
5384                            legendId,
5385                          );
5386
5387                          // Add legend CSS
5388                          const legendCss = document.createElement("style");
5389                          legendCss.innerHTML = legendData.css;
5390                          legendCss.setAttribute(
5391                            "data-mapgl-legend-css",
5392                            mapId,
5393                          );
5394                          document.head.appendChild(legendCss);
5395
5396                          // Add legend HTML
5397                          const legend = document.createElement("div");
5398                          legend.innerHTML = legendData.html;
5399                          legend.classList.add("mapboxgl-legend");
5400                          const mapContainer = document.getElementById(mapId);
5401                          if (mapContainer) {
5402                            mapContainer.appendChild(legend);
5403                          }
5404                        }
5405                      }
5406                    }
5407                  }
5408                }
5409              } catch (err) {
5410                console.error("[MapGL Debug] Error in second backup:", err);
5411              }
5412            }, 1000); // 1 second delay for second backup
5413          }
5414        }
5415
5416        // Change the style
5417        console.log(
5418          "[MapGL Debug] About to call setStyle with:",
5419          message.style,
5420        );
5421        console.log("[MapGL Debug] setStyle diff option:", message.diff);
5422        map.setStyle(message.style, { diff: message.diff });
5423
5424        if (message.config) {
5425          Object.keys(message.config).forEach(function (key) {
5426            map.setConfigProperty("basemap", key, message.config[key]);
5427          });
5428        }
5429
5430        // For non-preserve mode, still need to handle style loading completion
5431        // to re-add images and execute pending operations
5432        if (!preserveLayers) {
5433          map.once("style.load", function() {
5434            map.once("idle", function() {
5435              // Re-add user images after style change
5436              reAddUserImages(map, mapId);
5437
5438              // Mark style loading as complete and execute any pending operations
5439              window._mapgl.styleLoading[mapId] = false;
5440              console.log("[MapGL Debug] Style loading completed (non-preserve) for map:", mapId);
5441              executePendingOperations(mapId);
5442            });
5443          });
5444        }
5445      } else if (message.type === "add_navigation_control") {
5446        const nav = new maplibregl.NavigationControl({
5447          showCompass: message.options.show_compass,
5448          showZoom: message.options.show_zoom,
5449          visualizePitch: message.options.visualize_pitch,
5450        });
5451        map.addControl(nav, message.position);
5452        map.controls.push({ type: "navigation", control: nav });
5453
5454        if (message.orientation === "horizontal") {
5455          const navBar = map
5456            .getContainer()
5457            .querySelector(
5458              ".maplibregl-ctrl-group:not(.mapbox-gl-draw_ctrl-draw-btn)",
5459            );
5460          if (navBar) {
5461            navBar.style.display = "flex";
5462            navBar.style.flexDirection = "row";
5463          }
5464        }
5465      } else if (message.type === "add_draw_control") {
5466        MapboxDraw.constants.classes.CONTROL_BASE = "maplibregl-ctrl";
5467        MapboxDraw.constants.classes.CONTROL_PREFIX = "maplibregl-ctrl-";
5468        MapboxDraw.constants.classes.CONTROL_GROUP = "maplibregl-ctrl-group";
5469
5470        let drawOptions = message.options || {};
5471
5472        // Generate styles if styling parameters provided
5473        if (message.styling) {
5474          const generatedStyles = generateDrawStyles(message.styling);
5475          if (generatedStyles) {
5476            drawOptions.styles = generatedStyles;
5477          }
5478        }
5479
5480        if (message.freehand) {
5481          drawOptions = Object.assign({}, drawOptions, {
5482            modes: Object.assign({}, MapboxDraw.modes, {
5483              draw_polygon: MapboxDraw.modes.draw_freehand,
5484            }),
5485            // defaultMode: 'draw_polygon' # Don't set the default yet
5486          });
5487        }
5488
5489        // Add rectangle mode if enabled
5490        if (message.rectangle) {
5491          drawOptions.modes = Object.assign(
5492            {},
5493            drawOptions.modes || MapboxDraw.modes,
5494            {
5495              draw_rectangle: MapboxDraw.modes.draw_rectangle,
5496            },
5497          );
5498        }
5499
5500        // Add radius mode if enabled
5501        if (message.radius) {
5502          drawOptions.modes = Object.assign(
5503            {},
5504            drawOptions.modes || MapboxDraw.modes,
5505            {
5506              draw_radius: MapboxDraw.modes.draw_radius,
5507            },
5508          );
5509        }
5510
5511        if (message.bezier || message.bezier_polygon) {
5512          drawOptions = enableBezierDrawMode(drawOptions, message.styling);
5513        }
5514
5515        if (_mapglHasDrawAttributes(message.attributes)) {
5516          drawOptions = Object.assign({}, drawOptions, {
5517            userProperties: true,
5518          });
5519        }
5520
5521        // Create the draw control
5522        var drawControl = new MapboxDraw(drawOptions);
5523        map.addControl(drawControl, message.position);
5524        map.controls.push({ type: "draw", control: drawControl });
5525
5526        // Store the draw control on the widget for later access
5527        widget.drawControl = drawControl;
5528
5529        // Add lasso icon CSS for freehand mode
5530        if (message.freehand) {
5531          if (!document.querySelector("#mapgl-freehand-lasso-styles")) {
5532            const style = document.createElement("style");
5533            style.id = "mapgl-freehand-lasso-styles";
5534            style.textContent = `
5535              .mapbox-gl-draw_polygon {
5536                background-image: url('data:image/svg+xml;utf8,%3Csvg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"%3E%3Cpath d="M 7 3 C 5 2.5, 3.5 3, 3 5 C 2.5 7, 3 8.5, 4 9.5 C 5 10.5, 5.5 11.5, 5 13 C 4.5 14.5, 5 15.5, 6.5 16 C 8 16.5, 9.5 16, 11 15 C 12.5 14, 13.5 13, 14.5 11.5 C 15.5 10, 16 8.5, 15.5 7 C 15 5.5, 14 4.5, 12.5 3.5 C 11 2.5, 9 2.5, 7 3 Z" fill="none" stroke="%23000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/%3E%3C/svg%3E') !important;
5537              }
5538            `;
5539            document.head.appendChild(style);
5540          }
5541
5542          // Update tooltip for freehand mode
5543          setTimeout(() => {
5544            const polygonButton = map
5545              .getContainer()
5546              .querySelector(".mapbox-gl-draw_polygon");
5547            if (polygonButton) {
5548              polygonButton.title = "Freehand polygon tool (p)";
5549            }
5550          }, 100);
5551        }
5552
5553        // Add rectangle icon CSS if rectangle mode is enabled
5554        if (message.rectangle) {
5555          if (!document.querySelector("#mapgl-rectangle-styles")) {
5556            const style = document.createElement("style");
5557            style.id = "mapgl-rectangle-styles";
5558            style.textContent = `
5559              .mapbox-gl-draw_rectangle {
5560                background: transparent;
5561                border: none;
5562                cursor: pointer;
5563                display: block;
5564                height: 30px;
5565                width: 30px;
5566                padding: 0;
5567                outline: none;
5568                background-image: url('data:image/svg+xml;utf8,%3Csvg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"%3E%3Crect x="4" y="5" width="12" height="10" fill="none" stroke="%23000000" stroke-width="2"/%3E%3C/svg%3E') !important;
5569                background-repeat: no-repeat !important;
5570                background-position: center !important;
5571              }
5572              .mapbox-gl-draw_rectangle:hover {
5573                background-color: rgba(0, 0, 0, 0.05);
5574              }
5575              .mapbox-gl-draw_rectangle.active {
5576                background-color: rgba(0, 0, 0, 0.05);
5577              }
5578            `;
5579            document.head.appendChild(style);
5580          }
5581        }
5582
5583        // Add radius/circle icon CSS if radius mode is enabled
5584        if (message.radius) {
5585          if (!document.querySelector("#mapgl-radius-styles")) {
5586            const style = document.createElement("style");
5587            style.id = "mapgl-radius-styles";
5588            style.textContent = `
5589              .mapbox-gl-draw_radius {
5590                background: transparent;
5591                border: none;
5592                cursor: pointer;
5593                display: block;
5594                height: 30px;
5595                width: 30px;
5596                padding: 0;
5597                outline: none;
5598                background-image: url('data:image/svg+xml;utf8,%3Csvg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"%3E%3Ccircle cx="10" cy="10" r="7" fill="none" stroke="%23000000" stroke-width="2"/%3E%3Ccircle cx="10" cy="10" r="1.5" fill="%23000000"/%3E%3C/svg%3E') !important;
5599                background-repeat: no-repeat !important;
5600                background-position: center !important;
5601              }
5602              .mapbox-gl-draw_radius:hover {
5603                background-color: rgba(0, 0, 0, 0.05);
5604              }
5605              .mapbox-gl-draw_radius.active {
5606                background-color: rgba(0, 0, 0, 0.05);
5607              }
5608            `;
5609            document.head.appendChild(style);
5610          }
5611        }
5612
5613        addBezierButtons(map, drawControl, {
5614          bezier: message.bezier,
5615          bezierPolygon: message.bezier_polygon,
5616          controlGroupSelector:
5617            ".maplibregl-ctrl-group:has(.mapbox-gl-draw_polygon)",
5618          iconClass: "maplibregl-ctrl-icon",
5619        });
5620
5621        initializeDrawAttributeEditor(map, drawControl, {
5622          attributes: message.attributes,
5623          position: message.position,
5624          onChange: updateDrawnFeatures,
5625        });
5626
5627        // Add event listeners
5628        map.on("draw.create", () => updateDrawnFeatures(false));
5629        map.on("draw.delete", () => updateDrawnFeatures(false));
5630        map.on("draw.update", () => updateDrawnFeatures(true));
5631
5632        // Add measurement functionality if enabled
5633        if (message.show_measurements) {
5634          initializeMeasurements(map, drawControl, message.measurement_units);
5635        }
5636
5637        // Add initial features if provided
5638        if (message.source) {
5639          addSourceFeaturesToDraw(drawControl, message.source, map);
5640          updateDrawnFeatures(false);
5641        }
5642
5643        // Add rectangle and radius buttons if enabled
5644        setTimeout(() => {
5645          const drawControlGroup = map
5646            .getContainer()
5647            .querySelector(
5648              ".maplibregl-ctrl-group:has(.mapbox-gl-draw_polygon)",
5649            );
5650
5651          if (drawControlGroup) {
5652            // Find the trash button to insert before it
5653            const trashBtn = drawControlGroup.querySelector(
5654              ".mapbox-gl-draw_trash",
5655            );
5656
5657            if (message.rectangle) {
5658              const rectangleBtn = document.createElement("button");
5659              rectangleBtn.className =
5660                "mapbox-gl-draw_rectangle maplibregl-ctrl-icon";
5661              rectangleBtn.title = "Rectangle tool";
5662              rectangleBtn.type = "button";
5663              rectangleBtn.onclick = function () {
5664                drawControl.changeMode("draw_rectangle");
5665                // Remove active class from all buttons and add to this one
5666                drawControlGroup
5667                  .querySelectorAll("button")
5668                  .forEach((b) => b.classList.remove("active"));
5669                rectangleBtn.classList.add("active");
5670              };
5671              // Insert before trash button if it exists, otherwise append
5672              if (trashBtn) {
5673                drawControlGroup.insertBefore(rectangleBtn, trashBtn);
5674              } else {
5675                drawControlGroup.appendChild(rectangleBtn);
5676              }
5677            }
5678
5679            if (message.radius) {
5680              const radiusBtn = document.createElement("button");
5681              radiusBtn.className =
5682                "mapbox-gl-draw_radius maplibregl-ctrl-icon";
5683              radiusBtn.title = "Radius/Circle tool";
5684              radiusBtn.type = "button";
5685              radiusBtn.onclick = function () {
5686                drawControl.changeMode("draw_radius");
5687                // Remove active class from all buttons and add to this one
5688                drawControlGroup
5689                  .querySelectorAll("button")
5690                  .forEach((b) => b.classList.remove("active"));
5691                radiusBtn.classList.add("active");
5692              };
5693              // Insert before trash button if it exists, otherwise append
5694              if (trashBtn) {
5695                drawControlGroup.insertBefore(radiusBtn, trashBtn);
5696              } else {
5697                drawControlGroup.appendChild(radiusBtn);
5698              }
5699            }
5700          }
5701        }, 100);
5702
5703        // Apply orientation styling
5704        if (message.orientation === "horizontal") {
5705          const drawBar = map
5706            .getContainer()
5707            .querySelector(".maplibregl-ctrl-group");
5708          if (drawBar) {
5709            drawBar.style.display = "flex";
5710            drawBar.style.flexDirection = "row";
5711          }
5712        }
5713
5714        // Add download button if requested
5715        if (message.download_button) {
5716          // Add CSS for download button if not already added
5717          if (!document.querySelector("#mapgl-draw-download-styles")) {
5718            const style = document.createElement("style");
5719            style.id = "mapgl-draw-download-styles";
5720            style.textContent = `
5721              .mapbox-gl-draw_download {
5722                background: transparent;
5723                border: none;
5724                cursor: pointer;
5725                display: block;
5726                height: 30px;
5727                width: 30px;
5728                padding: 0;
5729                outline: none;
5730              }
5731              .mapbox-gl-draw_download:hover {
5732                background-color: rgba(0, 0, 0, 0.05);
5733              }
5734              .mapbox-gl-draw_download svg {
5735                width: 20px;
5736                height: 20px;
5737                margin: 5px;
5738                fill: #333;
5739              }
5740            `;
5741            document.head.appendChild(style);
5742          }
5743
5744          // Small delay to ensure Draw control is fully rendered
5745          setTimeout(() => {
5746            // Find the Draw control button group
5747            const drawButtons = map
5748              .getContainer()
5749              .querySelector(
5750                ".maplibregl-ctrl-group:has(.mapbox-gl-draw_polygon)",
5751              );
5752
5753            if (drawButtons) {
5754              // Create download button
5755              const downloadBtn = document.createElement("button");
5756              downloadBtn.className = "mapbox-gl-draw_download";
5757              downloadBtn.title = "Download drawn features as GeoJSON";
5758
5759              // Add SVG download icon
5760              downloadBtn.innerHTML = `
5761                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
5762                  <path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>
5763                </svg>
5764              `;
5765
5766              downloadBtn.addEventListener("click", () => {
5767                // Get all drawn features
5768                const data = drawControl.getAll();
5769
5770                if (data.features.length === 0) {
5771                  alert(
5772                    "No features to download. Please draw something first!",
5773                  );
5774                  return;
5775                }
5776
5777                // Convert to string with nice formatting
5778                const dataStr = JSON.stringify(data, null, 2);
5779
5780                // Create blob and download
5781                const blob = new Blob([dataStr], { type: "application/json" });
5782                const url = URL.createObjectURL(blob);
5783
5784                const a = document.createElement("a");
5785                a.href = url;
5786                a.download = `${message.download_filename || "drawn-features"}.geojson`;
5787                document.body.appendChild(a);
5788                a.click();
5789                document.body.removeChild(a);
5790                URL.revokeObjectURL(url);
5791              });
5792
5793              // Append to the Draw control button group
5794              drawButtons.appendChild(downloadBtn);
5795            }
5796          }, 100);
5797        }
5798      } else if (message.type === "get_drawn_features") {
5799        var drawControl = widget.drawControl || widget.getDraw();
5800        if (drawControl) {
5801          const features = drawControl.getAll();
5802          Shiny.setInputValue(data.id + "_drawn_features", features, {
5803            priority: "event",
5804          });
5805        } else {
5806          Shiny.setInputValue(data.id + "_drawn_features", null, {
5807            priority: "event",
5808          });
5809        }
5810      } else if (message.type === "clear_drawn_features") {
5811        var drawControl = widget.drawControl || widget.getDraw();
5812        if (drawControl) {
5813          drawControl.deleteAll();
5814          // Update the drawn features
5815          updateDrawnFeatures();
5816        }
5817      } else if (message.type.startsWith("turf_")) {
5818        // Delegate to shared turf operations module
5819        handleTurfOperation(map, message, data.id);
5820      } else if (message.type === "add_features_to_draw") {
5821        var drawControl = widget.drawControl || widget.getDraw();
5822        if (drawControl) {
5823          if (message.data.clear_existing) {
5824            drawControl.deleteAll();
5825          }
5826          addSourceFeaturesToDraw(drawControl, message.data.source, map);
5827          // Update the drawn features
5828          updateDrawnFeatures();
5829        } else {
5830          console.warn("Draw control not initialized");
5831        }
5832      } else if (message.type === "add_markers") {
5833        if (!window.maplibreMarkers) {
5834          window.maplibreMarkers = [];
5835        }
5836        message.markers.forEach(function (marker) {
5837          const markerOptions = {
5838            color: marker.color,
5839            rotation: marker.rotation,
5840            draggable: marker.options.draggable || false,
5841            ...marker.options,
5842          };
5843          const mapMarker = new maplibregl.Marker(markerOptions)
5844            .setLngLat([marker.lng, marker.lat])
5845            .addTo(map);
5846
5847          if (marker.popup) {
5848            mapMarker.setPopup(
5849              new maplibregl.Popup({ offset: 25, maxWidth: "400px" }).setHTML(
5850                marker.popup,
5851              ),
5852            );
5853          }
5854
5855          const markerId = marker.id;
5856          if (markerId) {
5857            const lngLat = mapMarker.getLngLat();
5858            Shiny.setInputValue(data.id + "_marker_" + markerId, {
5859              id: markerId,
5860              lng: lngLat.lng,
5861              lat: lngLat.lat,
5862            });
5863
5864            mapMarker.on("dragend", function () {
5865              const lngLat = mapMarker.getLngLat();
5866              Shiny.setInputValue(data.id + "_marker_" + markerId, {
5867                id: markerId,
5868                lng: lngLat.lng,
5869                lat: lngLat.lat,
5870              });
5871            });
5872          }
5873
5874          window.maplibreMarkers.push(mapMarker);
5875        });
5876      } else if (message.type === "clear_markers") {
5877        if (window.maplibreMarkers) {
5878          window.maplibreMarkers.forEach(function (marker) {
5879            marker.remove();
5880          });
5881          window.maplibreglMarkers = [];
5882        }
5883      } else if (message.type === "add_fullscreen_control") {
5884        const position = message.position || "top-right";
5885        const fullscreen = new maplibregl.FullscreenControl();
5886        map.addControl(fullscreen, position);
5887        map.controls.push({ type: "fullscreen", control: fullscreen });
5888      } else if (message.type === "add_slider_control") {
5889        if (typeof window.MapglSliderControl === "function") {
5890          if (map._mapglSliderControl) {
5891            try {
5892              map.removeControl(map._mapglSliderControl);
5893            } catch (e) { /* noop */ }
5894            map.controls = map.controls.filter(function (c) {
5895              return c.type !== "slider";
5896            });
5897          }
5898          const _ts = new window.MapglSliderControl(message.options || {});
5899          map.addControl(_ts, (message.options && message.options.position) || "top-left");
5900          map.controls.push({ type: "slider", control: _ts });
5901        } else {
5902          console.warn("MapglSliderControl is not loaded.");
5903        }
5904      } else if (message.type === "update_slider_control") {
5905        if (map._mapglSliderControl) {
5906          map._mapglSliderControl.update(message.options || {});
5907        }
5908      } else if (message.type === "add_scale_control") {
5909        const scaleControl = new maplibregl.ScaleControl({
5910          maxWidth: message.options.maxWidth,
5911          unit: message.options.unit,
5912        });
5913        map.addControl(scaleControl, message.options.position);
5914        map.controls.push({ type: "scale", control: scaleControl });
5915      } else if (message.type === "add_reset_control") {
5916        const resetControl = document.createElement("button");
5917        resetControl.className = "maplibregl-ctrl-icon maplibregl-ctrl-reset";
5918        resetControl.type = "button";
5919        resetControl.setAttribute("aria-label", "Reset");
5920        resetControl.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"></polyline><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path></svg>';
5921        resetControl.style.cssText = "display:flex;justify-content:center;align-items:center;cursor:pointer;";
5922
5923        const resetContainer = document.createElement("div");
5924        resetContainer.className = "maplibregl-ctrl maplibregl-ctrl-group";
5925        resetContainer.appendChild(resetControl);
5926
5927        const initialView = {
5928          center: map.getCenter(),
5929          zoom: map.getZoom(),
5930          pitch: map.getPitch(),
5931          bearing: map.getBearing(),
5932          animate: message.animate,
5933        };
5934
5935        if (message.duration) {
5936          initialView.duration = message.duration;
5937        }
5938
5939        resetControl.onclick = function () {
5940          map.easeTo(initialView);
5941        };
5942
5943        const resetControlObj = {
5944          onAdd: function () {
5945            return resetContainer;
5946          },
5947          onRemove: function () {
5948            resetContainer.parentNode.removeChild(resetContainer);
5949          },
5950        };
5951
5952        map.addControl(resetControlObj, message.position);
5953
5954        map.controls.push({ type: "reset", control: resetControlObj });
5955      } else if (message.type === "add_screenshot_control") {
5956        const screenshotControlObj = createScreenshotControl(map, message.options, true);
5957        map.addControl(screenshotControlObj, message.options.position || "top-right");
5958        map.controls.push({ type: "screenshot", control: screenshotControlObj });
5959      } else if (message.type === "add_coordinates_control") {
5960        const coordinatesControlObj = createCoordinatesControl(message.options);
5961        map.addControl(
5962          coordinatesControlObj,
5963          message.options.position || "bottom-right",
5964        );
5965        map.controls.push({ type: "coordinates", control: coordinatesControlObj });
5966      } else if (message.type === "add_geolocate_control") {
5967        const geolocate = new maplibregl.GeolocateControl({
5968          positionOptions: message.options.positionOptions,
5969          trackUserLocation: message.options.trackUserLocation,
5970          showAccuracyCircle: message.options.showAccuracyCircle,
5971          showUserLocation: message.options.showUserLocation,
5972          showUserHeading: message.options.showUserHeading,
5973          fitBoundsOptions: message.options.fitBoundsOptions,
5974        });
5975        map.addControl(geolocate, message.options.position);
5976        map.controls.push({ type: "geolocate", control: geolocate });
5977
5978        if (HTMLWidgets.shinyMode) {
5979          geolocate.on("geolocate", function (event) {
5980            Shiny.setInputValue(el.id + "_geolocate", {
5981              coords: event.coords,
5982              time: new Date(),
5983            });
5984          });
5985
5986          geolocate.on("trackuserlocationstart", function () {
5987            Shiny.setInputValue(el.id + "_geolocate_tracking", {
5988              status: "start",
5989              time: new Date(),
5990            });
5991          });
5992
5993          geolocate.on("trackuserlocationend", function () {
5994            Shiny.setInputValue(el.id + "_geolocate_tracking", {
5995              status: "end",
5996              time: new Date(),
5997            });
5998          });
5999
6000          geolocate.on("error", function (error) {
6001            if (error.error.code === 1) {
6002              Shiny.setInputValue(el.id + "_geolocate_error", {
6003                message: "Location permission denied",
6004                time: new Date(),
6005              });
6006            }
6007          });
6008        }
6009      } else if (message.type === "add_geocoder_control") {
6010        const provider = message.options.provider || "osm";
6011        let geocoder;
6012
6013        if (provider === "maptiler") {
6014          // MapTiler geocoder
6015          const maptilerOptions = {
6016            apiKey: message.options.api_key,
6017            maplibregl: maplibregl,
6018            ...message.options,
6019          };
6020
6021          // Create MapTiler geocoder
6022          geocoder = new maplibreglMaptilerGeocoder.GeocodingControl(
6023            maptilerOptions,
6024          );
6025        } else {
6026          // OSM/Nominatim geocoder (default)
6027          const geocoderApi = {
6028            forwardGeocode: async (config) => {
6029              const features = [];
6030              try {
6031                const request = `https://nominatim.openstreetmap.org/search?q=${
6032                  config.query
6033                }&format=geojson&polygon_geojson=1&addressdetails=1`;
6034                const response = await fetch(request);
6035                const geojson = await response.json();
6036                for (const feature of geojson.features) {
6037                  const center = [
6038                    feature.bbox[0] + (feature.bbox[2] - feature.bbox[0]) / 2,
6039                    feature.bbox[1] + (feature.bbox[3] - feature.bbox[1]) / 2,
6040                  ];
6041                  const point = {
6042                    type: "Feature",
6043                    geometry: {
6044                      type: "Point",
6045                      coordinates: center,
6046                    },
6047                    place_name: feature.properties.display_name,
6048                    properties: feature.properties,
6049                    text: feature.properties.display_name,
6050                    place_type: ["place"],
6051                    center,
6052                  };
6053                  features.push(point);
6054                }
6055              } catch (e) {
6056                console.error(`Failed to forwardGeocode with error: ${e}`);
6057              }
6058
6059              return {
6060                features,
6061              };
6062            },
6063          };
6064          geocoder = new MaplibreGeocoder(geocoderApi, {
6065            maplibregl: maplibregl,
6066            placeholder: message.options.placeholder,
6067            collapsed: message.options.collapsed,
6068          });
6069        }
6070
6071        map.addControl(geocoder, message.options.position);
6072        map.controls.push({ type: "geocoder", control: geocoder });
6073
6074        // Apply CSS fix for MapTiler geocoder to prevent cutoff
6075        if (provider === "maptiler") {
6076          setTimeout(() => {
6077            const controlContainer = document.querySelector(
6078              ".maplibregl-ctrl-geocoder",
6079            );
6080            if (controlContainer) {
6081              controlContainer.style.maxWidth = "300px";
6082              controlContainer.style.width = "auto";
6083            }
6084          }, 100);
6085        }
6086
6087        // Handle geocoder results in Shiny mode
6088        if (HTMLWidgets.shinyMode) {
6089          if (provider === "maptiler") {
6090            // MapTiler uses different event names
6091            geocoder.on("pick", function (e) {
6092              Shiny.setInputValue(data.id + "_geocoder", {
6093                result: e,
6094                time: new Date(),
6095              });
6096            });
6097          } else {
6098            // OSM geocoder
6099            geocoder.on("result", function (e) {
6100              Shiny.setInputValue(data.id + "_geocoder", {
6101                result: e,
6102                time: new Date(),
6103              });
6104            });
6105          }
6106        }
6107      } else if (message.type === "add_layers_control") {
6108        const layersControl = new MapglLayersControl(message);
6109        map.addControl(layersControl, message.position || "top-left");
6110        if (!map.controls) map.controls = [];
6111        map.controls.push({ type: "layers", control: layersControl });
6112      } else if (message.type === "clear_legend") {
6113        if (message.ids && Array.isArray(message.ids)) {
6114          message.ids.forEach((id) => {
6115            const legend = document.querySelector(
6116              `#${data.id} div[id="${id}"]`,
6117            );
6118            if (legend) {
6119              legend.remove();
6120            }
6121            // Remove from legend state
6122            delete layerState.legends[id];
6123          });
6124        } else if (message.ids) {
6125          const legend = document.querySelector(
6126            `#${data.id} div[id="${message.ids}"]`,
6127          );
6128          if (legend) {
6129            legend.remove();
6130          }
6131          // Remove from legend state
6132          delete layerState.legends[message.ids];
6133        } else {
6134          const existingLegends = document.querySelectorAll(
6135            `#${data.id} .mapboxgl-legend`,
6136          );
6137          existingLegends.forEach((legend) => {
6138            legend.remove();
6139          });
6140
6141          // Clear all legend state
6142          layerState.legends = {};
6143        }
6144      } else if (message.type === "clear_controls") {
6145        // If no specific controls specified, clear all
6146        if (!message.controls || message.controls.length === 0) {
6147          map.controls.forEach((controlObj) => {
6148            if (controlObj.control) {
6149              map.removeControl(controlObj.control);
6150            }
6151          });
6152          map.controls = [];
6153          if (map._mapglSliderControl) {
6154            delete map._mapglSliderControl;
6155          }
6156
6157          // Defensive sweep: remove any layers controls not tracked in
6158          // map.controls (onRemove cleans up the registry)
6159          (map._mapglLayersControls || [])
6160            .slice()
6161            .forEach((c) => map.removeControl(c));
6162
6163          // Remove globe minimap if it exists
6164          const globeMinimap = document.querySelector(
6165            ".mapboxgl-ctrl-globe-minimap",
6166          );
6167          if (globeMinimap) {
6168            globeMinimap.remove();
6169          }
6170        } else {
6171          // Clear specific controls
6172          const controlsToRemove = Array.isArray(message.controls)
6173            ? message.controls
6174            : [message.controls];
6175
6176          map.controls = map.controls.filter((controlObj) => {
6177            if (controlsToRemove.includes(controlObj.type)) {
6178              if (controlObj.control) {
6179                map.removeControl(controlObj.control);
6180              }
6181              return false; // Remove from array
6182            }
6183            return true; // Keep in array
6184          });
6185
6186          // Handle special controls that aren't in the controls array
6187          controlsToRemove.forEach((controlType) => {
6188            if (controlType === "layers") {
6189              // Defensive sweep for any untracked layers controls; normally
6190              // already removed via map.controls above
6191              (map._mapglLayersControls || [])
6192                .slice()
6193                .forEach((c) => map.removeControl(c));
6194            } else if (controlType === "globe_minimap") {
6195              const globeMinimap = document.querySelector(
6196                ".mapboxgl-ctrl-globe-minimap",
6197              );
6198              if (globeMinimap) {
6199                globeMinimap.remove();
6200              }
6201            } else if (controlType === "draw") {
6202              // Hide measurement box when draw control is cleared
6203              const measurementBox = document.getElementById(
6204                `measurement-box-${map.getContainer().id}`,
6205              );
6206              if (measurementBox) {
6207                measurementBox.style.display = "none";
6208              }
6209            }
6210          });
6211        }
6212      } else if (message.type === "move_layer") {
6213        if (map.getLayer(message.layer)) {
6214          if (message.before) {
6215            map.moveLayer(message.layer, message.before);
6216          } else {
6217            map.moveLayer(message.layer);
6218          }
6219        } else {
6220          console.error("Layer not found:", message.layer);
6221        }
6222      } else if (message.type === "add_image") {
6223        const mapId = map.getContainer().id;
6224
6225        // Helper function to actually add the image
6226        const doAddImage = function(imageId, url, options) {
6227          // Register the image for re-adding after style changes
6228          registerUserImage(mapId, imageId, url, options);
6229
6230          map
6231            .loadImage(url)
6232            .then((image) => {
6233              if (!map.hasImage(imageId)) {
6234                map.addImage(imageId, image.data, options);
6235                console.log("[MapGL Debug] Added image via proxy:", imageId);
6236              }
6237            })
6238            .catch((error) => {
6239              console.error("Error loading image:", error);
6240            });
6241        };
6242
6243        if (Array.isArray(message.images)) {
6244          message.images.forEach(function (imageInfo) {
6245            queueOrExecute(mapId, function() {
6246              doAddImage(imageInfo.id, imageInfo.url, imageInfo.options);
6247            });
6248          });
6249        } else if (message.url) {
6250          queueOrExecute(mapId, function() {
6251            doAddImage(message.imageId, message.url, message.options);
6252          });
6253        } else {
6254          console.error("Invalid image data:", message);
6255        }
6256      } else if (message.type === "set_tooltip") {
6257        const layerId = message.layer;
6258        const newTooltipProperty = message.tooltip;
6259        const newTooltipStyle = message.tooltip_style || null;
6260
6261        // Track tooltip state (content + style)
6262        layerState.tooltips[layerId] = newTooltipProperty;
6263        layerState.tooltipStyles[layerId] = newTooltipStyle;
6264
6265        // If there's an active tooltip open, remove it first
6266        if (window._activeTooltip) {
6267          window._activeTooltip.remove();
6268          delete window._activeTooltip;
6269        }
6270
6271        // Remove old handlers if any
6272        if (window._mapboxHandlers && window._mapboxHandlers[layerId]) {
6273          const handlers = window._mapboxHandlers[layerId];
6274          if (handlers.mousemove) {
6275            map.off("mousemove", layerId, handlers.mousemove);
6276          }
6277          if (handlers.mouseleave) {
6278            map.off("mouseleave", layerId, handlers.mouseleave);
6279          }
6280          delete window._mapboxHandlers[layerId];
6281        }
6282
6283        // Create a new tooltip popup
6284        const tooltip = new maplibregl.Popup({
6285          closeButton: false,
6286          closeOnClick: false,
6287          maxWidth: "400px",
6288        });
6289        applyPopupClass(tooltip, newTooltipStyle);
6290
6291        // Define new handlers referencing the updated tooltip property
6292        const mouseMoveHandler = function (e) {
6293          onMouseMoveTooltip(e, map, tooltip, newTooltipProperty, layerId);
6294        };
6295        const mouseLeaveHandler = function () {
6296          onMouseLeaveTooltip(map, tooltip);
6297        };
6298
6299        // Add the new event handlers
6300        map.on("mousemove", layerId, mouseMoveHandler);
6301        map.on("mouseleave", layerId, mouseLeaveHandler);
6302
6303        // Store these handlers so we can remove/update them in the future
6304        if (!window._mapboxHandlers) {
6305          window._mapboxHandlers = {};
6306        }
6307        window._mapboxHandlers[layerId] = {
6308          mousemove: mouseMoveHandler,
6309          mouseleave: mouseLeaveHandler,
6310        };
6311      } else if (message.type === "set_popup") {
6312        const layerId = message.layer;
6313        const newPopupProperty = message.popup;
6314        const newPopupStyle = message.popup_style || null;
6315
6316        // Track popup state (content + style)
6317        layerState.popups[layerId] = newPopupProperty;
6318        layerState.popupStyles[layerId] = newPopupStyle;
6319
6320        // Remove any existing popup for this layer
6321        if (window._mapboxPopups && window._mapboxPopups[layerId]) {
6322          window._mapboxPopups[layerId].remove();
6323          delete window._mapboxPopups[layerId];
6324        }
6325
6326        // Remove old click handler if any
6327        if (
6328          window._mapboxClickHandlers &&
6329          window._mapboxClickHandlers[layerId]
6330        ) {
6331          map.off("click", layerId, window._mapboxClickHandlers[layerId]);
6332          delete window._mapboxClickHandlers[layerId];
6333        }
6334
6335        // Remove old hover handlers for cursor change
6336        map.off("mouseenter", layerId);
6337        map.off("mouseleave", layerId);
6338
6339        // Create new click handler
6340        const clickHandler = function (e) {
6341          onClickPopup(e, map, newPopupProperty, layerId, newPopupStyle);
6342        };
6343
6344        // Add the new event handler
6345        map.on("click", layerId, clickHandler);
6346
6347        // Change cursor to pointer when hovering over the layer
6348        map.on("mouseenter", layerId, function () {
6349          map.getCanvas().style.cursor = "pointer";
6350        });
6351
6352        // Change cursor back to default when leaving the layer
6353        map.on("mouseleave", layerId, function () {
6354          map.getCanvas().style.cursor = "";
6355        });
6356
6357        // Store handler reference
6358        if (!window._mapboxClickHandlers) {
6359          window._mapboxClickHandlers = {};
6360        }
6361        window._mapboxClickHandlers[layerId] = clickHandler;
6362      } else if (message.type === "set_source") {
6363        const layerId = message.layer;
6364        const newData = message.source;
6365        const layerObject = map.getLayer(layerId);
6366
6367        if (!layerObject) {
6368          console.error("Layer not found: ", layerId);
6369          return;
6370        }
6371
6372        const sourceId = layerObject.source;
6373        const sourceObject = map.getSource(sourceId);
6374
6375        if (!sourceObject) {
6376          console.error("Source not found: ", sourceId);
6377          return;
6378        }
6379
6380        // Update the geojson data
6381        sourceObject.setData(newData);
6382      } else if (message.type === "set_projection") {
6383        if (map.loaded()) {
6384          const projection =
6385            typeof message.projection === "string"
6386              ? { type: message.projection }
6387              : message.projection;
6388
6389          try {
6390            map.setProjection(projection);
6391          } catch (e) {
6392            console.error("Failed to set projection:", e);
6393          }
6394        } else {
6395          console.error("Map not loaded yet");
6396        }
6397      } else if (message.type === "add_globe_minimap") {
6398        const globeMinimapOptions = {
6399          globeSize: message.options.globe_size || 100,
6400          landColor: message.options.land_color || "#404040",
6401          waterColor: message.options.water_color || "#090909",
6402          markerColor: message.options.marker_color || "#1da1f2",
6403          markerSize: message.options.marker_size || 2,
6404        };
6405        const globeMinimap = new GlobeMinimap(globeMinimapOptions);
6406        map.addControl(globeMinimap, message.position || "bottom-left");
6407        map.controls.push({ type: "globe_minimap", control: globeMinimap });
6408      } else if (message.type === "add_globe_control") {
6409        const globeControl = new maplibregl.GlobeControl();
6410        map.addControl(globeControl, message.position);
6411        map.controls.push({ type: "globe", control: globeControl });
6412      } else if (message.type === "add_custom_control") {
6413        const controlOptions = message.options;
6414        const customControlContainer = document.createElement("div");
6415
6416        if (controlOptions.className) {
6417          customControlContainer.className = controlOptions.className;
6418        } else {
6419          customControlContainer.className =
6420            "maplibregl-ctrl maplibregl-ctrl-group";
6421        }
6422
6423        customControlContainer.innerHTML = controlOptions.html;
6424
6425        const customControl = {
6426          onAdd: function () {
6427            return customControlContainer;
6428          },
6429          onRemove: function () {
6430            if (customControlContainer.parentNode) {
6431              customControlContainer.parentNode.removeChild(
6432                customControlContainer,
6433              );
6434            }
6435          },
6436        };
6437
6438        map.addControl(customControl, controlOptions.position || "top-right");
6439        map.controls.push({ type: message.control_id, control: customControl });
6440      }
6441    }
6442  });
6443}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.