From 0a41d4bfc6ebf8a11b6ca956ddfb3817efd26c47 Mon Sep 17 00:00:00 2001 From: gadgethd <111318106+gadgethd@users.noreply.github.com> Date: Thu, 27 Aug 2026 16:33:06 +0000 Subject: [PATCH] refactor(map): fix overlay anchors and migrate mini-maps to OpenFreeMap with attribution --- .../src/components/Map/RfCoverageOverlay.tsx | 14 +++++++---- frontend/src/components/Map/TopologyMap.tsx | 10 +++++++- .../src/components/stats/StatsPrimitives.tsx | 24 +++---------------- frontend/src/pages/SpamTransparencyPage.tsx | 4 ++-- frontend/src/pages/owner/OwnerMapView.tsx | 2 +- frontend/src/pages/ukmesh/PacketPathMap.tsx | 16 +++---------- 6 files changed, 27 insertions(+), 43 deletions(-) diff --git a/frontend/src/components/Map/RfCoverageOverlay.tsx b/frontend/src/components/Map/RfCoverageOverlay.tsx index 67d34fc..190c677 100644 --- a/frontend/src/components/Map/RfCoverageOverlay.tsx +++ b/frontend/src/components/Map/RfCoverageOverlay.tsx @@ -13,6 +13,14 @@ import { const SOURCE_ID = 'hopreach-rf-source'; const LAYER_ID = 'hopreach-rf-layer'; +function getFirstSymbolLayerId(map: maplibregl.Map): string | undefined { + const layers = map.getStyle().layers; + for (const layer of layers) { + if (layer.type === 'symbol') return layer.id; + } + return undefined; +} + function removeRfLayers(map: maplibregl.Map): void { if (map.getLayer(LAYER_ID)) map.removeLayer(LAYER_ID); if (map.getSource(SOURCE_ID)) map.removeSource(SOURCE_ID); @@ -61,11 +69,7 @@ export function RfCoverageOverlay({ releaseDataset = null; if (!visible || tiles.length === 0) return; - const beforeId = map.getLayer('map-labels-water') - ? 'map-labels-water' - : map.getLayer('privacy-rings-layer') - ? 'privacy-rings-layer' - : undefined; + const beforeId = getFirstSymbolLayerId(map); const dataset = registerRfRasterDataset( tiles.map((tile) => ({ diff --git a/frontend/src/components/Map/TopologyMap.tsx b/frontend/src/components/Map/TopologyMap.tsx index 18c38f2..efbc6fd 100644 --- a/frontend/src/components/Map/TopologyMap.tsx +++ b/frontend/src/components/Map/TopologyMap.tsx @@ -34,9 +34,17 @@ type TopologyMapOverlayProps = Omit void; }; +function getFirstSymbolLayerId(map: maplibregl.Map): string | undefined { + const layers = map.getStyle().layers; + for (const layer of layers) { + if (layer.type === 'symbol') return layer.id; + } + return undefined; +} + function addTopologyLayer(map: maplibregl.Map, layer: maplibregl.LayerSpecification): void { if (map.getLayer(layer.id)) return; - const beforeId = map.getLayer('map-labels-water') ? 'map-labels-water' : undefined; + const beforeId = getFirstSymbolLayerId(map); if (beforeId) map.addLayer(layer, beforeId); else map.addLayer(layer); } diff --git a/frontend/src/components/stats/StatsPrimitives.tsx b/frontend/src/components/stats/StatsPrimitives.tsx index a6bed13..fb149c2 100644 --- a/frontend/src/components/stats/StatsPrimitives.tsx +++ b/frontend/src/components/stats/StatsPrimitives.tsx @@ -1,6 +1,7 @@ import React, { lazy, memo, Suspense, useEffect } from 'react'; import type * as maplibregl from 'maplibre-gl'; import { AnimatedPathOverlay, type AerialPath } from '../Map/AnimatedPathOverlay.js'; +import { OPENFREEMAP_STYLE_DARK } from '../Map/mapConfig.js'; export const C_CYAN = '#00c4ff'; export const C_GREEN = '#00e676'; @@ -253,13 +254,6 @@ export const EmptyPacketState: React.FC<{ label?: string }> = ({ label = 'No packet data in this window.', }) =>
{label}
; -const CARTO_DARK_TILES = [ - 'https://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', - 'https://b.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', - 'https://c.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', - 'https://d.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', -]; - export const DecodedPathMapView: React.FC<{ nodes: DecodedPathNode[] }> = ({ nodes }) => { const containerRef = React.useRef(null); const [mapInstance, setMapInstance] = React.useState(null); @@ -284,23 +278,11 @@ export const DecodedPathMapView: React.FC<{ nodes: DecodedPathNode[] }> = ({ nod const maplibre = maplibreModule; map = new maplibre.Map({ container: containerRef.current, - style: { - version: 8, - sources: { - tiles: { - type: 'raster', - tiles: CARTO_DARK_TILES, - tileSize: 256, - maxzoom: 19, - attribution: '© OpenStreetMap © CARTO', - }, - }, - layers: [{ id: 'bg', type: 'raster', source: 'tiles' }], - }, + style: OPENFREEMAP_STYLE_DARK, center: [Number(nodes[0]!.lon), Number(nodes[0]!.lat)], zoom: 8, - attributionControl: false, }); + map.addControl(new maplibre.AttributionControl({ compact: true })); map.on('load', () => { if (cancelled || !map) return; const bounds = new maplibre.LngLatBounds(); diff --git a/frontend/src/pages/SpamTransparencyPage.tsx b/frontend/src/pages/SpamTransparencyPage.tsx index 610c786..61f235e 100644 --- a/frontend/src/pages/SpamTransparencyPage.tsx +++ b/frontend/src/pages/SpamTransparencyPage.tsx @@ -177,8 +177,8 @@ const OriginMiniMap = memo(function OriginMiniMap({ style: MAP_STYLE, bounds, fitBoundsOptions: { padding: 40, maxZoom: 10 }, - attributionControl: false, }); + map.addControl(new maplibregl.AttributionControl({ compact: true })); map.addControl(new maplibregl.NavigationControl({ showCompass: false }), 'top-right'); map.on('load', () => { map.addSource('zone', { @@ -488,8 +488,8 @@ export function SpamPage() { style: MAP_STYLE, center: [-2.5, 54.0], zoom: 4.6, - attributionControl: false, }); + map.addControl(new maplibregl.AttributionControl({ compact: true })); map.addControl(new maplibregl.NavigationControl({ showCompass: false }), 'top-right'); map.on('load', () => { map.addSource('zones', { type: 'geojson', data: EMPTY_FC }); diff --git a/frontend/src/pages/owner/OwnerMapView.tsx b/frontend/src/pages/owner/OwnerMapView.tsx index 339142d..0a3ba5e 100644 --- a/frontend/src/pages/owner/OwnerMapView.tsx +++ b/frontend/src/pages/owner/OwnerMapView.tsx @@ -111,8 +111,8 @@ export const OwnerMapView: React.FC<{ style: MAP_STYLE, center: [DEFAULT_CENTER[1], DEFAULT_CENTER[0]], zoom: 7, - attributionControl: false, }); + map.addControl(new maplibregl.AttributionControl({ compact: true })); mapRef.current = map; // Keep the map as a static regional backdrop while preserving marker clicks. diff --git a/frontend/src/pages/ukmesh/PacketPathMap.tsx b/frontend/src/pages/ukmesh/PacketPathMap.tsx index a7ad79a..c2a84af 100644 --- a/frontend/src/pages/ukmesh/PacketPathMap.tsx +++ b/frontend/src/pages/ukmesh/PacketPathMap.tsx @@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import type * as maplibregl from 'maplibre-gl'; import { AnimatedPathOverlay, type AerialPath, type AerialPathNode } from '../../components/Map/AnimatedPathOverlay.js'; import { LoadingIndicator } from '../../components/LoadingIndicator.js'; +import { OPENFREEMAP_STYLE_DARK } from '../../components/Map/mapConfig.js'; import type { MeshNode } from '../../hooks/useNodes.js'; import { multiObserverPathRoutes, @@ -42,13 +43,6 @@ export type LazyPathResult = { paths: LazyPath[]; }; -const CARTO_TILES = [ - 'https://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', - 'https://b.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', - 'https://c.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', - 'https://d.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', -]; - function lazyPathRuns(nodes: readonly LazyPathNode[]): LazyPathNode[][] { const runs: LazyPathNode[][] = []; let current: LazyPathNode[] = []; @@ -193,17 +187,13 @@ export const PathMap: React.FC<{ maplibreRef.current = maplibre; nextMap = new maplibre.Map({ container: containerRef.current, - style: { - version: 8, - sources: { tiles: { type: 'raster', tiles: CARTO_TILES, tileSize: 256, maxzoom: 19, attribution: '© OpenStreetMap © CARTO' } }, - layers: [{ id: 'bg', type: 'raster', source: 'tiles' }], - }, + style: OPENFREEMAP_STYLE_DARK, center: [0, 51.5], zoom: 6, pitch: 50, bearing: -8, - attributionControl: false, }); + nextMap.addControl(new maplibre.AttributionControl({ compact: true })); mapRef.current = nextMap; nextMap.on('load', () => { if (cancelled || !nextMap) return;