@ -27,7 +27,7 @@
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
z-index: 1000;
width: 30 0px;
width: 32 0px;
max-height: 90vh;
overflow-y: auto;
}
@ -90,7 +90,7 @@
display: flex;
align-items: center;
gap: 8px;
margin-top: 10 px;
margin-bottom: 6 px;
}
.checkbox-row label {
@ -98,6 +98,11 @@
cursor: pointer;
}
.checkbox-row input[type="checkbox"] {
width: 16px;
height: 16px;
}
.slider-row {
display: flex;
align-items: center;
@ -158,31 +163,12 @@
background: #c5372b;
}
.btn-secondary {
background: #666;
color: white;
}
.btn-secondary:hover {
background: #555;
}
.btn-block {
display: block;
width: 100%;
margin-bottom: 8px;
}
.btn-group {
display: flex;
gap: 8px;
margin-top: 10px;
}
.btn-group .btn {
flex: 1;
}
.theme-name-input {
width: 100%;
padding: 8px 12px;
@ -198,7 +184,7 @@
}
.saved-themes {
max-height: 20 0px;
max-height: 15 0px;
overflow-y: auto;
margin-top: 10px;
}
@ -311,9 +297,7 @@
border: 1px solid rgba(0,0,0,0.2);
}
.success-msg {
background: #d4edda;
color: #155724;
.success-msg, .error-msg {
padding: 8px 12px;
border-radius: 4px;
font-size: 12px;
@ -321,11 +305,36 @@
display: none;
}
.success-msg {
background: #d4edda;
color: #155724;
}
.error-msg {
background: #f8d7da;
color: #721c24;
}
.info-text {
font-size: 11px;
color: #666;
margin-top: 8px;
}
.login-prompt {
background: #fff3cd;
color: #856404;
padding: 10px;
border-radius: 4px;
font-size: 12px;
margin-bottom: 10px;
}
.toggle-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px;
}
< / style >
< / head >
< body >
@ -334,7 +343,12 @@
< div class = "control-panel" >
< h3 > Map Theme Editor< / h3 >
< div id = "loginPrompt" class = "login-prompt" style = "display:none;" >
Admin login required to save global game theme.
< / div >
< div id = "successMsg" class = "success-msg" > < / div >
< div id = "errorMsg" class = "error-msg" > < / div >
< div class = "section" >
< div class = "section-title" > Color Palette< / div >
@ -364,8 +378,37 @@
< input type = "color" id = "colorParks" value = "#1b4332" >
< span class = "hex-value" id = "hexParks" > #1b4332< / span >
< / div >
< / div >
< div class = "section" >
< div class = "section-title" > Map Elements< / div >
< div class = "toggle-grid" >
< div class = "checkbox-row" >
< input type = "checkbox" id = "showRoads" checked >
< label for = "showRoads" > Roads< / label >
< / div >
< div class = "checkbox-row" >
< input type = "checkbox" id = "showBuildings" checked >
< label for = "showBuildings" > Buildings< / label >
< / div >
< div class = "checkbox-row" >
< input type = "checkbox" id = "showParks" checked >
< label for = "showParks" > Parks< / label >
< / div >
< div class = "checkbox-row" >
< input type = "checkbox" id = "showWater" checked >
< label for = "showWater" > Water< / label >
< / div >
< div class = "checkbox-row" >
< input type = "checkbox" id = "showRoadLabels" checked >
< label for = "showRoadLabels" > Road Names< / label >
< / div >
< div class = "checkbox-row" >
< input type = "checkbox" id = "showPlaceLabels" checked >
< label for = "showPlaceLabels" > Place Names< / label >
< / div >
< / div >
< div class = "checkbox-row" style = "margin-top:8px;" >
< input type = "checkbox" id = "buildings3d" checked >
< label for = "buildings3d" > 3D Buildings (200% height)< / label >
< / div >
@ -441,7 +484,7 @@
< div class = "section" >
< div class = "section-title" > Apply to Game< / div >
< button class = "btn btn-primary btn-block" id = "applyToGame" > Set as Active Game Theme< / button >
< p class = "info-text" > This will save the current colors as the active theme used in HikeMap .< / p >
< p class = "info-text" > Saves to your account. Refresh HikeMap to see changes .< / p >
< / div >
< div class = "section" >
@ -482,7 +525,16 @@
}
};
// Load saved themes from localStorage
// Check if user is logged in
function getToken() {
return localStorage.getItem('accessToken');
}
function isLoggedIn() {
return !!getToken();
}
// Load saved themes from localStorage (local storage for theme library)
function loadSavedThemes() {
const themes = JSON.parse(localStorage.getItem('hikemap_themes') || '{}');
return themes;
@ -493,28 +545,40 @@
localStorage.setItem('hikemap_themes', JSON.stringify(themes));
}
// Get current colors from inputs
function getCurrentColors () {
// Get current theme from inputs
function getCurrentTheme () {
return {
land: document.getElementById('colorLand').value,
water: document.getElementById('colorWater').value,
roads: document.getElementById('colorRoads').value,
buildings: document.getElementById('colorBuildings').value,
parks: document.getElementById('colorParks').value,
buildings3d: document.getElementById('buildings3d').checked
buildings3d: document.getElementById('buildings3d').checked,
showRoads: document.getElementById('showRoads').checked,
showBuildings: document.getElementById('showBuildings').checked,
showParks: document.getElementById('showParks').checked,
showWater: document.getElementById('showWater').checked,
showRoadLabels: document.getElementById('showRoadLabels').checked,
showPlaceLabels: document.getElementById('showPlaceLabels').checked
};
}
// Set colors to inputs
function setColors(colors) {
document.getElementById('colorLand').value = colors.land;
document.getElementById('colorWater').value = colors.water;
document.getElementById('colorRoads').value = colors.roads;
document.getElementById('colorBuildings').value = colors.buildings;
document.getElementById('colorParks').value = colors.parks;
if (colors.buildings3d !== undefined) {
document.getElementById('buildings3d').checked = colors.buildings3d;
}
// Set theme to inputs
function setTheme(theme) {
if (theme.land) document.getElementById('colorLand').value = theme.land;
if (theme.water) document.getElementById('colorWater').value = theme.water;
if (theme.roads) document.getElementById('colorRoads').value = theme.roads;
if (theme.buildings) document.getElementById('colorBuildings').value = theme.buildings;
if (theme.parks) document.getElementById('colorParks').value = theme.parks;
document.getElementById('buildings3d').checked = theme.buildings3d !== false;
document.getElementById('showRoads').checked = theme.showRoads !== false;
document.getElementById('showBuildings').checked = theme.showBuildings !== false;
document.getElementById('showParks').checked = theme.showParks !== false;
document.getElementById('showWater').checked = theme.showWater !== false;
document.getElementById('showRoadLabels').checked = theme.showRoadLabels !== false;
document.getElementById('showPlaceLabels').checked = theme.showPlaceLabels !== false;
updateHexDisplays();
applyStyle();
}
@ -563,9 +627,8 @@
const themeName = item.dataset.theme;
const themes = loadSavedThemes();
if (themes[themeName]) {
setColors (themes[themeName]);
setTheme (themes[themeName]);
document.getElementById('themeName').value = themeName;
// Update active state
container.querySelectorAll('.theme-item').forEach(i => i.classList.remove('active'));
item.classList.add('active');
}
@ -591,91 +654,196 @@
// Show success message
function showSuccess(msg) {
const el = document.getElementById('successMsg');
document.getElementById('errorMsg').style.display = 'none';
el.textContent = msg;
el.style.display = 'block';
setTimeout(() => { el.style.display = 'none'; }, 3000);
}
// Show error message
function showError(msg) {
const el = document.getElementById('errorMsg');
document.getElementById('successMsg').style.display = 'none';
el.textContent = msg;
el.style.display = 'block';
setTimeout(() => { el.style.display = 'none'; }, 5000);
}
// Build and apply the map style
function applyStyle() {
const colors = getCurrentColors();
const theme = getCurrentTheme ();
const style = {
version: 8,
name: 'HikeMap Theme',
sources: {
'openmaptiles': {
type: 'vector',
url: 'https://tiles.openfreemap.org/planet'
}
},
glyphs: 'https://tiles.openfreemap.org/fonts/{fontstack}/{range}.pbf',
layers: [
{ id: 'background', type: 'background', paint: { 'background-color': colors.land } },
const layers = [
{ id: 'background', type: 'background', paint: { 'background-color': theme.land } }
];
// Parks
if (theme.showParks) {
layers.push(
{ id: 'park', type: 'fill', source: 'openmaptiles', 'source-layer': 'park',
paint: { 'fill-color': colors .parks, 'fill-opacity': 0.7 } },
paint: { 'fill-color': theme.parks, 'fill-opacity': 0.7 } },
{ id: 'landcover_wood', type: 'fill', source: 'openmaptiles', 'source-layer': 'landcover',
filter: ['==', ['get', 'class'], 'wood'],
paint: { 'fill-color': colors .parks, 'fill-opacity': 0.5 } },
paint: { 'fill-color': theme.parks, 'fill-opacity': 0.5 } },
{ id: 'landcover_grass', type: 'fill', source: 'openmaptiles', 'source-layer': 'landcover',
filter: ['==', ['get', 'class'], 'grass'],
paint: { 'fill-color': colors.parks, 'fill-opacity': 0.4 } },
paint: { 'fill-color': theme.parks, 'fill-opacity': 0.4 } }
);
}
// Water
if (theme.showWater) {
layers.push(
{ id: 'water', type: 'fill', source: 'openmaptiles', 'source-layer': 'water',
paint: { 'fill-color': colors.water } },
paint: { 'fill-color': theme .water } },
{ id: 'waterway', type: 'line', source: 'openmaptiles', 'source-layer': 'waterway',
paint: { 'line-color': colors.water, 'line-width': ['interpolate', ['linear'], ['zoom'], 8, 1, 14, 3] } },
colors.buildings3d ? {
paint: { 'line-color': theme.water, 'line-width': ['interpolate', ['linear'], ['zoom'], 8, 1, 14, 3] } }
);
}
// Buildings
if (theme.showBuildings) {
if (theme.buildings3d) {
layers.push({
id: 'buildings-3d', type: 'fill-extrusion', source: 'openmaptiles', 'source-layer': 'building', minzoom: 13,
paint: {
'fill-extrusion-color': colors.buildings,
'fill-extrusion-color': theme .buildings,
'fill-extrusion-height': ['*', 2, ['get', 'render_height']],
'fill-extrusion-base': ['*', 2, ['get', 'render_min_height']],
'fill-extrusion-opacity': 0.85
}
} : {
});
} else {
layers.push({
id: 'buildings', type: 'fill', source: 'openmaptiles', 'source-layer': 'building', minzoom: 13,
paint: { 'fill-color': colors.buildings, 'fill-opacity': 0.8 }
},
paint: { 'fill-color': theme.buildings, 'fill-opacity': 0.8 }
});
}
}
// Roads
if (theme.showRoads) {
layers.push(
{ id: 'roads-service', type: 'line', source: 'openmaptiles', 'source-layer': 'transportation',
filter: ['match', ['get', 'class'], ['service', 'track'], true, false],
paint: { 'line-color': colors.roads, 'line-width': 1, 'line-opacity': 0.4 } },
paint: { 'line-color': theme .roads, 'line-width': 1, 'line-opacity': 0.4 } },
{ id: 'roads-path', type: 'line', source: 'openmaptiles', 'source-layer': 'transportation',
filter: ['match', ['get', 'class'], ['path', 'pedestrian'], true, false],
paint: { 'line-color': colors.roads, 'line-width': 1, 'line-dasharray': [2, 1], 'line-opacity': 0.6 } },
paint: { 'line-color': theme .roads, 'line-width': 1, 'line-dasharray': [2, 1], 'line-opacity': 0.6 } },
{ id: 'roads-minor', type: 'line', source: 'openmaptiles', 'source-layer': 'transportation',
filter: ['==', ['get', 'class'], 'minor'],
layout: { 'line-cap': 'round', 'line-join': 'round' },
paint: { 'line-color': colors .roads, 'line-width': ['interpolate', ['exponential', 1.2], ['zoom'], 13, 1, 20, 10], 'line-opacity': 0.8 } },
paint: { 'line-color': theme .roads, 'line-width': ['interpolate', ['exponential', 1.2], ['zoom'], 13, 1, 20, 10], 'line-opacity': 0.8 } },
{ 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': colors .roads, 'line-width': ['interpolate', ['exponential', 1.2], ['zoom'], 8, 0.5, 20, 13] } },
paint: { 'line-color': theme .roads, 'line-width': ['interpolate', ['exponential', 1.2], ['zoom'], 8, 0.5, 20, 13] } },
{ 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': colors .roads, 'line-width': ['interpolate', ['exponential', 1.2], ['zoom'], 5, 0.5, 20, 18] } },
paint: { 'line-color': theme .roads, 'line-width': ['interpolate', ['exponential', 1.2], ['zoom'], 5, 0.5, 20, 18] } },
{ id: 'roads-motorway', type: 'line', source: 'openmaptiles', 'source-layer': 'transportation',
filter: ['==', ['get', 'class'], 'motorway'],
layout: { 'line-cap': 'round', 'line-join': 'round' },
paint: { 'line-color': colors.roads, 'line-width': ['interpolate', ['exponential', 1.2], ['zoom'], 5, 1, 20, 20] } },
paint: { 'line-color': theme.roads, 'line-width': ['interpolate', ['exponential', 1.2], ['zoom'], 5, 1, 20, 20] } }
);
}
// Rail and boundary (always show)
layers.push(
{ id: 'rail', type: 'line', source: 'openmaptiles', 'source-layer': 'transportation',
filter: ['==', ['get', 'class'], 'rail'],
paint: { 'line-color': '#888888', 'line-width': 2, 'line-dasharray': [3, 3] } },
{ 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] } },
{ id: 'road-labels', type: 'symbol', source: 'openmaptiles', 'source-layer': 'transportation_name', minzoom: 14,
paint: { 'line-color': '#888888', 'line-width': 1, 'line-dasharray': [4, 2] } }
);
// Road labels
if (theme.showRoadLabels) {
layers.push({
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': colors.land, 'text-halo-width': 1 } },
{ id: 'place-labels', type: 'symbol', source: 'openmaptiles', 'source-layer': 'place',
paint: { 'text-color': '#ffffff', 'text-halo-color': theme.land, 'text-halo-width': 1 }
});
}
// Place labels
if (theme.showPlaceLabels) {
layers.push({
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': colors.land, 'text-halo-width': 2 } }
]
paint: { 'text-color': '#ffffff', 'text-halo-color': theme.land, 'text-halo-width': 2 }
});
}
const style = {
version: 8,
name: 'HikeMap Theme',
sources: {
'openmaptiles': {
type: 'vector',
url: 'https://tiles.openfreemap.org/planet'
}
},
glyphs: 'https://tiles.openfreemap.org/fonts/{fontstack}/{range}.pbf',
layers: layers
};
map.setStyle(style);
}
// Load active theme from server (public endpoint)
async function loadActiveTheme() {
try {
const response = await fetch('/api/map-theme');
if (response.ok) {
const data = await response.json();
return data.theme;
}
} catch (err) {
console.error('Error loading theme from server:', err);
}
return null;
}
// Save active theme to server (admin only)
async function saveActiveTheme(theme) {
const token = getToken();
if (!token) {
showError('Please log in as admin to HikeMap first');
return false;
}
try {
const response = await fetch('/api/admin/map-theme', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ theme })
});
if (response.ok) {
return true;
} else if (response.status === 403) {
showError('Admin access required to set game theme');
return false;
} else {
const err = await response.json();
showError(err.error || 'Failed to save theme');
return false;
}
} catch (err) {
console.error('Error saving theme to server:', err);
showError('Failed to save theme to server');
return false;
}
}
// Initialize map
const map = new maplibregl.Map({
container: 'map',
@ -689,18 +857,16 @@
map.addControl(new maplibregl.NavigationControl());
// Apply initial style after map loads
map.on('load', () => {
map.on('load', async () => {
// Try to load active theme from server
const serverTheme = await loadActiveTheme();
if (serverTheme) {
setTheme(serverTheme);
} else {
applyStyle();
renderSavedThemes();
// Load active game theme if exists
const activeTheme = localStorage.getItem('hikemap_active_theme');
if (activeTheme) {
try {
const colors = JSON.parse(activeTheme);
setColors(colors);
} catch (e) {}
}
renderSavedThemes();
});
// Color input handlers
@ -711,19 +877,22 @@
});
});
document.getElementById('buildings3d').addEventListener('change', applyStyle);
// Toggle handlers
['buildings3d', 'showRoads', 'showBuildings', 'showParks', 'showWater', 'showRoadLabels', 'showPlaceLabels'].forEach(id => {
document.getElementById(id).addEventListener('change', applyStyle);
});
// Preset handlers
document.querySelectorAll('.preset-btn').forEach(btn => {
btn.addEventListener('click', () => {
const presetName = btn.dataset.preset;
if (presets[presetName]) {
setColors({ ...presets[presetName], buildings3d : true });
setTheme({ ...presets[presetName], buildings3d: true, showRoads: true, showBuildings: true, showParks: true, showWater: true, showRoadLabels: true, showPlaceLabels : true });
}
});
});
// Save theme
// Save theme to local library
document.getElementById('saveTheme').addEventListener('click', () => {
const name = document.getElementById('themeName').value.trim();
if (!name) {
@ -731,17 +900,19 @@
return;
}
const themes = loadSavedThemes();
themes[name] = getCurrentColors ();
themes[name] = getCurrentTheme ();
saveSavedThemes(themes);
renderSavedThemes();
showSuccess(`Saved "${name}"`);
showSuccess(`Saved "${name}" to library `);
});
// Apply to game
document.getElementById('applyToGame').addEventListener('click', () => {
const colors = getCurrentColors();
localStorage.setItem('hikemap_active_theme', JSON.stringify(colors));
showSuccess('Theme applied to game! Refresh HikeMap to see changes.');
// Apply to game (save to server)
document.getElementById('applyToGame').addEventListener('click', async () => {
const theme = getCurrentTheme();
const success = await saveActiveTheme(theme);
if (success) {
showSuccess('Theme applied! Refresh HikeMap to see changes.');
}
});
// Camera controls