fix RF coverage terrain artifacts

This commit is contained in:
gadgethd
2026-08-02 19:31:38 +01:00
parent e57f463146
commit 1e7cd3bbff
6 changed files with 121 additions and 8 deletions
+30 -8
View File
@@ -36,6 +36,7 @@ import {
writeFiltersToUrl,
writeMapViewToUrl,
} from './utils/mapUrlState.js';
import { resolveTerrainCoverageConflict } from './utils/mapLayerCompatibility.js';
type PacketHistorySegment = {
positions: [[number, number], [number, number]];
@@ -106,7 +107,10 @@ export const App: React.FC = () => {
const withRfVisibility = savedRfVisibility === null
? modeFilters
: { ...modeFilters, coverage: savedRfVisibility === '1' };
return filtersFromUrl(withRfVisibility);
return resolveTerrainCoverageConflict(
{ terrain: false, coverage: false },
filtersFromUrl(withRfVisibility),
);
});
const [activeMode, setActiveMode] = useState<MapMode | null>(() => {
const requested = new URLSearchParams(window.location.search).get('mode');
@@ -193,7 +197,9 @@ export const App: React.FC = () => {
if (!RF_COVERAGE_ENABLED || !rfCoverage.availableTiers.includes('standard')) return;
if (localStorage.getItem(RF_VISIBILITY_KEY) !== null) return;
if (initialLayersSpecifiedRef.current) return;
setFilters((current) => current.coverage ? current : { ...current, coverage: true });
setFilters((current) => current.coverage || current.terrain
? current
: { ...current, coverage: true });
}, [rfCoverage.availableTiers]);
useEffect(() => {
@@ -240,11 +246,12 @@ export const App: React.FC = () => {
const handleFiltersChange = useCallback((next: Filters) => {
setActiveMode(null);
useOverlayStore.getState().setPlanRepeaterMode(false);
if (next.coverage !== filters.coverage) {
localStorage.setItem(RF_VISIBILITY_KEY, next.coverage ? '1' : '0');
const compatible = resolveTerrainCoverageConflict(filters, next);
if (compatible.coverage !== filters.coverage) {
localStorage.setItem(RF_VISIBILITY_KEY, compatible.coverage ? '1' : '0');
}
setFilters(next);
}, [filters.coverage]);
setFilters(compatible);
}, [filters]);
const handleRfTierChange = useCallback((tier: RfCoverageTierName) => {
localStorage.setItem(RF_TIER_KEY, tier);
@@ -253,7 +260,13 @@ export const App: React.FC = () => {
const handleModeChange = useCallback((mode: MapMode) => {
setActiveMode(mode);
setFilters((current) => filtersForMapMode(mode, current));
setFilters((current) => {
const compatible = resolveTerrainCoverageConflict(current, filtersForMapMode(mode, current));
if (compatible.coverage !== current.coverage) {
localStorage.setItem(RF_VISIBILITY_KEY, compatible.coverage ? '1' : '0');
}
return compatible;
});
useOverlayStore.getState().setPlanRepeaterMode(mode === 'plan');
}, []);
@@ -560,7 +573,16 @@ export const App: React.FC = () => {
event.preventDefault();
void handleShare();
} else if (!isEditing && event.key.toLowerCase() === 't') {
setFilters((current) => ({ ...current, terrain: !current.terrain }));
setFilters((current) => {
const compatible = resolveTerrainCoverageConflict(
current,
{ ...current, terrain: !current.terrain },
);
if (compatible.coverage !== current.coverage) {
localStorage.setItem(RF_VISIBILITY_KEY, compatible.coverage ? '1' : '0');
}
return compatible;
});
} else if (!isEditing && event.key.toLowerCase() === 'l') {
setFilters((current) => ({ ...current, betaPaths: !current.betaPaths }));
}
@@ -154,6 +154,11 @@ export const FilterPanel: React.FC<FilterPanelProps> = ({
className="filter-row"
onClick={() => toggle(key)}
aria-pressed={filters[key] as boolean}
title={key === 'terrain'
? '3D Terrain and RF Coverage render separately; enabling terrain turns RF coverage off.'
: key === 'coverage'
? 'RF Coverage uses the flat map; enabling it turns 3D terrain off.'
: undefined}
>
<span className="filter-row__label">
{hollow ? (
@@ -114,6 +114,11 @@ export const MobileControls: React.FC<MobileControlsProps> = ({
className={`filter-row${filters[key] ? ' filter-row--on' : ''}`}
onClick={() => onFiltersChange({ ...filters, [key]: !filters[key] })}
aria-pressed={!!filters[key]}
title={key === 'terrain'
? '3D Terrain and RF Coverage render separately; enabling terrain turns RF coverage off.'
: key === 'coverage'
? 'RF Coverage uses the flat map; enabling it turns 3D terrain off.'
: undefined}
>
<span className="filter-row__label">
{hollow ? (
@@ -0,0 +1,45 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import type { Filters } from '../components/FilterPanel/FilterPanel.js';
import { resolveTerrainCoverageConflict } from './mapLayerCompatibility.js';
const filters = (terrain: boolean, coverage: boolean): Filters => ({
livePackets: true,
links: false,
terrain,
clientNodes: false,
packetHistory: false,
coverage,
heatmap: false,
betaPaths: false,
betaPathThreshold: 0.45,
hexClashes: false,
hexClashMaxHops: 3,
});
test('enabling RF coverage turns off 3D terrain', () => {
assert.deepEqual(
resolveTerrainCoverageConflict(filters(true, false), filters(true, true)),
filters(false, true),
);
});
test('enabling 3D terrain turns off RF coverage', () => {
assert.deepEqual(
resolveTerrainCoverageConflict(filters(false, true), filters(true, true)),
filters(true, false),
);
});
test('ambiguous persisted state and shared URLs prefer RF coverage', () => {
assert.deepEqual(
resolveTerrainCoverageConflict(filters(false, false), filters(true, true)),
filters(false, true),
);
});
test('compatible layer combinations are unchanged', () => {
for (const next of [filters(false, false), filters(true, false), filters(false, true)]) {
assert.equal(resolveTerrainCoverageConflict(filters(false, false), next), next);
}
});
@@ -0,0 +1,22 @@
import type { Filters } from '../components/FilterPanel/FilterPanel.js';
type TerrainCoverageState = Pick<Filters, 'terrain' | 'coverage'>;
/**
* MapLibre cannot reliably drape our georeferenced HopReach image sources over
* its 3D terrain mesh: the images become large black/distorted quadrilaterals.
* Keep the controls deterministic by preserving whichever incompatible layer
* the user just enabled. Existing state/URLs that already contain both prefer
* RF coverage, which is the more specific data view.
*/
export function resolveTerrainCoverageConflict(
current: TerrainCoverageState,
next: Filters,
): Filters {
if (!next.terrain || !next.coverage) return next;
const enabledTerrain = !current.terrain && next.terrain;
const enabledCoverage = !current.coverage && next.coverage;
if (enabledTerrain && !enabledCoverage) return { ...next, coverage: false };
return { ...next, terrain: false };
}
+14
View File
@@ -162,6 +162,20 @@ test('enabled HopReach coverage survives metadata winning the initial map-load r
await expect.poll(() => tileRequests.length, { timeout: 15_000 }).toBeGreaterThan(0);
});
test('RF coverage and 3D terrain cannot create black terrain polygons together', async ({ page }, testInfo) => {
await page.goto('/?layers=feed%2Cterrain%2Ccoverage', { waitUntil: 'domcontentloaded' });
await page.getByText('Live Map', { exact: true }).waitFor({ state: 'visible', timeout: 15_000 });
if (testInfo.project.name === 'dashboard-mobile') {
await page.getByRole('button', { name: 'Layers' }).first().click();
}
const terrain = page.getByRole('button', { name: '3D Terrain' }).first();
const coverage = page.getByRole('button', { name: 'RF Coverage' }).first();
await expect(coverage).toHaveAttribute('aria-pressed', 'true');
await expect(terrain).toHaveAttribute('aria-pressed', 'false');
await expect(page).toHaveURL(/layers=(?![^&]*terrain)[^&]*coverage/);
});
test('map modes update layers and produce a shareable URL', async ({ page }) => {
await page.goto('/');
await expect(page.getByText('Live Map', { exact: true })).toBeVisible();