mirror of
https://github.com/gadgethd/ukmesh.git
synced 2026-09-10 00:55:33 +00:00
fix RF coverage terrain artifacts
This commit is contained in:
+30
-8
@@ -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 };
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user