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, "&") 1073 .replace(/</g, "<") 1074 .replace(/>/g, ">") 1075 .replace(/"/g, """) 1076 .replace(/'/g, "'"); 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.