UI polish, bug fixes, and README
Changes since last commit: NATS airspace - Remove D and D_OTHER types from parser — UK small-arms ranges covered the entire country and made the layer unusable - Replace title-attribute tooltips on type filters with clickable ⓘ icons (mobile-friendly); info text appears in a shared box below the grid - Add Select All / Deselect All controls (moved to bottom of all layers, now applies to every layer checkbox not just NATS types) - Fix per-type filter using expression syntax ['match', ...] — legacy filter syntax was unreliable in MapLibre GL JS 4.x Map style switching - Fix overlay layers being lost when switching Terrain / Satellite / Streets by waiting for the 'idle' event instead of 'style.load'; MapTiler styles fire style.load before the map is ready to accept addSource/addLayer calls - Defensive cleanup at top of addAllLayers() removes all custom layers and sources before re-adding, preventing "source already exists" crashes Location tracking - Move locate button from floating bottom-right into the left panel - Auto-request location on page load - Dynamic button label: "Getting location…" → "Stop Tracking" → "Track location" - Suppress alert on permission-denied (error code 1); show inline message for other errors Panel UX - Move planning disclaimer out of panel body into a ⚠ icon in the header; click to expand, includes hyperlinked NOTAM link - Add NOTAM link to the data-links section at the bottom - Danger / Restricted and MoD / Military rows now have ⓘ icons explaining that these layers have no built-in data and require a GeoJSON upload; removes the old "Override: load GeoJSON below" note and "(override)" labels from the dropdown - Load Data section: stacked full-width layout (dropdown above button) - Lighten grey text (#555 → #888, #666 → #999) across section labels, layer notes, info icons, hints, data links, and popup close button Map bounds and attribution - Restrict panning to UK + Channel Islands with maxBounds - Replace default attribution control with custom one appending "© Bournemouth Technology" with link to bournemouthtechnology.co.uk README - Add full project description, feature list, tech stack table, setup instructions, and data source reference table
This commit is contained in:
89
app.js
89
app.js
@@ -532,11 +532,17 @@ function createLocationElement() {
|
||||
return el;
|
||||
}
|
||||
|
||||
function setLocateLabel(text) {
|
||||
const el = document.getElementById('locate-label');
|
||||
if (el) el.textContent = text;
|
||||
}
|
||||
|
||||
function onLocationUpdate(pos) {
|
||||
const { longitude: lon, latitude: lat, accuracy } = pos.coords;
|
||||
const locateBtn = document.getElementById('locate-btn');
|
||||
locateBtn.classList.remove('locating');
|
||||
locateBtn.classList.add('tracking');
|
||||
const btn = document.getElementById('locate-btn');
|
||||
btn.classList.remove('locating');
|
||||
btn.classList.add('tracking');
|
||||
setLocateLabel('Stop Tracking');
|
||||
|
||||
if (locationMarker) {
|
||||
locationMarker.setLngLat([lon, lat]);
|
||||
@@ -562,12 +568,14 @@ function onLocationUpdate(pos) {
|
||||
|
||||
function onLocationError(err) {
|
||||
stopTracking();
|
||||
alert(`Location error: ${err.message}`);
|
||||
// Permission denied (code 1) — user chose not to share, no need to alert
|
||||
if (err.code !== 1) setLocateLabel('Location error — click to retry');
|
||||
}
|
||||
|
||||
function startTracking() {
|
||||
if (!navigator.geolocation) { alert('Geolocation not supported by this browser.'); return; }
|
||||
if (!navigator.geolocation) { setLocateLabel('Geolocation not supported'); return; }
|
||||
document.getElementById('locate-btn').classList.add('locating');
|
||||
setLocateLabel('Getting location…');
|
||||
isFirstFix = true;
|
||||
locationWatcher = navigator.geolocation.watchPosition(
|
||||
onLocationUpdate, onLocationError,
|
||||
@@ -580,6 +588,7 @@ function stopTracking() {
|
||||
if (locationMarker) { locationMarker.remove(); locationMarker = null; }
|
||||
if (map && map.getSource('location-accuracy')) map.getSource('location-accuracy').setData(emptyFC());
|
||||
document.getElementById('locate-btn').classList.remove('locating', 'tracking');
|
||||
setLocateLabel('Track location');
|
||||
}
|
||||
|
||||
// ── Auto-load NATS XML from local path ───────────────────────────────────────
|
||||
@@ -645,15 +654,26 @@ function initMap(key) {
|
||||
zoom: 5.5,
|
||||
minZoom: 4,
|
||||
maxPitch: 85,
|
||||
attributionControl: true,
|
||||
attributionControl: false,
|
||||
// Constrain panning to the UK + Channel Islands + a small buffer
|
||||
maxBounds: [[-11, 47.5], [4, 62.5]],
|
||||
});
|
||||
|
||||
map.addControl(new maplibregl.AttributionControl({
|
||||
customAttribution: '© <a href="https://bournemouthtechnology.co.uk" target="_blank" rel="noopener">Bournemouth Technology</a>',
|
||||
}), 'bottom-right');
|
||||
map.addControl(new maplibregl.NavigationControl({ showCompass: true }), 'bottom-right');
|
||||
|
||||
map.on('load', () => {
|
||||
addAllLayers();
|
||||
// style.load fires when the initial style is ready — add layers here.
|
||||
// Style *switches* use idle (see switchStyle) because setStyle fires style.load
|
||||
// prematurely on some MapTiler styles before the map is ready to accept sources.
|
||||
map.once('style.load', addAllLayers);
|
||||
|
||||
// load fires once after the first full render.
|
||||
map.once('load', () => {
|
||||
document.getElementById('key-modal').classList.add('hidden');
|
||||
loadNATSLocal();
|
||||
startTracking();
|
||||
});
|
||||
|
||||
map.on('moveend', () => {
|
||||
@@ -682,8 +702,13 @@ function initMap(key) {
|
||||
function switchStyle(styleKey) {
|
||||
if (styleKey === currentStyle) return;
|
||||
currentStyle = styleKey;
|
||||
// Cancel any previous pending idle handler (rapid clicks), then wait for
|
||||
// the map to be truly idle after the style swap before re-adding layers.
|
||||
// Using 'idle' rather than 'style.load' because MapTiler styles fire
|
||||
// style.load before they're fully ready to accept addSource/addLayer calls.
|
||||
map.off('idle', addAllLayers);
|
||||
map.once('idle', addAllLayers);
|
||||
map.setStyle(STYLES[styleKey](apiKey));
|
||||
map.once('style.load', () => addAllLayers());
|
||||
}
|
||||
|
||||
// ── API key modal ─────────────────────────────────────────────────────────────
|
||||
@@ -698,6 +723,10 @@ function wireControls() {
|
||||
document.getElementById('panel-body').classList.toggle('collapsed');
|
||||
});
|
||||
|
||||
document.getElementById('warn-btn').addEventListener('click', () => {
|
||||
document.getElementById('warn-popup').classList.toggle('hidden');
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-layer]').forEach(cb => {
|
||||
cb.addEventListener('change', () => {
|
||||
const id = cb.dataset.layer;
|
||||
@@ -714,6 +743,48 @@ function wireControls() {
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('layers-select-all').addEventListener('click', () => {
|
||||
document.querySelectorAll('[data-layer]').forEach(cb => {
|
||||
cb.checked = true;
|
||||
const id = cb.dataset.layer;
|
||||
layerVisibility[id] = true;
|
||||
if (map) setLayerVisibility(id, true);
|
||||
if (id === 'sssi') scheduleSSSIRefresh();
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('layers-deselect-all').addEventListener('click', () => {
|
||||
document.querySelectorAll('[data-layer]').forEach(cb => {
|
||||
cb.checked = false;
|
||||
const id = cb.dataset.layer;
|
||||
layerVisibility[id] = false;
|
||||
if (map) setLayerVisibility(id, false);
|
||||
});
|
||||
});
|
||||
|
||||
// Info buttons — click to show/hide description; each button specifies its box via data-infobox
|
||||
let activeInfoBtn = null;
|
||||
document.querySelectorAll('.info-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const box = document.getElementById(btn.dataset.infobox || 'nats-type-info');
|
||||
if (activeInfoBtn === btn) {
|
||||
btn.classList.remove('active');
|
||||
box.classList.add('hidden');
|
||||
activeInfoBtn = null;
|
||||
} else {
|
||||
if (activeInfoBtn) {
|
||||
const prevBox = document.getElementById(activeInfoBtn.dataset.infobox || 'nats-type-info');
|
||||
prevBox.classList.add('hidden');
|
||||
activeInfoBtn.classList.remove('active');
|
||||
}
|
||||
btn.classList.add('active');
|
||||
box.textContent = btn.dataset.info;
|
||||
box.classList.remove('hidden');
|
||||
activeInfoBtn = btn;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.style-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.style-btn').forEach(b => b.classList.remove('active'));
|
||||
|
||||
Reference in New Issue
Block a user