Proof of concept for the other one
at main 420 lines 21 kB view raw
1import * as L from "leaflet"; 2import type { CalendarEvent } from "./parser"; 3 4/** 5 * Read the Obsidian accent color from CSS custom properties. 6 * Falls back to the default Obsidian purple if unavailable. 7 */ 8function getAccentColor(): string { 9 return getComputedStyle(document.body).getPropertyValue("--interactive-accent").trim() || "#7f6df2"; 10} 11 12/** 13 * Darken a hex color by a given factor (0–1). Used for the pin stroke. 14 */ 15function darkenHex(hex: string, factor: number): string { 16 const h = hex.replace("#", ""); 17 const r = Math.max(0, Math.round(parseInt(h.slice(0, 2), 16) * (1 - factor))); 18 const g = Math.max(0, Math.round(parseInt(h.slice(2, 4), 16) * (1 - factor))); 19 const b = Math.max(0, Math.round(parseInt(h.slice(4, 6), 16) * (1 - factor))); 20 return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`; 21} 22 23/** 24 * Create an SVG teardrop pin icon for map markers. 25 * Uses the Obsidian accent color, or a muted grey for sold-out events. 26 */ 27function createPinIcon(soldOut: boolean): L.DivIcon { 28 const accent = getAccentColor(); 29 const fill = soldOut ? "#999" : accent; 30 const stroke = soldOut ? "#777" : darkenHex(accent, 0.25); 31 const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="28" height="40" viewBox="0 0 28 40">` + 32 `<path d="M14 0C6.3 0 0 6.3 0 14c0 10.5 14 26 14 26s14-15.5 14-26C28 6.3 21.7 0 14 0z" ` + 33 `fill="${fill}" stroke="${stroke}" stroke-width="1.5"/>` + 34 `<circle cx="14" cy="14" r="5" fill="white" opacity="0.85"/>` + 35 `</svg>`; 36 return L.divIcon({ 37 html: svg, 38 className: "cal-pin-icon", 39 iconSize: [28, 40], 40 iconAnchor: [14, 40], 41 popupAnchor: [0, -36], 42 }); 43} 44 45export interface MapCallbacks { 46 onMarkerClick: (event: CalendarEvent) => void; 47} 48 49export interface MapController { 50 /** Update markers to reflect current events. Pass fitBounds=true to auto-zoom. */ 51 updateMarkers(events: CalendarEvent[], fitBounds?: boolean): void; 52 /** Select an event: pan to its marker, open popup, highlight it. */ 53 selectEvent(event: CalendarEvent | null): void; 54 /** Auto-fit map bounds to all current markers. */ 55 fitBounds(): void; 56 /** Call when container is resized. */ 57 invalidateSize(): void; 58 /** Clean up the Leaflet instance. */ 59 destroy(): void; 60} 61 62// Stamen Watercolor tiles hosted by the Smithsonian / Cooper Hewitt (free, no API key) 63const WATERCOLOR_URL = "https://watercolormaps.collection.cooperhewitt.org/tile/watercolor/{z}/{x}/{y}.jpg"; 64// CartoDB light labels overlay (free, no API key) 65const LABELS_URL = "https://{s}.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}{r}.png"; 66const TILE_ATTRIBUTION = 67 'Map tiles by <a href="https://stamen.com/">Stamen Design</a>, hosted by <a href="https://collection.cooperhewitt.org/">Cooper Hewitt</a>. ' + 68 'Labels by <a href="https://carto.com/">CARTO</a>. ' + 69 'Data &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'; 70 71/** 72 * Format a popup's HTML content for a list of events at the same location. 73 */ 74function popupContent(events: CalendarEvent[]): string { 75 return events 76 .map((ev) => { 77 const title = ev.url 78 ? `<a href="${ev.url}" target="_blank">${escapeHtml(ev.title)}</a>` 79 : escapeHtml(ev.title); 80 const soldOut = ev.soldOut ? ' <span class="cal-sold-out">(Sold out)</span>' : ""; 81 const date = ev.date.toLocaleDateString("en-US", { 82 weekday: "short", 83 month: "short", 84 day: "numeric", 85 }); 86 const time = ev.rawTime ? `, ${escapeHtml(ev.rawTime)}` : ""; 87 const venue = ev.venue ? `<br><span class="cal-popup-venue">${escapeHtml(ev.venue)}</span>` : ""; 88 return `<div class="cal-popup-event"><strong>${title}</strong>${soldOut}<br>${date}${time}${venue}</div>`; 89 }) 90 .join(""); 91} 92 93function escapeHtml(s: string): string { 94 return s 95 .replace(/&/g, "&amp;") 96 .replace(/</g, "&lt;") 97 .replace(/>/g, "&gt;") 98 .replace(/"/g, "&quot;"); 99} 100 101/** 102 * Group events by their lat/lng coordinates (rounded to avoid floating point issues). 103 */ 104function groupByLocation(events: CalendarEvent[]): Map<string, CalendarEvent[]> { 105 const groups = new Map<string, CalendarEvent[]>(); 106 for (const ev of events) { 107 if (ev.lat === undefined || ev.lng === undefined) continue; 108 // Round to 6 decimal places for grouping 109 const key = `${ev.lat.toFixed(6)},${ev.lng.toFixed(6)}`; 110 if (!groups.has(key)) groups.set(key, []); 111 groups.get(key)!.push(ev); 112 } 113 return groups; 114} 115 116/** 117 * Create a Leaflet map in the given container and return a MapController. 118 */ 119export function createMap(container: HTMLElement, events: CalendarEvent[], callbacks: MapCallbacks): MapController { 120 121 // Inject Leaflet CSS if not already present 122 if (!document.getElementById("leaflet-css")) { 123 const link = document.createElement("style"); 124 link.id = "leaflet-css"; 125 link.textContent = leafletCss(); 126 document.head.appendChild(link); 127 } 128 129 const mapDiv = container.createDiv({ cls: "cal-map" }); 130 131 const map = L.map(mapDiv, { 132 zoomControl: true, 133 attributionControl: true, 134 }); 135 136 // Watercolor base layer 137 L.tileLayer(WATERCOLOR_URL, { 138 attribution: TILE_ATTRIBUTION, 139 maxZoom: 19, 140 }).addTo(map); 141 142 // Labels overlay on top of watercolor 143 L.tileLayer(LABELS_URL, { 144 maxZoom: 19, 145 subdomains: "abcd", 146 pane: "overlayPane", 147 }).addTo(map); 148 149 // Track markers and their associated events 150 const markerLayer = L.layerGroup().addTo(map); 151 let markerMap = new Map<string, { marker: L.Marker; events: CalendarEvent[] }>(); 152 let selectedMarker: L.Marker | null = null; 153 let highlightCircle: L.CircleMarker | null = null; 154 155 function clearHighlight(): void { 156 if (highlightCircle) { 157 highlightCircle.remove(); 158 highlightCircle = null; 159 } 160 selectedMarker = null; 161 } 162 163 function highlightMarker(marker: L.Marker): void { 164 clearHighlight(); 165 selectedMarker = marker; 166 const latlng = marker.getLatLng(); 167 highlightCircle = L.circleMarker(latlng, { 168 radius: 18, 169 color: "var(--interactive-accent, #7b6cd9)", 170 fillColor: "var(--interactive-accent, #7b6cd9)", 171 fillOpacity: 0.2, 172 weight: 2, 173 className: "cal-marker-highlight", 174 }).addTo(map); 175 } 176 177 function buildMarkers(evts: CalendarEvent[]): void { 178 markerLayer.clearLayers(); 179 markerMap.clear(); 180 clearHighlight(); 181 182 const groups = groupByLocation(evts); 183 184 for (const [key, groupEvents] of groups) { 185 const [lat, lng] = key.split(",").map(Number); 186 187 const allSoldOut = groupEvents.every((e) => e.soldOut); 188 189 const marker = L.marker([lat, lng], { 190 icon: createPinIcon(allSoldOut), 191 }); 192 193 marker.bindPopup(popupContent(groupEvents), { 194 maxWidth: 240, 195 className: "cal-map-popup", 196 }); 197 198 marker.on("click", () => { 199 highlightMarker(marker); 200 // Fire callback for the first event in the group 201 callbacks.onMarkerClick(groupEvents[0]); 202 }); 203 204 marker.addTo(markerLayer); 205 markerMap.set(key, { marker, events: groupEvents }); 206 } 207 } 208 209 buildMarkers(events); 210 211 function doFitBounds(): void { 212 const allMarkers = Array.from(markerMap.values()).map((m) => m.marker); 213 if (allMarkers.length === 0) { 214 // Default to a world view 215 map.setView([20, 0], 2); 216 return; 217 } 218 if (allMarkers.length === 1) { 219 map.setView(allMarkers[0].getLatLng(), 13); 220 return; 221 } 222 const group = L.featureGroup(allMarkers); 223 map.fitBounds(group.getBounds().pad(0.15)); 224 } 225 226 // Initial fit after a tick (let the container settle) 227 setTimeout(() => { 228 map.invalidateSize(); 229 doFitBounds(); 230 }, 50); 231 232 const controller: MapController = { 233 updateMarkers(evts: CalendarEvent[], fit = true): void { 234 buildMarkers(evts); 235 if (fit) { 236 doFitBounds(); 237 } 238 }, 239 240 selectEvent(event: CalendarEvent | null): void { 241 if (!event) { 242 clearHighlight(); 243 map.closePopup(); 244 return; 245 } 246 if (event.lat === undefined || event.lng === undefined) return; 247 248 const key = `${event.lat.toFixed(6)},${event.lng.toFixed(6)}`; 249 const entry = markerMap.get(key); 250 if (entry) { 251 highlightMarker(entry.marker); 252 entry.marker.openPopup(); 253 map.panTo(entry.marker.getLatLng(), { animate: true }); 254 } 255 }, 256 257 fitBounds(): void { 258 doFitBounds(); 259 }, 260 261 invalidateSize(): void { 262 map.invalidateSize(); 263 }, 264 265 destroy(): void { 266 map.remove(); 267 }, 268 }; 269 270 return controller; 271} 272 273/** 274 * Inline Leaflet CSS — we inject this rather than importing a CSS file 275 * so esbuild doesn't need a CSS loader and the plugin stays self-contained. 276 * 277 * This is the minified core Leaflet 1.9.4 CSS. 278 */ 279function leafletCss(): string { 280 // We'll load from the installed leaflet package at runtime via a simpler approach: 281 // Import the CSS content as a string. Since esbuild can't handle CSS imports 282 // from node_modules without a loader, we inline the essential Leaflet styles. 283 return ` 284/* Leaflet 1.9.4 — essential styles */ 285.leaflet-pane, .leaflet-tile, .leaflet-marker-icon, .leaflet-marker-shadow, 286.leaflet-tile-container, .leaflet-pane > svg, .leaflet-pane > canvas, 287.leaflet-zoom-box, .leaflet-image-layer, .leaflet-layer { 288 position: absolute; left: 0; top: 0; 289} 290.leaflet-container { overflow: hidden; } 291.leaflet-tile, .leaflet-marker-icon, .leaflet-marker-shadow { user-select: none; -webkit-user-select: none; } 292.leaflet-tile::selection { background: transparent; } 293.leaflet-safari .leaflet-tile { image-rendering: -webkit-optimize-contrast; } 294.leaflet-safari .leaflet-tile-container { width: 1600px; height: 1600px; -webkit-transform-origin: 0 0; } 295.leaflet-marker-icon, .leaflet-marker-shadow { display: block; } 296.leaflet-container .leaflet-overlay-pane svg { max-width: none !important; max-height: none !important; } 297.leaflet-container .leaflet-marker-pane img, 298.leaflet-container .leaflet-shadow-pane img, 299.leaflet-container .leaflet-tile-pane img, 300.leaflet-container img.leaflet-image-layer, 301.leaflet-container .leaflet-tile { max-width: none !important; max-height: none !important; width: auto; padding: 0; } 302.leaflet-container.leaflet-touch-zoom { touch-action: pan-x pan-y; } 303.leaflet-container.leaflet-touch-drag { touch-action: none; touch-action: pinch-zoom; } 304.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom { touch-action: none; } 305.leaflet-container { -webkit-tap-highlight-color: transparent; } 306.leaflet-container a { -webkit-tap-highlight-color: rgba(51,181,229,0.4); } 307.leaflet-tile { filter: inherit; visibility: hidden; } 308.leaflet-tile-loaded { visibility: inherit; } 309.leaflet-zoom-box { width: 0; height: 0; box-sizing: border-box; z-index: 800; } 310.leaflet-overlay-pane svg { -moz-user-select: none; } 311.leaflet-pane { z-index: 400; } 312.leaflet-tile-pane { z-index: 200; } 313.leaflet-overlay-pane { z-index: 400; } 314.leaflet-shadow-pane { z-index: 500; } 315.leaflet-marker-pane { z-index: 600; } 316.leaflet-tooltip-pane { z-index: 650; } 317.leaflet-popup-pane { z-index: 700; } 318.leaflet-map-pane canvas { z-index: 100; } 319.leaflet-map-pane svg { z-index: 200; } 320.leaflet-vml-shape { width: 1px; height: 1px; } 321.lvml { behavior: url(#default#VML); display: inline-block; position: absolute; } 322.leaflet-control { position: relative; z-index: 800; pointer-events: visiblePainted; pointer-events: auto; } 323.leaflet-top, .leaflet-bottom { position: absolute; z-index: 1000; pointer-events: none; } 324.leaflet-top { top: 0; } 325.leaflet-right { right: 0; } 326.leaflet-bottom { bottom: 0; } 327.leaflet-left { left: 0; } 328.leaflet-control { float: left; clear: both; } 329.leaflet-right .leaflet-control { float: right; } 330.leaflet-top .leaflet-control { margin-top: 10px; } 331.leaflet-bottom .leaflet-control { margin-bottom: 10px; } 332.leaflet-left .leaflet-control { margin-left: 10px; } 333.leaflet-right .leaflet-control { margin-right: 10px; } 334.leaflet-fade-anim .leaflet-popup { opacity: 1; transition: opacity 0.2s linear; } 335.leaflet-fade-anim .leaflet-map-pane .leaflet-popup { opacity: 1; } 336.leaflet-zoom-animated { transform-origin: 0 0; } 337.leaflet-zoom-anim .leaflet-zoom-animated { will-change: transform; transition: transform 0.25s cubic-bezier(0,0,0.25,1); } 338.leaflet-zoom-anim .leaflet-tile, .leaflet-pan-anim .leaflet-tile { transition: none; } 339.leaflet-zoom-anim .leaflet-zoom-hide { visibility: hidden; } 340.leaflet-interactive { cursor: pointer; } 341.leaflet-grab { cursor: grab; } 342.leaflet-crosshair, .leaflet-crosshair .leaflet-interactive { cursor: crosshair; } 343.leaflet-popup-pane, .leaflet-control { cursor: auto; } 344.leaflet-dragging .leaflet-grab, .leaflet-dragging .leaflet-grab .leaflet-interactive, 345.leaflet-dragging .leaflet-marker-draggable { cursor: move; cursor: grabbing; } 346.leaflet-marker-icon, .leaflet-marker-shadow, .leaflet-image-layer, 347.leaflet-pane > svg path, .leaflet-tile-container { pointer-events: none; } 348.leaflet-marker-icon.leaflet-interactive, .leaflet-image-layer.leaflet-interactive, 349.leaflet-pane > svg path.leaflet-interactive, svg.leaflet-image-layer.leaflet-interactive path { pointer-events: visiblePainted; pointer-events: auto; } 350.leaflet-container a.leaflet-active { outline: 2px solid orange; } 351.leaflet-zoom-box { border: 2px dotted #38f; background: rgba(255,255,255,0.5); } 352.leaflet-bar { box-shadow: 0 1px 5px rgba(0,0,0,0.65); border-radius: 4px; } 353.leaflet-bar a, .leaflet-bar a:hover { background-color: #fff; border-bottom: 1px solid #ccc; width: 26px; height: 26px; line-height: 26px; display: block; text-align: center; text-decoration: none; color: black; } 354.leaflet-bar a, .leaflet-control-layers-toggle { background-position: 50% 50%; background-repeat: no-repeat; display: block; } 355.leaflet-bar a:hover, .leaflet-bar a:focus { background-color: #f4f4f4; } 356.leaflet-bar a:first-child { border-top-left-radius: 4px; border-top-right-radius: 4px; } 357.leaflet-bar a:last-child { border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; border-bottom: none; } 358.leaflet-bar a.leaflet-disabled { cursor: default; background-color: #f4f4f4; color: #bbb; } 359.leaflet-touch .leaflet-bar a { width: 30px; height: 30px; line-height: 30px; } 360.leaflet-touch .leaflet-bar a:first-child { border-top-left-radius: 2px; border-top-right-radius: 2px; } 361.leaflet-touch .leaflet-bar a:last-child { border-bottom-left-radius: 2px; border-bottom-right-radius: 2px; } 362.leaflet-control-zoom-in, .leaflet-control-zoom-out { font: bold 18px 'Lucida Console', Monaco, monospace; text-indent: 1px; } 363.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out { font-size: 22px; } 364.leaflet-control-layers { box-shadow: 0 1px 5px rgba(0,0,0,0.4); background: #fff; border-radius: 5px; } 365.leaflet-control-layers-toggle { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAAaCAQAAAADQ4RFAAACf0lEQVR4AY1TA5AVURDsmc22bdu2bdu2bdu2bdu2bZ3v3P3f1t2q7mL6AQBSNG03EMHQF8MwEiNxBmfxMz7FJ/iROI0zOINfCF2RC9lRG8MxFRdwSw0EcBu3cBEXcRt31ECq4AFMI3RFLVRHZ3RBf0zAHJzBDdzEHdzFfTzAQzzCYzzBUzzDc7zASwjBV3iDb/AO3+MHkiSf8dPf4mt8ha/xJb7AF/gcn+FTfIJP8DE+wof4AO/jPbyLd/A23sKbeAOv43W8hlfxCl7Gy3gJL+IFPIdncQ+P8RRP8ARP8BSvwGOAkBm50AsrsR+38BCv4BLO4DTu4h6e4SVe4CXe4CU+wPt4D+/iHbyNt/Am3sDreA2v4hW8jJfwIl7Ac3gWz+AuHuMpnuAJnuI5eMQjP4cLauA+buE6LuMCzuE0TuE8ruEObuMhHuEWHuABnuExnuIRnuAJnuEZ+FfFa3gVr+BlvISX8CJewPN4Ds/iGdzFYzzFEzzBUzzHSwjBV/gG3+I7fI8fCKLkM37+W3yNr/A1vsSX+AKf4zN8ik/wMT7Ch/gA7+M9vIt38DbewsN4C2/iDbyO1/EaXsUreBkv4SW8iBfwPJ7DU7iHx3iKx3iCJ3iGF+CRH+Ey7uEOLuMMzuMiruEGbuI2buMO7uE+HuIhHuMpHuMJnuI5XoJHvIJX8Rpexat4Ba/gZbyEl/ACnsdzeBbP4C4e4yme4Ame4hlewZ8+DJmRC30xAbtxDQ/xGu7gNE7hMu7gLu7jAR7iMZ7gKR7jCZ7hOV6AAG/hTbyBt/AWHsJbeAsP4x28i3fwNt7CG3gdr+E1vIpX8DJewot4Ac/hWTyNu3iEx3j67w8P/z9FWIJUIAAAAABJRU5ErkJggg==); width: 36px; height: 36px; } 366.leaflet-retina .leaflet-control-layers-toggle { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADQAAAA0CAQAAABvcdNgAAAEsklEQVR4AWL4TydIhpZK1kAAAABJRU5ErkJggg==); background-size: 26px 26px; } 367.leaflet-touch .leaflet-control-layers-toggle { width: 44px; height: 44px; } 368.leaflet-control-layers .leaflet-control-layers-list, .leaflet-control-layers-expanded .leaflet-control-layers-toggle { display: none; } 369.leaflet-control-layers-expanded .leaflet-control-layers-list { display: block; position: relative; } 370.leaflet-control-layers-expanded { padding: 6px 10px 6px 6px; color: #333; background: #fff; } 371.leaflet-control-layers-scrollbar { overflow-y: scroll; overflow-x: hidden; padding-right: 5px; } 372.leaflet-control-layers-selector { margin-top: 2px; position: relative; top: 1px; } 373.leaflet-control-layers label { display: block; font-size: 13px; font-size: 1.08333em; } 374.leaflet-control-layers-separator { height: 0; border-top: 1px solid #ddd; margin: 5px -10px 5px -6px; } 375.leaflet-default-icon-path { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAApCAYAAADAk4LOAAAFgUlEQVR4Aa1XA5BjWRTN2oW17d3YassW17LFNesR17LWtAnOez/J5tsupervised/cLOGeli2lGZjUtMPail370a57y6u6deuj75NAfO/6fOM0M0SUR4JYAAAAABJRU5ErkJggg==); } 376.leaflet-container .leaflet-control-attribution { background: #fff; background: rgba(255,255,255,0.8); margin: 0; } 377.leaflet-control-attribution, .leaflet-control-scale-line { padding: 0 5px; color: #333; line-height: 1.4; } 378.leaflet-control-attribution a { text-decoration: none; } 379.leaflet-control-attribution a:hover, .leaflet-control-attribution a:focus { text-decoration: underline; } 380.leaflet-attribution-flag { display: none !important; } 381.leaflet-left .leaflet-control-scale { margin-left: 5px; } 382.leaflet-bottom .leaflet-control-scale { margin-bottom: 5px; } 383.leaflet-control-scale-line { border: 2px solid #777; border-top: none; line-height: 1.1; padding: 2px 5px 1px; white-space: nowrap; overflow: hidden; box-sizing: border-box; background: rgba(255,255,255,0.5); } 384.leaflet-control-scale-line:not(:first-child) { border-top: 2px solid #777; border-bottom: none; margin-top: -2px; } 385.leaflet-control-scale-line:not(:first-child):not(:last-child) { border-bottom: 2px solid #777; } 386.leaflet-touch .leaflet-control-attribution, .leaflet-touch .leaflet-control-layers, 387.leaflet-touch .leaflet-bar { box-shadow: none; } 388.leaflet-touch .leaflet-control-layers, .leaflet-touch .leaflet-bar { border: 2px solid rgba(0,0,0,0.2); background-clip: padding-box; } 389.leaflet-popup { position: absolute; text-align: center; margin-bottom: 20px; } 390.leaflet-popup-content-wrapper { padding: 1px; text-align: left; border-radius: 12px; } 391.leaflet-popup-content { margin: 13px 24px 13px 20px; line-height: 1.3; font-size: 13px; font-size: 1.08333em; min-height: 1px; } 392.leaflet-popup-content p { margin: 17px 0; margin: 1.3em 0; } 393.leaflet-popup-tip-container { width: 40px; height: 20px; position: absolute; left: 50%; margin-top: -1px; margin-left: -20px; overflow: hidden; pointer-events: none; } 394.leaflet-popup-tip { width: 17px; height: 17px; padding: 1px; margin: -10px auto 0; pointer-events: auto; transform: rotate(45deg); } 395.leaflet-popup-content-wrapper, .leaflet-popup-tip { background: white; color: #333; box-shadow: 0 3px 14px rgba(0,0,0,0.4); } 396.leaflet-container a.leaflet-popup-close-button { position: absolute; top: 0; right: 0; border: none; text-align: center; width: 24px; height: 24px; font: 16px/24px Tahoma, Verdana, sans-serif; color: #757575; text-decoration: none; background: transparent; } 397.leaflet-container a.leaflet-popup-close-button:hover, .leaflet-container a.leaflet-popup-close-button:focus { color: #585858; } 398.leaflet-popup-scrolled { overflow: auto; } 399.leaflet-oldie .leaflet-popup-content-wrapper { -ms-zoom: 1; } 400.leaflet-oldie .leaflet-popup-tip { width: 24px; margin: 0 auto; -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)"; filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678); } 401.leaflet-oldie .leaflet-control-zoom, .leaflet-oldie .leaflet-control-layers, 402.leaflet-oldie .leaflet-popup-content-wrapper, .leaflet-oldie .leaflet-popup-tip { border: 1px solid #999; } 403.leaflet-div-icon { background: #fff; border: 1px solid #666; } 404.leaflet-tooltip { position: absolute; padding: 6px; background-color: #fff; border: 1px solid #fff; border-radius: 3px; color: #222; white-space: nowrap; user-select: none; pointer-events: none; box-shadow: 0 1px 3px rgba(0,0,0,0.4); } 405.leaflet-tooltip.leaflet-interactive { cursor: pointer; pointer-events: auto; } 406.leaflet-tooltip-top:before, .leaflet-tooltip-bottom:before, 407.leaflet-tooltip-left:before, .leaflet-tooltip-right:before { position: absolute; pointer-events: none; border: 6px solid transparent; background: transparent; content: ""; } 408.leaflet-tooltip-bottom { margin-top: 6px; } 409.leaflet-tooltip-top { margin-top: -6px; } 410.leaflet-tooltip-bottom:before, .leaflet-tooltip-top:before { left: 50%; margin-left: -6px; } 411.leaflet-tooltip-top:before { bottom: 0; margin-bottom: -12px; border-top-color: #fff; } 412.leaflet-tooltip-bottom:before { top: 0; margin-top: -12px; margin-left: -6px; border-bottom-color: #fff; } 413.leaflet-tooltip-left { margin-left: -6px; } 414.leaflet-tooltip-right { margin-left: 6px; } 415.leaflet-tooltip-left:before, .leaflet-tooltip-right:before { top: 50%; margin-top: -6px; } 416.leaflet-tooltip-left:before { right: 0; margin-right: -12px; border-left-color: #fff; } 417.leaflet-tooltip-right:before { left: 0; margin-left: -12px; border-right-color: #fff; } 418@media print { .leaflet-control { -webkit-print-color-adjust: exact; print-color-adjust: exact; } } 419`; 420}