Browse Source

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
HikeMap User 4 weeks ago
parent
commit
e8545656f7
  1. 1
      docker-compose.yml
  2. 624
      maplibre-test.html

1
docker-compose.yml

@ -6,6 +6,7 @@ services:
volumes:
- ./index.html:/app/index.html:ro
- ./admin.html:/app/admin.html:ro
- ./maplibre-test.html:/app/maplibre-test.html:ro
- ./server.js:/app/server.js:ro
- ./database.js:/app/database.js:ro
- ./service-worker.js:/app/service-worker.js:ro

624
maplibre-test.html

@ -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"></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"></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>
Loading…
Cancel
Save