mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 06:40:00 -07:00
Apply global map theme updates and UI improvements
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -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"
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -22,15 +22,16 @@ const Settings = {
|
||||
cartodb_dark: {
|
||||
url: 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png',
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OSM</a> © <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: '© <a href="https://www.openstreetmap.org/copyright">OSM</a> © <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
|
||||
// =============================================================================
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: '© OpenStreetMap contributors © 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: '© OpenStreetMap contributors © 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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user