5.2 KiB
5.2 KiB
Leaflet.js + OpenStreetMap Map Integration
Replace SVG world maps with real interactive Leaflet.js maps using OpenStreetMap tiles.
Setup
<!-- In <head> -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
Container div (replaces old SVG viewBox)
<div id="leafletMap"></div>
#leafletMap {
width: 100%;
height: 500px;
border-radius: 12px;
overflow: hidden;
border: 1px solid rgba(14, 165, 233, 0.25);
}
Dark theme overrides
Apply these to match a dark mockup theme:
.leaflet-container {
background: #0a1628;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.leaflet-popup-content-wrapper {
background: #0f2b4a;
color: #e2e8f0;
border: 1px solid rgba(14, 165, 233, 0.25);
border-radius: 12px;
padding: 0;
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
}
.leaflet-popup-tip {
background: #0f2b4a;
border: 1px solid rgba(14, 165, 233, 0.25);
}
.leaflet-popup-content {
margin: 12px 16px;
font-size: 13px;
line-height: 1.5;
min-width: 200px;
}
.leaflet-popup-close-button {
color: #94a3b8 !important;
}
.leaflet-popup-close-button:hover {
color: #e2e8f0 !important;
}
.leaflet-control-zoom a {
background: #0f2b4a !important;
color: #e2e8f0 !important;
border-color: rgba(14, 165, 233, 0.25) !important;
}
.leaflet-control-zoom a:hover {
background: #1a3a5c !important;
}
.leaflet-control-attribution {
background: rgba(10, 22, 40, 0.8) !important;
color: #64748b !important;
font-size: 10px !important;
}
.leaflet-control-attribution a {
color: #0ea5e9 !important;
}
Region data (real coordinates)
Old SVG pattern used pixel coords + display strings. New pattern uses real lat/lng:
const MAP_REGIONS = [
{ id: 1, name: "Florida East Coast", emoji: "🏖️", lat: 29.0, lng: -80.5 },
// ...
];
Map initialization + markers
function renderMapView() {
// Destroy previous instance (important for view-switching UIs)
if (window._leafletMap) {
window._leafletMap.remove();
window._leafletMap = null;
}
const map = L.map('leafletMap', {
center: [20, 0],
zoom: 2,
maxZoom: 5,
minZoom: 2,
zoomControl: true,
});
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© <a href="https://openstreetmap.org/copyright">OpenStreetMap</a> contributors',
maxNativeZoom: 5,
}).addTo(map);
window._leafletMap = map;
MAP_REGIONS.forEach(mr => {
// Color by state (available / yours / taken / waiting)
const marker = L.circleMarker([mr.lat, mr.lng], {
radius: 10,
color: '#0ea5e9',
fillColor: '#0ea5e9',
fillOpacity: 0.5,
opacity: 0.8,
weight: 2,
}).addTo(map);
marker.bindPopup(`<div class="lp-region">Content</div>`, {
closeButton: true,
className: 'dark-popup',
maxWidth: 280,
});
});
// Fix layout after container becomes visible
setTimeout(() => map.invalidateSize(), 100);
}
State-based marker styling
| State | Color | Radius | Opacity |
|---|---|---|---|
| Available | #0ea5e9 (teal) |
10 | 0.8 |
| Your pick | #f59e0b (amber) |
12 | 0.9 |
| Taken/drafted | #dc2626 (red) |
8 | 0.5 |
| Waiting/draft-not-started | #64748b (gray) |
8 | 0.4 |
View-switching lifecycle
When toggling between list and map views, destroy the Leaflet instance on leaving the map view:
function switchView(view) {
if (view === 'list') {
if (window._leafletMap) {
window._leafletMap.remove();
window._leafletMap = null;
}
} else {
renderMapView();
}
}
Without this, Leaflet doesn't re-render correctly when its container is shown again — invalidateSize() alone is unreliable after display toggles.
Popup buttons with inline onclick
Buttons inside Leaflet popups can call global functions:
marker.bindPopup(`
<div class="lp-region">${mr.emoji} ${escapeHtml(mr.name)}</div>
<div class="lp-stats">👁️${sightings} Sightings</div>
<button class="map-popup-btn draft" onclick="makePickFromMap(${mr.id})">🦈 Draft</button>
`, { ... });
Programmatic popup open/close
function openMapPopup(regionId) {
const mr = MAP_REGIONS.find(r => r.id === regionId);
if (mr && window._leafletMap) {
window._leafletMap.eachLayer(layer => {
if (layer instanceof L.CircleMarker &&
layer.getLatLng().lat === mr.lat &&
layer.getLatLng().lng === mr.lng) {
layer.openPopup();
}
});
}
}
function closeMapPopup() {
if (window._leafletMap) {
window._leafletMap.closePopup();
}
}
Removing custom popup overlays
When switching to Leaflet popups, remove the old custom overlay HTML and CSS:
- HTML to remove: custom overlay divs (
#mapPopupOverlay,#mapPopup,#mapPopupContent) - CSS to remove:
.map-popup-overlay,.map-popup,.map-popup-handle,.map-popup-header,.map-popup-flag,.map-popup-title,.map-popup-stats,.map-popup-stat,.map-popup-footer - CSS to keep:
.map-popup-btnand its variants — these still style buttons inside Leaflet popups - JS to replace:
openMapPopup()/closeMapPopup()— redirect to Leaflet API as shown above