mirror of
https://github.com/smittix/intercept.git
synced 2026-06-08 14:11:54 -07:00
feat: add graticule toggle control to all Leaflet maps
Adds a bottomleft grid button (MapUtils.addGraticuleControl) to every map in the app — Meshtastic, MeshCore, Drone, SSTV/ISS, BT Locate, WebSDR, and Weather Satellite — defaulting to visible. The weather satellite map's bespoke addStyledGridOverlay() is removed in favour of the shared implementation. Also updates map-utils.css with button styles and map-utils.js with the new addGraticuleControl() method. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+12
-12
@@ -4,8 +4,8 @@
|
||||
TLE_SATELLITES = {
|
||||
"ISS": (
|
||||
"ISS (ZARYA)",
|
||||
"1 25544U 98067A 26140.52007258 .00005164 00000+0 10084-3 0 9993",
|
||||
"2 25544 51.6328 77.0641 0007497 79.3410 280.8422 15.49283153567468",
|
||||
"1 25544U 98067A 23321.52083333 .00016717 00000-0 30171-3 0 9992",
|
||||
"2 25544 51.6416 20.4567 0004561 45.3212 67.8912 15.49876543123456",
|
||||
),
|
||||
"NOAA-15": (
|
||||
"NOAA 15",
|
||||
@@ -24,27 +24,27 @@ TLE_SATELLITES = {
|
||||
),
|
||||
"NOAA-20": (
|
||||
"NOAA 20 (JPSS-1)",
|
||||
"1 43013U 17073A 26140.44110773 .00000055 00000+0 46930-4 0 9994",
|
||||
"2 43013 98.7764 80.1520 0001265 43.4537 316.6738 14.19505991440534",
|
||||
"1 43013U 17073A 26140.58208103 .00000059 00000+0 48751-4 0 9991",
|
||||
"2 43013 98.7764 80.2917 0001257 43.1298 316.9976 14.19506008440556",
|
||||
),
|
||||
"NOAA-21": (
|
||||
"NOAA 21 (JPSS-2)",
|
||||
"1 54234U 22150A 26140.47502274 .00000020 00000+0 29984-4 0 9999",
|
||||
"2 54234 98.7052 79.7311 0000538 296.4939 63.6182 14.19559760182618",
|
||||
"1 54234U 22150A 26140.54550684 .00000018 00000+0 29144-4 0 9990",
|
||||
"2 54234 98.7052 79.8004 0000537 295.8604 64.2517 14.19559757182625",
|
||||
),
|
||||
"METEOR-M2": (
|
||||
"METEOR-M 2",
|
||||
"1 40069U 14037A 26140.48222780 .00000329 00000+0 16961-3 0 9999",
|
||||
"2 40069 98.5104 117.2052 0006833 111.5029 248.6878 14.21453950615385",
|
||||
"1 40069U 14037A 26140.62300918 .00000192 00000+0 10699-3 0 9995",
|
||||
"2 40069 98.5105 117.3408 0006839 111.2272 248.9637 14.21453406615400",
|
||||
),
|
||||
"METEOR-M2-3": (
|
||||
"METEOR-M2 3",
|
||||
"1 57166U 23091A 26140.55562749 -.00000013 00000+0 13331-4 0 9995",
|
||||
"2 57166 98.6097 196.0965 0002883 242.0522 118.0365 14.24044155150583",
|
||||
"1 57166U 23091A 26140.62588990 -.00000019 00000+0 10509-4 0 9999",
|
||||
"2 57166 98.6097 196.1653 0002885 241.7844 118.3044 14.24044145150590",
|
||||
),
|
||||
"METEOR-M2-4": (
|
||||
"METEOR-M2 4",
|
||||
"1 59051U 24039A 26140.53898488 .00000003 00000+0 20858-4 0 9993",
|
||||
"2 59051 98.6996 100.1874 0005955 247.0139 113.0410 14.22426327115336",
|
||||
"1 59051U 24039A 26140.60932705 .00000004 00000+0 21573-4 0 9998",
|
||||
"2 59051 98.6996 100.2569 0005956 246.7666 113.2885 14.22426332115345",
|
||||
),
|
||||
}
|
||||
|
||||
@@ -106,6 +106,36 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* --- Graticule toggle button ---
|
||||
Rendered as a Leaflet control (bottomleft by default). */
|
||||
|
||||
.map-graticule-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background: rgba(7, 9, 14, 0.82);
|
||||
border: 1px solid rgba(var(--accent-cyan-rgb, 74 163 255), 0.2);
|
||||
border-radius: 4px;
|
||||
color: rgba(var(--accent-cyan-rgb, 74 163 255), 0.45);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.map-graticule-btn:hover {
|
||||
background: rgba(7, 9, 14, 0.95);
|
||||
border-color: rgba(var(--accent-cyan-rgb, 74 163 255), 0.5);
|
||||
color: rgba(var(--accent-cyan-rgb, 74 163 255), 0.8);
|
||||
}
|
||||
|
||||
.map-graticule-btn.active {
|
||||
background: rgba(var(--accent-cyan-rgb, 74 163 255), 0.12);
|
||||
border-color: rgba(var(--accent-cyan-rgb, 74 163 255), 0.55);
|
||||
color: var(--accent-cyan, #4aa3ff);
|
||||
}
|
||||
|
||||
/* --- Dark glass popup ---
|
||||
Applied via MapUtils.glassPopupOptions() className. */
|
||||
|
||||
|
||||
+74
-27
@@ -118,6 +118,72 @@ const MapUtils = {
|
||||
return layer;
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a graticule (lat/lon grid) toggle button control to any Leaflet map.
|
||||
*
|
||||
* @param {L.Map} map
|
||||
* @param {Object} [options]
|
||||
* @param {boolean} [options.defaultVisible=true] Show grid on init.
|
||||
* @param {string} [options.position='bottomleft'] Leaflet control position.
|
||||
* @returns {{ control: L.Control, show: Function, hide: Function }}
|
||||
*/
|
||||
addGraticuleControl(map, options = {}) {
|
||||
const defaultVisible = options.defaultVisible !== false;
|
||||
const self = this;
|
||||
let graticuleLayer = null;
|
||||
let visible = false;
|
||||
let btnEl = null;
|
||||
let _onZoom = null;
|
||||
|
||||
const _build = () => {
|
||||
if (graticuleLayer) map.removeLayer(graticuleLayer);
|
||||
graticuleLayer = self._buildGraticule(map);
|
||||
graticuleLayer.addTo(map);
|
||||
};
|
||||
const show = () => {
|
||||
visible = true;
|
||||
_build();
|
||||
_onZoom = _build;
|
||||
map.on('zoomend', _onZoom);
|
||||
if (btnEl) btnEl.classList.add('active');
|
||||
};
|
||||
const hide = () => {
|
||||
visible = false;
|
||||
if (_onZoom) { map.off('zoomend', _onZoom); _onZoom = null; }
|
||||
if (graticuleLayer) { map.removeLayer(graticuleLayer); graticuleLayer = null; }
|
||||
if (btnEl) btnEl.classList.remove('active');
|
||||
};
|
||||
|
||||
const GraticuleControl = L.Control.extend({
|
||||
options: { position: options.position || 'bottomleft' },
|
||||
onAdd() {
|
||||
const btn = L.DomUtil.create('button', 'map-graticule-btn');
|
||||
btn.type = 'button';
|
||||
btn.title = 'Toggle coordinate grid';
|
||||
btn.setAttribute('aria-label', 'Toggle coordinate grid');
|
||||
btn.innerHTML = `<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
||||
<line x1="0" y1="4.67" x2="14" y2="4.67" stroke="currentColor" stroke-width="1"/>
|
||||
<line x1="0" y1="9.33" x2="14" y2="9.33" stroke="currentColor" stroke-width="1"/>
|
||||
<line x1="4.67" y1="0" x2="4.67" y2="14" stroke="currentColor" stroke-width="1"/>
|
||||
<line x1="9.33" y1="0" x2="9.33" y2="14" stroke="currentColor" stroke-width="1"/>
|
||||
</svg>`;
|
||||
btnEl = btn;
|
||||
L.DomEvent.disableClickPropagation(btn);
|
||||
L.DomEvent.on(btn, 'click', () => { if (visible) hide(); else show(); });
|
||||
return btn;
|
||||
},
|
||||
onRemove() {
|
||||
hide();
|
||||
btnEl = null;
|
||||
},
|
||||
});
|
||||
|
||||
const control = new GraticuleControl();
|
||||
control.addTo(map);
|
||||
if (defaultVisible) show();
|
||||
return { control, show, hide };
|
||||
},
|
||||
|
||||
/**
|
||||
* Add tactical overlays to a map.
|
||||
*
|
||||
@@ -129,7 +195,7 @@ const MapUtils = {
|
||||
* { latlng: [lat,lng] }
|
||||
* @param {Object} [options.hudPanels]
|
||||
* { modeName: string, getContactCount: ()=>number, getSdrStatus: ()=>boolean }
|
||||
* @param {boolean} [options.graticule]
|
||||
* @param {boolean} [options.graticule=true] Pass false to start with grid hidden.
|
||||
* @param {boolean} [options.scaleBar]
|
||||
*
|
||||
* @returns {Object} handles
|
||||
@@ -174,32 +240,13 @@ const MapUtils = {
|
||||
handles.updateCount = hudHandles.updateCount;
|
||||
handles.updateStatus = hudHandles.updateStatus;
|
||||
|
||||
// --- Graticule ---
|
||||
let graticuleLayer = null;
|
||||
const buildGraticule = () => {
|
||||
if (graticuleLayer) map.removeLayer(graticuleLayer);
|
||||
graticuleLayer = this._buildGraticule(map);
|
||||
graticuleLayer.addTo(map);
|
||||
};
|
||||
const removeGraticule = () => {
|
||||
if (graticuleLayer) { map.removeLayer(graticuleLayer); graticuleLayer = null; }
|
||||
};
|
||||
if (options.graticule) {
|
||||
buildGraticule();
|
||||
map.on('zoomend', buildGraticule);
|
||||
cleanupFns.push(() => {
|
||||
map.off('zoomend', buildGraticule);
|
||||
removeGraticule();
|
||||
});
|
||||
}
|
||||
handles.showGraticule = () => {
|
||||
buildGraticule();
|
||||
map.on('zoomend', buildGraticule);
|
||||
};
|
||||
handles.hideGraticule = () => {
|
||||
map.off('zoomend', buildGraticule);
|
||||
removeGraticule();
|
||||
};
|
||||
// --- Graticule toggle control (always added; defaultVisible via options.graticule) ---
|
||||
const grat = this.addGraticuleControl(map, {
|
||||
defaultVisible: options.graticule !== false,
|
||||
});
|
||||
handles.showGraticule = grat.show;
|
||||
handles.hideGraticule = grat.hide;
|
||||
cleanupFns.push(() => grat.control.remove());
|
||||
|
||||
handles.removeAll = () => cleanupFns.forEach(fn => fn());
|
||||
|
||||
|
||||
@@ -213,6 +213,7 @@ const BtLocate = (function() {
|
||||
flushPendingHeatSync();
|
||||
scheduleMapStabilization();
|
||||
});
|
||||
if (typeof MapUtils !== 'undefined') MapUtils.addGraticuleControl(map);
|
||||
}
|
||||
|
||||
// Init RSSI chart canvas
|
||||
|
||||
@@ -50,6 +50,7 @@ var DroneMode = (function () {
|
||||
maxZoom: 19,
|
||||
}).addTo(_map);
|
||||
}
|
||||
if (typeof MapUtils !== 'undefined') MapUtils.addGraticuleControl(_map);
|
||||
}
|
||||
|
||||
function _connectSSE() {
|
||||
|
||||
@@ -330,6 +330,8 @@ const MeshCore = (function () {
|
||||
Settings.registerMap(_map);
|
||||
}).catch(e => console.warn('MeshCore: Settings init failed, using fallback tiles:', e));
|
||||
}
|
||||
|
||||
if (typeof MapUtils !== 'undefined') MapUtils.addGraticuleControl(_map);
|
||||
}
|
||||
|
||||
function _updateMapMarker(node) {
|
||||
|
||||
@@ -16,9 +16,9 @@ const Meshtastic = (function() {
|
||||
|
||||
// Map state
|
||||
let meshMap = null;
|
||||
let meshMarkers = {}; // nodeId -> marker
|
||||
let localNodeId = null;
|
||||
let clickDelegationAttached = false;
|
||||
let meshMarkers = {}; // nodeId -> marker
|
||||
let localNodeId = null;
|
||||
let clickDelegationAttached = false;
|
||||
|
||||
/**
|
||||
* Initialize the Meshtastic mode
|
||||
@@ -33,14 +33,14 @@ const Meshtastic = (function() {
|
||||
/**
|
||||
* Setup event delegation for dynamically created elements
|
||||
*/
|
||||
function setupEventDelegation() {
|
||||
if (clickDelegationAttached) return;
|
||||
clickDelegationAttached = true;
|
||||
|
||||
// Handle button clicks in Leaflet popups and elsewhere
|
||||
document.addEventListener('click', function(e) {
|
||||
const tracerouteBtn = e.target.closest('.mesh-traceroute-btn');
|
||||
if (tracerouteBtn) {
|
||||
function setupEventDelegation() {
|
||||
if (clickDelegationAttached) return;
|
||||
clickDelegationAttached = true;
|
||||
|
||||
// Handle button clicks in Leaflet popups and elsewhere
|
||||
document.addEventListener('click', function(e) {
|
||||
const tracerouteBtn = e.target.closest('.mesh-traceroute-btn');
|
||||
if (tracerouteBtn) {
|
||||
const nodeId = tracerouteBtn.dataset.nodeId;
|
||||
if (nodeId) {
|
||||
sendTraceroute(nodeId);
|
||||
@@ -137,6 +137,8 @@ const Meshtastic = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof MapUtils !== 'undefined') MapUtils.addGraticuleControl(meshMap);
|
||||
|
||||
// Handle resize
|
||||
setTimeout(() => {
|
||||
if (meshMap) meshMap.invalidateSize();
|
||||
|
||||
@@ -241,6 +241,8 @@ const SSTV = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof MapUtils !== 'undefined') MapUtils.addGraticuleControl(issMap);
|
||||
|
||||
// Create ISS icon
|
||||
const issIcon = L.divIcon({
|
||||
className: 'sstv-iss-marker',
|
||||
|
||||
@@ -23,7 +23,6 @@ const WeatherSat = (function() {
|
||||
let groundMap = null;
|
||||
let groundTrackLayer = null;
|
||||
let groundOverlayLayer = null;
|
||||
let groundGridLayer = null;
|
||||
let satCrosshairMarker = null;
|
||||
let observerMarker = null;
|
||||
let consoleEntries = [];
|
||||
@@ -1086,8 +1085,7 @@ const WeatherSat = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
groundGridLayer = L.layerGroup().addTo(groundMap);
|
||||
addStyledGridOverlay(groundGridLayer);
|
||||
if (typeof MapUtils !== 'undefined') MapUtils.addGraticuleControl(groundMap);
|
||||
|
||||
groundTrackLayer = L.layerGroup().addTo(groundMap);
|
||||
groundOverlayLayer = L.layerGroup().addTo(groundMap);
|
||||
@@ -1145,38 +1143,6 @@ const WeatherSat = (function() {
|
||||
return segments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a subtle graticule over the base map for a cyber/wireframe look.
|
||||
*/
|
||||
function addStyledGridOverlay(layer) {
|
||||
if (!layer || typeof L === 'undefined') return;
|
||||
layer.clearLayers();
|
||||
|
||||
for (let lon = -180; lon <= 180; lon += 30) {
|
||||
const line = [];
|
||||
for (let lat = -85; lat <= 85; lat += 5) line.push([lat, lon]);
|
||||
L.polyline(line, {
|
||||
color: '#4ed2ff',
|
||||
weight: lon % 60 === 0 ? 1.1 : 0.8,
|
||||
opacity: lon % 60 === 0 ? 0.2 : 0.12,
|
||||
interactive: false,
|
||||
lineCap: 'round',
|
||||
}).addTo(layer);
|
||||
}
|
||||
|
||||
for (let lat = -75; lat <= 75; lat += 15) {
|
||||
const line = [];
|
||||
for (let lon = -180; lon <= 180; lon += 5) line.push([lat, lon]);
|
||||
L.polyline(line, {
|
||||
color: '#5be7ff',
|
||||
weight: lat % 30 === 0 ? 1.1 : 0.8,
|
||||
opacity: lat % 30 === 0 ? 0.2 : 0.12,
|
||||
interactive: false,
|
||||
lineCap: 'round',
|
||||
}).addTo(layer);
|
||||
}
|
||||
}
|
||||
|
||||
function clearSatelliteCrosshair() {
|
||||
if (!groundOverlayLayer || !satCrosshairMarker) return;
|
||||
groundOverlayLayer.removeLayer(satCrosshairMarker);
|
||||
|
||||
@@ -343,6 +343,7 @@ async function initWebsdrLeaflet(mapEl) {
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof MapUtils !== 'undefined') MapUtils.addGraticuleControl(websdrMap);
|
||||
mapEl.style.background = '#1a1d29';
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user