Browse Source
Add MapLibre GL JS test page for custom map styling
Add MapLibre GL JS test page for custom map styling
- Test page at /maplibre-test.html for evaluating MapLibre migration - 5 pre-made styles: Liberty, Positron, Dark Matter, Bright, Custom Fantasy - Custom Fantasy allows picking colors for land, water, roads, buildings, parks - 3D extruded buildings option with pitch/bearing controls - Uses free OpenFreeMap vector tiles (no API key needed) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>master
2 changed files with 625 additions and 0 deletions
@ -0,0 +1,624 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html> |
||||
|
<head> |
||||
|
<meta charset="utf-8"> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
|
<title>MapLibre Test - HikeMap</title> |
||||
|
<script src="https://unpkg.com/maplibre-gl@4.1.0/dist/maplibre-gl.js"></script> |
||||
|
<link href="https://unpkg.com/maplibre-gl@4.1.0/dist/maplibre-gl.css" rel="stylesheet" /> |
||||
|
<style> |
||||
|
* { margin: 0; padding: 0; box-sizing: border-box; } |
||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } |
||||
|
|
||||
|
#map { |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
bottom: 0; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
} |
||||
|
|
||||
|
.control-panel { |
||||
|
position: absolute; |
||||
|
top: 10px; |
||||
|
left: 10px; |
||||
|
background: white; |
||||
|
padding: 15px; |
||||
|
border-radius: 8px; |
||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.3); |
||||
|
z-index: 1000; |
||||
|
max-width: 280px; |
||||
|
max-height: 90vh; |
||||
|
overflow-y: auto; |
||||
|
} |
||||
|
|
||||
|
.control-panel h3 { |
||||
|
margin-bottom: 10px; |
||||
|
color: #333; |
||||
|
font-size: 14px; |
||||
|
border-bottom: 1px solid #ddd; |
||||
|
padding-bottom: 8px; |
||||
|
} |
||||
|
|
||||
|
.style-btn { |
||||
|
display: block; |
||||
|
width: 100%; |
||||
|
padding: 10px; |
||||
|
margin-bottom: 8px; |
||||
|
border: 2px solid #ddd; |
||||
|
border-radius: 6px; |
||||
|
background: #f8f8f8; |
||||
|
cursor: pointer; |
||||
|
text-align: left; |
||||
|
font-size: 13px; |
||||
|
transition: all 0.2s; |
||||
|
} |
||||
|
|
||||
|
.style-btn:hover { |
||||
|
border-color: #4285f4; |
||||
|
background: #e8f0fe; |
||||
|
} |
||||
|
|
||||
|
.style-btn.active { |
||||
|
border-color: #4285f4; |
||||
|
background: #4285f4; |
||||
|
color: white; |
||||
|
} |
||||
|
|
||||
|
.style-btn .style-name { |
||||
|
font-weight: bold; |
||||
|
display: block; |
||||
|
} |
||||
|
|
||||
|
.style-btn .style-desc { |
||||
|
font-size: 11px; |
||||
|
opacity: 0.7; |
||||
|
margin-top: 2px; |
||||
|
} |
||||
|
|
||||
|
.section { |
||||
|
margin-bottom: 15px; |
||||
|
} |
||||
|
|
||||
|
.section-title { |
||||
|
font-size: 12px; |
||||
|
font-weight: bold; |
||||
|
color: #666; |
||||
|
margin-bottom: 8px; |
||||
|
text-transform: uppercase; |
||||
|
} |
||||
|
|
||||
|
.slider-row { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 8px; |
||||
|
margin-bottom: 8px; |
||||
|
} |
||||
|
|
||||
|
.slider-row label { |
||||
|
font-size: 12px; |
||||
|
width: 60px; |
||||
|
flex-shrink: 0; |
||||
|
} |
||||
|
|
||||
|
.slider-row input[type="range"] { |
||||
|
flex: 1; |
||||
|
} |
||||
|
|
||||
|
.slider-row .value { |
||||
|
font-size: 11px; |
||||
|
width: 35px; |
||||
|
text-align: right; |
||||
|
} |
||||
|
|
||||
|
.info-box { |
||||
|
background: #f0f7ff; |
||||
|
border: 1px solid #b3d4fc; |
||||
|
border-radius: 6px; |
||||
|
padding: 10px; |
||||
|
font-size: 12px; |
||||
|
color: #333; |
||||
|
margin-top: 15px; |
||||
|
} |
||||
|
|
||||
|
.info-box strong { |
||||
|
display: block; |
||||
|
margin-bottom: 5px; |
||||
|
} |
||||
|
|
||||
|
.color-row { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 8px; |
||||
|
margin-bottom: 6px; |
||||
|
} |
||||
|
|
||||
|
.color-row label { |
||||
|
font-size: 12px; |
||||
|
width: 80px; |
||||
|
} |
||||
|
|
||||
|
.color-row input[type="color"] { |
||||
|
width: 40px; |
||||
|
height: 25px; |
||||
|
border: 1px solid #ccc; |
||||
|
border-radius: 4px; |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
|
||||
|
#customControls { |
||||
|
display: none; |
||||
|
} |
||||
|
|
||||
|
.toggle-btn { |
||||
|
background: #333; |
||||
|
color: white; |
||||
|
border: none; |
||||
|
padding: 8px 12px; |
||||
|
border-radius: 4px; |
||||
|
cursor: pointer; |
||||
|
font-size: 12px; |
||||
|
margin-top: 10px; |
||||
|
} |
||||
|
|
||||
|
.toggle-btn:hover { |
||||
|
background: #555; |
||||
|
} |
||||
|
</style> |
||||
|
</head> |
||||
|
<body> |
||||
|
<div id="map"></div> |
||||
|
|
||||
|
<div class="control-panel"> |
||||
|
<h3>MapLibre Style Test</h3> |
||||
|
|
||||
|
<div class="section"> |
||||
|
<div class="section-title">Pre-made Styles</div> |
||||
|
|
||||
|
<button class="style-btn active" data-style="liberty"> |
||||
|
<span class="style-name">OSM Liberty</span> |
||||
|
<span class="style-desc">Clean, colorful, open source</span> |
||||
|
</button> |
||||
|
|
||||
|
<button class="style-btn" data-style="positron"> |
||||
|
<span class="style-name">Positron</span> |
||||
|
<span class="style-desc">Light, minimal, muted colors</span> |
||||
|
</button> |
||||
|
|
||||
|
<button class="style-btn" data-style="dark-matter"> |
||||
|
<span class="style-name">Dark Matter</span> |
||||
|
<span class="style-desc">Dark theme, neon accents</span> |
||||
|
</button> |
||||
|
|
||||
|
<button class="style-btn" data-style="bright"> |
||||
|
<span class="style-name">OSM Bright</span> |
||||
|
<span class="style-desc">Vibrant, detailed</span> |
||||
|
</button> |
||||
|
|
||||
|
<button class="style-btn" data-style="custom"> |
||||
|
<span class="style-name">Custom Fantasy</span> |
||||
|
<span class="style-desc">Editable colors below</span> |
||||
|
</button> |
||||
|
</div> |
||||
|
|
||||
|
<div id="customControls" class="section"> |
||||
|
<div class="section-title">Custom Colors</div> |
||||
|
|
||||
|
<div class="color-row"> |
||||
|
<label>Land</label> |
||||
|
<input type="color" id="colorLand" value="#1a1a2e"> |
||||
|
</div> |
||||
|
<div class="color-row"> |
||||
|
<label>Water</label> |
||||
|
<input type="color" id="colorWater" value="#0f3460"> |
||||
|
</div> |
||||
|
<div class="color-row"> |
||||
|
<label>Roads</label> |
||||
|
<input type="color" id="colorRoads" value="#e94560"> |
||||
|
</div> |
||||
|
<div class="color-row"> |
||||
|
<label>Buildings</label> |
||||
|
<input type="color" id="colorBuildings" value="#16213e"> |
||||
|
</div> |
||||
|
<div class="color-row"> |
||||
|
<label>Parks</label> |
||||
|
<input type="color" id="colorParks" value="#1b4332"> |
||||
|
</div> |
||||
|
|
||||
|
<div style="margin-top: 10px;"> |
||||
|
<label style="font-size: 12px;"> |
||||
|
<input type="checkbox" id="buildings3d"> 3D Buildings |
||||
|
</label> |
||||
|
</div> |
||||
|
|
||||
|
<button class="toggle-btn" id="applyColors">Apply Colors</button> |
||||
|
</div> |
||||
|
|
||||
|
<div class="section"> |
||||
|
<div class="section-title">3D Controls</div> |
||||
|
|
||||
|
<div class="slider-row"> |
||||
|
<label>Pitch</label> |
||||
|
<input type="range" id="pitch" min="0" max="85" value="0"> |
||||
|
<span class="value" id="pitchVal">0°</span> |
||||
|
</div> |
||||
|
|
||||
|
<div class="slider-row"> |
||||
|
<label>Bearing</label> |
||||
|
<input type="range" id="bearing" min="0" max="360" value="0"> |
||||
|
<span class="value" id="bearingVal">0°</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="info-box"> |
||||
|
<strong>What you're seeing:</strong> |
||||
|
Vector tiles rendered client-side. Every color, label, and feature can be customized via style JSON. |
||||
|
<br><br> |
||||
|
<strong>Controls:</strong> |
||||
|
• Drag to pan |
||||
|
• Scroll to zoom |
||||
|
• Right-drag to rotate/tilt |
||||
|
• Ctrl+drag to pitch |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<script> |
||||
|
// Available styles (using free tile sources) |
||||
|
const styles = { |
||||
|
'liberty': 'https://tiles.openfreemap.org/styles/liberty', |
||||
|
'positron': 'https://tiles.openfreemap.org/styles/positron', |
||||
|
'dark-matter': 'https://tiles.openfreemap.org/styles/dark', |
||||
|
'bright': 'https://tiles.openfreemap.org/styles/bright' |
||||
|
}; |
||||
|
|
||||
|
// Custom style template (we'll modify this) |
||||
|
const customStyleBase = { |
||||
|
version: 8, |
||||
|
name: 'Custom Fantasy', |
||||
|
sources: { |
||||
|
'openfreemap': { |
||||
|
type: 'vector', |
||||
|
url: 'https://tiles.openfreemap.org/planet' |
||||
|
} |
||||
|
}, |
||||
|
layers: [] |
||||
|
}; |
||||
|
|
||||
|
// Initialize map with default style |
||||
|
const map = new maplibregl.Map({ |
||||
|
container: 'map', |
||||
|
style: styles['liberty'], |
||||
|
center: [-97.84, 30.49], // Same as HikeMap default |
||||
|
zoom: 13, |
||||
|
pitch: 0, |
||||
|
bearing: 0 |
||||
|
}); |
||||
|
|
||||
|
// Add navigation controls |
||||
|
map.addControl(new maplibregl.NavigationControl()); |
||||
|
|
||||
|
// Add a test marker |
||||
|
const marker = new maplibregl.Marker({ color: '#e94560' }) |
||||
|
.setLngLat([-97.84, 30.49]) |
||||
|
.setPopup(new maplibregl.Popup().setHTML('<b>Test Marker</b><br>This is where HikeMap centers')) |
||||
|
.addTo(map); |
||||
|
|
||||
|
// Style button handlers |
||||
|
document.querySelectorAll('.style-btn').forEach(btn => { |
||||
|
btn.addEventListener('click', () => { |
||||
|
// Update active state |
||||
|
document.querySelectorAll('.style-btn').forEach(b => b.classList.remove('active')); |
||||
|
btn.classList.add('active'); |
||||
|
|
||||
|
const styleName = btn.dataset.style; |
||||
|
|
||||
|
// Show/hide custom controls |
||||
|
document.getElementById('customControls').style.display = |
||||
|
styleName === 'custom' ? 'block' : 'none'; |
||||
|
|
||||
|
if (styleName === 'custom') { |
||||
|
applyCustomStyle(); |
||||
|
} else if (styles[styleName]) { |
||||
|
map.setStyle(styles[styleName]); |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
// Custom color application |
||||
|
function applyCustomStyle() { |
||||
|
const land = document.getElementById('colorLand').value; |
||||
|
const water = document.getElementById('colorWater').value; |
||||
|
const roads = document.getElementById('colorRoads').value; |
||||
|
const buildings = document.getElementById('colorBuildings').value; |
||||
|
const parks = document.getElementById('colorParks').value; |
||||
|
const buildings3d = document.getElementById('buildings3d').checked; |
||||
|
|
||||
|
// Build custom style using correct OpenMapTiles schema |
||||
|
const customStyle = { |
||||
|
version: 8, |
||||
|
name: 'Custom Fantasy', |
||||
|
sources: { |
||||
|
'openmaptiles': { |
||||
|
type: 'vector', |
||||
|
url: 'https://tiles.openfreemap.org/planet' |
||||
|
} |
||||
|
}, |
||||
|
glyphs: 'https://tiles.openfreemap.org/fonts/{fontstack}/{range}.pbf', |
||||
|
layers: [ |
||||
|
// Background |
||||
|
{ |
||||
|
id: 'background', |
||||
|
type: 'background', |
||||
|
paint: { 'background-color': land } |
||||
|
}, |
||||
|
// Parks (from park layer) |
||||
|
{ |
||||
|
id: 'park', |
||||
|
type: 'fill', |
||||
|
source: 'openmaptiles', |
||||
|
'source-layer': 'park', |
||||
|
paint: { |
||||
|
'fill-color': parks, |
||||
|
'fill-opacity': 0.7 |
||||
|
} |
||||
|
}, |
||||
|
// Landcover - wood/grass |
||||
|
{ |
||||
|
id: 'landcover_wood', |
||||
|
type: 'fill', |
||||
|
source: 'openmaptiles', |
||||
|
'source-layer': 'landcover', |
||||
|
filter: ['==', ['get', 'class'], 'wood'], |
||||
|
paint: { |
||||
|
'fill-color': parks, |
||||
|
'fill-opacity': 0.5 |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
id: 'landcover_grass', |
||||
|
type: 'fill', |
||||
|
source: 'openmaptiles', |
||||
|
'source-layer': 'landcover', |
||||
|
filter: ['==', ['get', 'class'], 'grass'], |
||||
|
paint: { |
||||
|
'fill-color': parks, |
||||
|
'fill-opacity': 0.4 |
||||
|
} |
||||
|
}, |
||||
|
// Water |
||||
|
{ |
||||
|
id: 'water', |
||||
|
type: 'fill', |
||||
|
source: 'openmaptiles', |
||||
|
'source-layer': 'water', |
||||
|
paint: { 'fill-color': water } |
||||
|
}, |
||||
|
// Waterways |
||||
|
{ |
||||
|
id: 'waterway', |
||||
|
type: 'line', |
||||
|
source: 'openmaptiles', |
||||
|
'source-layer': 'waterway', |
||||
|
paint: { |
||||
|
'line-color': water, |
||||
|
'line-width': ['interpolate', ['linear'], ['zoom'], 8, 1, 14, 3] |
||||
|
} |
||||
|
}, |
||||
|
// Buildings (2D or 3D based on checkbox) |
||||
|
buildings3d ? { |
||||
|
id: 'buildings-3d', |
||||
|
type: 'fill-extrusion', |
||||
|
source: 'openmaptiles', |
||||
|
'source-layer': 'building', |
||||
|
minzoom: 13, |
||||
|
paint: { |
||||
|
'fill-extrusion-color': buildings, |
||||
|
'fill-extrusion-height': ['get', 'render_height'], |
||||
|
'fill-extrusion-base': ['get', 'render_min_height'], |
||||
|
'fill-extrusion-opacity': 0.85 |
||||
|
} |
||||
|
} : { |
||||
|
id: 'buildings', |
||||
|
type: 'fill', |
||||
|
source: 'openmaptiles', |
||||
|
'source-layer': 'building', |
||||
|
minzoom: 13, |
||||
|
paint: { |
||||
|
'fill-color': buildings, |
||||
|
'fill-opacity': 0.8 |
||||
|
} |
||||
|
}, |
||||
|
// Roads - service/track (smallest) |
||||
|
{ |
||||
|
id: 'roads-service', |
||||
|
type: 'line', |
||||
|
source: 'openmaptiles', |
||||
|
'source-layer': 'transportation', |
||||
|
filter: ['match', ['get', 'class'], ['service', 'track'], true, false], |
||||
|
paint: { |
||||
|
'line-color': roads, |
||||
|
'line-width': 1, |
||||
|
'line-opacity': 0.4 |
||||
|
} |
||||
|
}, |
||||
|
// Roads - path/pedestrian |
||||
|
{ |
||||
|
id: 'roads-path', |
||||
|
type: 'line', |
||||
|
source: 'openmaptiles', |
||||
|
'source-layer': 'transportation', |
||||
|
filter: ['match', ['get', 'class'], ['path', 'pedestrian'], true, false], |
||||
|
paint: { |
||||
|
'line-color': roads, |
||||
|
'line-width': 1, |
||||
|
'line-dasharray': [2, 1], |
||||
|
'line-opacity': 0.6 |
||||
|
} |
||||
|
}, |
||||
|
// Roads - minor |
||||
|
{ |
||||
|
id: 'roads-minor', |
||||
|
type: 'line', |
||||
|
source: 'openmaptiles', |
||||
|
'source-layer': 'transportation', |
||||
|
filter: ['==', ['get', 'class'], 'minor'], |
||||
|
layout: { 'line-cap': 'round', 'line-join': 'round' }, |
||||
|
paint: { |
||||
|
'line-color': roads, |
||||
|
'line-width': ['interpolate', ['exponential', 1.2], ['zoom'], 13, 1, 20, 10], |
||||
|
'line-opacity': 0.8 |
||||
|
} |
||||
|
}, |
||||
|
// Roads - secondary/tertiary |
||||
|
{ |
||||
|
id: 'roads-secondary', |
||||
|
type: 'line', |
||||
|
source: 'openmaptiles', |
||||
|
'source-layer': 'transportation', |
||||
|
filter: ['match', ['get', 'class'], ['secondary', 'tertiary'], true, false], |
||||
|
layout: { 'line-cap': 'round', 'line-join': 'round' }, |
||||
|
paint: { |
||||
|
'line-color': roads, |
||||
|
'line-width': ['interpolate', ['exponential', 1.2], ['zoom'], 8, 0.5, 20, 13] |
||||
|
} |
||||
|
}, |
||||
|
// Roads - primary/trunk |
||||
|
{ |
||||
|
id: 'roads-primary', |
||||
|
type: 'line', |
||||
|
source: 'openmaptiles', |
||||
|
'source-layer': 'transportation', |
||||
|
filter: ['match', ['get', 'class'], ['primary', 'trunk'], true, false], |
||||
|
layout: { 'line-cap': 'round', 'line-join': 'round' }, |
||||
|
paint: { |
||||
|
'line-color': roads, |
||||
|
'line-width': ['interpolate', ['exponential', 1.2], ['zoom'], 5, 0.5, 20, 18] |
||||
|
} |
||||
|
}, |
||||
|
// Roads - motorway |
||||
|
{ |
||||
|
id: 'roads-motorway', |
||||
|
type: 'line', |
||||
|
source: 'openmaptiles', |
||||
|
'source-layer': 'transportation', |
||||
|
filter: ['==', ['get', 'class'], 'motorway'], |
||||
|
layout: { 'line-cap': 'round', 'line-join': 'round' }, |
||||
|
paint: { |
||||
|
'line-color': roads, |
||||
|
'line-width': ['interpolate', ['exponential', 1.2], ['zoom'], 5, 1, 20, 20] |
||||
|
} |
||||
|
}, |
||||
|
// Rail |
||||
|
{ |
||||
|
id: 'rail', |
||||
|
type: 'line', |
||||
|
source: 'openmaptiles', |
||||
|
'source-layer': 'transportation', |
||||
|
filter: ['==', ['get', 'class'], 'rail'], |
||||
|
paint: { |
||||
|
'line-color': '#888888', |
||||
|
'line-width': 2, |
||||
|
'line-dasharray': [3, 3] |
||||
|
} |
||||
|
}, |
||||
|
// Boundaries |
||||
|
{ |
||||
|
id: 'boundary', |
||||
|
type: 'line', |
||||
|
source: 'openmaptiles', |
||||
|
'source-layer': 'boundary', |
||||
|
filter: ['==', ['get', 'admin_level'], 2], |
||||
|
paint: { |
||||
|
'line-color': '#888888', |
||||
|
'line-width': 1, |
||||
|
'line-dasharray': [4, 2] |
||||
|
} |
||||
|
}, |
||||
|
// Road labels |
||||
|
{ |
||||
|
id: 'road-labels', |
||||
|
type: 'symbol', |
||||
|
source: 'openmaptiles', |
||||
|
'source-layer': 'transportation_name', |
||||
|
minzoom: 14, |
||||
|
layout: { |
||||
|
'text-field': ['coalesce', ['get', 'name_en'], ['get', 'name']], |
||||
|
'text-size': 10, |
||||
|
'symbol-placement': 'line', |
||||
|
'text-font': ['Noto Sans Regular'] |
||||
|
}, |
||||
|
paint: { |
||||
|
'text-color': '#ffffff', |
||||
|
'text-halo-color': land, |
||||
|
'text-halo-width': 1 |
||||
|
} |
||||
|
}, |
||||
|
// Place labels |
||||
|
{ |
||||
|
id: 'place-labels', |
||||
|
type: 'symbol', |
||||
|
source: 'openmaptiles', |
||||
|
'source-layer': 'place', |
||||
|
layout: { |
||||
|
'text-field': ['coalesce', ['get', 'name_en'], ['get', 'name']], |
||||
|
'text-size': ['interpolate', ['linear'], ['zoom'], 10, 12, 14, 16], |
||||
|
'text-font': ['Noto Sans Bold'] |
||||
|
}, |
||||
|
paint: { |
||||
|
'text-color': '#ffffff', |
||||
|
'text-halo-color': land, |
||||
|
'text-halo-width': 2 |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
}; |
||||
|
|
||||
|
map.setStyle(customStyle); |
||||
|
} |
||||
|
|
||||
|
document.getElementById('applyColors').addEventListener('click', applyCustomStyle); |
||||
|
|
||||
|
// Color input change listeners |
||||
|
['colorLand', 'colorWater', 'colorRoads', 'colorBuildings', 'colorParks'].forEach(id => { |
||||
|
document.getElementById(id).addEventListener('input', () => { |
||||
|
if (document.querySelector('.style-btn.active').dataset.style === 'custom') { |
||||
|
applyCustomStyle(); |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
// 3D buildings checkbox |
||||
|
document.getElementById('buildings3d').addEventListener('change', () => { |
||||
|
if (document.querySelector('.style-btn.active').dataset.style === 'custom') { |
||||
|
applyCustomStyle(); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// Pitch/bearing sliders |
||||
|
document.getElementById('pitch').addEventListener('input', (e) => { |
||||
|
const val = parseInt(e.target.value); |
||||
|
map.setPitch(val); |
||||
|
document.getElementById('pitchVal').textContent = val + '°'; |
||||
|
}); |
||||
|
|
||||
|
document.getElementById('bearing').addEventListener('input', (e) => { |
||||
|
const val = parseInt(e.target.value); |
||||
|
map.setBearing(val); |
||||
|
document.getElementById('bearingVal').textContent = val + '°'; |
||||
|
}); |
||||
|
|
||||
|
// Sync sliders when map moves |
||||
|
map.on('move', () => { |
||||
|
document.getElementById('pitch').value = Math.round(map.getPitch()); |
||||
|
document.getElementById('pitchVal').textContent = Math.round(map.getPitch()) + '°'; |
||||
|
document.getElementById('bearing').value = Math.round(map.getBearing()); |
||||
|
document.getElementById('bearingVal').textContent = Math.round(map.getBearing()) + '°'; |
||||
|
}); |
||||
|
|
||||
|
// Re-add marker after style change |
||||
|
map.on('style.load', () => { |
||||
|
// Marker persists across style changes automatically in MapLibre |
||||
|
}); |
||||
|
</script> |
||||
|
</body> |
||||
|
</html> |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue