spb/internetpressure
Public
TypeScript 36.3%
Python 31.8%
Go 18%
JavaScript 9.8%
Shell 1.9%
SQL 1.4%
CSS 0.5%
1'use client';23import * as maplibregl from 'maplibre-gl';4import type { ExpressionSpecification, GeoJSONSource, StyleSpecification } from 'maplibre-gl';5import 'maplibre-gl/dist/maplibre-gl.css';6import { useRouter } from 'next/navigation';7import { useEffect, useMemo, useRef, useState } from 'react';8import { feature } from 'topojson-client';9import type { Topology, GeometryCollection } from 'topojson-specification';10import countries110 from 'world-atlas/countries-110m.json';11import { fmt, fmtDelta } from '@/lib/format';12import { greatCircle } from '@/lib/geo';13import { numericToAlpha2 } from '@/lib/iso-numeric-to-alpha2';14import { NEUTRAL, levelWord, pressureColor } from '@/lib/pressure';15import type { Country, Front, Incident, LatencyPair, LevelId, Probe, Region } from '@/lib/types';16import { MODES, lossToScale, type MapMode } from './modes';1718const STYLE_URL = 'https://tiles.openfreemap.org/styles/dark';19/** Offline/blocked fallback: our own countries layer on a near-black plane — never a fake basemap. */20const FALLBACK_STYLE: StyleSpecification = { version: 8, name: 'ip-fallback', glyphs: 'https://tiles.openfreemap.org/fonts/{fontstack}/{range}.pbf', sources: {}, layers: [{ id: 'bg', type: 'background', paint: { 'background-color': '#070A0F' } }] };21const DASH_FRAMES: number[][] = [22 [0, 4, 3],23 [0.5, 4, 2.5],24 [1, 4, 2],25 [1.5, 4, 1.5],26 [2, 4, 1],27 [2.5, 4, 0.5],28 [3, 4, 0],29 [0, 0.5, 3, 3.5],30 [0, 1, 3, 3],31 [0, 1.5, 3, 2.5],32 [0, 2, 3, 2],33 [0, 2.5, 3, 1.5],34 [0, 3, 3, 1],35 [0, 3.5, 3, 0.5],36];3738type FC = GeoJSON.FeatureCollection;39const EMPTY_FC: FC = { type: 'FeatureCollection', features: [] };4041interface Props {42 mode: MapMode;43 regions: Region[] | null;44 countries: Country[] | null;45 probes: Probe[] | null;46 fronts: Front[] | null;47 incidents: Incident[] | null;48 matrix: LatencyPair[] | null;49}5051function scopeValue(mode: MapMode, components: Record<string, number | null | undefined> | undefined, pressure: number, loss?: number | null): number | null {52 const def = MODES.find((m) => m.id === mode);53 if (mode === 'pressure' || mode === 'incidents' || mode === 'probes') return pressure;54 if (mode === 'loss') return lossToScale(loss);55 if (def?.component) return components?.[def.component] ?? null;56 return pressure;57}5859export function WorldMap({ mode, regions, countries, probes, fronts, incidents, matrix }: Props) {60 const el = useRef<HTMLDivElement>(null);61 const mapRef = useRef<maplibregl.Map | null>(null);62 const [ready, setReady] = useState(false);63 const [tip, setTip] = useState<{ x: number; y: number; html: string } | null>(null);64 const router = useRouter();6566 // Base countries GeoJSON (computed once; ~180 features).67 const baseCountries = useMemo<FC>(() => {68 const topo = countries110 as unknown as Topology<{ countries: GeometryCollection }>;69 const fc = feature(topo, topo.objects.countries) as unknown as FC;70 for (const f of fc.features) {71 const cc = numericToAlpha2(f.id as string);72 f.properties = { ...(f.properties ?? {}), cc: cc ?? null };73 }74 return fc;75 }, []);7677 // Per-region loss from the latency matrix (source view) — used by the Packet-loss mode.78 const lossByRegion = useMemo(() => {79 const m = new Map<string, { sum: number; n: number }>();80 for (const p of matrix ?? []) {81 const e = m.get(p.from) ?? { sum: 0, n: 0 };82 e.sum += p.loss_pct;83 e.n++;84 m.set(p.from, e);85 }86 return new Map([...m].map(([k, v]) => [k, v.sum / v.n]));87 }, [matrix]);8889 // ---- init map once90 useEffect(() => {91 if (!el.current || mapRef.current) return;92 // MapLibre 6 module worker: served from /public (scripts/copy-maplibre-worker.mjs) — the bundler cannot resolve it.93 maplibregl.setWorkerUrl('/maplibre/maplibre-gl-worker.mjs');94 const map = new maplibregl.Map({95 container: el.current,96 style: STYLE_URL,97 center: [12, 22],98 zoom: 1.15,99 minZoom: 0.7,100 maxZoom: 6,101 renderWorldCopies: false,102 attributionControl: { compact: true },103 dragRotate: false,104 pitchWithRotate: false,105 touchPitch: false,106 canvasContextAttributes: { preserveDrawingBuffer: true, antialias: true },107 });108 map.addControl(new maplibregl.NavigationControl({ showCompass: false }), 'top-right');109 if (process.env.NODE_ENV !== 'production') (window as unknown as { __ipMap?: maplibregl.Map }).__ipMap = map;110 let fellBack = false;111 map.on('error', (e) => {112 // Style/tiles unreachable (offline, blocked): fall back to our own layers only.113 const msg = String((e as { error?: { message?: string } }).error?.message ?? '');114 if (!fellBack && !map.isStyleLoaded() && /style|fetch|Failed|NetworkError|403|404|5\d\d/i.test(msg)) {115 fellBack = true;116 map.setStyle(FALLBACK_STYLE);117 }118 });119 const onStyle = () => {120 if (map.getSource('countries')) return;121 // dim basemap labels/roads: keep only water/land/boundaries/country labels from the vendor style122 for (const layer of map.getStyle().layers ?? []) {123 if (/road|transit|building|poi|housenumber|aeroway|rail|ferry|path|place_(?!country|continent)|water_name|waterway/.test(layer.id)) {124 try {125 map.setLayoutProperty(layer.id, 'visibility', 'none');126 } catch {127 /* ignore */128 }129 }130 }131 map.addSource('countries', { type: 'geojson', data: EMPTY_FC });132 map.addSource('regions', { type: 'geojson', data: EMPTY_FC });133 map.addSource('probes', { type: 'geojson', data: EMPTY_FC });134 map.addSource('fronts', { type: 'geojson', data: EMPTY_FC });135 map.addSource('arcs', { type: 'geojson', data: EMPTY_FC });136 map.addSource('incidents', { type: 'geojson', data: EMPTY_FC });137138 const firstSymbol = (map.getStyle().layers ?? []).find((l) => l.type === 'symbol')?.id;139 map.addLayer({ id: 'countries-fill', type: 'fill', source: 'countries', paint: { 'fill-color': ['get', 'color'] as ExpressionSpecification, 'fill-opacity': ['case', ['boolean', ['get', 'observed'], false], 0.42, 0.9] as ExpressionSpecification } }, firstSymbol);140 map.addLayer({ id: 'countries-line', type: 'line', source: 'countries', paint: { 'line-color': '#1B2430', 'line-width': 0.5 } }, firstSymbol);141 map.addLayer({ id: 'arcs', type: 'line', source: 'arcs', layout: { 'line-cap': 'round' }, paint: { 'line-color': ['get', 'color'] as ExpressionSpecification, 'line-width': ['get', 'width'] as ExpressionSpecification, 'line-opacity': 0.55 } });142 map.addLayer({ id: 'fronts-glow', type: 'line', source: 'fronts', layout: { 'line-cap': 'round' }, paint: { 'line-color': ['get', 'color'] as ExpressionSpecification, 'line-width': ['+', ['get', 'width'], 6] as ExpressionSpecification, 'line-opacity': 0.12, 'line-blur': 4 } });143 map.addLayer({ id: 'fronts', type: 'line', source: 'fronts', layout: { 'line-cap': 'round' }, paint: { 'line-color': ['get', 'color'] as ExpressionSpecification, 'line-width': ['get', 'width'] as ExpressionSpecification, 'line-opacity': 0.9, 'line-dasharray': [0, 4, 3] } });144 map.addLayer({ id: 'fronts-arrow', type: 'symbol', source: 'fronts', layout: { 'symbol-placement': 'line', 'symbol-spacing': 140, 'text-field': '›', 'text-size': 16, 'text-font': ['Noto Sans Regular'], 'text-keep-upright': false, 'text-allow-overlap': true }, paint: { 'text-color': ['get', 'color'] as ExpressionSpecification } });145 map.addLayer({ id: 'incidents-ring', type: 'circle', source: 'incidents', paint: { 'circle-radius': ['get', 'radius'] as ExpressionSpecification, 'circle-color': 'rgba(0,0,0,0)', 'circle-stroke-color': ['get', 'color'] as ExpressionSpecification, 'circle-stroke-width': 1.5, 'circle-stroke-opacity': 0.9 } });146 map.addLayer({ id: 'incidents-dot', type: 'circle', source: 'incidents', paint: { 'circle-radius': 3, 'circle-color': ['get', 'color'] as ExpressionSpecification } });147 map.addLayer({ id: 'regions-dot', type: 'circle', source: 'regions', paint: { 'circle-radius': ['interpolate', ['linear'], ['zoom'], 0.7, 9, 4, 16] as ExpressionSpecification, 'circle-color': ['get', 'color'] as ExpressionSpecification, 'circle-opacity': 0.95, 'circle-stroke-color': '#070A0F', 'circle-stroke-width': 1.5 } });148 map.addLayer({ id: 'regions-label', type: 'symbol', source: 'regions', layout: { 'text-field': ['get', 'label'] as ExpressionSpecification, 'text-size': 10, 'text-font': ['Noto Sans Bold'], 'text-allow-overlap': true, 'text-ignore-placement': true }, paint: { 'text-color': '#070A0F' } });149 map.addLayer({ id: 'regions-name', type: 'symbol', source: 'regions', minzoom: 2, layout: { 'text-field': ['get', 'name'] as ExpressionSpecification, 'text-size': 10.5, 'text-offset': [0, 1.6], 'text-anchor': 'top', 'text-font': ['Noto Sans Regular'] }, paint: { 'text-color': '#8B98A5', 'text-halo-color': '#070A0F', 'text-halo-width': 1 } });150 map.addLayer({ id: 'probes', type: 'circle', source: 'probes', paint: { 'circle-radius': ['get', 'radius'] as ExpressionSpecification, 'circle-color': ['get', 'color'] as ExpressionSpecification, 'circle-opacity': 0.9, 'circle-stroke-color': '#070A0F', 'circle-stroke-width': 1 } });151 setReady(true);152 };153 map.on('style.load', onStyle);154 mapRef.current = map;155 return () => {156 map.remove();157 mapRef.current = null;158 setReady(false);159 };160 }, []);161162 // ---- data → sources (recomputed only when data/mode change)163 useEffect(() => {164 const map = mapRef.current;165 if (!map || !ready) return;166 const isProbes = mode === 'probes';167 const isIncidents = mode === 'incidents';168 const byCc = new Map((countries ?? []).map((c) => [c.cc, c]));169 const countriesFc: FC = {170 type: 'FeatureCollection',171 features: baseCountries.features.map((f) => {172 const cc = f.properties?.cc as string | null;173 const c = cc ? byCc.get(cc) : undefined;174 const v = c ? scopeValue(mode, c.components as Record<string, number | null>, c.pressure, lossByRegion.get(c.region) ?? null) : null;175 const observed = Boolean(c) && v != null && !isProbes && !isIncidents;176 return { ...f, properties: { ...f.properties, observed, color: observed ? pressureColor(v!) : NEUTRAL, value: v, pressure: c?.pressure ?? null, level: c?.level ?? null, delta: c?.delta_1h ?? null, name: c?.name ?? null, probes: c?.probes ?? 0, targets: c?.targets ?? 0 } };177 }),178 };179 (map.getSource('countries') as GeoJSONSource | undefined)?.setData(countriesFc);180181 const regionsFc: FC = {182 type: 'FeatureCollection',183 features: (regions ?? [])184 .filter((r) => r.id !== 'global' && !isProbes && !isIncidents)185 .map((r) => {186 const v = scopeValue(mode, r.components as Record<string, number | null>, r.pressure, lossByRegion.get(r.id) ?? null);187 return { type: 'Feature', geometry: { type: 'Point', coordinates: [r.lon, r.lat] }, properties: { id: r.id, name: r.name, label: v == null ? '·' : fmt(v, 0), color: v == null ? '#3A4756' : pressureColor(v), value: v, pressure: r.pressure, delta: r.delta_1h, level: r.level, probes: r.probes, targets: r.targets, incidents: r.incidents } };188 }),189 };190 (map.getSource('regions') as GeoJSONSource | undefined)?.setData(regionsFc);191192 const probesFc: FC = {193 type: 'FeatureCollection',194 features: (probes ?? []).map((p) => ({195 type: 'Feature',196 geometry: { type: 'Point', coordinates: [p.lon, p.lat] },197 properties: { id: p.probe_id, name: p.name, status: p.status, radius: isProbes ? 4 + Math.min(6, p.measurements_1h / 1200) : 3.5, color: p.status === 'online' ? '#7FB77E' : p.status === 'stale' ? '#E9C46A' : p.status === 'offline' ? '#D62828' : '#8B98A5', provider: p.provider, asn: p.asn },198 })),199 };200 (map.getSource('probes') as GeoJSONSource | undefined)?.setData(probesFc);201202 const frontsFc: FC = {203 type: 'FeatureCollection',204 features: isProbes || isIncidents205 ? []206 : (fronts ?? []).map((f) => ({ type: 'Feature', geometry: { type: 'LineString', coordinates: greatCircle([f.from.lon, f.from.lat], [f.to.lon, f.to.lat], 64) }, properties: { id: f.id, name: f.name, status: f.status, color: pressureColor(f.intensity), width: 1.5 + f.confidence * 2, intensity: f.intensity } })),207 };208 (map.getSource('fronts') as GeoJSONSource | undefined)?.setData(frontsFc);209210 // Probe-network mode: inter-region latency matrix as arcs (thickness = |z|)211 const centroid = new Map((regions ?? []).map((r) => [r.id, [r.lon, r.lat] as [number, number]]));212 const arcsFc: FC = {213 type: 'FeatureCollection',214 features: isProbes215 ? (matrix ?? [])216 .filter((m) => m.from !== m.to && centroid.has(m.from) && centroid.has(m.to))217 .map((m) => ({ type: 'Feature', geometry: { type: 'LineString', coordinates: greatCircle(centroid.get(m.from)!, centroid.get(m.to)!, 48) }, properties: { color: m.z >= 3 ? '#E76F51' : m.z >= 1.5 ? '#E9C46A' : '#3A4756', width: 0.6 + Math.min(5, Math.abs(m.z)) * 0.8, from: m.from, to: m.to, rtt: m.rtt_ms, z: m.z } }))218 : [],219 };220 (map.getSource('arcs') as GeoJSONSource | undefined)?.setData(arcsFc);221222 const incFc: FC = {223 type: 'FeatureCollection',224 features: isIncidents225 ? (incidents ?? [])226 .map((i) => {227 const c = i.scope_type === 'region' ? centroid.get(i.scope_id ?? '') : i.scope_type === 'country' ? (byCc.get((i.scope_id ?? '').toUpperCase()) ? [byCc.get((i.scope_id ?? '').toUpperCase())!.lon, byCc.get((i.scope_id ?? '').toUpperCase())!.lat] : undefined) : undefined;228 if (!c) return null;229 return { type: 'Feature' as const, geometry: { type: 'Point' as const, coordinates: c }, properties: { slug: i.slug, title: i.title, status: i.status, color: pressureColor(i.current_pressure), radius: 8 + Math.min(20, i.affected_targets / 4), pressure: i.current_pressure } };230 })231 .filter((f): f is NonNullable<typeof f> => f != null)232 : [],233 };234 (map.getSource('incidents') as GeoJSONSource | undefined)?.setData(incFc);235 }, [ready, mode, regions, countries, probes, fronts, incidents, matrix, baseCountries, lossByRegion]);236237 // ---- animated dash offset ONLY while a front is developing/active (real state, not decoration)238 useEffect(() => {239 const map = mapRef.current;240 if (!map || !ready) return;241 const moving = mode !== 'probes' && mode !== 'incidents' && (fronts ?? []).some((f) => f.status === 'developing' || f.status === 'active');242 if (!moving || window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) {243 try {244 map.setPaintProperty('fronts', 'line-dasharray', [0, 4, 3]);245 } catch {246 /* layer missing */247 }248 return;249 }250 let step = 0;251 let raf = 0;252 let last = 0;253 const tick = (t: number) => {254 if (t - last > 70) {255 last = t;256 step = (step + 1) % DASH_FRAMES.length;257 try {258 map.setPaintProperty('fronts', 'line-dasharray', DASH_FRAMES[step]!);259 } catch {260 /* layer missing */261 }262 }263 raf = requestAnimationFrame(tick);264 };265 raf = requestAnimationFrame(tick);266 return () => cancelAnimationFrame(raf);267 }, [ready, fronts, mode]);268269 // ---- interactions270 useEffect(() => {271 const map = mapRef.current;272 if (!map || !ready) return;273 const layers = ['regions-dot', 'probes', 'incidents-dot', 'incidents-ring', 'countries-fill', 'arcs'];274 const onMove = (e: maplibregl.MapMouseEvent) => {275 const feats = map.queryRenderedFeatures(e.point, { layers: layers.filter((l) => map.getLayer(l)) });276 const f = feats[0];277 if (!f) {278 setTip(null);279 map.getCanvas().style.cursor = '';280 return;281 }282 const p = f.properties as Record<string, unknown>;283 let html = '';284 if (f.layer.id === 'regions-dot') html = `<b>${p.name}</b><br>pressure <b>${fmt(p.pressure as number)}</b> ${levelWord(p.level as LevelId)} · Δ1h ${fmtDelta(p.delta as number)}<br>${p.probes} probes · ${p.targets} targets · ${p.incidents} incidents${mode !== 'pressure' ? `<br>${MODES.find((m) => m.id === mode)?.label}: <b>${p.value == null ? 'not observed' : fmt(p.value as number)}</b>` : ''}`;285 else if (f.layer.id === 'probes') html = `<b>${p.id}</b> · ${p.status}<br>${p.name}<br>${p.provider} · AS${p.asn}`;286 else if (f.layer.id.startsWith('incidents')) html = `<b>${p.title}</b><br>${p.status} · pressure ${fmt(p.pressure as number)}`;287 else if (f.layer.id === 'arcs') html = `<b>${p.from} → ${p.to}</b><br>RTT ${fmt(p.rtt as number)} ms · z ${fmt(p.z as number)}`;288 else if (f.layer.id === 'countries-fill') {289 if (!p.name) {290 setTip(null);291 map.getCanvas().style.cursor = '';292 return;293 }294 html = `<b>${p.name}</b> (${p.cc})<br>pressure <b>${fmt(p.pressure as number)}</b> ${levelWord(p.level as LevelId)} · Δ1h ${fmtDelta(p.delta as number)}<br>${p.probes} probes · ${p.targets} targets${mode !== 'pressure' && mode !== 'probes' && mode !== 'incidents' ? `<br>${MODES.find((m) => m.id === mode)?.label}: <b>${p.value == null ? 'not observed' : fmt(p.value as number)}</b>` : ''}`;295 }296 map.getCanvas().style.cursor = 'pointer';297 const w = map.getContainer().clientWidth;298 setTip({ x: Math.min(e.point.x + 12, w - 270), y: e.point.y + 12, html });299 };300 const onLeave = () => setTip(null);301 const onClick = (e: maplibregl.MapMouseEvent) => {302 const feats = map.queryRenderedFeatures(e.point, { layers: ['regions-dot', 'incidents-dot', 'incidents-ring', 'countries-fill'].filter((l) => map.getLayer(l)) });303 const f = feats[0];304 if (!f) return;305 const p = f.properties as Record<string, unknown>;306 if (f.layer.id === 'regions-dot') router.push(`/internet/${p.id}`);307 else if (f.layer.id.startsWith('incidents')) router.push(`/event/${p.slug}`);308 else if (f.layer.id === 'countries-fill' && p.name && p.cc) router.push(`/country/${String(p.cc).toLowerCase()}`);309 };310 map.on('mousemove', onMove);311 map.on('mouseout', onLeave);312 map.on('click', onClick);313 return () => {314 map.off('mousemove', onMove);315 map.off('mouseout', onLeave);316 map.off('click', onClick);317 };318 }, [ready, mode, router]);319320 return (321 <div className="relative h-full w-full">322 <div ref={el} className="h-full w-full" aria-label="World map of Internet pressure" role="application" />323 {tip && (324 <div className="pointer-events-none absolute z-10 max-w-[260px] rounded-[3px] border border-line bg-panel px-2.5 py-1.5 text-[11.5px] leading-snug text-ink" style={{ left: tip.x, top: tip.y }} dangerouslySetInnerHTML={{ __html: tip.html }} />325 )}326 </div>327 );328}329