Skip to content

Commit b84287a

Browse files
authored
Merge pull request #17 from hothsys/hillside-shading-fix
Hillside shading fix
2 parents ca011f8 + ef019e8 commit b84287a

4 files changed

Lines changed: 114 additions & 23 deletions

File tree

css/styles.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ input,textarea,select{font-family:var(--font)}
5151
#countries-bar{display:none;margin:0 14px 7px;padding:6px 10px;border-radius:9px;background:var(--surface2);border:1px solid var(--border);flex-shrink:0;text-align:center}
5252
#countries-bar .cb-label{font-size:.58rem;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:8px}
5353
#countries-bar .cb-status{font-size:.68rem;color:var(--text);height:1.1em;margin-top:10px;transition:opacity .15s;opacity:.5}
54-
#countries-bar .cb-flags{font-size:.8rem;letter-spacing:2px;line-height:1.4;overflow:hidden}
54+
#countries-bar .cb-flags{font-size:1rem;letter-spacing:2px;line-height:1.4;overflow:hidden}
5555
#countries-bar .cb-flags span{cursor:default;transition:transform .1s}
5656
#countries-bar .cb-flags span:hover{transform:scale(1.25)}
5757
#countries-bar .cb-flags.collapsed{max-height:2.8em}
@@ -170,6 +170,7 @@ input,textarea,select{font-family:var(--font)}
170170
#esri-attribution{display:none;position:absolute;bottom:6px;left:6px;background:rgba(30,30,30,.6);color:#aaa;font-size:.55rem;padding:2px 6px;border-radius:4px;z-index:2;pointer-events:auto}
171171
#esri-attribution a{color:var(--accent);text-decoration:none}
172172
#map.sat-mode~#esri-attribution{display:block}
173+
.maplibregl-ctrl-bottom-right{bottom:16px!important;right:2px!important}
173174
.maplibregl-ctrl-group{background:var(--surface)!important;border:1px solid var(--border)!important;border-radius:10px!important;overflow:hidden;box-shadow:var(--shadow)!important}
174175
.maplibregl-ctrl-group button{background:var(--surface)!important;border-bottom:1px solid var(--border)!important}
175176
.maplibregl-ctrl-group button:hover{background:var(--surface2)!important}

index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@
123123
<button class="tb-btn" id="tb-style-btn" onclick="toggleStyleMenu(event)">Dark Map ▾</button>
124124
<div class="style-menu" id="style-menu">
125125
<div class="style-menu-item" data-style="light" onclick="setMapStyle('light')">Light Map</div>
126+
<div class="style-menu-item" data-style="bright" onclick="setMapStyle('bright')">Bright Map</div>
126127
<div class="style-menu-item active" data-style="dark" onclick="setMapStyle('dark')">Dark Map</div>
127128
<div class="style-menu-item" data-style="enriched" onclick="setMapStyle('enriched')">Terrain</div>
128129
<div class="style-menu-item" data-style="terrain3d" onclick="setMapStyle('terrain3d')">3D Terrain</div>

js/demo.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,24 @@
55

66
// Ctrl+Shift+D — automated app walkthrough with fake cursor
77
// Ctrl+Shift+G — globe rotation demo
8+
// L/D/T/3/S/G — map style shortcuts F — fit all pins
89
document.addEventListener('keydown', e => {
910
if (e.ctrlKey && e.shiftKey && e.key === 'D') { e.preventDefault(); runDemo(); }
1011
if (e.ctrlKey && e.shiftKey && e.key === 'G') { e.preventDefault(); runGlobeDemo(); }
12+
13+
// Map style + navigation shortcuts — skip when typing in inputs or using modifier keys
14+
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') return;
15+
if (e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) return;
16+
switch (e.key) {
17+
case 'l': case 'L': setMapStyle('light'); break;
18+
case 'b': case 'B': setMapStyle('bright'); break;
19+
case 'd': case 'D': setMapStyle('dark'); break;
20+
case 't': case 'T': setMapStyle('enriched'); break;
21+
case '3': setMapStyle('terrain3d'); break;
22+
case 's': case 'S': setMapStyle('satellite'); break;
23+
case 'g': case 'G': setMapStyle('globe'); break;
24+
case 'f': case 'F': fitAll(); break;
25+
}
1126
});
1227

1328
// ═══════════════════════════════════════

js/map.js

Lines changed: 96 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
function _styleUrl() {
55
if (_mapStyle === 'satellite') return STYLE_SAT;
66
if (_mapStyle === 'dark') return STYLE_DARK;
7+
if (_mapStyle === 'bright') return STYLE_BRIGHT;
78
return STYLE_STREET; // light, enriched, terrain3d, globe all use Liberty as base
89
}
910
const STYLE_STREET = 'https://tiles.openfreemap.org/styles/liberty';
11+
const STYLE_BRIGHT = 'https://tiles.openfreemap.org/styles/bright';
1012
const STYLE_DARK = 'https://tiles.openfreemap.org/styles/dark';
1113
const STYLE_SAT = {
1214
version:8,
@@ -94,18 +96,24 @@ function _patchStyleBoundaries(styleObj) {
9496
// - Sets ocean fill color for light mode
9597
function _patchStyleWater(styleObj) {
9698
if (!styleObj || !styleObj.layers) return styleObj;
99+
// Remove unsupported layer types that cause MapLibre v5 validation errors
100+
styleObj.layers = styleObj.layers.filter(l => l.type !== 'sky');
101+
if (styleObj.sky) delete styleObj.sky;
102+
if (styleObj.terrain) delete styleObj.terrain;
97103
// Strip natural-earth terrain raster in Light Map (not enriched) to speed up rendering
98104
if (_mapStyle === 'light') {
99105
styleObj.layers = styleObj.layers.filter(l => l.id !== 'natural_earth');
100106
if (styleObj.sources) delete styleObj.sources['ne2_shaded'];
101107
}
102108
// Apple Maps Dark palette — neutral dark land, distinctly blue water.
103109
// Colors pre-compensated for canvas filter brightness(1.8) contrast(0.9).
104-
if (_mapStyle === 'dark') {
110+
if (_mapStyle === 'dark' || _mapStyle === 'terrain3d') {
111+
// Darker water for both Dark Map and 3D Terrain modes
112+
const waterColor = _mapStyle === 'terrain3d' ? '#1a2e3d' : '#131619';
105113
for (const layer of styleObj.layers) {
106114
if (!layer.paint) layer.paint = {};
107115
if (layer.type === 'fill' && /^water/.test(layer.id)) {
108-
layer.paint['fill-color'] = '#131619';
116+
layer.paint['fill-color'] = waterColor;
109117
}
110118
}
111119
}
@@ -299,12 +307,18 @@ function addPinLayers() {
299307
try { openPinPopup(lat, lng); } catch(err) { console.error(err); }
300308
const isEmpty = f.properties.iconId === 'pin-empty';
301309
if (!isEmpty) {
302-
const targetZoom = Math.max(map.getZoom(), 14);
310+
// Zoom in by at most 4 levels from current zoom, capped at 14
311+
// Prevents a jarring jump from low zoom levels (e.g. zoom 3 → 14)
312+
const targetZoom = Math.min(Math.max(map.getZoom(), map.getZoom() + 4), 14);
303313
const needsZoom = map.getZoom() < targetZoom;
304314
const dist = Math.hypot(map.getCenter().lng - lng, map.getCenter().lat - lat);
305315
const alreadyThere = dist < 0.005 && !needsZoom;
306316
if (!alreadyThere) {
317+
// In 3D Terrain (pitched view), offset the focal point downward in screen space
318+
// so the pin appears in the visible ground area rather than drifting off-screen
319+
const pitchOffset = _mapStyle === 'terrain3d' ? [0, Math.round(map.transform?.height * 0.15 || 100)] : [0, 0];
307320
map.flyTo({ center: [lng, lat], zoom: targetZoom, speed: 0.8, curve: 1.0, essential: true,
321+
offset: pitchOffset,
308322
easing: t => t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t+2, 3)/2 });
309323
}
310324
}
@@ -318,7 +332,7 @@ function addPinLayers() {
318332
// ═══════════════════════════════════════
319333
function initTheme() {
320334
const stored = localStorage.getItem('matrix-theme');
321-
if (stored && ['dark', 'light', 'enriched', 'terrain3d', 'globe'].includes(stored)) {
335+
if (stored && ['dark', 'light', 'bright', 'enriched', 'terrain3d', 'globe'].includes(stored)) {
322336
_mapStyle = stored;
323337
} else {
324338
_mapStyle = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
@@ -330,8 +344,9 @@ function applyTheme() {
330344
_tileTemplatesCache = null;
331345
// Set initial button label
332346
const btn = document.getElementById('tb-style-btn');
333-
const labels = { light: 'Light Map', enriched: 'Terrain', dark: 'Dark Map', satellite: 'Satellite', terrain3d: '3D Terrain', globe: 'Globe' };
347+
const labels = { light: 'Light Map', bright: 'Bright Map', enriched: 'Terrain', dark: 'Dark Map', satellite: 'Satellite', terrain3d: '3D Terrain', globe: 'Globe' };
334348
if (btn) btn.textContent = (labels[_mapStyle] || _mapStyle) + ' ▾';
349+
335350
// Set initial active state in menu
336351
document.querySelectorAll('.style-menu-item').forEach(el => el.classList.toggle('active', el.dataset.style === _mapStyle));
337352
// Disable Export Video in Globe mode
@@ -429,7 +444,13 @@ async function initMap() {
429444
const otherUrl = styleUrl === STYLE_DARK ? STYLE_STREET : STYLE_DARK;
430445
fetch(otherUrl).then(r => r.json()).then(j => { _styleJsonCache[otherUrl] = j; }).catch(() => {});
431446
map = new maplibregl.Map({ container:'map', style: initStyle, center:[0,20], zoom:1.8, attributionControl:false, maxTileCacheSize:200, canvasContextAttributes:{ preserveDrawingBuffer:true } });
432-
map.on('error', (e) => console.error('MapLibre error:', e.error?.message || e.message || e));
447+
// Suppress non-critical style validation errors (sky/terrain-dem references in
448+
// unpatched style JSON when the pre-fetch falls back to URL loading)
449+
const _suppressedErrors = /sky|terrain-dem|non-existing layer|same source.*hillshade/i;
450+
map.on('error', (e) => {
451+
const msg = e.error?.message || e.message || '';
452+
if (!_suppressedErrors.test(msg)) console.error('MapLibre error:', msg);
453+
});
433454
map.addControl(new maplibregl.NavigationControl({showCompass:false}), 'bottom-right');
434455
// Recover from WebGL context loss (Safari loses context after sleep or memory pressure)
435456
const canvas = map.getCanvas();
@@ -464,29 +485,40 @@ async function initMap() {
464485
zoomEl.style.cssText = 'position:absolute;bottom:24px;left:8px;background:rgba(0,0,0,.6);color:#fff;font-size:11px;padding:2px 6px;border-radius:4px;z-index:10;pointer-events:none;font-family:monospace';
465486
document.getElementById('map').appendChild(zoomEl);
466487
const pitchWrap = document.createElement('div');
467-
pitchWrap.style.cssText = 'position:absolute;bottom:24px;left:70px;display:none;align-items:center;gap:4px;z-index:10';
468-
const pitchEl = document.createElement('div');
488+
pitchWrap.id = 'pitch-wrap';
489+
pitchWrap.style.cssText = 'position:absolute;bottom:24px;left:70px;display:none;align-items:center;z-index:10;background:rgba(0,0,0,.6);border-radius:4px;font-family:monospace;font-size:11px;color:#fff';
490+
const pitchEl = document.createElement('span');
469491
pitchEl.id = 'pitch-debug';
470-
pitchEl.style.cssText = 'background:rgba(0,0,0,.6);color:#fff;font-size:11px;padding:2px 6px;border-radius:4px;pointer-events:none;font-family:monospace';
492+
pitchEl.style.cssText = 'padding:2px 6px;pointer-events:none';
471493
const resetViewEl = document.createElement('button');
472494
resetViewEl.id = 'reset-view-btn';
473-
resetViewEl.title = 'Reset to top-down view';
495+
resetViewEl.title = 'Reset north';
474496
resetViewEl.textContent = '⊙';
475-
resetViewEl.style.cssText = 'background:rgba(0,0,0,.6);color:#fff;font-size:14px;border:none;border-radius:4px;cursor:pointer;padding:2px 8px;font-family:monospace;display:none;line-height:1';
497+
resetViewEl.style.cssText = 'background:none;color:#fff;font-size:15px;border:none;border-left:1px solid rgba(255,255,255,.2);cursor:pointer;padding:0 6px;font-family:monospace;display:none;line-height:1';
476498
resetViewEl.addEventListener('click', () => { map.easeTo({ bearing: 0, duration: 500 }); });
477499
pitchWrap.appendChild(pitchEl);
478500
pitchWrap.appendChild(resetViewEl);
479501
document.getElementById('map').appendChild(pitchWrap);
480502
const exaggerationEl = document.createElement('div');
481503
exaggerationEl.id = 'exaggeration-ctrl';
482504
exaggerationEl.style.cssText = 'position:absolute;bottom:24px;left:310px;background:rgba(0,0,0,.6);color:#fff;font-size:11px;padding:2px 8px;border-radius:4px;z-index:10;font-family:monospace;display:none;align-items:center;gap:6px';
483-
exaggerationEl.innerHTML = '⛰ <input type="range" id="exaggeration-slider" min="1" max="3" step="0.1" value="1.5" style="width:70px;accent-color:#fff;vertical-align:middle"> <span id="exaggeration-val">1.5×</span>';
505+
exaggerationEl.innerHTML = '⛰ <input type="range" id="exaggeration-slider" min="1" max="3" step="0.1" value="1.2" style="width:70px;accent-color:#fff;vertical-align:middle"> <span id="exaggeration-val">1.2×</span>';
484506
document.getElementById('map').appendChild(exaggerationEl);
485507
document.getElementById('exaggeration-slider').addEventListener('input', (e) => {
486508
const val = parseFloat(e.target.value);
487509
document.getElementById('exaggeration-val').textContent = val.toFixed(1) + '×';
488510
if (map.getTerrain()) map.setTerrain({ source: 'terrain-dem', exaggeration: val });
489511
});
512+
// Live GPS coordinates on mouse move
513+
const coordsEl = document.createElement('div');
514+
coordsEl.id = 'coords-debug';
515+
coordsEl.style.cssText = 'position:absolute;bottom:24px;right:50px;background:rgba(0,0,0,.6);color:#fff;font-size:11px;padding:2px 6px;border-radius:4px;z-index:10;pointer-events:none;font-family:monospace;display:none';
516+
document.getElementById('map').appendChild(coordsEl);
517+
map.on('mousemove', (e) => {
518+
coordsEl.textContent = `${e.lngLat.lat.toFixed(4)}°, ${e.lngLat.lng.toFixed(4)}°`;
519+
coordsEl.style.display = '';
520+
});
521+
map.getCanvas().addEventListener('mouseout', () => { coordsEl.style.display = 'none'; });
490522
const updateZoom = () => {
491523
zoomEl.textContent = 'z' + map.getZoom().toFixed(2);
492524
const is3D = _mapStyle === 'terrain3d';
@@ -614,9 +646,13 @@ async function initMap() {
614646
// Elevation — only in 3D Terrain mode
615647
let elevationStr = '';
616648
if (_mapStyle === 'terrain3d') {
617-
const elev = map.queryTerrainElevation([lng, lat]);
618-
if (elev !== null && elev !== undefined) {
619-
elevationStr = `${Math.round(elev).toLocaleString()}m`;
649+
try {
650+
const elev = map.queryTerrainElevation([lng, lat]);
651+
if (elev !== null && elev !== undefined) {
652+
elevationStr = `${Math.round(elev).toLocaleString()}m`;
653+
}
654+
} catch(e) {
655+
// Elevation error when zoom exceeds source maxzoom — ignore
620656
}
621657
}
622658

@@ -736,7 +772,7 @@ function setMapStyle(mode) {
736772
}
737773

738774
// Update button label
739-
const labels = { light: 'Light Map', enriched: 'Terrain', dark: 'Dark Map', satellite: 'Satellite', terrain3d: '3D Terrain', globe: 'Globe' };
775+
const labels = { light: 'Light Map', bright: 'Bright Map', enriched: 'Terrain', dark: 'Dark Map', satellite: 'Satellite', terrain3d: '3D Terrain', globe: 'Globe' };
740776
const btn = document.getElementById('tb-style-btn');
741777
if (btn) btn.textContent = (labels[mode] || mode) + ' ▾';
742778

@@ -760,37 +796,75 @@ function _applyTerrainAndProjection() {
760796

761797
// Set projection FIRST — before any camera moves — so easeTo never runs under the wrong projection
762798
if (map.setProjection) {
799+
// Add atmospheric fog for depth perception in 3D modes
800+
if (map.setFog) {
801+
map.setFog({
802+
color: 'white',
803+
'high-color': '#add8e6',
804+
'horizon-blend': 0.3,
805+
atmosphere: 0.5
806+
});
807+
}
763808
map.setProjection({ type: isGlobe ? 'globe' : 'mercator' });
764809
}
765810

766811
// Terrain — AWS Terrain Tiles (free, no API key, terrarium encoding)
767812
if (is3D) {
813+
// Use separate sources for terrain mesh and hillshade to improve rendering quality
768814
if (!map.getSource('terrain-dem')) {
769815
map.addSource('terrain-dem', {
770816
type: 'raster-dem',
771817
tiles: ['https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png'],
772818
tileSize: 512, maxzoom: 15, encoding: 'terrarium'
773819
});
774820
}
821+
if (!map.getSource('hillshade-dem')) {
822+
map.addSource('hillshade-dem', {
823+
type: 'raster-dem',
824+
tiles: ['https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png'],
825+
tileSize: 512, maxzoom: 15, encoding: 'terrarium'
826+
});
827+
}
775828
const sliderEl = document.getElementById('exaggeration-slider');
776-
const exaggeration = sliderEl ? parseFloat(sliderEl.value) : 1.5;
829+
const exaggeration = sliderEl ? parseFloat(sliderEl.value) : 1.2;
777830
map.setTerrain({ source: 'terrain-dem', exaggeration });
831+
map.setPaintProperty('terrain-dem', 'raster-blur', 3);
832+
// Add sky layer for atmospheric background in 3D
833+
if (map.getLayer('sky') === undefined) {
834+
map.addLayer({ id: 'sky', type: 'sky', paint: { 'sky-type': 'gradient', 'sky-gradient': ['interpolate', ['linear'], ['sky-radial-progress'], 0, 'rgba(255,255,255,0)', 0.5, '#87cefa', 1, '#4682b4'] } });
835+
}
778836
if (!map.getLayer('terrain-hillshade')) {
779837
// Insert below the first road/label layer so hillshading shows through
780838
const firstSymbol = map.getStyle().layers.find(l => l.type === 'line' || l.type === 'symbol');
781839
map.addLayer({
782840
id: 'terrain-hillshade',
783841
type: 'hillshade',
784-
source: 'terrain-dem',
842+
source: 'hillshade-dem',
785843
paint: {
786-
'hillshade-exaggeration': 0.75,
787-
'hillshade-illumination-direction': 315,
788-
'hillshade-illumination-anchor': 'map',
844+
'hillshade-exaggeration': 1.0,
845+
'hillshade-illumination-direction': 270,
846+
'hillshade-illumination-anchor': 'viewport',
789847
'hillshade-shadow-color': '#1a2a35',
790848
'hillshade-highlight-color': '#f0f4f8',
791849
'hillshade-accent-color': '#2d4a5a',
792850
}
793851
}, firstSymbol?.id);
852+
// Add 3D building extrusion if OpenMapTiles source is present
853+
if (map.getSource('openmaptiles') && !map.getLayer('3d-buildings')) {
854+
map.addLayer({
855+
id: '3d-buildings',
856+
source: 'openmaptiles',
857+
'source-layer': 'building',
858+
type: 'fill-extrusion',
859+
minzoom: 15,
860+
paint: {
861+
'fill-extrusion-color': '#ddd',
862+
'fill-extrusion-height': ['get', 'height'],
863+
'fill-extrusion-base': ['get', 'min_height'],
864+
'fill-extrusion-opacity': 0.6
865+
}
866+
}, 'terrain-hillshade');
867+
}
794868
}
795869
map.easeTo({ pitch: 50, bearing: 0, duration: 800 });
796870
} else {
@@ -801,7 +875,7 @@ function _applyTerrainAndProjection() {
801875
map.flyTo({ center: [0, 0], zoom: 2, pitch: 0, bearing: 0, duration: 800 });
802876
} else {
803877
map.setMinZoom(-2);
804-
map.easeTo({ pitch: 0, bearing: 0, duration: 600 });
878+
map.jumpTo({ pitch: 0, bearing: 0 });
805879
}
806880
}
807881
}

0 commit comments

Comments
 (0)