[fix](trx-frontend): refine map locator styling
Co-authored-by: OpenAI Codex <codex@openai.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -625,6 +625,7 @@ if (themeToggleBtn) {
|
||||
themeToggleBtn.addEventListener("click", () => {
|
||||
setTheme(currentTheme() === "dark" ? "light" : "dark");
|
||||
updateMapBaseLayerForTheme(currentTheme());
|
||||
syncLocatorMarkerStyles();
|
||||
scheduleOverviewDraw();
|
||||
if (typeof scheduleSpectrumDraw === "function" && lastSpectrumData) scheduleSpectrumDraw();
|
||||
});
|
||||
@@ -634,6 +635,7 @@ if (headerStylePickSelect) {
|
||||
headerStylePickSelect.addEventListener("change", () => {
|
||||
setStyle(headerStylePickSelect.value);
|
||||
updateMapBaseLayerForTheme(currentTheme());
|
||||
syncLocatorMarkerStyles();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3326,6 +3328,160 @@ function assignLocatorMarkerMeta(marker, sourceType, bandMeta) {
|
||||
};
|
||||
}
|
||||
|
||||
function parseMapColor(input) {
|
||||
const value = String(input || "").trim();
|
||||
if (!value) return null;
|
||||
const hex = value.match(/^#([0-9a-f]{3,8})$/i);
|
||||
if (hex) {
|
||||
const raw = hex[1];
|
||||
if (raw.length === 3 || raw.length === 4) {
|
||||
const chars = raw.split("");
|
||||
return {
|
||||
r: parseInt(chars[0] + chars[0], 16),
|
||||
g: parseInt(chars[1] + chars[1], 16),
|
||||
b: parseInt(chars[2] + chars[2], 16),
|
||||
};
|
||||
}
|
||||
if (raw.length === 6 || raw.length === 8) {
|
||||
return {
|
||||
r: parseInt(raw.slice(0, 2), 16),
|
||||
g: parseInt(raw.slice(2, 4), 16),
|
||||
b: parseInt(raw.slice(4, 6), 16),
|
||||
};
|
||||
}
|
||||
}
|
||||
const rgb = value.match(/^rgba?\(\s*([0-9.]+)\s*,\s*([0-9.]+)\s*,\s*([0-9.]+)/i);
|
||||
if (rgb) {
|
||||
return {
|
||||
r: Math.max(0, Math.min(255, Number(rgb[1]))),
|
||||
g: Math.max(0, Math.min(255, Number(rgb[2]))),
|
||||
b: Math.max(0, Math.min(255, Number(rgb[3]))),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function rgbToHsl(rgb) {
|
||||
if (!rgb) return null;
|
||||
const r = rgb.r / 255;
|
||||
const g = rgb.g / 255;
|
||||
const b = rgb.b / 255;
|
||||
const max = Math.max(r, g, b);
|
||||
const min = Math.min(r, g, b);
|
||||
const l = (max + min) / 2;
|
||||
if (max === min) {
|
||||
return { h: 0, s: 0, l: l * 100 };
|
||||
}
|
||||
const d = max - min;
|
||||
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
let h;
|
||||
switch (max) {
|
||||
case r:
|
||||
h = ((g - b) / d) + (g < b ? 6 : 0);
|
||||
break;
|
||||
case g:
|
||||
h = ((b - r) / d) + 2;
|
||||
break;
|
||||
default:
|
||||
h = ((r - g) / d) + 4;
|
||||
break;
|
||||
}
|
||||
return { h: (h * 60) % 360, s: s * 100, l: l * 100 };
|
||||
}
|
||||
|
||||
function wrapHue(hue) {
|
||||
const value = Number(hue) || 0;
|
||||
return ((value % 360) + 360) % 360;
|
||||
}
|
||||
|
||||
function paletteHue(input, fallback) {
|
||||
const hsl = rgbToHsl(parseMapColor(input));
|
||||
return Number.isFinite(hsl?.h) ? hsl.h : fallback;
|
||||
}
|
||||
|
||||
function locatorThemeHues() {
|
||||
const pal = canvasPalette();
|
||||
const baseHue = paletteHue(pal?.spectrumLine, 145);
|
||||
const waveHue = paletteHue(pal?.waveformLine, baseHue + 34);
|
||||
const peakHue = paletteHue(pal?.waveformPeak, baseHue - 42);
|
||||
return {
|
||||
bookmark: wrapHue(baseHue),
|
||||
ft8: wrapHue(peakHue),
|
||||
wspr: wrapHue((waveHue + baseHue) / 2),
|
||||
bandBase: wrapHue((baseHue * 0.65) + (peakHue * 0.35)),
|
||||
};
|
||||
}
|
||||
|
||||
function locatorBandIndex(label) {
|
||||
const idx = HAM_BANDS.findIndex((band) => band.label === label);
|
||||
return idx >= 0 ? idx : 0;
|
||||
}
|
||||
|
||||
function locatorBandLabelForEntry(entry) {
|
||||
const meta = entry?.bandMeta instanceof Map ? entry.bandMeta : new Map();
|
||||
if (meta.size === 0) return null;
|
||||
if (mapLocatorFilter.phase === "band" && mapLocatorFilter.bands.size > 0) {
|
||||
for (const label of mapLocatorFilter.bands) {
|
||||
if (meta.has(label)) return label;
|
||||
}
|
||||
}
|
||||
let bestLabel = null;
|
||||
let bestHz = -Infinity;
|
||||
for (const [label, hz] of meta.entries()) {
|
||||
const value = Number.isFinite(hz) ? Number(hz) : 0;
|
||||
if (value > bestHz) {
|
||||
bestHz = value;
|
||||
bestLabel = label;
|
||||
}
|
||||
}
|
||||
return bestLabel;
|
||||
}
|
||||
|
||||
function locatorHueForEntry(entry) {
|
||||
const hues = locatorThemeHues();
|
||||
if (mapLocatorFilter.phase === "band") {
|
||||
const label = locatorBandLabelForEntry(entry);
|
||||
if (label) {
|
||||
return wrapHue(hues.bandBase + locatorBandIndex(label) * 137.508);
|
||||
}
|
||||
}
|
||||
if (entry?.sourceType === "bookmark") return hues.bookmark;
|
||||
if (entry?.sourceType === "wspr") return hues.wspr;
|
||||
return hues.ft8;
|
||||
}
|
||||
|
||||
function locatorStyleForEntry(entry, count) {
|
||||
const safeCount = Math.max(1, Number.isFinite(count) ? count : 1);
|
||||
const intensity = Math.min(1, Math.log2(safeCount + 1) / 5);
|
||||
const hue = locatorHueForEntry(entry);
|
||||
const lightTheme = currentTheme() === "light";
|
||||
const strokeSat = lightTheme ? 62 : 74;
|
||||
const fillSat = lightTheme ? 68 : 78;
|
||||
const strokeLight = lightTheme ? 40 : 56;
|
||||
const fillLight = lightTheme ? 60 : 42;
|
||||
return {
|
||||
color: `hsl(${hue.toFixed(1)} ${Math.min(92, strokeSat + intensity * 10).toFixed(1)}% ${Math.max(24, strokeLight - intensity * 4).toFixed(1)}%)`,
|
||||
opacity: 0.42 + intensity * 0.5,
|
||||
weight: 1 + intensity * 1.2,
|
||||
fillColor: `hsl(${hue.toFixed(1)} ${Math.min(96, fillSat + intensity * 8).toFixed(1)}% ${Math.max(20, fillLight - intensity * 5).toFixed(1)}%)`,
|
||||
fillOpacity: 0.16 + intensity * 0.34,
|
||||
};
|
||||
}
|
||||
|
||||
function locatorEntryCount(entry) {
|
||||
if (Array.isArray(entry?.bookmarks)) return Math.max(entry.bookmarks.length, 1);
|
||||
if (entry?.stationDetails instanceof Map) return Math.max(entry.stationDetails.size, 1);
|
||||
if (entry?.stations instanceof Set) return Math.max(entry.stations.size, 1);
|
||||
return 1;
|
||||
}
|
||||
|
||||
function syncLocatorMarkerStyles() {
|
||||
for (const entry of locatorMarkers.values()) {
|
||||
if (!entry?.marker) continue;
|
||||
entry.marker.setStyle(locatorStyleForEntry(entry, locatorEntryCount(entry)));
|
||||
}
|
||||
}
|
||||
|
||||
function clearMapRadioPath() {
|
||||
if (aprsRadioPath) {
|
||||
aprsRadioPath.remove();
|
||||
@@ -3513,6 +3669,7 @@ function rebuildMapLocatorFilters() {
|
||||
choiceLabelEl.textContent = "Visible Sources";
|
||||
renderMapLocatorChipRow(choiceEl, sourceItems, null, "source");
|
||||
}
|
||||
syncLocatorMarkerStyles();
|
||||
}
|
||||
|
||||
function markerPassesLocatorFilters(marker) {
|
||||
@@ -3803,7 +3960,7 @@ function initAprsMap() {
|
||||
if (!bounds) continue;
|
||||
entry.sourceType = "bookmark";
|
||||
entry.bandMeta = collectBandMeta((entry.bookmarks || []).map((bm) => Number(bm?.freq_hz)));
|
||||
entry.marker = L.rectangle(bounds, locatorStyleForCount(entry.bookmarks?.length || 1, "bookmark"))
|
||||
entry.marker = L.rectangle(bounds, locatorStyleForEntry(entry, entry.bookmarks?.length || 1))
|
||||
.addTo(aprsMap)
|
||||
.bindPopup(buildBookmarkLocatorPopupHtml(entry.grid, entry.bookmarks || []));
|
||||
entry.marker.__trxType = "bookmark";
|
||||
@@ -3896,10 +4053,10 @@ function sizeAprsMapToViewport() {
|
||||
if (fr.top > mapRect.top + 50) bottom = fr.top;
|
||||
}
|
||||
const available = Math.max(0, Math.floor(bottom - mapRect.top - 8));
|
||||
const widthDriven = width > 0 ? Math.floor(width / 2.05) : available;
|
||||
const widthDriven = width > 0 ? Math.floor(width / 1.9) : available;
|
||||
const viewportCap = mapIsFullscreen()
|
||||
? Math.floor(window.innerHeight * 0.9)
|
||||
: Math.floor(window.innerHeight * 0.56);
|
||||
: Math.floor(window.innerHeight * 0.6);
|
||||
const minHeight = Math.min(260, available);
|
||||
const target = Math.max(minHeight, Math.min(available, viewportCap, widthDriven));
|
||||
mapEl.style.height = `${target}px`;
|
||||
@@ -4443,20 +4600,6 @@ function escapeMapHtml(input) {
|
||||
.replaceAll("\"", """);
|
||||
}
|
||||
|
||||
function locatorStyleForCount(count, type) {
|
||||
const safeCount = Math.max(1, Number.isFinite(count) ? count : 1);
|
||||
const intensity = Math.min(1, Math.log2(safeCount + 1) / 5);
|
||||
const isWspr = type === "wspr";
|
||||
const isBookmark = type === "bookmark";
|
||||
return {
|
||||
color: isBookmark ? "#38b48b" : (isWspr ? "#ff8f2a" : "#ffb020"),
|
||||
opacity: 0.45 + intensity * 0.5,
|
||||
weight: 1 + intensity * 1.2,
|
||||
fillColor: isBookmark ? "#22c55e" : (isWspr ? "#ff6a3d" : "#ff9b1a"),
|
||||
fillOpacity: 0.18 + intensity * 0.55,
|
||||
};
|
||||
}
|
||||
|
||||
function formatDecodeLocatorTime(tsMs) {
|
||||
if (!Number.isFinite(tsMs)) return "--:--:--";
|
||||
return new Date(tsMs).toLocaleTimeString([], {
|
||||
@@ -4574,7 +4717,7 @@ window.syncBookmarkMapLocators = function(bookmarks) {
|
||||
existing.bandMeta = bandMeta;
|
||||
if (existing.marker) {
|
||||
existing.marker.setBounds(next.bounds);
|
||||
existing.marker.setStyle(locatorStyleForCount(next.bookmarks.length, "bookmark"));
|
||||
existing.marker.setStyle(locatorStyleForEntry(existing, next.bookmarks.length));
|
||||
existing.marker.setPopupContent(popupHtml);
|
||||
sendLocatorOverlayToBack(existing.marker);
|
||||
assignLocatorMarkerMeta(existing.marker, existing.sourceType, existing.bandMeta);
|
||||
@@ -4592,7 +4735,7 @@ window.syncBookmarkMapLocators = function(bookmarks) {
|
||||
};
|
||||
locatorMarkers.set(key, entry);
|
||||
if (aprsMap) {
|
||||
entry.marker = L.rectangle(next.bounds, locatorStyleForCount(next.bookmarks.length, "bookmark"))
|
||||
entry.marker = L.rectangle(next.bounds, locatorStyleForEntry(entry, next.bookmarks.length))
|
||||
.addTo(aprsMap)
|
||||
.bindPopup(popupHtml);
|
||||
entry.marker.__trxType = "bookmark";
|
||||
@@ -4637,7 +4780,7 @@ window.ft8MapAddLocator = function(message, grids, type = "ft8", station = null,
|
||||
);
|
||||
const count = Math.max(existing.stationDetails.size, existing.stations.size || 0, 1);
|
||||
const tooltipHtml = buildDecodeLocatorTooltipHtml(grid, existing, markerType);
|
||||
existing.marker.setStyle(locatorStyleForCount(count, markerType));
|
||||
existing.marker.setStyle(locatorStyleForEntry(existing, count));
|
||||
existing.marker.setPopupContent(tooltipHtml);
|
||||
sendLocatorOverlayToBack(existing.marker);
|
||||
assignLocatorMarkerMeta(existing.marker, existing.sourceType, existing.bandMeta);
|
||||
@@ -4650,16 +4793,16 @@ window.ft8MapAddLocator = function(message, grids, type = "ft8", station = null,
|
||||
if (stationId) stations.add(stationId);
|
||||
const stationDetails = new Map();
|
||||
stationDetails.set(detailKey, { ...detailEntry });
|
||||
const bandMeta = collectBandMeta(
|
||||
Array.from(stationDetails.values()).map((detail) => Number(detail?.freq_hz))
|
||||
);
|
||||
const count = Math.max(stationDetails.size, stations.size || 0, 1);
|
||||
const tooltipHtml = buildDecodeLocatorTooltipHtml(grid, { stations, stationDetails }, markerType);
|
||||
const marker = L.rectangle(bounds, locatorStyleForCount(count, markerType))
|
||||
const marker = L.rectangle(bounds, locatorStyleForEntry({ sourceType: markerType, bandMeta }, count))
|
||||
.addTo(aprsMap)
|
||||
.bindPopup(tooltipHtml);
|
||||
marker.__trxType = markerType;
|
||||
sendLocatorOverlayToBack(marker);
|
||||
const bandMeta = collectBandMeta(
|
||||
Array.from(stationDetails.values()).map((detail) => Number(detail?.freq_hz))
|
||||
);
|
||||
assignLocatorMarkerMeta(marker, markerType, bandMeta);
|
||||
locatorMarkers.set(key, { marker, stations, stationDetails, sourceType: markerType, bandMeta });
|
||||
mapMarkers.add(marker);
|
||||
|
||||
Reference in New Issue
Block a user