183 lines
6.2 KiB
JavaScript
183 lines
6.2 KiB
JavaScript
import maplibregl from 'maplibre-gl';
|
|
import 'maplibre-gl/dist/maplibre-gl.css';
|
|
import { FogCanvas, metersToPixels } from './fog.js';
|
|
import { MapSync } from './sync.js';
|
|
|
|
const BOTTOM_STYLE = 'https://tiles.openfreemap.org/styles/bright';
|
|
const TOP_STYLE = 'https://tiles.openfreemap.org/styles/liberty';
|
|
|
|
let mode = 'explore'; // 'explore' | 'navigate'
|
|
let brushRadius = 8000;
|
|
|
|
// ── Maps ──
|
|
const mapBottom = new maplibregl.Map({
|
|
container: 'map-bottom',
|
|
style: BOTTOM_STYLE,
|
|
center: [0, 30],
|
|
zoom: 3,
|
|
attributionControl: false,
|
|
});
|
|
|
|
const mapTop = new maplibregl.Map({
|
|
container: 'map-top',
|
|
style: TOP_STYLE,
|
|
center: [0, 30],
|
|
zoom: 3,
|
|
attributionControl: false,
|
|
interactive: false, // will be toggled
|
|
});
|
|
|
|
// Make top map transparent background and mute layers
|
|
mapTop.on('load', () => {
|
|
const layers = mapTop.getStyle().layers;
|
|
for (const layer of layers) {
|
|
const id = layer.id.toLowerCase();
|
|
const src = (layer['source-layer'] || '').toLowerCase();
|
|
|
|
const isWater = id.includes('ocean') || id.includes('sea') || id.includes('water') ||
|
|
src.includes('water') || src.includes('ocean');
|
|
const isAdmin = id.includes('admin') || id.includes('boundar') || src.includes('admin') || src.includes('boundar');
|
|
const isPlace = id.includes('place') || id.includes('label') || id.includes('capital') ||
|
|
id.includes('city') || id.includes('country') || id.includes('continent') ||
|
|
src.includes('place');
|
|
|
|
if (!isWater && !isAdmin && !isPlace) {
|
|
mapTop.setLayoutProperty(layer.id, 'visibility', 'none');
|
|
} else if (isPlace && layer.type === 'symbol') {
|
|
// Only keep country labels and major/capital cities, hide small places
|
|
const isCountryOrCapital = id.includes('country') || id.includes('capital') ||
|
|
id.includes('continent') || id.includes('state');
|
|
if (!isCountryOrCapital) {
|
|
// Filter to only show cities with high rank (major cities)
|
|
try {
|
|
mapTop.setFilter(layer.id, ['<=', ['get', 'rank'], 3]);
|
|
} catch {}
|
|
}
|
|
try { mapTop.setPaintProperty(layer.id, 'text-color', 'rgba(200,215,255,0.45)'); } catch {}
|
|
try { mapTop.setPaintProperty(layer.id, 'text-halo-color', 'rgba(6,10,24,0.7)'); } catch {}
|
|
} else if (isAdmin && layer.type === 'line') {
|
|
try { mapTop.setPaintProperty(layer.id, 'line-color', 'rgba(200,215,255,0.25)'); } catch {}
|
|
try { mapTop.setPaintProperty(layer.id, 'line-opacity', 0.4); } catch {}
|
|
} else if (isWater) {
|
|
// Mute water to a subtle blue-grey
|
|
if (layer.type === 'fill') {
|
|
try { mapTop.setPaintProperty(layer.id, 'fill-color', 'rgba(140,160,185,0.3)'); } catch {}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Sync maps
|
|
const sync = new MapSync(mapTop, mapBottom);
|
|
mapTop.on('load', () => { mapBottom.on('load', () => sync.enable()); });
|
|
|
|
// ── Fog ──
|
|
const fogCanvas = new FogCanvas(document.getElementById('fog'), mapTop);
|
|
|
|
// Re-render fog on map move
|
|
mapTop.on('move', () => fogCanvas.markDirty());
|
|
mapBottom.on('load', () => {
|
|
// initial render once both ready
|
|
fogCanvas.render();
|
|
});
|
|
|
|
// Resize
|
|
const ro = new ResizeObserver(() => fogCanvas.resize());
|
|
ro.observe(document.getElementById('container'));
|
|
|
|
// ── Brush cursor ──
|
|
const cursor = document.getElementById('brush-cursor');
|
|
|
|
function updateCursorSize(e) {
|
|
const r = metersToPixels(mapTop, [e.lngLat.lng, e.lngLat.lat], brushRadius);
|
|
const d = r * 2;
|
|
cursor.style.width = d + 'px';
|
|
cursor.style.height = d + 'px';
|
|
cursor.style.left = e.point.x + 'px';
|
|
cursor.style.top = e.point.y + 'px';
|
|
}
|
|
|
|
// ── Interaction ──
|
|
let dragging = false;
|
|
let lastPt = null;
|
|
|
|
function setMode(m) {
|
|
mode = m;
|
|
document.getElementById('btn-explore').classList.toggle('active', m === 'explore');
|
|
document.getElementById('btn-navigate').classList.toggle('active', m === 'navigate');
|
|
|
|
if (m === 'explore') {
|
|
mapTop.dragPan.disable();
|
|
mapTop.scrollZoom.enable();
|
|
mapTop.doubleClickZoom.enable();
|
|
document.getElementById('map-top').style.cursor = 'none';
|
|
cursor.style.display = 'block';
|
|
} else {
|
|
mapTop.dragPan.enable();
|
|
mapTop.scrollZoom.enable();
|
|
mapTop.doubleClickZoom.enable();
|
|
document.getElementById('map-top').style.cursor = '';
|
|
cursor.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
mapTop.on('load', () => setMode('explore'));
|
|
|
|
mapTop.on('mousedown', (e) => {
|
|
if (mode !== 'explore') return;
|
|
dragging = true;
|
|
lastPt = e.point;
|
|
addPoint(e);
|
|
});
|
|
|
|
mapTop.on('mousemove', (e) => {
|
|
if (mode === 'explore') updateCursorSize(e);
|
|
if (!dragging || mode !== 'explore') return;
|
|
|
|
// Interpolate between last and current to avoid gaps
|
|
const dx = e.point.x - lastPt.x;
|
|
const dy = e.point.y - lastPt.y;
|
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
const step = 10;
|
|
if (dist > step) {
|
|
const steps = Math.ceil(dist / step);
|
|
for (let i = 1; i <= steps; i++) {
|
|
const t = i / steps;
|
|
const px = lastPt.x + dx * t;
|
|
const py = lastPt.y + dy * t;
|
|
const ll = mapTop.unproject([px, py]);
|
|
fogCanvas.addErasePoint([ll.lng, ll.lat], brushRadius);
|
|
}
|
|
} else {
|
|
addPoint(e);
|
|
}
|
|
lastPt = e.point;
|
|
});
|
|
|
|
mapTop.on('mouseup', () => { dragging = false; lastPt = null; });
|
|
mapTop.on('mouseleave', () => { dragging = false; lastPt = null; cursor.style.display = 'none'; });
|
|
mapTop.on('mouseenter', () => { if (mode === 'explore') cursor.style.display = 'block'; });
|
|
|
|
function addPoint(e) {
|
|
fogCanvas.addErasePoint([e.lngLat.lng, e.lngLat.lat], brushRadius);
|
|
}
|
|
|
|
// ── Toolbar ──
|
|
document.getElementById('btn-explore').addEventListener('click', () => setMode('explore'));
|
|
document.getElementById('btn-navigate').addEventListener('click', () => setMode('navigate'));
|
|
document.getElementById('btn-reset').addEventListener('click', () => fogCanvas.reset());
|
|
document.getElementById('btn-zoom-in').addEventListener('click', () => mapTop.zoomIn());
|
|
document.getElementById('btn-zoom-out').addEventListener('click', () => mapTop.zoomOut());
|
|
document.getElementById('brush-slider').addEventListener('input', (e) => {
|
|
brushRadius = Number(e.target.value);
|
|
});
|
|
|
|
// Keyboard shortcuts
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'e' || e.key === 'E') setMode('explore');
|
|
if (e.key === 'n' || e.key === 'N') setMode('navigate');
|
|
});
|
|
|
|
|
|
|