mirror of
https://github.com/smittix/intercept.git
synced 2026-07-05 16:18:12 -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 = {
|
TLE_SATELLITES = {
|
||||||
"ISS": (
|
"ISS": (
|
||||||
"ISS (ZARYA)",
|
"ISS (ZARYA)",
|
||||||
"1 25544U 98067A 26140.52007258 .00005164 00000+0 10084-3 0 9993",
|
"1 25544U 98067A 23321.52083333 .00016717 00000-0 30171-3 0 9992",
|
||||||
"2 25544 51.6328 77.0641 0007497 79.3410 280.8422 15.49283153567468",
|
"2 25544 51.6416 20.4567 0004561 45.3212 67.8912 15.49876543123456",
|
||||||
),
|
),
|
||||||
"NOAA-15": (
|
"NOAA-15": (
|
||||||
"NOAA 15",
|
"NOAA 15",
|
||||||
@@ -24,27 +24,27 @@ TLE_SATELLITES = {
|
|||||||
),
|
),
|
||||||
"NOAA-20": (
|
"NOAA-20": (
|
||||||
"NOAA 20 (JPSS-1)",
|
"NOAA 20 (JPSS-1)",
|
||||||
"1 43013U 17073A 26140.44110773 .00000055 00000+0 46930-4 0 9994",
|
"1 43013U 17073A 26140.58208103 .00000059 00000+0 48751-4 0 9991",
|
||||||
"2 43013 98.7764 80.1520 0001265 43.4537 316.6738 14.19505991440534",
|
"2 43013 98.7764 80.2917 0001257 43.1298 316.9976 14.19506008440556",
|
||||||
),
|
),
|
||||||
"NOAA-21": (
|
"NOAA-21": (
|
||||||
"NOAA 21 (JPSS-2)",
|
"NOAA 21 (JPSS-2)",
|
||||||
"1 54234U 22150A 26140.47502274 .00000020 00000+0 29984-4 0 9999",
|
"1 54234U 22150A 26140.54550684 .00000018 00000+0 29144-4 0 9990",
|
||||||
"2 54234 98.7052 79.7311 0000538 296.4939 63.6182 14.19559760182618",
|
"2 54234 98.7052 79.8004 0000537 295.8604 64.2517 14.19559757182625",
|
||||||
),
|
),
|
||||||
"METEOR-M2": (
|
"METEOR-M2": (
|
||||||
"METEOR-M 2",
|
"METEOR-M 2",
|
||||||
"1 40069U 14037A 26140.48222780 .00000329 00000+0 16961-3 0 9999",
|
"1 40069U 14037A 26140.62300918 .00000192 00000+0 10699-3 0 9995",
|
||||||
"2 40069 98.5104 117.2052 0006833 111.5029 248.6878 14.21453950615385",
|
"2 40069 98.5105 117.3408 0006839 111.2272 248.9637 14.21453406615400",
|
||||||
),
|
),
|
||||||
"METEOR-M2-3": (
|
"METEOR-M2-3": (
|
||||||
"METEOR-M2 3",
|
"METEOR-M2 3",
|
||||||
"1 57166U 23091A 26140.55562749 -.00000013 00000+0 13331-4 0 9995",
|
"1 57166U 23091A 26140.62588990 -.00000019 00000+0 10509-4 0 9999",
|
||||||
"2 57166 98.6097 196.0965 0002883 242.0522 118.0365 14.24044155150583",
|
"2 57166 98.6097 196.1653 0002885 241.7844 118.3044 14.24044145150590",
|
||||||
),
|
),
|
||||||
"METEOR-M2-4": (
|
"METEOR-M2-4": (
|
||||||
"METEOR-M2 4",
|
"METEOR-M2 4",
|
||||||
"1 59051U 24039A 26140.53898488 .00000003 00000+0 20858-4 0 9993",
|
"1 59051U 24039A 26140.60932705 .00000004 00000+0 21573-4 0 9998",
|
||||||
"2 59051 98.6996 100.1874 0005955 247.0139 113.0410 14.22426327115336",
|
"2 59051 98.6996 100.2569 0005956 246.7666 113.2885 14.22426332115345",
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,6 +106,36 @@
|
|||||||
white-space: nowrap;
|
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 ---
|
/* --- Dark glass popup ---
|
||||||
Applied via MapUtils.glassPopupOptions() className. */
|
Applied via MapUtils.glassPopupOptions() className. */
|
||||||
|
|
||||||
|
|||||||
+74
-27
@@ -118,6 +118,72 @@ const MapUtils = {
|
|||||||
return layer;
|
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.
|
* Add tactical overlays to a map.
|
||||||
*
|
*
|
||||||
@@ -129,7 +195,7 @@ const MapUtils = {
|
|||||||
* { latlng: [lat,lng] }
|
* { latlng: [lat,lng] }
|
||||||
* @param {Object} [options.hudPanels]
|
* @param {Object} [options.hudPanels]
|
||||||
* { modeName: string, getContactCount: ()=>number, getSdrStatus: ()=>boolean }
|
* { 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]
|
* @param {boolean} [options.scaleBar]
|
||||||
*
|
*
|
||||||
* @returns {Object} handles
|
* @returns {Object} handles
|
||||||
@@ -174,32 +240,13 @@ const MapUtils = {
|
|||||||
handles.updateCount = hudHandles.updateCount;
|
handles.updateCount = hudHandles.updateCount;
|
||||||
handles.updateStatus = hudHandles.updateStatus;
|
handles.updateStatus = hudHandles.updateStatus;
|
||||||
|
|
||||||
// --- Graticule ---
|
// --- Graticule toggle control (always added; defaultVisible via options.graticule) ---
|
||||||
let graticuleLayer = null;
|
const grat = this.addGraticuleControl(map, {
|
||||||
const buildGraticule = () => {
|
defaultVisible: options.graticule !== false,
|
||||||
if (graticuleLayer) map.removeLayer(graticuleLayer);
|
});
|
||||||
graticuleLayer = this._buildGraticule(map);
|
handles.showGraticule = grat.show;
|
||||||
graticuleLayer.addTo(map);
|
handles.hideGraticule = grat.hide;
|
||||||
};
|
cleanupFns.push(() => grat.control.remove());
|
||||||
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();
|
|
||||||
};
|
|
||||||
|
|
||||||
handles.removeAll = () => cleanupFns.forEach(fn => fn());
|
handles.removeAll = () => cleanupFns.forEach(fn => fn());
|
||||||
|
|
||||||
|
|||||||
@@ -213,6 +213,7 @@ const BtLocate = (function() {
|
|||||||
flushPendingHeatSync();
|
flushPendingHeatSync();
|
||||||
scheduleMapStabilization();
|
scheduleMapStabilization();
|
||||||
});
|
});
|
||||||
|
if (typeof MapUtils !== 'undefined') MapUtils.addGraticuleControl(map);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init RSSI chart canvas
|
// Init RSSI chart canvas
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ var DroneMode = (function () {
|
|||||||
maxZoom: 19,
|
maxZoom: 19,
|
||||||
}).addTo(_map);
|
}).addTo(_map);
|
||||||
}
|
}
|
||||||
|
if (typeof MapUtils !== 'undefined') MapUtils.addGraticuleControl(_map);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _connectSSE() {
|
function _connectSSE() {
|
||||||
|
|||||||
@@ -330,6 +330,8 @@ const MeshCore = (function () {
|
|||||||
Settings.registerMap(_map);
|
Settings.registerMap(_map);
|
||||||
}).catch(e => console.warn('MeshCore: Settings init failed, using fallback tiles:', e));
|
}).catch(e => console.warn('MeshCore: Settings init failed, using fallback tiles:', e));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof MapUtils !== 'undefined') MapUtils.addGraticuleControl(_map);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _updateMapMarker(node) {
|
function _updateMapMarker(node) {
|
||||||
|
|||||||
@@ -137,6 +137,8 @@ const Meshtastic = (function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof MapUtils !== 'undefined') MapUtils.addGraticuleControl(meshMap);
|
||||||
|
|
||||||
// Handle resize
|
// Handle resize
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (meshMap) meshMap.invalidateSize();
|
if (meshMap) meshMap.invalidateSize();
|
||||||
|
|||||||
@@ -241,6 +241,8 @@ const SSTV = (function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof MapUtils !== 'undefined') MapUtils.addGraticuleControl(issMap);
|
||||||
|
|
||||||
// Create ISS icon
|
// Create ISS icon
|
||||||
const issIcon = L.divIcon({
|
const issIcon = L.divIcon({
|
||||||
className: 'sstv-iss-marker',
|
className: 'sstv-iss-marker',
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ const WeatherSat = (function() {
|
|||||||
let groundMap = null;
|
let groundMap = null;
|
||||||
let groundTrackLayer = null;
|
let groundTrackLayer = null;
|
||||||
let groundOverlayLayer = null;
|
let groundOverlayLayer = null;
|
||||||
let groundGridLayer = null;
|
|
||||||
let satCrosshairMarker = null;
|
let satCrosshairMarker = null;
|
||||||
let observerMarker = null;
|
let observerMarker = null;
|
||||||
let consoleEntries = [];
|
let consoleEntries = [];
|
||||||
@@ -1086,8 +1085,7 @@ const WeatherSat = (function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
groundGridLayer = L.layerGroup().addTo(groundMap);
|
if (typeof MapUtils !== 'undefined') MapUtils.addGraticuleControl(groundMap);
|
||||||
addStyledGridOverlay(groundGridLayer);
|
|
||||||
|
|
||||||
groundTrackLayer = L.layerGroup().addTo(groundMap);
|
groundTrackLayer = L.layerGroup().addTo(groundMap);
|
||||||
groundOverlayLayer = L.layerGroup().addTo(groundMap);
|
groundOverlayLayer = L.layerGroup().addTo(groundMap);
|
||||||
@@ -1145,38 +1143,6 @@ const WeatherSat = (function() {
|
|||||||
return segments;
|
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() {
|
function clearSatelliteCrosshair() {
|
||||||
if (!groundOverlayLayer || !satCrosshairMarker) return;
|
if (!groundOverlayLayer || !satCrosshairMarker) return;
|
||||||
groundOverlayLayer.removeLayer(satCrosshairMarker);
|
groundOverlayLayer.removeLayer(satCrosshairMarker);
|
||||||
|
|||||||
@@ -343,6 +343,7 @@ async function initWebsdrLeaflet(mapEl) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof MapUtils !== 'undefined') MapUtils.addGraticuleControl(websdrMap);
|
||||||
mapEl.style.background = '#1a1d29';
|
mapEl.style.background = '#1a1d29';
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user