From 9a0a910a94541cc7011afee3a7059f1d0b0028e8 Mon Sep 17 00:00:00 2001 From: hgosalia Date: Wed, 27 May 2026 21:15:38 -0400 Subject: [PATCH] Feature: Apply additional layers to map --- css/styles.css | 2 +- js/map.js | 141 +++++++++++++++++++++++++++++++++++++++++++++++-- js/state.js | 1 + 3 files changed, 140 insertions(+), 4 deletions(-) diff --git a/css/styles.css b/css/styles.css index f7f81d0..5734469 100644 --- a/css/styles.css +++ b/css/styles.css @@ -187,7 +187,7 @@ input,textarea,select{font-family:var(--font)} #sidebar.collapsed~#sidebar-toggle{left:0} /* MAP TOOLBAR */ -#map-toolbar{position:absolute;top:12px;left:50%;transform:translateX(-50%);z-index:5;display:flex;gap:5px;align-items:center;background:var(--surface);border:1px solid var(--border);border-radius:28px;padding:5px 8px;box-shadow:var(--shadow)} +#map-toolbar{position:absolute;top:12px;left:50%;transform:translateX(-50%) translateZ(0);z-index:5;display:flex;gap:5px;align-items:center;background:var(--surface);border:1px solid var(--border);border-radius:28px;padding:5px 8px;box-shadow:var(--shadow);-webkit-font-smoothing:antialiased} .tb-btn{background:none;border:none;color:var(--muted);font-size:.73rem;font-weight:500;padding:5px 10px;border-radius:20px;transition:all .15s;white-space:nowrap} .tb-btn:disabled{opacity:0.35;cursor:not-allowed;pointer-events:none} .tb-btn:hover{background:var(--surface2);color:var(--text)} diff --git a/js/map.js b/js/map.js index d0b0df0..1b6d19e 100644 --- a/js/map.js +++ b/js/map.js @@ -107,9 +107,9 @@ function _patchStyleWater(styleObj) { } // Apple Maps Dark palette — neutral dark land, distinctly blue water. // Colors pre-compensated for canvas filter brightness(1.8) contrast(0.9). - if (_mapStyle === 'dark' || _mapStyle === 'terrain3d') { - // Darker water for both Dark Map and 3D Terrain modes - const waterColor = _mapStyle === 'terrain3d' ? '#1a2e3d' : '#131619'; + if (_mapStyle === 'dark') { + // Darker water for Dark Map + const waterColor = '#131619'; for (const layer of styleObj.layers) { if (!layer.paint) layer.paint = {}; if (layer.type === 'fill' && /^water/.test(layer.id)) { @@ -273,6 +273,120 @@ function toggleLabels() { } } +// Apply vivid parks, airport runways, mountain peaks, and optional 3D buildings +function applyExtraLayers() { + const hasOmt = map.getSource && map.getSource('openmaptiles'); + if (!hasOmt || ['satellite', 'globe'].includes(_mapStyle)) return; + + // Vivid park fill — more saturated green over the base park layer + if (!map.getLayer('matrix-park-vivid')) { + map.addLayer({ + id: 'matrix-park-vivid', + type: 'fill', + source: 'openmaptiles', + 'source-layer': 'park', + minzoom: 5, + paint: { + 'fill-color': '#5aab5a', + 'fill-opacity': ['interpolate', ['linear'], ['zoom'], 5, 0.08, 12, 0.18] + } + }, 'park_outline'); + } + + // Airport runway & taxiway fill — visible from zoom 9+ + if (!map.getLayer('matrix-aeroway-fill')) { + map.addLayer({ + id: 'matrix-aeroway-fill', + type: 'fill', + source: 'openmaptiles', + 'source-layer': 'aeroway', + minzoom: 9, + filter: ['match', ['geometry-type'], ['Polygon', 'MultiPolygon'], true, false], + paint: { + 'fill-color': '#d8d4cb', + 'fill-opacity': 0.6 + } + }, 'aeroway_fill'); + } + if (!map.getLayer('matrix-aeroway-runway')) { + map.addLayer({ + id: 'matrix-aeroway-runway', + type: 'line', + source: 'openmaptiles', + 'source-layer': 'aeroway', + minzoom: 10, + filter: ['==', ['get', 'class'], 'runway'], + paint: { + 'line-color': '#c8c4bb', + 'line-width': ['interpolate', ['exponential', 1.2], ['zoom'], 10, 1, 18, 12] + } + }); + } + + // Mountain peak labels — shows summit names at zoom 10+ + if (!map.getLayer('matrix-mountain-peaks')) { + map.addLayer({ + id: 'matrix-mountain-peaks', + type: 'symbol', + source: 'openmaptiles', + 'source-layer': 'poi', + minzoom: 10, + filter: ['==', ['get', 'class'], 'mountain'], + layout: { + 'text-field': ['coalesce', ['get', 'name_en'], ['get', 'name']], + 'text-font': ['Noto Sans Bold'], + 'text-size': 11, + 'text-anchor': 'top', + 'text-offset': [0, 0.5], + 'icon-image': 'mountain_11', + 'icon-size': 0.8, + 'icon-allow-overlap': false, + 'text-allow-overlap': false + }, + paint: { + 'text-color': _mapStyle === 'dark' ? '#a0b8c0' : '#5a7a8a', + 'text-halo-color': _mapStyle === 'dark' ? '#1a2530' : '#ffffff', + 'text-halo-width': 1.5 + } + }); + } + + // 3D buildings — only when toggled on, zoom 15+ + apply3DBuildings(); +} + +function apply3DBuildings() { + if (!map.getSource || !map.getSource('openmaptiles')) return; + const shouldShow = buildings3DVisible && !['satellite', 'terrain3d', 'globe'].includes(_mapStyle); + if (shouldShow && !map.getLayer('matrix-buildings-3d')) { + // Insert below the first symbol layer so road/POI labels render on top of buildings + const firstSymbol = map.getStyle()?.layers?.find(l => l.type === 'symbol'); + map.addLayer({ + id: 'matrix-buildings-3d', + type: 'fill-extrusion', + source: 'openmaptiles', + 'source-layer': 'building', + minzoom: 15, + paint: { + 'fill-extrusion-color': _mapStyle === 'dark' ? '#2a2e3a' : '#d9d6d0', + 'fill-extrusion-height': ['get', 'render_height'], + 'fill-extrusion-base': ['get', 'render_min_height'], + 'fill-extrusion-opacity': 0.75 + } + }, firstSymbol?.id); + } else if (!shouldShow && map.getLayer('matrix-buildings-3d')) { + map.removeLayer('matrix-buildings-3d'); + } + const btn = document.getElementById('tb-3d-buildings'); + if (btn) btn.style.opacity = buildings3DVisible ? '1' : '.4'; +} + +function toggle3DBuildings() { + buildings3DVisible = !buildings3DVisible; + localStorage.setItem('matrix-3d-buildings', buildings3DVisible ? 'visible' : 'hidden'); + apply3DBuildings(); +} + function addPinLayers() { if (!map.getSource('photo-pins')) { map.addSource('photo-pins', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } }); @@ -383,6 +497,7 @@ async function _doStyleSwap(style) { raiseLabelsAboveRoads(); applyLabelScale(); applyLabelVisibility(); + applyExtraLayers(); // Re-add pin icons from pixel cache (fast) and refresh clusters // without rebuilding the Supercluster index (unchanged) // Update dark-map CSS class after pin icons are re-added with correct compensation @@ -547,9 +662,26 @@ async function initMap() { btn.addEventListener('click', toggleLabels); wrap.appendChild(btn); navGroup.after(wrap); + + // 3D Buildings toggle — inserted before navGroup so it appears above zoom controls + const bldgWrap = document.createElement('div'); + bldgWrap.className = 'maplibregl-ctrl maplibregl-ctrl-group'; + bldgWrap.id = 'buildings-toggle-wrap'; + const bldgBtn = document.createElement('button'); + bldgBtn.id = 'tb-3d-buildings'; + bldgBtn.type = 'button'; + bldgBtn.title = '3D Buildings (zoom 15+)'; + bldgBtn.setAttribute('aria-label', 'Toggle 3D buildings'); + bldgBtn.style.opacity = buildings3DVisible ? '1' : '.4'; + bldgBtn.innerHTML = '3D'; + bldgBtn.addEventListener('click', toggle3DBuildings); + bldgWrap.appendChild(bldgBtn); + if (['satellite', 'terrain3d', 'globe'].includes(_mapStyle)) bldgWrap.style.display = 'none'; + navGroup.before(bldgWrap); } applyLabelScale(); applyLabelVisibility(); + applyExtraLayers(); _applyTerrainAndProjection(); // Tile loading spinner const tileSpinner = document.getElementById('tile-spinner'); @@ -751,6 +883,9 @@ function setMapStyle(mode) { // Labels toggle visibility const labelsWrap = document.getElementById('labels-toggle-wrap'); if (labelsWrap) labelsWrap.style.visibility = _mapStyle === 'satellite' ? 'hidden' : 'visible'; + // Hide 3D buildings toggle in modes where buildings don't make sense + const bldgWrap = document.getElementById('buildings-toggle-wrap'); + if (bldgWrap) bldgWrap.style.display = ['satellite', 'terrain3d', 'globe'].includes(_mapStyle) ? 'none' : ''; // Disable Export Video in Globe mode (flyTo animation doesn't translate to globe projection) const exportBtn = document.getElementById('tb-export-video'); diff --git a/js/state.js b/js/state.js index 06a67b6..1e197b7 100644 --- a/js/state.js +++ b/js/state.js @@ -35,6 +35,7 @@ let pinPickerCoords = null; // map style + labels let _mapStyle = 'dark'; // 'dark' | 'light' | 'enriched' | 'satellite' let labelsVisible = (() => { const v = localStorage.getItem('matrix-labels'); return v === null || v === 'visible'; })(); +let buildings3DVisible = (() => localStorage.getItem('matrix-3d-buildings') === 'visible')(); // helpers let toastT;