fix(cluster): let the two display options compose, and stop claiming a station was worked
Three defects in the options added in the previous commit, all found on air. Order. Mute returned early, so with both options on the mute swallowed exactly the spots the slot option existed to surface: the mute test is entity-level, and an unworked callsign inside a worked entity passes it. Slot promotion now runs first, and protects itself for free since bringsNothingNew is false on new-slot. The two were documented as composing - "mute what is done, light up what is not" - and they did not. Empty status. Muting emptied the status, but an empty status already meant "entity not resolved" in the band map, so every muted spot claimed its entity was unknown with the country printed two words earlier in the same tooltip. A muted flag now records WHY it went empty; blanking still drives the colour, the badges and the ranking. Wording. The muted tooltip said "already worked" of a station never worked - LZ8NG on a 421st Bulgarian. What is worked is the ENTITY on this band and mode, so that is what it says now. new-mode had no case in the band map's statusLabel and fell through to "entity not resolved" too. Pre-existing, same one-line switch.
This commit is contained in:
@@ -38,6 +38,9 @@ type SpotStatusEntry = {
|
|||||||
new_county?: boolean;
|
new_county?: boolean;
|
||||||
new_pota?: boolean;
|
new_pota?: boolean;
|
||||||
new_pfx?: boolean;
|
new_pfx?: boolean;
|
||||||
|
// muted: the status was emptied on purpose by the "no colour on worked
|
||||||
|
// stations" option, so an empty status here is a choice, not an unknown.
|
||||||
|
muted?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// The extra markers are ORTHOGONAL to the entity status: a spot can be a worked
|
// The extra markers are ORTHOGONAL to the entity status: a spot can be a worked
|
||||||
@@ -149,13 +152,18 @@ function LegendDot({ cls, colour, label }: { cls?: string; colour?: string; labe
|
|||||||
|
|
||||||
// Human-readable label for a spot status — used in the pill hover tooltip
|
// Human-readable label for a spot status — used in the pill hover tooltip
|
||||||
// so the operator can see WHY a spot is coloured the way it is.
|
// so the operator can see WHY a spot is coloured the way it is.
|
||||||
function statusLabel(s: string, t: (k: string) => string): string {
|
function statusLabel(s: string, t: (k: string) => string, muted = false): string {
|
||||||
switch (s) {
|
switch (s) {
|
||||||
case 'new': return t('bmp.statusNew');
|
case 'new': return t('bmp.statusNew');
|
||||||
case 'new-band': return t('bmp.statusNewBand');
|
case 'new-band': return t('bmp.statusNewBand');
|
||||||
|
case 'new-mode': return t('bmp.statusNewMode');
|
||||||
case 'new-slot': return t('bmp.statusNewSlot');
|
case 'new-slot': return t('bmp.statusNewSlot');
|
||||||
case 'worked': return t('bmp.statusWorked');
|
case 'worked': return t('bmp.statusWorked');
|
||||||
default: return t('bmp.statusUnresolved');
|
// An empty status means the entity could not be resolved — EXCEPT when the
|
||||||
|
// "no colour on worked stations" option emptied it on purpose. Saying
|
||||||
|
// "entity not resolved" there was a flat contradiction of the country
|
||||||
|
// printed two words earlier in the same tooltip.
|
||||||
|
default: return muted ? t('bmp.statusMuted') : t('bmp.statusUnresolved');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -617,7 +625,7 @@ export function BandMap({ band, spots, spotStatus: spotStatusRaw, currentFreqHz,
|
|||||||
'hover:translate-x-0.5 hover:shadow',
|
'hover:translate-x-0.5 hover:shadow',
|
||||||
style.pill,
|
style.pill,
|
||||||
)}
|
)}
|
||||||
title={`${p.spot.dx_call}${entry?.country ? ' · ' + entry.country : ''} · ${p.spot.freq_khz.toFixed(1)} kHz · ${statusLabel(st, t)}${markersFor(entry).map((m) => ' · ' + t(BMP_MARKER_LABEL[m.key])).join('')}${p.spot.comment ? ' · ' + p.spot.comment : ''}${p.spot.spotter ? ' · de ' + p.spot.spotter : ''}`}
|
title={`${p.spot.dx_call}${entry?.country ? ' · ' + entry.country : ''} · ${p.spot.freq_khz.toFixed(1)} kHz · ${statusLabel(st, t, entry?.muted)}${markersFor(entry).map((m) => ' · ' + t(BMP_MARKER_LABEL[m.key])).join('')}${p.spot.comment ? ' · ' + p.spot.comment : ''}${p.spot.spotter ? ' · de ' + p.spot.spotter : ''}`}
|
||||||
>
|
>
|
||||||
{/* Left accent strip. With no extra marker it repeats the status
|
{/* Left accent strip. With no extra marker it repeats the status
|
||||||
colour, exactly as before; otherwise it splits into one
|
colour, exactly as before; otherwise it splits into one
|
||||||
|
|||||||
@@ -343,7 +343,7 @@ const en: Dict = {
|
|||||||
'chp.workedBefore': 'Worked before', 'chp.typeCallsign': 'Type a callsign to see prior contacts.', 'chp.checking': 'checking…', 'chp.new': 'NEW',
|
'chp.workedBefore': 'Worked before', 'chp.typeCallsign': 'Type a callsign to see prior contacts.', 'chp.checking': 'checking…', 'chp.new': 'NEW',
|
||||||
'chp.noPriorQso': 'No prior QSO with this callsign.', 'chp.first': 'First', 'chp.last': 'Last', 'chp.dateUtc': 'Date UTC', 'chp.band': 'Band', 'chp.mode': 'Mode',
|
'chp.noPriorQso': 'No prior QSO with this callsign.', 'chp.first': 'First', 'chp.last': 'Last', 'chp.dateUtc': 'Date UTC', 'chp.band': 'Band', 'chp.mode': 'Mode',
|
||||||
'chp.lotwRcvd': 'LoTW rcvd', 'chp.bureauRcvd': 'Bureau rcvd', 'chp.olderQsos': '+ {n} older QSOs',
|
'chp.lotwRcvd': 'LoTW rcvd', 'chp.bureauRcvd': 'Bureau rcvd', 'chp.olderQsos': '+ {n} older QSOs',
|
||||||
'bmp.statusNew': 'NEW DXCC (entity never worked)', 'bmp.statusNewBand': 'NEW BAND (entity not worked on this band)', 'bmp.statusNewSlot': 'NEW SLOT (mode not worked on this band)',
|
'bmp.statusNew': 'NEW DXCC (entity never worked)', 'bmp.statusNewBand': 'NEW BAND (entity not worked on this band)', 'bmp.statusNewSlot': 'NEW SLOT (mode not worked on this band)', 'bmp.statusNewMode': 'NEW MODE (mode never worked for this entity)', 'bmp.statusMuted': 'Nothing new here — entity already worked (muted by your display option)',
|
||||||
'bmp.statusWorked': 'Worked (this band + mode already in log)', 'bmp.statusUnresolved': 'Entity not resolved', 'bmp.bandMap': 'Band map', 'bmp.notConfigured': 'Not configured for {band}.',
|
'bmp.statusWorked': 'Worked (this band + mode already in log)', 'bmp.statusUnresolved': 'Entity not resolved', 'bmp.bandMap': 'Band map', 'bmp.notConfigured': 'Not configured for {band}.',
|
||||||
'bmp.map': 'Map', 'bmp.zoomOut': 'Zoom out', 'bmp.zoomIn': 'Zoom in', 'bmp.scrollToRig': 'Scroll to current rig frequency', 'bmp.moveLeft': 'Move band map to the left', 'bmp.moveRight': 'Move band map to the right', 'bmp.hide': 'Hide band map',
|
'bmp.map': 'Map', 'bmp.zoomOut': 'Zoom out', 'bmp.zoomIn': 'Zoom in', 'bmp.scrollToRig': 'Scroll to current rig frequency', 'bmp.moveLeft': 'Move band map to the left', 'bmp.moveRight': 'Move band map to the right', 'bmp.hide': 'Hide band map',
|
||||||
'bmp.bandsLabel': 'Bands:', 'bmp.fit': 'FIT', 'bmp.hideFt': 'Hide FTx', 'bmp.hideFtTitle': 'Hide all digital (FT8/FT4/JS8/…) spots on every band map', 'bmp.fitBand': 'Fit to band', 'bmp.widthTip': 'Drag to resize — double-click to reset', 'bmp.fitTitle': 'Size each band map to show the whole band edge-to-edge',
|
'bmp.bandsLabel': 'Bands:', 'bmp.fit': 'FIT', 'bmp.hideFt': 'Hide FTx', 'bmp.hideFtTitle': 'Hide all digital (FT8/FT4/JS8/…) spots on every band map', 'bmp.fitBand': 'Fit to band', 'bmp.widthTip': 'Drag to resize — double-click to reset', 'bmp.fitTitle': 'Size each band map to show the whole band edge-to-edge',
|
||||||
@@ -755,7 +755,7 @@ const fr: Dict = {
|
|||||||
'chp.workedBefore': 'Déjà contacté', 'chp.typeCallsign': 'Saisis un indicatif pour voir les contacts précédents.', 'chp.checking': 'vérification…', 'chp.new': 'NOUVEAU',
|
'chp.workedBefore': 'Déjà contacté', 'chp.typeCallsign': 'Saisis un indicatif pour voir les contacts précédents.', 'chp.checking': 'vérification…', 'chp.new': 'NOUVEAU',
|
||||||
'chp.noPriorQso': 'Aucun QSO antérieur avec cet indicatif.', 'chp.first': 'Premier', 'chp.last': 'Dernier', 'chp.dateUtc': 'Date UTC', 'chp.band': 'Bande', 'chp.mode': 'Mode',
|
'chp.noPriorQso': 'Aucun QSO antérieur avec cet indicatif.', 'chp.first': 'Premier', 'chp.last': 'Dernier', 'chp.dateUtc': 'Date UTC', 'chp.band': 'Bande', 'chp.mode': 'Mode',
|
||||||
'chp.lotwRcvd': 'LoTW reçue', 'chp.bureauRcvd': 'Bureau reçue', 'chp.olderQsos': '+ {n} QSO plus anciens',
|
'chp.lotwRcvd': 'LoTW reçue', 'chp.bureauRcvd': 'Bureau reçue', 'chp.olderQsos': '+ {n} QSO plus anciens',
|
||||||
'bmp.statusNew': 'NOUVEAU DXCC (entité jamais contactée)', 'bmp.statusNewBand': 'NOUVELLE BANDE (entité non contactée sur cette bande)', 'bmp.statusNewSlot': 'NOUVEAU MODE (mode non contacté sur cette bande)',
|
'bmp.statusNew': 'NOUVEAU DXCC (entité jamais contactée)', 'bmp.statusNewBand': 'NOUVELLE BANDE (entité non contactée sur cette bande)', 'bmp.statusNewSlot': 'NOUVEAU MODE (mode non contacté sur cette bande)', 'bmp.statusNewMode': 'NOUVEAU MODE (mode jamais contacté pour cette entité)', 'bmp.statusMuted': 'Rien de nouveau ici — entité déjà contactée (masqué par ton option d affichage)',
|
||||||
'bmp.statusWorked': 'Contacté (cette bande + mode déjà au log)', 'bmp.statusUnresolved': 'Entité non résolue', 'bmp.bandMap': 'Carte de bande', 'bmp.notConfigured': 'Non configurée pour {band}.',
|
'bmp.statusWorked': 'Contacté (cette bande + mode déjà au log)', 'bmp.statusUnresolved': 'Entité non résolue', 'bmp.bandMap': 'Carte de bande', 'bmp.notConfigured': 'Non configurée pour {band}.',
|
||||||
'bmp.map': 'Carte', 'bmp.zoomOut': 'Dézoomer', 'bmp.zoomIn': 'Zoomer', 'bmp.scrollToRig': 'Aller à la fréquence actuelle du poste', 'bmp.moveLeft': 'Déplacer la carte de bande à gauche', 'bmp.moveRight': 'Déplacer la carte de bande à droite', 'bmp.hide': 'Masquer la carte de bande',
|
'bmp.map': 'Carte', 'bmp.zoomOut': 'Dézoomer', 'bmp.zoomIn': 'Zoomer', 'bmp.scrollToRig': 'Aller à la fréquence actuelle du poste', 'bmp.moveLeft': 'Déplacer la carte de bande à gauche', 'bmp.moveRight': 'Déplacer la carte de bande à droite', 'bmp.hide': 'Masquer la carte de bande',
|
||||||
'bmp.bandsLabel': 'Bandes :', 'bmp.fit': 'FIT', 'bmp.hideFt': 'Masquer FTx', 'bmp.hideFtTitle': 'Masquer tous les spots numériques (FT8/FT4/JS8/…) sur toutes les cartes', 'bmp.fitBand': 'Ajuster à la bande', 'bmp.widthTip': 'Glisser pour redimensionner — double-clic pour réinitialiser', 'bmp.fitTitle': 'Dimensionner chaque carte pour afficher toute la bande',
|
'bmp.bandsLabel': 'Bandes :', 'bmp.fit': 'FIT', 'bmp.hideFt': 'Masquer FTx', 'bmp.hideFtTitle': 'Masquer tous les spots numériques (FT8/FT4/JS8/…) sur toutes les cartes', 'bmp.fitBand': 'Ajuster à la bande', 'bmp.widthTip': 'Glisser pour redimensionner — double-clic pour réinitialiser', 'bmp.fitTitle': 'Dimensionner chaque carte pour afficher toute la bande',
|
||||||
|
|||||||
@@ -31,12 +31,18 @@ type Entry = {
|
|||||||
new_county?: boolean;
|
new_county?: boolean;
|
||||||
new_pota?: boolean;
|
new_pota?: boolean;
|
||||||
new_pfx?: boolean;
|
new_pfx?: boolean;
|
||||||
|
muted?: boolean;
|
||||||
} | undefined;
|
} | undefined;
|
||||||
|
|
||||||
// bringsNothingNew: the entity is resolved and worked, and no other dimension
|
// bringsNothingNew: the ENTITY is resolved and worked, and no other dimension
|
||||||
// (county, park, prefix) is new. Same test the cluster list already used to dim
|
// (county, park, prefix) is new. Same test the cluster list already used to dim
|
||||||
// a row — muting reuses it rather than inventing a second notion of "done".
|
// a row — muting reuses it rather than inventing a second notion of "done".
|
||||||
//
|
//
|
||||||
|
// Entity-level, NOT callsign-level: an operator can be on their 421st Bulgarian
|
||||||
|
// and still have never worked that particular station. That spot is muted here
|
||||||
|
// by design — it brings nothing to an award — and turning on the slot option is
|
||||||
|
// what brings it back, which is why the promotion above runs first.
|
||||||
|
//
|
||||||
// Note what is NOT muted: a status of new-band / new-slot survives, because
|
// Note what is NOT muted: a status of new-band / new-slot survives, because
|
||||||
// having worked that callsign once on another band says nothing about the band
|
// having worked that callsign once on another band says nothing about the band
|
||||||
// in front of you.
|
// in front of you.
|
||||||
@@ -51,15 +57,29 @@ function bringsNothingNew(s: Entry): boolean {
|
|||||||
// re-deriving it.
|
// re-deriving it.
|
||||||
export function applySpotDisplay<T extends Entry>(s: T, o: SpotDisplayOptions): T {
|
export function applySpotDisplay<T extends Entry>(s: T, o: SpotDisplayOptions): T {
|
||||||
if (!s) return s;
|
if (!s) return s;
|
||||||
if (o.muteWorked && bringsNothingNew(s)) {
|
let e = s;
|
||||||
|
|
||||||
|
// ORDER MATTERS, and it is the whole difference between the two options
|
||||||
|
// composing and one cancelling the other.
|
||||||
|
//
|
||||||
|
// Slot promotion runs FIRST. A callsign not yet worked on this band and mode
|
||||||
|
// is not "done", so it must never be swallowed by the mute below — yet the
|
||||||
|
// mute test only looks at the entity, and an unworked callsign inside a worked
|
||||||
|
// entity is precisely the spot the second option exists to surface. Promoting
|
||||||
|
// first protects it for free: bringsNothingNew() returns false on new-slot.
|
||||||
|
if (o.slotHighlight && e.worked_slot === false && (!e.status || e.status === 'worked')) {
|
||||||
|
e = { ...e, status: 'new-slot' } as NonNullable<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (o.muteWorked && bringsNothingNew(e)) {
|
||||||
// Strip everything that paints: the row keeps its data, loses its emphasis.
|
// Strip everything that paints: the row keeps its data, loses its emphasis.
|
||||||
return { ...s, status: '', worked_call: false } as T;
|
//
|
||||||
|
// muted says WHY the status went empty. An empty status already meant
|
||||||
|
// "entity not resolved" in the band map, so without this flag every muted
|
||||||
|
// spot claimed its entity was unknown — with the country printed right next
|
||||||
|
// to it. Blanking is still what drives the colour, the badges and the
|
||||||
|
// ranking; muted only lets the tooltip stay honest.
|
||||||
|
return { ...e, status: '', worked_call: false, muted: true } as T;
|
||||||
}
|
}
|
||||||
if (o.slotHighlight && s.worked_slot === false) {
|
return e;
|
||||||
// Not worked on this band+mode. If the entity check found nothing new, say
|
|
||||||
// so with the slot status rather than leaving the spot colourless — that is
|
|
||||||
// exactly the row this option exists to surface.
|
|
||||||
if (!s.status || s.status === 'worked') return { ...s, status: 'new-slot' } as T;
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user