feat(ftmap): the who-hears-me marks are filled and take their own colour
Filled, with a hairline white edge — the same trick the home marker uses. A ring is an outline drawn over whatever is beneath it, and eight pixels of one over the satellite imagery was barely visible, which is the whole reason the layer exists to be looked at. Its own colour, in its own key, beside the switch that turns the layer on. Separate from the decode colour deliberately: that one exists because the band palette disappears over some basemaps, and cyan over a pale sea has exactly the same problem for exactly the same reason. One control for both would have forced the two layers into a single colour, which is the distinction it took a shape to draw in the first place. The picker only appears while the layer is on, since it configures that layer and nothing else, and it is a portable UI pref like the other map colours, so it travels with data/.
This commit is contained in:
@@ -54,6 +54,16 @@ const bandColour = (b?: string) => BAND_COLOURS[(b ?? '').toLowerCase()] || '#9c
|
||||
// colour that shows up against the ground he chose.
|
||||
const COL_KEY = 'opslog.ftMapColour';
|
||||
|
||||
// The reverse layer's own colour. Empty means the default below.
|
||||
//
|
||||
// Separate from COL_KEY on purpose: the decode colour exists because the
|
||||
// band palette vanishes over some basemaps, and this one has exactly the
|
||||
// same problem for exactly the same reason — cyan over a pale sea reads no
|
||||
// better than 60m navy does. One control for both would have forced the
|
||||
// two layers into one colour, which is the distinction it took a shape to
|
||||
// make in the first place.
|
||||
const HEARD_COL_KEY = 'opslog.ftMapHeardColour';
|
||||
|
||||
// A colour input only accepts #rrggbb, so anything else stored here is
|
||||
// treated as no choice at all rather than driving the swatch to black.
|
||||
const asHex = (v: string) => (/^#[0-9a-f]{6}$/i.test(v.trim()) ? v.trim() : '');
|
||||
@@ -72,9 +82,13 @@ type Heard = { call: string; grid: string; band: string; mode: string; snr: numb
|
||||
//
|
||||
// Shape, then, rather than colour, carries the distinction: the decode dots
|
||||
// are small filled circles, and the arcs already use fourteen colours, so a
|
||||
// fifteenth would read as another band. A hollow diamond is unmistakably not
|
||||
// one of them at a glance.
|
||||
const HEARD_COLOUR = '#22d3ee';
|
||||
// fifteenth would read as another band. A diamond is unmistakably not one of
|
||||
// them at a glance.
|
||||
//
|
||||
// Filled, with a hairline white edge — the same trick the home marker uses.
|
||||
// It was a ring, and a ring is an outline drawn over whatever is beneath it:
|
||||
// eight pixels of it over the satellite imagery was barely there.
|
||||
const HEARD_COLOUR = '#22d3ee'; // the default, when nothing is chosen
|
||||
|
||||
const MAX_ARCS = 300;
|
||||
const MAX_AGE_MS = 30 * 60_000;
|
||||
@@ -89,6 +103,8 @@ export function FTMapPanel({ decodes, myGrid, onSelect, onCall }: {
|
||||
// Held in refs so the redraw below does not have to list them as dependencies
|
||||
// and rebuild every arc whenever the parent re-renders.
|
||||
const [colour, setColour] = useState(() => asHex(localStorage.getItem(COL_KEY) ?? ''));
|
||||
const [heardColour, setHeardColour] = useState(() => asHex(localStorage.getItem(HEARD_COL_KEY) ?? ''));
|
||||
const heardInk = heardColour || HEARD_COLOUR;
|
||||
// Whether the reverse feed is wanted lives in the DB, not here: it is what
|
||||
// starts an MQTT subscription, so the backend has to be the one that knows.
|
||||
const [hearMe, setHearMe] = useState(false);
|
||||
@@ -302,13 +318,14 @@ export function FTMapPanel({ decodes, myGrid, onSelect, onCall }: {
|
||||
iconSize: [16, 16],
|
||||
iconAnchor: [8, 8],
|
||||
html: `<div style="width:16px;height:16px;display:flex;align-items:center;justify-content:center">`
|
||||
+ `<div style="width:8px;height:8px;transform:rotate(45deg);border:1.6px solid ${HEARD_COLOUR};opacity:${fade.toFixed(2)}"></div></div>`,
|
||||
+ `<div style="width:9px;height:9px;transform:rotate(45deg);background:${heardInk};`
|
||||
+ `box-shadow:0 0 0 1px rgba(255,255,255,.75);opacity:${fade.toFixed(2)}"></div></div>`,
|
||||
});
|
||||
L.marker([to.lat, to.lon], { icon, interactive: true, keyboard: false })
|
||||
.bindTooltip(label, { direction: 'top' })
|
||||
.addTo(layer);
|
||||
}
|
||||
}, [heard, hearMe]);
|
||||
}, [heard, hearMe, heardInk]);
|
||||
|
||||
const bands = [...new Set(decodes.map((d) => (d.band ?? '').toLowerCase()).filter(Boolean))];
|
||||
return (
|
||||
@@ -357,11 +374,22 @@ export function FTMapPanel({ decodes, myGrid, onSelect, onCall }: {
|
||||
title={t('ftmap.hearMeTip')}
|
||||
className={cn('flex items-center gap-1 px-1.5 h-6 rounded text-[11px] disabled:opacity-50',
|
||||
hearMe ? 'font-semibold' : 'text-muted-foreground hover:bg-muted')}
|
||||
style={hearMe ? { color: HEARD_COLOUR } : undefined}>
|
||||
style={hearMe ? { color: heardInk } : undefined}>
|
||||
<Ear className="size-3" />
|
||||
{t('ftmap.hearMe')}
|
||||
{hearMe && <span className="tabular-nums opacity-80">{heard.length}</span>}
|
||||
</button>
|
||||
{hearMe && (
|
||||
<input type="color" title={t('ftmap.heardColour')}
|
||||
className="size-5 self-center rounded border border-border bg-transparent p-0 cursor-pointer"
|
||||
value={heardInk}
|
||||
onChange={(e) => { const v = asHex(e.target.value); setHeardColour(v); writeUiPref(HEARD_COL_KEY, v); }} />
|
||||
)}
|
||||
{hearMe && !!heardColour && (
|
||||
<button type="button" title={t('ftmap.heardColourReset')}
|
||||
onClick={() => { setHeardColour(''); writeUiPref(HEARD_COL_KEY, ''); }}
|
||||
className="px-1 text-[11px] text-muted-foreground hover:text-foreground">↺</button>
|
||||
)}
|
||||
</div>
|
||||
{/* What the diamonds are. In the legend rather than a tooltip because
|
||||
they are the only thing on the map that is not a decode of ours, and
|
||||
@@ -371,7 +399,7 @@ export function FTMapPanel({ decodes, myGrid, onSelect, onCall }: {
|
||||
stack into one block and read as one key. */}
|
||||
{hearMe && heard.length > 0 && (
|
||||
<div className="absolute bottom-2 right-2 z-[1000] flex items-center gap-1.5 rounded-md bg-background/85 backdrop-blur px-2 py-1.5 border border-border text-[11px]">
|
||||
<span className="inline-block size-2 rotate-45 border-[1.6px]" style={{ borderColor: HEARD_COLOUR }} />
|
||||
<span className="inline-block size-2 rotate-45" style={{ background: heardInk }} />
|
||||
{t('ftmap.hearMeLegend', { n: heard.length })}
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user