Apply global map theme updates and UI improvements

This commit is contained in:
Smittix
2026-02-20 00:32:58 +00:00
parent 963bcdf9fa
commit 1466fc2d30
22 changed files with 1365 additions and 351 deletions

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,4 @@
{
"version": "2026-02-01_ba81b697",
"downloaded": "2026-02-04T17:06:54.806043Z"
"version": "2026-02-15_ae16bb62",
"downloaded": "2026-02-20T00:29:06.228007Z"
}

View File

@@ -7,9 +7,10 @@
gap: 10px;
padding: 8px 14px;
margin: 6px 12px 0;
border: 1px solid var(--border-color, #1e2d3d);
border: 1px solid rgba(74, 163, 255, 0.32);
border-radius: 8px;
background: linear-gradient(180deg, rgba(17, 26, 37, 0.95), rgba(13, 20, 30, 0.95));
background: linear-gradient(180deg, rgba(19, 30, 44, 0.96), rgba(11, 18, 28, 0.97));
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.28), inset 0 1px 0 rgba(255, 255, 255, 0.04);
}
.run-state-left {
@@ -41,8 +42,8 @@
gap: 6px;
padding: 3px 7px;
border-radius: 999px;
border: 1px solid var(--border-color, #1e2d3d);
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(74, 163, 255, 0.25);
background: linear-gradient(180deg, rgba(17, 26, 38, 0.82), rgba(12, 18, 28, 0.84));
font-size: 10px;
color: var(--text-secondary, #b1c2d4);
white-space: nowrap;
@@ -58,12 +59,13 @@
.run-state-chip.running .dot {
background: var(--accent-green, #28c27a);
box-shadow: 0 0 0 4px rgba(40, 194, 122, 0.15);
box-shadow: 0 0 0 4px rgba(40, 194, 122, 0.16), 0 0 12px rgba(40, 194, 122, 0.35);
}
.run-state-chip.active {
border-color: rgba(74, 163, 255, 0.55);
border-color: rgba(74, 163, 255, 0.65);
color: var(--text-primary, #e6edf5);
box-shadow: inset 0 0 0 1px rgba(74, 163, 255, 0.18);
}
.run-state-right {
@@ -78,17 +80,20 @@
}
.run-state-btn {
background: transparent;
background: linear-gradient(180deg, rgba(17, 27, 41, 0.9), rgba(10, 16, 25, 0.92));
color: var(--accent-cyan, #4aa3ff);
border: 1px solid rgba(74, 163, 255, 0.45);
border-radius: 6px;
font-size: 10px;
padding: 4px 8px;
cursor: pointer;
transition: background 0.16s ease, border-color 0.16s ease, transform 0.16s ease;
}
.run-state-btn:hover {
background: rgba(74, 163, 255, 0.12);
background: rgba(74, 163, 255, 0.14);
border-color: rgba(74, 163, 255, 0.7);
transform: translateY(-1px);
}
.command-palette-overlay {
@@ -109,10 +114,10 @@
.command-palette {
width: min(760px, 100%);
border: 1px solid var(--border-color, #1e2d3d);
border: 1px solid rgba(74, 163, 255, 0.32);
border-radius: 12px;
background: #0f1823;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.55);
background: linear-gradient(180deg, rgba(16, 26, 39, 0.98), rgba(10, 17, 27, 0.98));
box-shadow: 0 26px 60px rgba(0, 0, 0, 0.56), inset 0 1px 0 rgba(255, 255, 255, 0.04);
overflow: hidden;
}

View File

@@ -28,14 +28,16 @@ body {
color: var(--text-primary);
background-color: var(--bg-primary);
background-image:
radial-gradient(1200px 620px at 8% -12%, var(--ambient-top-left), transparent 62%),
radial-gradient(980px 560px at 92% -16%, var(--ambient-top-right), transparent 64%),
radial-gradient(900px 520px at 50% 126%, var(--ambient-bottom), transparent 68%),
var(--noise-image),
radial-gradient(circle at 15% 0%, var(--grid-dot), transparent 45%),
linear-gradient(180deg, var(--grid-dot), transparent 35%),
linear-gradient(var(--grid-line) 1px, transparent 1px),
linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
background-size: 40px 40px, auto, auto, 48px 48px, 48px 48px;
background-size: auto, auto, auto, 40px 40px, 48px 48px, 48px 48px;
background-attachment: fixed;
min-height: 100vh;
font-variant-numeric: tabular-nums;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@@ -123,11 +123,12 @@
CARDS / PANELS
============================================ */
.card {
background: linear-gradient(180deg, var(--bg-card) 0%, var(--bg-secondary) 100%);
border: 1px solid var(--border-color);
background: var(--surface-panel-gradient);
border: 1px solid rgba(74, 163, 255, 0.24);
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: var(--shadow-sm);
box-shadow: var(--shadow-sm), inset 0 1px 0 rgba(255, 255, 255, 0.04);
backdrop-filter: blur(5px);
}
.card-header {
@@ -135,8 +136,8 @@
align-items: center;
justify-content: space-between;
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--border-color);
background: var(--bg-secondary);
border-bottom: 1px solid rgba(74, 163, 255, 0.18);
background: linear-gradient(180deg, rgba(25, 38, 55, 0.88) 0%, rgba(17, 27, 40, 0.9) 100%);
position: relative;
}
@@ -160,11 +161,12 @@
/* Panel variant (used in dashboards) */
.panel {
background: linear-gradient(180deg, var(--bg-card) 0%, var(--bg-secondary) 100%);
border: 1px solid var(--border-color);
background: var(--surface-panel-gradient);
border: 1px solid rgba(74, 163, 255, 0.24);
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: var(--shadow-sm);
box-shadow: var(--shadow-sm), inset 0 1px 0 rgba(255, 255, 255, 0.04);
backdrop-filter: blur(5px);
}
@supports (clip-path: polygon(0 0)) {
@@ -190,8 +192,8 @@
align-items: center;
justify-content: space-between;
padding: var(--space-2) var(--space-3);
border-bottom: 1px solid var(--border-color);
background: linear-gradient(180deg, var(--bg-elevated) 0%, var(--bg-secondary) 100%);
border-bottom: 1px solid rgba(74, 163, 255, 0.18);
background: linear-gradient(180deg, rgba(25, 38, 55, 0.88) 0%, rgba(17, 27, 40, 0.9) 100%);
font-size: var(--text-xs);
font-weight: var(--font-semibold);
text-transform: uppercase;
@@ -720,10 +722,23 @@
transform var(--transition-base);
}
.card:hover,
.panel:hover {
border-color: var(--border-light);
}
.card:hover,
.panel:hover {
border-color: var(--border-glow);
box-shadow: var(--shadow-md), var(--shadow-glow), inset 0 1px 0 rgba(255, 255, 255, 0.06);
transform: translateY(-1px);
}
[data-theme="light"] .card,
[data-theme="light"] .panel {
border-color: rgba(31, 95, 168, 0.24);
}
[data-theme="light"] .card-header,
[data-theme="light"] .panel-header {
border-bottom-color: rgba(31, 95, 168, 0.2);
background: linear-gradient(180deg, rgba(243, 247, 252, 0.96) 0%, rgba(233, 239, 247, 0.95) 100%);
}
/* Stats strip value highlight on hover */
.strip-stat {

View File

@@ -16,6 +16,11 @@
--bg-card: #121a25;
--bg-elevated: #1b2734;
--bg-overlay: rgba(8, 13, 20, 0.75);
--surface-glass: rgba(16, 25, 37, 0.82);
--surface-panel-gradient: linear-gradient(160deg, rgba(20, 32, 47, 0.94) 0%, rgba(11, 18, 27, 0.96) 100%);
--ambient-top-left: rgba(74, 163, 255, 0.14);
--ambient-top-right: rgba(56, 193, 128, 0.09);
--ambient-bottom: rgba(214, 168, 94, 0.06);
/* Background aliases for components */
--bg-dark: var(--bg-primary);
@@ -158,6 +163,11 @@
--bg-card: #ffffff;
--bg-elevated: #f1f4f9;
--bg-overlay: rgba(244, 247, 251, 0.92);
--surface-glass: rgba(255, 255, 255, 0.84);
--surface-panel-gradient: linear-gradient(160deg, rgba(255, 255, 255, 0.96) 0%, rgba(241, 245, 251, 0.97) 100%);
--ambient-top-left: rgba(31, 95, 168, 0.1);
--ambient-top-right: rgba(31, 138, 87, 0.06);
--ambient-bottom: rgba(181, 134, 58, 0.05);
/* Background aliases for components */
--bg-dark: var(--bg-primary);

View File

@@ -5115,6 +5115,46 @@ header h1 .tagline {
padding: 4px 4px 0 42px;
}
/* Locate action on Bluetooth device rows (must be in index.css so it styles in scanner mode) */
.bt-row-actions .bt-locate-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
min-height: 28px;
padding: 5px 10px;
font-size: 10px;
line-height: 1;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.07em;
color: var(--accent-green, #38c180);
background: linear-gradient(180deg, rgba(56, 193, 128, 0.2), rgba(56, 193, 128, 0.12));
border: 1px solid rgba(56, 193, 128, 0.42);
border-radius: 999px;
cursor: pointer;
white-space: nowrap;
transition: background 0.18s ease, border-color 0.18s ease, transform 0.18s ease, box-shadow 0.18s ease;
}
.bt-row-actions .bt-locate-btn:hover {
background: linear-gradient(180deg, rgba(56, 193, 128, 0.28), rgba(56, 193, 128, 0.18));
border-color: rgba(56, 193, 128, 0.72);
box-shadow: 0 0 0 1px rgba(56, 193, 128, 0.2), 0 6px 16px rgba(20, 80, 54, 0.35);
transform: translateY(-1px);
}
.bt-row-actions .bt-locate-btn:active {
transform: translateY(0);
}
.bt-row-actions .bt-locate-btn svg {
width: 12px;
height: 12px;
stroke: currentColor;
flex-shrink: 0;
}
.bt-device-filter-state {
margin-top: 8px;
}
@@ -7084,3 +7124,256 @@ body::before {
[data-animations="off"] .welcome-logo {
animation: none !important;
}
/* ============================================
VISUAL REFRESH OVERRIDES
============================================ */
:root {
--visual-surface-soft: linear-gradient(180deg, rgba(18, 28, 40, 0.9) 0%, rgba(10, 16, 24, 0.95) 100%);
--visual-surface-panel: linear-gradient(160deg, rgba(20, 33, 48, 0.95) 0%, rgba(11, 18, 27, 0.96) 100%);
--visual-edge-cyan: rgba(74, 163, 255, 0.34);
--visual-edge-green: rgba(56, 193, 128, 0.28);
--visual-glow-soft: 0 14px 30px rgba(0, 0, 0, 0.32);
--visual-glow-cyan: 0 0 24px rgba(74, 163, 255, 0.16);
--mode-ambient-left: rgba(74, 163, 255, 0.12);
--mode-ambient-right: rgba(56, 193, 128, 0.08);
--mode-ambient-bottom: rgba(214, 168, 94, 0.05);
--top-rail-gutter: 12px;
--top-rail-gap: 6px;
--top-rail-height: 44px;
}
body {
background-image:
radial-gradient(1200px 560px at 8% -10%, var(--mode-ambient-left), transparent 60%),
radial-gradient(900px 520px at 92% -18%, var(--mode-ambient-right), transparent 60%),
radial-gradient(800px 440px at 50% 130%, var(--mode-ambient-bottom), transparent 65%),
var(--noise-image),
linear-gradient(var(--grid-line) 1px, transparent 1px),
linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
background-size: auto, auto, auto, 40px 40px, 48px 48px, 48px 48px;
}
body[data-mode="wifi"],
body[data-mode="bluetooth"],
body[data-mode="bt_locate"] {
--mode-ambient-left: rgba(56, 193, 128, 0.14);
--mode-ambient-right: rgba(74, 163, 255, 0.08);
}
body[data-mode="satellite"],
body[data-mode="weathersat"],
body[data-mode="sstv"],
body[data-mode="sstv_general"] {
--mode-ambient-left: rgba(74, 163, 255, 0.14);
--mode-ambient-right: rgba(143, 123, 214, 0.09);
--mode-ambient-bottom: rgba(56, 193, 128, 0.05);
}
body[data-mode="analytics"],
body[data-mode="spystations"],
body[data-mode="tscm"] {
--mode-ambient-left: rgba(214, 168, 94, 0.12);
--mode-ambient-right: rgba(74, 163, 255, 0.08);
}
[data-theme="light"] body {
--mode-ambient-left: rgba(31, 95, 168, 0.09);
--mode-ambient-right: rgba(31, 138, 87, 0.05);
--mode-ambient-bottom: rgba(181, 134, 58, 0.04);
}
.mode-nav {
background: linear-gradient(180deg, rgba(22, 33, 48, 0.96) 0%, rgba(14, 22, 33, 0.98) 100%);
border-bottom-color: rgba(74, 163, 255, 0.24);
}
#mainNav.mode-nav {
margin: var(--top-rail-gap) var(--top-rail-gutter) 0;
padding: 0 12px;
min-height: var(--top-rail-height);
height: var(--top-rail-height);
border: 1px solid rgba(74, 163, 255, 0.22);
border-radius: 10px;
box-shadow: var(--visual-glow-soft), inset 0 1px 0 rgba(255, 255, 255, 0.03);
}
.run-state-strip {
margin: 8px var(--top-rail-gutter) 0;
border-color: rgba(74, 163, 255, 0.3);
background: linear-gradient(180deg, rgba(20, 31, 44, 0.96) 0%, rgba(12, 19, 29, 0.97) 100%);
box-shadow: var(--visual-glow-soft), inset 0 1px 0 rgba(255, 255, 255, 0.04);
min-height: var(--top-rail-height);
padding: 6px 12px;
border-radius: 10px;
}
.run-state-strip .run-state-chip {
min-height: 22px;
padding: 2px 8px;
}
.run-state-strip .run-state-btn {
min-height: 26px;
padding: 4px 10px;
}
.run-state-strip .run-state-right {
gap: 6px;
}
.main-content {
margin: 0 12px;
border: 1px solid rgba(74, 163, 255, 0.22);
border-radius: 10px;
box-shadow: var(--visual-glow-soft), inset 0 0 0 1px rgba(255, 255, 255, 0.02);
backdrop-filter: blur(6px);
}
.sidebar {
background: var(--visual-surface-soft);
border-right-color: rgba(74, 163, 255, 0.22);
}
.section {
background: var(--visual-surface-panel);
border-color: rgba(74, 163, 255, 0.22);
border-radius: 8px;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03);
transition: border-color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease;
}
.section:hover {
border-color: var(--visual-edge-cyan);
box-shadow: var(--visual-glow-cyan), inset 0 1px 0 rgba(255, 255, 255, 0.06);
transform: translateY(-1px);
}
.section h3 {
background: linear-gradient(180deg, rgba(28, 44, 63, 0.88) 0%, rgba(20, 31, 44, 0.9) 100%);
border-bottom-color: rgba(74, 163, 255, 0.2);
}
.section h3::before {
background: linear-gradient(180deg, var(--accent-cyan) 0%, var(--accent-green) 100%);
}
.section h3::after {
background: rgba(12, 18, 28, 0.9);
border: 1px solid rgba(74, 163, 255, 0.24);
}
.form-group input,
.form-group select {
background: rgba(8, 13, 20, 0.72);
border-color: rgba(74, 163, 255, 0.2);
border-radius: 6px;
}
.preset-btn,
.control-btn,
.clear-btn,
.run-btn,
.stop-btn {
border-radius: 7px;
}
.preset-btn,
.control-btn,
.clear-btn {
border-color: rgba(74, 163, 255, 0.24);
background: linear-gradient(180deg, rgba(16, 24, 35, 0.88) 0%, rgba(10, 15, 24, 0.9) 100%);
}
.output-panel {
background: linear-gradient(180deg, rgba(8, 13, 19, 0.98) 0%, rgba(7, 11, 18, 0.99) 100%);
}
.output-header {
background: linear-gradient(180deg, rgba(18, 28, 42, 0.95) 0%, rgba(13, 21, 31, 0.98) 100%);
border-bottom-color: rgba(74, 163, 255, 0.22);
}
.output-content {
background: linear-gradient(180deg, rgba(8, 13, 19, 0.6) 0%, rgba(8, 13, 19, 0.9) 100%);
}
.stats > div {
border-color: rgba(74, 163, 255, 0.2);
background: linear-gradient(180deg, rgba(19, 28, 40, 0.8) 0%, rgba(12, 18, 27, 0.82) 100%);
}
.message {
border-color: rgba(74, 163, 255, 0.26);
border-left-width: 4px;
border-radius: 8px;
background: linear-gradient(180deg, rgba(21, 31, 44, 0.8) 0%, rgba(15, 23, 33, 0.82) 100%);
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.24);
}
.status-bar {
position: sticky;
bottom: 0;
z-index: 9;
border-top-color: rgba(74, 163, 255, 0.24);
background: linear-gradient(180deg, rgba(17, 26, 39, 0.96) 0%, rgba(10, 16, 24, 0.97) 100%);
backdrop-filter: blur(7px);
}
.status-indicator,
.control-group {
border-color: rgba(74, 163, 255, 0.2);
background: linear-gradient(180deg, rgba(15, 23, 34, 0.78) 0%, rgba(9, 14, 23, 0.8) 100%);
border-radius: 6px;
}
.status-dot.running {
box-shadow: 0 0 0 4px rgba(56, 193, 128, 0.15), 0 0 14px rgba(56, 193, 128, 0.4);
}
.mode-content.active {
animation: modePanelEntrance 220ms ease both;
}
@keyframes modePanelEntrance {
from {
opacity: 0;
transform: translateY(5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
[data-theme="light"] .run-state-strip,
[data-theme="light"] .main-content,
[data-theme="light"] .section,
[data-theme="light"] #mainNav.mode-nav,
[data-theme="light"] .output-header,
[data-theme="light"] .status-bar,
[data-theme="light"] .status-indicator,
[data-theme="light"] .control-group {
box-shadow: 0 10px 24px rgba(18, 40, 66, 0.08);
}
[data-theme="light"] .section,
[data-theme="light"] .stats > div,
[data-theme="light"] .message,
[data-theme="light"] .preset-btn,
[data-theme="light"] .control-btn,
[data-theme="light"] .clear-btn {
border-color: rgba(31, 95, 168, 0.26);
}
[data-animations="off"] .mode-content.active {
animation: none !important;
}
@media (max-width: 1023px) {
.run-state-strip {
margin-left: 8px;
margin-right: 8px;
}
}

View File

@@ -510,8 +510,24 @@
}
.wxsat-ground-map {
position: relative;
height: 200px;
background: var(--bg-primary, #0d1117);
overflow: hidden;
background: linear-gradient(180deg, #061329 0%, #050d1a 54%, #061325 100%);
}
.wxsat-ground-map .leaflet-container {
width: 100%;
height: 100%;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
}
.leaflet-container.map-theme-cyber .leaflet-overlay-pane path.wxsat-pass-track {
filter: drop-shadow(0 0 5px rgba(91, 240, 255, 0.35));
}
.leaflet-container.map-theme-cyber .leaflet-overlay-pane path.wxsat-pass-track.lrpt {
filter: drop-shadow(0 0 6px rgba(0, 255, 190, 0.35));
}
.wxsat-crosshair-icon {
@@ -521,8 +537,8 @@
.wxsat-crosshair-marker {
position: relative;
width: 26px;
height: 26px;
width: 30px;
height: 30px;
}
.wxsat-crosshair-h,
@@ -538,7 +554,7 @@
left: 2px;
right: 2px;
height: 1px;
background: rgba(255, 76, 76, 0.9);
background: rgba(255, 93, 93, 0.95);
transform: translateY(-50%);
}
@@ -547,26 +563,41 @@
top: 2px;
bottom: 2px;
width: 1px;
background: rgba(255, 76, 76, 0.9);
background: rgba(255, 93, 93, 0.95);
transform: translateX(-50%);
}
.wxsat-crosshair-ring {
inset: 5px;
border: 1px solid rgba(255, 76, 76, 0.95);
inset: 6px;
border: 1.5px solid rgba(255, 93, 93, 0.95);
border-radius: 50%;
box-shadow: 0 0 8px rgba(255, 76, 76, 0.45);
box-shadow: 0 0 10px rgba(255, 93, 93, 0.55);
}
.wxsat-crosshair-dot {
width: 4px;
height: 4px;
width: 5px;
height: 5px;
left: 50%;
top: 50%;
border-radius: 50%;
background: #ff4c4c;
background: #ffa0a0;
box-shadow: 0 0 6px rgba(255, 100, 100, 0.65);
transform: translate(-50%, -50%);
}
.wxsat-map-tooltip {
background: rgba(5, 15, 32, 0.92);
border: 1px solid rgba(102, 229, 255, 0.65);
border-radius: 4px;
color: #8fe8ff;
box-shadow: 0 0 12px rgba(0, 210, 255, 0.24);
font-size: 10px;
letter-spacing: 0.25px;
}
.wxsat-map-tooltip.leaflet-tooltip-top:before {
border-top-color: rgba(102, 229, 255, 0.65);
}
/* ===== Image Gallery Panel ===== */
.wxsat-gallery-panel {

View File

@@ -479,6 +479,54 @@
filter: sepia(0.35) hue-rotate(185deg) saturate(1.75) brightness(1.06) contrast(1.05);
}
/* Global Leaflet map theme: cyber overlay */
.leaflet-container.map-theme-cyber {
position: relative;
background: #020813;
isolation: isolate;
}
.leaflet-container.map-theme-cyber .leaflet-tile-pane {
filter: sepia(0.74) hue-rotate(176deg) saturate(1.72) brightness(1.05) contrast(1.08);
opacity: 1;
}
/* Hard global fallback: enforce cyber tint on all Leaflet tile images */
html.map-cyber-enabled .leaflet-container .leaflet-tile {
filter: sepia(0.74) hue-rotate(176deg) saturate(1.72) brightness(1.05) contrast(1.08) !important;
}
/* Hard global fallback: cyber glow + grid overlay */
html.map-cyber-enabled .leaflet-container {
position: relative;
isolation: isolate;
}
html.map-cyber-enabled .leaflet-container::before {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
z-index: 620;
background:
radial-gradient(95% 78% at 50% 44%, rgba(18, 170, 255, 0.17), rgba(18, 170, 255, 0) 64%),
linear-gradient(180deg, rgba(24, 118, 255, 0.045), rgba(24, 118, 255, 0));
}
html.map-cyber-enabled .leaflet-container::after {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
z-index: 621;
opacity: 0.42;
mix-blend-mode: screen;
background-image:
linear-gradient(rgba(78, 188, 255, 0.14) 1px, transparent 1px),
linear-gradient(90deg, rgba(78, 188, 255, 0.14) 1px, transparent 1px);
background-size: 52px 52px, 52px 52px;
}
/* Responsive */
@media (max-width: 960px) {
.settings-tabs {

View File

@@ -22,15 +22,16 @@ const Settings = {
cartodb_dark: {
url: 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png',
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OSM</a> &copy; <a href="https://carto.com/">CARTO</a>',
subdomains: 'abcd'
subdomains: 'abcd',
mapTheme: 'cyber',
options: {}
},
cartodb_dark_cyan: {
url: 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png',
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OSM</a> &copy; <a href="https://carto.com/">CARTO</a>',
subdomains: 'abcd',
options: {
className: 'tile-layer-cyan'
}
mapTheme: 'cyber',
options: {}
},
cartodb_light: {
url: 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png',
@@ -50,26 +51,153 @@ const Settings = {
// Current settings cache
_cache: {},
// Init guard to prevent concurrent fetch races across pages/modes
_initialized: false,
_initPromise: null,
_themeObserver: null,
_themeObserverStarted: false,
_themeObserverRaf: null,
/**
* Check if a tile provider key is valid.
* @param {string} provider
* @returns {boolean}
*/
_isKnownTileProvider(provider) {
if (typeof provider !== 'string') return false;
const key = provider.trim();
return key === 'custom' || Object.prototype.hasOwnProperty.call(this.tileProviders, key);
},
/**
* Normalize tile provider values from storage/UI.
* @param {string} provider
* @returns {string}
*/
_normalizeTileProvider(provider) {
if (typeof provider !== 'string') return this.defaults['offline.tile_provider'];
const key = provider.trim();
if (this._isKnownTileProvider(key)) return key;
return this.defaults['offline.tile_provider'];
},
/**
* Persist and retrieve preferred map theme behavior for dark Carto tiles.
* Helps keep Cyber style enabled even if server-side tile provider drifts.
*/
_getMapThemePreference() {
if (typeof localStorage === 'undefined') return 'cyber';
const pref = localStorage.getItem('intercept_map_theme_pref');
if (pref === 'none' || pref === 'cyber') return pref;
return 'cyber';
},
_setMapThemePreference(pref) {
if (typeof localStorage === 'undefined') return;
if (pref !== 'none' && pref !== 'cyber') return;
localStorage.setItem('intercept_map_theme_pref', pref);
},
/**
* Whether Cyber map theme should be considered active globally.
* @param {Object} [config]
* @returns {boolean}
*/
_isCyberThemeEnabled(config) {
const resolvedConfig = config || this.getTileConfig();
return this._getMapThemeClass(resolvedConfig) === 'map-theme-cyber';
},
/**
* Toggle root class used for hard global Leaflet theming.
* @param {Object} [config]
*/
_syncRootMapThemeClass(config) {
if (typeof document === 'undefined' || !document.documentElement) return;
const enabled = this._isCyberThemeEnabled(config);
document.documentElement.classList.toggle('map-cyber-enabled', enabled);
},
/**
* Prefer localStorage tile settings when available to avoid stale server values.
*/
_applyLocalTileOverrides() {
const stored = localStorage.getItem('intercept_settings');
if (!stored) return;
try {
const local = JSON.parse(stored) || {};
const localProvider = this._normalizeTileProvider(local['offline.tile_provider']);
if (localProvider) {
this._cache['offline.tile_provider'] = localProvider;
}
if (typeof local['offline.tile_server_url'] === 'string') {
this._cache['offline.tile_server_url'] = local['offline.tile_server_url'];
}
} catch (e) {
// Ignore malformed local settings and keep current cache.
}
},
/**
* Initialize settings - load from server/localStorage
*/
async init() {
try {
const response = await fetch('/offline/settings');
if (response.ok) {
const data = await response.json();
this._cache = { ...this.defaults, ...data.settings };
} else {
// Fall back to localStorage
this._loadFromLocalStorage();
}
} catch (e) {
console.warn('Failed to load settings from server, using localStorage:', e);
this._loadFromLocalStorage();
async init(options = {}) {
const force = Boolean(options && options.force);
if (!force && this._initialized) {
return this._cache;
}
this._updateUI();
return this._cache;
if (!force && this._initPromise) {
return this._initPromise;
}
this._initPromise = (async () => {
try {
const response = await fetch('/offline/settings');
if (response.ok) {
const data = await response.json();
this._cache = { ...this.defaults, ...data.settings };
} else {
// Fall back to localStorage
this._loadFromLocalStorage();
}
} catch (e) {
console.warn('Failed to load settings from server, using localStorage:', e);
this._loadFromLocalStorage();
}
this._applyLocalTileOverrides();
this._cache['offline.tile_provider'] = this._normalizeTileProvider(this._cache['offline.tile_provider']);
// If dark Carto was restored by stale server settings but user prefers Cyber,
// keep the visible provider aligned with Cyber selection.
if (this._cache['offline.tile_provider'] === 'cartodb_dark' && this._getMapThemePreference() === 'cyber') {
this._cache['offline.tile_provider'] = 'cartodb_dark_cyan';
}
this._updateUI();
// Re-apply map theme to already-registered maps in case init happened after map creation.
const allMaps = this._collectMaps();
if (allMaps.length > 0) {
const config = this.getTileConfig();
allMaps.forEach((map) => this._applyMapTheme(map, config));
}
const activeConfig = this.getTileConfig();
this._syncRootMapThemeClass(activeConfig);
this._applyThemeToAllContainers(activeConfig);
this._ensureThemeObserver();
this._initialized = true;
return this._cache;
})();
try {
return await this._initPromise;
} finally {
this._initPromise = null;
}
},
/**
@@ -99,11 +227,14 @@ const Settings = {
// Save to server
try {
await fetch('/offline/settings', {
const response = await fetch('/offline/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key, value })
});
if (!response.ok) {
throw new Error(`Save failed (${response.status})`);
}
} catch (e) {
console.warn('Failed to save setting to server:', e);
}
@@ -152,6 +283,16 @@ const Settings = {
* Set tile provider
*/
async setTileProvider(provider) {
provider = this._normalizeTileProvider(provider);
if (provider === 'cartodb_dark_cyan') {
this._setMapThemePreference('cyber');
} else if (provider === 'cartodb_dark') {
this._setMapThemePreference('none');
} else {
this._setMapThemePreference('none');
}
await this._save('offline.tile_provider', provider);
// Show/hide custom URL input
@@ -160,10 +301,11 @@ const Settings = {
customRow.style.display = provider === 'custom' ? 'block' : 'none';
}
// If not custom and we have a map, update tiles immediately
if (provider !== 'custom') {
this._updateMapTiles();
}
// Update tiles immediately for all providers.
this._updateMapTiles();
const activeConfig = this.getTileConfig();
this._syncRootMapThemeClass(activeConfig);
this._applyThemeToAllContainers(activeConfig);
},
/**
@@ -178,7 +320,7 @@ const Settings = {
* Get current tile configuration
*/
getTileConfig() {
const provider = this.get('offline.tile_provider');
const provider = this._normalizeTileProvider(this.get('offline.tile_provider'));
if (provider === 'custom') {
const customUrl = this.get('offline.tile_server_url');
@@ -189,7 +331,170 @@ const Settings = {
};
}
return this.tileProviders[provider] || this.tileProviders.cartodb_dark;
const config = this.tileProviders[provider] || this.tileProviders.cartodb_dark;
// Robust fallback: if dark Carto is active and Cyber is preferred,
// keep Cyber theme enabled even when provider temporarily reverts.
if (provider === 'cartodb_dark' && this._getMapThemePreference() === 'cyber') {
return { ...config, mapTheme: 'cyber' };
}
return config;
},
/**
* Resolve map theme class from tile config.
* @param {Object} config
* @returns {string|null}
*/
_getMapThemeClass(config) {
if (!config || !config.mapTheme) return null;
if (config.mapTheme === 'cyber') return 'map-theme-cyber';
return null;
},
/**
* Apply or clear map theme styles for a Leaflet container.
* @param {HTMLElement} container
* @param {Object} [config]
*/
_applyThemeToContainer(container, config) {
if (!container || !container.classList) return;
const tilePane = container.querySelector('.leaflet-tile-pane');
container.querySelectorAll('.intercept-map-theme-overlay').forEach((el) => el.remove());
if (tilePane && tilePane.style) {
tilePane.style.filter = '';
tilePane.style.opacity = '';
tilePane.style.willChange = '';
}
if (container.style) {
container.style.background = '';
}
container.classList.remove('map-theme-cyber');
const resolvedConfig = config || this.getTileConfig();
const themeClass = this._getMapThemeClass(resolvedConfig);
if (!themeClass) return;
container.classList.add(themeClass);
if (container.style) {
container.style.background = '#020813';
}
if (tilePane && tilePane.style) {
tilePane.style.filter = 'sepia(0.74) hue-rotate(176deg) saturate(1.72) brightness(1.05) contrast(1.08)';
tilePane.style.opacity = '1';
tilePane.style.willChange = 'filter';
}
// Grid/glow overlays are rendered via CSS pseudo elements on
// `html.map-cyber-enabled .leaflet-container` for consistent stacking.
},
/**
* Apply/remove map theme class on a Leaflet map container.
* @param {L.Map} map
* @param {Object} [config]
*/
_applyMapTheme(map, config) {
if (!map || typeof map.getContainer !== 'function') return;
const container = map.getContainer();
this._applyThemeToContainer(container, config);
},
/**
* Apply current map theme to all rendered Leaflet containers.
* Covers maps that were not explicitly registered with Settings.
* @param {Object} [config]
*/
_applyThemeToAllContainers(config) {
if (typeof document === 'undefined') return;
const containers = document.querySelectorAll('.leaflet-container');
if (!containers.length) return;
const resolvedConfig = config || this.getTileConfig();
this._syncRootMapThemeClass(resolvedConfig);
containers.forEach((container) => this._applyThemeToContainer(container, resolvedConfig));
},
/**
* Watch the DOM for new Leaflet maps and apply current theme automatically.
*/
_ensureThemeObserver() {
if (this._themeObserverStarted || typeof MutationObserver === 'undefined') return;
if (typeof document === 'undefined' || !document.body) return;
const scheduleApply = () => {
if (this._themeObserverRaf && typeof cancelAnimationFrame === 'function') {
cancelAnimationFrame(this._themeObserverRaf);
}
if (typeof requestAnimationFrame === 'function') {
this._themeObserverRaf = requestAnimationFrame(() => {
this._themeObserverRaf = null;
this._applyThemeToAllContainers(this.getTileConfig());
});
} else {
this._applyThemeToAllContainers(this.getTileConfig());
}
};
this._themeObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (!mutation.addedNodes || mutation.addedNodes.length === 0) continue;
for (const node of mutation.addedNodes) {
if (!(node instanceof Element)) continue;
if (node.classList.contains('leaflet-container') || node.querySelector('.leaflet-container')) {
scheduleApply();
return;
}
}
}
});
this._themeObserver.observe(document.body, {
childList: true,
subtree: true
});
this._themeObserverStarted = true;
},
/**
* Collect all known map instances.
* @returns {L.Map[]}
*/
_collectMaps() {
const windowMaps = [
window.map,
window.leafletMap,
window.aprsMap,
window.radarMap,
window.vesselMap,
window.groundMap,
window.groundTrackMap,
window.meshMap,
window.issMap
].filter(m => m && typeof m.eachLayer === 'function');
return [...new Set([...this._registeredMaps, ...windowMaps])];
},
/**
* Keep map theme stable if map internals or layers are refreshed.
* @param {L.Map} map - Leaflet map instance
*/
_attachMapThemeHooks(map) {
if (!map || typeof map.on !== 'function' || map._interceptThemeHookBound) return;
const reapplyTheme = () => this._applyMapTheme(map);
const hookEvents = ['layeradd', 'layerremove', 'zoomend', 'resize', 'load'];
hookEvents.forEach((eventName) => map.on(eventName, reapplyTheme));
map._interceptThemeHookBound = true;
map._interceptThemeHookHandler = reapplyTheme;
},
/**
@@ -200,6 +505,18 @@ const Settings = {
if (map && typeof map.eachLayer === 'function' && !this._registeredMaps.includes(map)) {
this._registeredMaps.push(map);
}
this._ensureThemeObserver();
this._attachMapThemeHooks(map);
this._applyMapTheme(map);
this._applyThemeToAllContainers(this.getTileConfig());
// Some maps create tile DOM asynchronously; re-apply after first paint.
if (typeof window !== 'undefined' && typeof window.setTimeout === 'function') {
window.setTimeout(() => {
this._applyMapTheme(map);
this._applyThemeToAllContainers(this.getTileConfig());
}, 120);
}
},
/**
@@ -211,6 +528,15 @@ const Settings = {
if (idx > -1) {
this._registeredMaps.splice(idx, 1);
}
if (map && map._interceptThemeHookBound && typeof map.off === 'function') {
const handler = map._interceptThemeHookHandler;
['layeradd', 'layerremove', 'zoomend', 'resize', 'load'].forEach((eventName) => {
map.off(eventName, handler);
});
delete map._interceptThemeHookBound;
delete map._interceptThemeHookHandler;
}
},
/**
@@ -341,25 +667,11 @@ const Settings = {
* Update map tiles on all known maps
*/
_updateMapTiles() {
// Combine registered maps with common window map variables
const windowMaps = [
window.map,
window.leafletMap,
window.aprsMap,
window.radarMap,
window.vesselMap,
window.groundMap,
window.groundTrackMap,
window.meshMap,
window.issMap
].filter(m => m && typeof m.eachLayer === 'function');
// Combine with registered maps, removing duplicates
const allMaps = [...new Set([...this._registeredMaps, ...windowMaps])];
const allMaps = this._collectMaps();
if (allMaps.length === 0) return;
const config = this.getTileConfig();
this._syncRootMapThemeClass(config);
allMaps.forEach(map => {
// Remove existing tile layers
@@ -380,7 +692,10 @@ const Settings = {
}
L.tileLayer(config.url, options).addTo(map);
this._applyMapTheme(map, config);
});
this._applyThemeToAllContainers(config);
},
/**
@@ -572,12 +887,6 @@ function loadSettingsTools() {
});
}
// Initialize settings on page load
document.addEventListener('DOMContentLoaded', () => {
Settings.init();
switchSettingsTab('offline');
});
// =============================================================================
// Location Settings Functions
// =============================================================================

View File

@@ -36,6 +36,7 @@ const BtLocate = (function() {
let autoFollowEnabled = true;
let smoothingEnabled = true;
let lastRenderedDetectionKey = null;
let pendingHeatSync = false;
const MAX_HEAT_POINTS = 1200;
const MAX_TRAIL_POINTS = 1200;
@@ -63,6 +64,23 @@ const BtLocate = (function() {
},
};
function getMapContainer() {
if (!map || typeof map.getContainer !== 'function') return null;
return map.getContainer();
}
function isMapContainerVisible() {
const container = getMapContainer();
if (!container) return false;
if (container.offsetWidth <= 0 || container.offsetHeight <= 0) return false;
if (container.style && container.style.display === 'none') return false;
if (typeof window.getComputedStyle === 'function') {
const style = window.getComputedStyle(container);
if (style.display === 'none' || style.visibility === 'hidden') return false;
}
return true;
}
function init() {
loadOverlayPreferences();
syncOverlayControls();
@@ -71,20 +89,21 @@ const BtLocate = (function() {
// Re-invalidate map on re-entry and ensure tiles are present
if (map) {
setTimeout(() => {
map.invalidateSize();
// Re-apply user's tile layer if tiles were lost
let hasTiles = false;
map.eachLayer(layer => {
if (layer instanceof L.TileLayer) hasTiles = true;
});
if (!hasTiles && typeof Settings !== 'undefined' && Settings.createTileLayer) {
Settings.createTileLayer().addTo(map);
}
}, 150);
}
checkStatus();
return;
}
safeInvalidateMap();
// Re-apply user's tile layer if tiles were lost
let hasTiles = false;
map.eachLayer(layer => {
if (layer instanceof L.TileLayer) hasTiles = true;
});
if (!hasTiles && typeof Settings !== 'undefined' && Settings.createTileLayer) {
Settings.createTileLayer().addTo(map);
}
flushPendingHeatSync();
}, 150);
}
checkStatus();
return;
}
// Init map
const mapEl = document.getElementById('btLocateMap');
@@ -107,7 +126,13 @@ const BtLocate = (function() {
ensureHeatLayer();
syncMovementLayer();
syncHeatLayer();
setTimeout(() => map.invalidateSize(), 100);
map.on('resize moveend zoomend', () => {
flushPendingHeatSync();
});
setTimeout(() => {
safeInvalidateMap();
flushPendingHeatSync();
}, 100);
}
// Init RSSI chart canvas
@@ -432,7 +457,12 @@ const BtLocate = (function() {
// Map marker
let mapPointAdded = false;
if (d.lat != null && d.lon != null) {
mapPointAdded = addMapMarker(d, { suppressFollow: options.suppressFollow === true });
try {
mapPointAdded = addMapMarker(d, { suppressFollow: options.suppressFollow === true });
} catch (error) {
console.warn('[BtLocate] Map update skipped:', error);
mapPointAdded = false;
}
}
// Update stats
@@ -535,7 +565,11 @@ const BtLocate = (function() {
}
syncHeatLayer();
if (autoFollowEnabled && !options.suppressFollow) {
if (!isMapRenderable()) {
safeInvalidateMap();
}
const canFollowMap = isMapRenderable();
if (autoFollowEnabled && !options.suppressFollow && canFollowMap) {
if (!gpsLocked) {
gpsLocked = true;
map.setView([lat, lon], Math.max(map.getZoom(), 16));
@@ -631,7 +665,11 @@ const BtLocate = (function() {
const latestGps = trailPoints[trailPoints.length - 1];
gpsLocked = true;
const targetZoom = Math.max(map.getZoom(), 15);
map.setView([latestGps.lat, latestGps.lon], targetZoom);
if (isMapRenderable()) {
map.setView([latestGps.lat, latestGps.lon], targetZoom);
} else {
pendingHeatSync = true;
}
}
syncMovementLayer();
syncStrongestMarker();
@@ -667,7 +705,15 @@ const BtLocate = (function() {
confidenceCircle = null;
}
if (heatLayer) {
heatLayer.setLatLngs([]);
try {
if (isMapRenderable()) {
heatLayer.setLatLngs([]);
} else {
pendingHeatSync = true;
}
} catch (error) {
pendingHeatSync = true;
}
}
updateStrongestInfo(null);
updateConfidenceInfo(null);
@@ -817,14 +863,54 @@ const BtLocate = (function() {
if (!map) return;
ensureHeatLayer();
if (!heatLayer) return;
heatLayer.setLatLngs(heatPoints);
if (heatmapEnabled) {
if (!map.hasLayer(heatLayer)) {
heatLayer.addTo(map);
}
} else if (map.hasLayer(heatLayer)) {
map.removeLayer(heatLayer);
if (!isMapContainerVisible()) {
pendingHeatSync = true;
return;
}
if (!isMapRenderable()) {
safeInvalidateMap();
if (!isMapRenderable()) {
pendingHeatSync = true;
return;
}
}
try {
heatLayer.setLatLngs(heatPoints);
if (heatmapEnabled) {
if (!map.hasLayer(heatLayer)) {
heatLayer.addTo(map);
}
} else if (map.hasLayer(heatLayer)) {
map.removeLayer(heatLayer);
}
pendingHeatSync = false;
} catch (error) {
pendingHeatSync = true;
if (map.hasLayer(heatLayer)) {
map.removeLayer(heatLayer);
}
console.warn('[BtLocate] Heatmap redraw deferred:', error);
}
}
function isMapRenderable() {
if (!map || !isMapContainerVisible()) return false;
if (typeof map.getSize === 'function') {
const size = map.getSize();
if (!size || size.x <= 0 || size.y <= 0) return false;
}
return true;
}
function safeInvalidateMap() {
if (!map || !isMapContainerVisible()) return false;
map.invalidateSize({ pan: false, animate: false });
return true;
}
function flushPendingHeatSync() {
if (!pendingHeatSync) return;
syncHeatLayer();
}
function syncMovementLayer() {
@@ -1473,9 +1559,14 @@ const BtLocate = (function() {
.catch(err => console.error('[BtLocate] Clear trail error:', err));
}
function invalidateMap() {
if (map) map.invalidateSize();
}
function invalidateMap() {
if (safeInvalidateMap()) {
flushPendingHeatSync();
syncMovementLayer();
syncStrongestMarker();
updateConfidenceLayer();
}
}
return {
init,

View File

@@ -12,11 +12,12 @@ const SSTV = (function() {
let progress = 0;
let issMap = null;
let issMarker = null;
let issTrackLine = null;
let issPosition = null;
let issUpdateInterval = null;
let countdownInterval = null;
let nextPassData = null;
let issTrackLine = null;
let issPosition = null;
let issUpdateInterval = null;
let countdownInterval = null;
let nextPassData = null;
let pendingMapInvalidate = false;
// ISS frequency
const ISS_FREQ = 145.800;
@@ -37,15 +38,31 @@ const SSTV = (function() {
/**
* Initialize the SSTV mode
*/
function init() {
checkStatus();
loadImages();
loadLocationInputs();
loadIssSchedule();
initMap();
startIssTracking();
startCountdown();
}
function init() {
checkStatus();
loadImages();
loadLocationInputs();
loadIssSchedule();
initMap();
startIssTracking();
startCountdown();
// Ensure Leaflet recomputes dimensions after the SSTV pane becomes visible.
setTimeout(() => invalidateMap(), 80);
setTimeout(() => invalidateMap(), 260);
}
function isMapContainerVisible() {
if (!issMap || typeof issMap.getContainer !== 'function') return false;
const container = issMap.getContainer();
if (!container) return false;
if (container.offsetWidth <= 0 || container.offsetHeight <= 0) return false;
if (container.style && container.style.display === 'none') return false;
if (typeof window.getComputedStyle === 'function') {
const style = window.getComputedStyle(container);
if (style.display === 'none' || style.visibility === 'hidden') return false;
}
return true;
}
/**
* Load location into input fields
@@ -172,9 +189,9 @@ const SSTV = (function() {
/**
* Initialize Leaflet map for ISS tracking
*/
async function initMap() {
const mapContainer = document.getElementById('sstvIssMap');
if (!mapContainer || issMap) return;
async function initMap() {
const mapContainer = document.getElementById('sstvIssMap');
if (!mapContainer || issMap) return;
// Create map
issMap = L.map('sstvIssMap', {
@@ -214,13 +231,21 @@ const SSTV = (function() {
issMarker = L.marker([0, 0], { icon: issIcon }).addTo(issMap);
// Create ground track line
issTrackLine = L.polyline([], {
color: '#00d4ff',
weight: 2,
opacity: 0.6,
dashArray: '5, 5'
}).addTo(issMap);
}
issTrackLine = L.polyline([], {
color: '#00d4ff',
weight: 2,
opacity: 0.6,
dashArray: '5, 5'
}).addTo(issMap);
issMap.on('resize moveend zoomend', () => {
if (pendingMapInvalidate) invalidateMap();
});
// Initial layout passes for first-time mode load.
setTimeout(() => invalidateMap(), 40);
setTimeout(() => invalidateMap(), 180);
}
/**
* Start ISS position tracking
@@ -429,8 +454,9 @@ const SSTV = (function() {
/**
* Update map with ISS position
*/
function updateMap() {
if (!issMap || !issPosition) return;
function updateMap() {
if (!issMap || !issPosition) return;
if (pendingMapInvalidate) invalidateMap();
const lat = issPosition.lat;
const lon = issPosition.lon;
@@ -490,9 +516,13 @@ const SSTV = (function() {
issTrackLine.setLatLngs(segments.length > 0 ? segments : []);
}
// Pan map to follow ISS
issMap.panTo([lat, lon], { animate: true, duration: 0.5 });
}
// Pan map to follow ISS only when the map pane is currently renderable.
if (isMapContainerVisible()) {
issMap.panTo([lat, lon], { animate: true, duration: 0.5 });
} else {
pendingMapInvalidate = true;
}
}
/**
* Check current decoder status
@@ -1305,13 +1335,27 @@ const SSTV = (function() {
/**
* Show status message
*/
function showStatusMessage(message, type) {
if (typeof showNotification === 'function') {
showNotification('SSTV', message);
} else {
console.log(`[SSTV ${type}] ${message}`);
}
}
function showStatusMessage(message, type) {
if (typeof showNotification === 'function') {
showNotification('SSTV', message);
} else {
console.log(`[SSTV ${type}] ${message}`);
}
}
/**
* Invalidate ISS map size after pane/layout changes.
*/
function invalidateMap() {
if (!issMap) return false;
if (!isMapContainerVisible()) {
pendingMapInvalidate = true;
return false;
}
issMap.invalidateSize({ pan: false, animate: false });
pendingMapInvalidate = false;
return true;
}
// Public API
return {
@@ -1326,11 +1370,12 @@ const SSTV = (function() {
deleteAllImages,
downloadImage,
useGPS,
updateTLE,
stopIssTracking,
stopCountdown
};
})();
updateTLE,
stopIssTracking,
stopCountdown,
invalidateMap
};
})();
// Initialize when DOM is ready (will be called by selectMode)
document.addEventListener('DOMContentLoaded', function() {

View File

@@ -1,8 +1,8 @@
/**
* Weather Satellite Mode
/**
* Weather Satellite Mode
* NOAA APT and Meteor LRPT decoder interface with auto-scheduler,
* polar plot, mercator map, countdown, and timeline.
*/
* polar plot, styled real-world map, countdown, and timeline.
*/
const WeatherSat = (function() {
// State
@@ -11,39 +11,73 @@ const WeatherSat = (function() {
let images = [];
let passes = [];
let selectedPassIndex = -1;
let currentSatellite = null;
let countdownInterval = null;
let currentSatellite = null;
let countdownInterval = null;
let schedulerEnabled = false;
let groundMap = null;
let groundTrackLayer = null;
let groundOverlayLayer = null;
let groundGridLayer = null;
let satCrosshairMarker = null;
let observerMarker = null;
let consoleEntries = [];
let consoleEntries = [];
let consoleCollapsed = false;
let currentPhase = 'idle';
let consoleAutoHideTimer = null;
let currentModalFilename = null;
let locationListenersAttached = false;
let consoleAutoHideTimer = null;
let currentModalFilename = null;
let locationListenersAttached = false;
/**
* Initialize the Weather Satellite mode
*/
function init() {
checkStatus();
loadImages();
loadLocationInputs();
loadPasses();
function init() {
checkStatus();
loadImages();
loadLocationInputs();
loadPasses();
startCountdownTimer();
checkSchedulerStatus();
initGroundMap();
}
/**
* Load observer location into input fields
*/
initGroundMap();
}
/**
* Get observer coordinates from shared location or local storage.
*/
function getObserverCoords() {
let lat;
let lon;
if (window.ObserverLocation && ObserverLocation.isSharedEnabled()) {
const shared = ObserverLocation.getShared();
lat = Number(shared?.lat);
lon = Number(shared?.lon);
} else {
lat = Number(localStorage.getItem('observerLat'));
lon = Number(localStorage.getItem('observerLon'));
}
if (!isFinite(lat) || !isFinite(lon)) return null;
if (lat < -90 || lat > 90 || lon < -180 || lon > 180) return null;
return { lat, lon };
}
/**
* Center the ground map on current observer coordinates when available.
*/
function centerGroundMapOnObserver(zoom = 1) {
if (!groundMap) return;
const observer = getObserverCoords();
if (!observer) return;
const lat = Math.max(-85, Math.min(85, observer.lat));
const lon = normalizeLon(observer.lon);
groundMap.setView([lat, lon], zoom, { animate: false });
}
/**
* Load observer location into input fields
*/
function loadLocationInputs() {
const latInput = document.getElementById('wxsatObsLat');
const latInput = document.getElementById('wxsatObsLat');
const lonInput = document.getElementById('wxsatObsLon');
let storedLat = localStorage.getItem('observerLat');
@@ -80,13 +114,14 @@ const WeatherSat = (function() {
!isNaN(lon) && lon >= -180 && lon <= 180) {
if (window.ObserverLocation && ObserverLocation.isSharedEnabled()) {
ObserverLocation.setShared({ lat, lon });
} else {
localStorage.setItem('observerLat', lat.toString());
localStorage.setItem('observerLon', lon.toString());
}
loadPasses();
}
}
} else {
localStorage.setItem('observerLat', lat.toString());
localStorage.setItem('observerLon', lon.toString());
}
loadPasses();
centerGroundMapOnObserver(1);
}
}
/**
* Use GPS for location
@@ -119,11 +154,12 @@ const WeatherSat = (function() {
localStorage.setItem('observerLon', lon);
}
btn.innerHTML = originalText;
btn.disabled = false;
showNotification('Weather Sat', 'Location updated');
loadPasses();
},
btn.innerHTML = originalText;
btn.disabled = false;
showNotification('Weather Sat', 'Location updated');
loadPasses();
centerGroundMapOnObserver(1);
},
(err) => {
btn.innerHTML = originalText;
btn.disabled = false;
@@ -749,118 +785,140 @@ const WeatherSat = (function() {
ctx.fillText(Math.round(maxEl) + '\u00b0', cx + r * maxR * Math.cos(maxAz), cy + r * maxR * Math.sin(maxAz) - 8);
}
// ========================
// Ground Track Map
// ========================
/**
* Initialize Leaflet ground track map
*/
function initGroundMap() {
// ========================
// Ground Track Map
// ========================
/**
* Initialize styled real-world map panel.
*/
async function initGroundMap() {
const container = document.getElementById('wxsatGroundMap');
if (!container || groundMap) return;
if (!container) return;
if (typeof L === 'undefined') return;
const observer = getObserverCoords();
const defaultCenter = observer
? [Math.max(-85, Math.min(85, observer.lat)), normalizeLon(observer.lon)]
: [12, 0];
const defaultZoom = 1;
groundMap = L.map(container, {
center: [20, 0],
zoom: 2,
zoomControl: false,
attributionControl: false,
crs: L.CRS.EPSG3857, // Web Mercator projection
});
// Check tile provider from settings
let tileUrl = 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png';
try {
const provider = localStorage.getItem('tileProvider');
if (provider === 'osm') {
tileUrl = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
}
} catch (e) {}
L.tileLayer(tileUrl, { maxZoom: 10 }).addTo(groundMap);
if (!groundMap) {
groundMap = L.map(container, {
center: defaultCenter,
zoom: defaultZoom,
minZoom: 1,
maxZoom: 7,
zoomControl: false,
attributionControl: false,
worldCopyJump: true,
preferCanvas: true,
});
groundTrackLayer = L.layerGroup().addTo(groundMap);
groundOverlayLayer = L.layerGroup().addTo(groundMap);
if (typeof Settings !== 'undefined' && Settings.createTileLayer) {
await Settings.init();
Settings.createTileLayer().addTo(groundMap);
Settings.registerMap(groundMap);
} else {
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
subdomains: 'abcd',
maxZoom: 18,
noWrap: false,
crossOrigin: true,
className: 'tile-layer-cyan',
}).addTo(groundMap);
}
const selected = getSelectedPass();
if (selected) {
updateGroundTrack(selected);
} else {
updateSatelliteCrosshair(null);
groundGridLayer = L.layerGroup().addTo(groundMap);
addStyledGridOverlay(groundGridLayer);
groundTrackLayer = L.layerGroup().addTo(groundMap);
groundOverlayLayer = L.layerGroup().addTo(groundMap);
}
// Delayed invalidation to fix sizing
setTimeout(() => { if (groundMap) groundMap.invalidateSize(); }, 200);
}
/**
* Update ground track on the map
*/
function updateGroundTrack(pass) {
if (!groundMap || !groundTrackLayer) return;
groundTrackLayer.clearLayers();
if (!pass) {
updateSatelliteCrosshair(null);
return;
}
const track = pass.groundTrack;
if (!track || track.length === 0) {
updateSatelliteCrosshair(null);
return;
}
const color = pass.mode === 'LRPT' ? '#00ff88' : '#00d4ff';
// Draw polyline
const latlngs = track.map(p => [p.lat, p.lon]);
L.polyline(latlngs, { color, weight: 2, opacity: 0.8 }).addTo(groundTrackLayer);
// Start marker
L.circleMarker(latlngs[0], {
radius: 5, color: '#00ff88', fillColor: '#00ff88', fillOpacity: 1, weight: 0,
}).addTo(groundTrackLayer);
// End marker
L.circleMarker(latlngs[latlngs.length - 1], {
radius: 5, color: '#ff4444', fillColor: '#ff4444', fillOpacity: 1, weight: 0,
}).addTo(groundTrackLayer);
// Observer marker
let obsLat, obsLon;
if (window.ObserverLocation && ObserverLocation.isSharedEnabled()) {
const shared = ObserverLocation.getShared();
obsLat = shared?.lat;
obsLon = shared?.lon;
} else {
obsLat = parseFloat(localStorage.getItem('observerLat'));
obsLon = parseFloat(localStorage.getItem('observerLon'));
}
const lat = obsLat;
const lon = obsLon;
if (!isNaN(lat) && !isNaN(lon)) {
L.circleMarker([lat, lon], {
radius: 6, color: '#ffbb00', fillColor: '#ffbb00', fillOpacity: 0.8, weight: 1,
}).addTo(groundTrackLayer);
}
// Fit bounds
try {
const bounds = L.latLngBounds(latlngs);
if (!isNaN(lat) && !isNaN(lon)) bounds.extend([lat, lon]);
groundMap.fitBounds(bounds, { padding: [20, 20] });
} catch (e) {}
updateSatelliteCrosshair(pass);
setTimeout(() => {
if (!groundMap) return;
groundMap.invalidateSize(false);
groundMap.setView(defaultCenter, defaultZoom, { animate: false });
updateGroundTrack(getSelectedPass());
}, 140);
}
function updateMercatorInfo(text) {
const infoEl = document.getElementById('wxsatMercatorInfo');
/**
* Update map panel subtitle.
*/
function updateProjectionInfo(text) {
const infoEl = document.getElementById('wxsatMapInfo');
if (infoEl) infoEl.textContent = text || '--';
}
/**
* Normalize longitude to [-180, 180).
*/
function normalizeLon(value) {
const lon = Number(value);
if (!isFinite(lon)) return 0;
return ((((lon + 180) % 360) + 360) % 360) - 180;
}
/**
* Build track segments that do not cross the date line.
*/
function buildTrackSegments(track) {
const segments = [];
let currentSegment = [];
track.forEach((point) => {
const lat = Number(point?.lat);
const lon = normalizeLon(point?.lon);
if (!isFinite(lat) || !isFinite(lon)) return;
if (currentSegment.length > 0) {
const prevLon = currentSegment[currentSegment.length - 1][1];
if (Math.abs(lon - prevLon) > 180) {
if (currentSegment.length > 1) segments.push(currentSegment);
currentSegment = [];
}
}
currentSegment.push([lat, lon]);
});
if (currentSegment.length > 1) segments.push(currentSegment);
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);
@@ -870,8 +928,8 @@ const WeatherSat = (function() {
function createSatelliteCrosshairIcon() {
return L.divIcon({
className: 'wxsat-crosshair-icon',
iconSize: [26, 26],
iconAnchor: [13, 13],
iconSize: [30, 30],
iconAnchor: [15, 15],
html: `
<div class="wxsat-crosshair-marker">
<span class="wxsat-crosshair-h"></span>
@@ -883,6 +941,92 @@ const WeatherSat = (function() {
});
}
/**
* Update selected ground track and redraw map overlays.
*/
function updateGroundTrack(pass) {
if (!groundMap || !groundTrackLayer) return;
groundTrackLayer.clearLayers();
observerMarker = null;
if (!pass) {
clearSatelliteCrosshair();
updateProjectionInfo('--');
return;
}
const track = pass?.groundTrack;
if (!Array.isArray(track) || track.length === 0) {
clearSatelliteCrosshair();
updateProjectionInfo(`${pass.name || pass.satellite || '--'} --`);
return;
}
const color = pass.mode === 'LRPT' ? '#27ffc6' : '#58ddff';
const glowClass = pass.mode === 'LRPT' ? 'wxsat-pass-track lrpt' : 'wxsat-pass-track apt';
const segments = buildTrackSegments(track);
const validPoints = track
.map((point) => [Number(point?.lat), normalizeLon(point?.lon)])
.filter((point) => isFinite(point[0]) && isFinite(point[1]));
segments.forEach((segment) => {
L.polyline(segment, {
color,
weight: 2.3,
opacity: 0.9,
className: glowClass,
interactive: false,
lineJoin: 'round',
}).addTo(groundTrackLayer);
});
if (validPoints.length > 0) {
L.circleMarker(validPoints[0], {
radius: 4.5,
color: '#00ffa2',
fillColor: '#00ffa2',
fillOpacity: 0.95,
weight: 0,
interactive: false,
}).addTo(groundTrackLayer);
L.circleMarker(validPoints[validPoints.length - 1], {
radius: 4.5,
color: '#ff5e5e',
fillColor: '#ff5e5e',
fillOpacity: 0.95,
weight: 0,
interactive: false,
}).addTo(groundTrackLayer);
}
let obsLat;
let obsLon;
if (window.ObserverLocation && ObserverLocation.isSharedEnabled()) {
const shared = ObserverLocation.getShared();
obsLat = shared?.lat;
obsLon = shared?.lon;
} else {
obsLat = parseFloat(localStorage.getItem('observerLat'));
obsLon = parseFloat(localStorage.getItem('observerLon'));
}
if (isFinite(obsLat) && isFinite(obsLon)) {
observerMarker = L.circleMarker([obsLat, obsLon], {
radius: 5.5,
color: '#ffd45b',
fillColor: '#ffd45b',
fillOpacity: 0.8,
weight: 1,
className: 'wxsat-observer-marker',
interactive: false,
}).addTo(groundTrackLayer);
}
updateSatelliteCrosshair(pass);
}
function getSelectedPass() {
if (selectedPassIndex < 0 || selectedPassIndex >= passes.length) return null;
return passes[selectedPassIndex];
@@ -938,41 +1082,44 @@ const WeatherSat = (function() {
if (!pass) {
clearSatelliteCrosshair();
updateMercatorInfo('--');
updateProjectionInfo('--');
return;
}
const position = getSatellitePositionForPass(pass);
if (!position) {
clearSatelliteCrosshair();
updateMercatorInfo(`${pass.name || pass.satellite || '--'} --`);
updateProjectionInfo(`${pass.name || pass.satellite || '--'} --`);
return;
}
const latlng = [position.lat, position.lon];
const latlng = [position.lat, normalizeLon(position.lon)];
if (!satCrosshairMarker) {
satCrosshairMarker = L.marker(latlng, {
icon: createSatelliteCrosshairIcon(),
interactive: false,
keyboard: false,
zIndexOffset: 800,
zIndexOffset: 900,
}).addTo(groundOverlayLayer);
} else {
satCrosshairMarker.setLatLng(latlng);
}
const tooltipText = `${pass.name || pass.satellite || 'Satellite'} ${position.lat.toFixed(2)}°, ${position.lon.toFixed(2)}°`;
const infoText =
`${pass.name || pass.satellite || 'Satellite'} ` +
`${position.lat.toFixed(2)}°, ${normalizeLon(position.lon).toFixed(2)}°`;
updateProjectionInfo(infoText);
if (!satCrosshairMarker.getTooltip()) {
satCrosshairMarker.bindTooltip(tooltipText, {
satCrosshairMarker.bindTooltip(infoText, {
direction: 'top',
offset: [0, -10],
opacity: 0.9,
offset: [0, -12],
opacity: 0.92,
className: 'wxsat-map-tooltip',
});
} else {
satCrosshairMarker.setTooltipContent(tooltipText);
satCrosshairMarker.setTooltipContent(infoText);
}
updateMercatorInfo(tooltipText);
}
// ========================
@@ -1502,14 +1649,19 @@ const WeatherSat = (function() {
return div.innerHTML;
}
/**
* Invalidate ground map size (call after container becomes visible)
*/
function invalidateMap() {
if (groundMap) {
setTimeout(() => groundMap.invalidateSize(), 100);
}
}
/**
* Invalidate ground map size (call after container becomes visible)
*/
function invalidateMap() {
setTimeout(() => {
if (!groundMap) {
initGroundMap();
return;
}
groundMap.invalidateSize(false);
updateGroundTrack(getSelectedPass());
}, 100);
}
// ========================
// Decoder Console

View File

@@ -27,7 +27,7 @@ const KIWI_SAMPLE_RATE = 12000;
// ============== INITIALIZATION ==============
function initWebSDR() {
async function initWebSDR() {
if (websdrInitialized) {
if (websdrMap) {
setTimeout(() => websdrMap.invalidateSize(), 100);
@@ -51,11 +51,18 @@ function initWebSDR() {
maxBoundsViscosity: 1.0,
});
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
attribution: '&copy; OpenStreetMap contributors &copy; CARTO',
subdomains: 'abcd',
maxZoom: 19,
}).addTo(websdrMap);
if (typeof Settings !== 'undefined' && Settings.createTileLayer) {
await Settings.init();
Settings.createTileLayer().addTo(websdrMap);
Settings.registerMap(websdrMap);
} else {
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
attribution: '&copy; OpenStreetMap contributors &copy; CARTO',
subdomains: 'abcd',
maxZoom: 19,
className: 'tile-layer-cyan',
}).addTo(websdrMap);
}
// Match background to tile ocean color so any remaining edge is seamless
mapEl.style.background = '#1a1d29';

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" class="{% if offline_settings.tile_provider in ['cartodb_dark', 'cartodb_dark_cyan'] %}map-cyber-enabled{% endif %}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -22,7 +22,7 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/variables.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/global-nav.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}?v={{ version }}&r=maptheme17">
<link rel="stylesheet" href="{{ url_for('static', filename='css/help-modal.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/adsb_dashboard.css') }}">
<script>
@@ -1417,19 +1417,19 @@ ACARS: ${r.statistics.acarsMessages} messages`;
const meters = nm * 1852;
const circle = L.circle([observerLocation.lat, observerLocation.lon], {
radius: meters,
color: '#00ff88',
color: '#4a9eff',
fillColor: 'transparent',
fillOpacity: 0,
weight: 1,
opacity: 0.4,
dashArray: '5, 5'
opacity: 0.3,
dashArray: '4 4'
});
const labelLat = observerLocation.lat + (nm * 0.0166);
const label = L.marker([labelLat, observerLocation.lon], {
icon: L.divIcon({
className: 'range-label',
html: `<span style="color: #00ff88; font-size: 10px; background: rgba(0,0,0,0.7); padding: 1px 4px; border-radius: 2px;">${Math.round(nm)} nm</span>`,
html: `<span style="color: #4a9eff; font-size: 10px; background: rgba(0,0,0,0.7); padding: 1px 4px; border-radius: 2px;">${Math.round(nm)} nm</span>`,
iconSize: [40, 12],
iconAnchor: [20, 6]
})
@@ -4882,7 +4882,7 @@ sudo make install</code>
<!-- Help Modal -->
{% include 'partials/help-modal.html' %}
<script src="{{ url_for('static', filename='js/core/settings-manager.js') }}"></script>
<script src="{{ url_for('static', filename='js/core/settings-manager.js') }}?v={{ version }}&r=maptheme17"></script>
<!-- Agent Manager -->
<script src="{{ url_for('static', filename='js/core/agents.js') }}"></script>

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" class="{% if offline_settings.tile_provider in ['cartodb_dark', 'cartodb_dark_cyan'] %}map-cyber-enabled{% endif %}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -11,7 +11,7 @@
{% endif %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/global-nav.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}?v={{ version }}&r=maptheme17">
<link rel="stylesheet" href="{{ url_for('static', filename='css/help-modal.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/adsb_history.css') }}">
</head>
@@ -778,7 +778,7 @@
<!-- Help Modal -->
{% include 'partials/help-modal.html' %}
<script src="{{ url_for('static', filename='js/core/settings-manager.js') }}"></script>
<script src="{{ url_for('static', filename='js/core/settings-manager.js') }}?v={{ version }}&r=maptheme17"></script>
<script src="{{ url_for('static', filename='js/core/global-nav.js') }}"></script>
</body>
</html>

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" class="{% if offline_settings.tile_provider in ['cartodb_dark', 'cartodb_dark_cyan'] %}map-cyber-enabled{% endif %}">
<head>
<meta charset="UTF-8">
@@ -11,7 +11,7 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/global-nav.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/agents.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}?v={{ version }}&r=maptheme17">
<link rel="stylesheet" href="{{ url_for('static', filename='css/help-modal.css') }}">
<style>
.agents-container {
@@ -562,7 +562,7 @@
<!-- Help Modal -->
{% include 'partials/help-modal.html' %}
<script src="{{ url_for('static', filename='js/core/settings-manager.js') }}"></script>
<script src="{{ url_for('static', filename='js/core/settings-manager.js') }}?v={{ version }}&r=maptheme17"></script>
<script src="{{ url_for('static', filename='js/core/global-nav.js') }}"></script>
</body>
</html>

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" class="{% if offline_settings.tile_provider in ['cartodb_dark', 'cartodb_dark_cyan'] %}map-cyber-enabled{% endif %}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -23,7 +23,7 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/ais_dashboard.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/global-nav.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}?v={{ version }}&r=maptheme17">
<link rel="stylesheet" href="{{ url_for('static', filename='css/help-modal.css') }}">
<script>
window.INTERCEPT_SHARED_OBSERVER_LOCATION = {{ shared_observer_location | tojson }};
@@ -1562,7 +1562,7 @@
<!-- Help Modal -->
{% include 'partials/help-modal.html' %}
<script src="{{ url_for('static', filename='js/core/settings-manager.js') }}"></script>
<script src="{{ url_for('static', filename='js/core/settings-manager.js') }}?v={{ version }}&r=maptheme17"></script>
<!-- Agent Manager -->
<script src="{{ url_for('static', filename='js/core/agents.js') }}"></script>

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" class="{% if offline_settings.tile_provider in ['cartodb_dark', 'cartodb_dark_cyan'] %}map-cyber-enabled{% endif %}">
<head>
<meta charset="UTF-8">
@@ -57,7 +57,7 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/components/activity-timeline.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/components/device-cards.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/components/proximity-viz.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}?v={{ version }}&r=maptheme17">
<link rel="stylesheet" href="{{ url_for('static', filename='css/components/function-strip.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/components/toast.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/components/ux-platform.css') }}">
@@ -91,7 +91,7 @@
</script>
</head>
<body>
<body data-mode="pager">
<!-- Welcome Page -->
<div class="welcome-overlay" id="welcomePage">
<!-- Spinning Globe Background -->
@@ -2880,8 +2880,8 @@
</div>
<div class="wxsat-map-container">
<div class="wxsat-panel-header">
<span class="wxsat-panel-title">Mercator Projection</span>
<span class="wxsat-panel-subtitle" id="wxsatMercatorInfo">--</span>
<span class="wxsat-panel-title">Global Projection</span>
<span class="wxsat-panel-subtitle" id="wxsatMapInfo">--</span>
</div>
<div id="wxsatGroundMap" class="wxsat-ground-map"></div>
</div>
@@ -3970,6 +3970,7 @@
}
currentMode = mode;
document.body.setAttribute('data-mode', mode);
if (updateUrl) {
updateModeUrl(mode);
}
@@ -4219,6 +4220,9 @@
}, 100);
} else if (mode === 'sstv') {
SSTV.init();
setTimeout(() => {
if (typeof SSTV !== 'undefined' && SSTV.invalidateMap) SSTV.invalidateMap();
}, 120);
} else if (mode === 'weathersat') {
WeatherSat.init();
setTimeout(() => {
@@ -4248,6 +4252,7 @@
if (aprsMap) aprsMap.invalidateSize();
if (typeof Meshtastic !== 'undefined') Meshtastic.invalidateMap();
if (typeof BtLocate !== 'undefined') BtLocate.invalidateMap();
if (typeof SSTV !== 'undefined' && SSTV.invalidateMap) SSTV.invalidateMap();
});
window.addEventListener('popstate', function () {
@@ -4262,6 +4267,7 @@
setTimeout(() => {
if (aprsMap) aprsMap.invalidateSize();
if (typeof Meshtastic !== 'undefined') Meshtastic.invalidateMap();
if (typeof SSTV !== 'undefined' && SSTV.invalidateMap) SSTV.invalidateMap();
}, 200);
});
@@ -15149,7 +15155,7 @@
<!-- Updater -->
<script src="{{ url_for('static', filename='js/core/updater.js') }}"></script>
<!-- Settings Manager -->
<script src="{{ url_for('static', filename='js/core/settings-manager.js') }}"></script>
<script src="{{ url_for('static', filename='js/core/settings-manager.js') }}?v={{ version }}&r=maptheme17"></script>
<!-- Alerts + Recording -->
<script src="{{ url_for('static', filename='js/core/alerts.js') }}"></script>
<script src="{{ url_for('static', filename='js/core/recordings.js') }}"></script>

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" class="{% if offline_settings.tile_provider in ['cartodb_dark', 'cartodb_dark_cyan'] %}map-cyber-enabled{% endif %}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -12,7 +12,7 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/agents.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/global-nav.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}?v={{ version }}&r=maptheme17">
<link rel="stylesheet" href="{{ url_for('static', filename='css/help-modal.css') }}">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
@@ -1117,7 +1117,7 @@
<!-- Help Modal -->
{% include 'partials/help-modal.html' %}
<script src="{{ url_for('static', filename='js/core/settings-manager.js') }}"></script>
<script src="{{ url_for('static', filename='js/core/settings-manager.js') }}?v={{ version }}&r=maptheme17"></script>
<script src="{{ url_for('static', filename='js/core/global-nav.js') }}"></script>
</body>
</html>

View File

@@ -72,9 +72,9 @@
<span class="settings-label-desc">Map background imagery</span>
</div>
<select id="tileProvider" class="settings-select" onchange="Settings.setTileProvider(this.value)">
<option value="openstreetmap">OpenStreetMap</option>
<option value="cartodb_dark_cyan">Intercept Default</option>
<option value="cartodb_dark">CartoDB Dark</option>
<option value="cartodb_dark_cyan">CartoDB Dark (Cyan Tint)</option>
<option value="openstreetmap">OpenStreetMap</option>
<option value="cartodb_light">CartoDB Positron</option>
<option value="esri_world">ESRI World Imagery</option>
<option value="custom">Custom URL</option>

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" class="{% if offline_settings.tile_provider in ['cartodb_dark', 'cartodb_dark_cyan'] %}map-cyber-enabled{% endif %}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -22,7 +22,7 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/variables.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/global-nav.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}?v={{ version }}&r=maptheme17">
<link rel="stylesheet" href="{{ url_for('static', filename='css/help-modal.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/satellite_dashboard.css') }}">
<script>
@@ -1138,7 +1138,7 @@
<!-- Help Modal -->
{% include 'partials/help-modal.html' %}
<script src="{{ url_for('static', filename='js/core/settings-manager.js') }}"></script>
<script src="{{ url_for('static', filename='js/core/settings-manager.js') }}?v={{ version }}&r=maptheme17"></script>
<script src="{{ url_for('static', filename='js/core/global-nav.js') }}"></script>
</body>
</html>