From af9b3d08cc23ea397e89741a7cf781615d2c2eb7 Mon Sep 17 00:00:00 2001 From: BigBodyCobain <43977454+BigBodyCobain@users.noreply.github.com> Date: Mon, 8 Jun 2026 21:04:08 -0600 Subject: [PATCH] feat: Telegram OSINT map layer, Osiris intel ports, and maritime settings Add Telegram OSINT with hourly incremental t.me scraping, metro geocoding separate from news centroids, threat-intercept popup UI with inline media, and HTML markers above alert boxes so pins stay clickable. Expose GFW_API_TOKEN in onboarding and Settings Maritime; harden GFW/CCTV/geo fetchers. Port Osiris- derived recon, SCM, entity graph, malware/cyber feeds, sanctions, and submarine cable layers with tests and documentation. Co-authored-by: Cursor --- .env.example | 17 + README.md | 98 +++- backend/.env.example | 13 + backend/main.py | 98 +++- backend/routers/cctv.py | 38 +- backend/routers/data.py | 35 ++ backend/routers/entity_graph.py | 30 + backend/routers/intel_feeds.py | 122 ++++ backend/routers/osint.py | 151 +++++ backend/routers/scm.py | 16 + backend/services/api_settings.py | 9 + backend/services/cctv_pipeline.py | 525 ++++++++++++++---- backend/services/data_fetcher.py | 83 ++- backend/services/env_check.py | 1 + backend/services/fetchers/_store.py | 13 + backend/services/fetchers/cyber_status.py | 62 +++ backend/services/fetchers/geo.py | 37 +- backend/services/fetchers/infrastructure.py | 8 +- backend/services/fetchers/malware.py | 107 ++++ backend/services/fetchers/news.py | 23 +- backend/services/fetchers/telegram_osint.py | 381 +++++++++++++ backend/services/intel_feeds/__init__.py | 0 backend/services/intel_feeds/country_risk.py | 94 ++++ backend/services/osint/__init__.py | 1 + backend/services/osint/lookups.py | 492 ++++++++++++++++ backend/services/osint_intel/__init__.py | 1 + backend/services/osint_intel/resolve.py | 268 +++++++++ backend/services/sanctions/__init__.py | 1 + backend/services/sanctions/ofac.py | 154 +++++ backend/services/scm/__init__.py | 1 + backend/services/scm/suppliers.py | 154 +++++ backend/services/ssrf_guard.py | 141 +++++ backend/tests/test_cctv_pipeline.py | 59 ++ backend/tests/test_datacenters_fetch.py | 10 + backend/tests/test_geo_fetchers.py | 49 ++ backend/tests/test_osiris_port.py | 43 ++ backend/tests/test_scm_suppliers.py | 13 + backend/tests/test_telegram_osint.py | 103 ++++ backend/third_party/osiris/NOTICE.md | 14 + docker-compose.yml | 9 + frontend/public/data/submarine-cables.json | 1 + .../hooks/useDataPollingViewport.test.ts | 26 + .../src/__tests__/lib/submarineCables.test.ts | 61 ++ .../src/__tests__/map/telegramGeoJSON.test.ts | 94 ++++ .../__tests__/utils/viewportPrivacy.test.ts | 12 + frontend/src/app/page.tsx | 30 +- frontend/src/components/EntityGraphPanel.tsx | 165 ++++++ frontend/src/components/MapLegend.tsx | 1 + frontend/src/components/MaplibreViewer.tsx | 215 ++++++- .../popups/TelegramOsintPopup.tsx | 255 +++++++++ frontend/src/components/NewsFeed.tsx | 20 +- frontend/src/components/OnboardingModal.tsx | 40 +- frontend/src/components/ReconPanel.tsx | 176 ++++++ frontend/src/components/ReconResults.tsx | 416 ++++++++++++++ frontend/src/components/ScmPanel.tsx | 137 +++++ .../src/components/WorldviewLeftPanel.tsx | 45 +- frontend/src/components/map/MapMarkers.tsx | 58 ++ .../components/map/dynamicMapLayers.worker.ts | 41 +- .../src/components/map/geoJSONBuilders.ts | 183 +++++- .../map/hooks/useStaticMapLayersWorker.ts | 2 + .../components/map/staticMapLayers.worker.ts | 24 + frontend/src/hooks/useDataPolling.ts | 68 ++- frontend/src/i18n/translations/en.json | 23 +- frontend/src/i18n/translations/fr.json | 23 +- frontend/src/i18n/translations/zh-CN.json | 23 +- frontend/src/lib/entityGraph.ts | 22 + frontend/src/lib/liveDataViewport.ts | 13 + frontend/src/lib/submarineCables.ts | 97 ++++ frontend/src/lib/telegramProxy.ts | 6 + frontend/src/middleware.ts | 3 +- frontend/src/types/dashboard.ts | 76 +++ frontend/src/utils/alertSpread.test.ts | 1 + frontend/src/utils/alertSpread.ts | 1 + scripts/data/ne_110m_land.geojson | 1 + scripts/data/ne_50m_land.geojson | 1 + scripts/sanitize_submarine_cables.py | 153 +++++ 76 files changed, 5769 insertions(+), 218 deletions(-) create mode 100644 backend/routers/entity_graph.py create mode 100644 backend/routers/intel_feeds.py create mode 100644 backend/routers/osint.py create mode 100644 backend/routers/scm.py create mode 100644 backend/services/fetchers/cyber_status.py create mode 100644 backend/services/fetchers/malware.py create mode 100644 backend/services/fetchers/telegram_osint.py create mode 100644 backend/services/intel_feeds/__init__.py create mode 100644 backend/services/intel_feeds/country_risk.py create mode 100644 backend/services/osint/__init__.py create mode 100644 backend/services/osint/lookups.py create mode 100644 backend/services/osint_intel/__init__.py create mode 100644 backend/services/osint_intel/resolve.py create mode 100644 backend/services/sanctions/__init__.py create mode 100644 backend/services/sanctions/ofac.py create mode 100644 backend/services/scm/__init__.py create mode 100644 backend/services/scm/suppliers.py create mode 100644 backend/services/ssrf_guard.py create mode 100644 backend/tests/test_datacenters_fetch.py create mode 100644 backend/tests/test_osiris_port.py create mode 100644 backend/tests/test_scm_suppliers.py create mode 100644 backend/tests/test_telegram_osint.py create mode 100644 backend/third_party/osiris/NOTICE.md create mode 100644 frontend/public/data/submarine-cables.json create mode 100644 frontend/src/__tests__/hooks/useDataPollingViewport.test.ts create mode 100644 frontend/src/__tests__/lib/submarineCables.test.ts create mode 100644 frontend/src/__tests__/map/telegramGeoJSON.test.ts create mode 100644 frontend/src/components/EntityGraphPanel.tsx create mode 100644 frontend/src/components/MaplibreViewer/popups/TelegramOsintPopup.tsx create mode 100644 frontend/src/components/ReconPanel.tsx create mode 100644 frontend/src/components/ReconResults.tsx create mode 100644 frontend/src/components/ScmPanel.tsx create mode 100644 frontend/src/lib/entityGraph.ts create mode 100644 frontend/src/lib/submarineCables.ts create mode 100644 frontend/src/lib/telegramProxy.ts create mode 100644 scripts/data/ne_110m_land.geojson create mode 100644 scripts/data/ne_50m_land.geojson create mode 100644 scripts/sanitize_submarine_cables.py diff --git a/.env.example b/.env.example index 7ee27f7..fb69c7b 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,23 @@ OPENSKY_CLIENT_ID= OPENSKY_CLIENT_SECRET= AIS_API_KEY= +# Global Fishing Watch — fishing vessel activity events (Fishing Activity map layer). +# Free API token from https://globalfishingwatch.org/our-apis/tokens +# Without this the fishing_activity layer stays empty. +# GFW_API_TOKEN= +# Optional tuning — GFW can return 40k+ global events; defaults cap fetch for map paint. +# GFW_EVENTS_PAGE_SIZE=500 +# GFW_EVENTS_MAX_PAGES=10 +# GFW_EVENTS_LOOKBACK_DAYS=7 +# GFW_EVENTS_TIMEOUT_S=90 + +# Windy Webcams global CCTV layer — free key from https://api.windy.com/webcams/docs +# WINDY_API_KEY= + +# Telegram OSINT map layer — scrapes public t.me/s channel previews (no bot token). +# TELEGRAM_OSINT_ENABLED=true +# TELEGRAM_OSINT_CHANNELS=osintdefender,insiderpaper,aljazeeraenglish,nexta_live,war_monitor + # Admin key to protect sensitive endpoints (settings, updates). # If blank, loopback/localhost requests still work for local single-host dev. # Remote/non-loopback admin access requires ADMIN_KEY, or ALLOW_INSECURE_ADMIN=true in debug-only setups. diff --git a/README.md b/README.md index 64f58d2..d9844ec 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ **ShadowBroker** is a decentralized intelligence platform that aggregates real-time, multi-domain OSINT telemetry from 60+ live intelligence feeds into a single dark-ops map interface. Aircraft, ships, satellites, conflict zones, CCTV networks, GPS jamming, internet-connected devices, police scanners, mesh radio nodes, and breaking geopolitical events — all updating in real time on one screen as well as an obfuscated communications protocol and information exchange infrastructure. -Built with **Next.js**, **MapLibre GL**, **FastAPI**, and **Python**. 35+ toggleable data layers, including SAR ground-change detection. Multiple visual modes (DEFAULT / SATELLITE / FLIR / NVG / CRT). Right-click any point on Earth for a country dossier, head-of-state lookup, and the latest Sentinel-2 satellite photo. ShadowBroker has no accounts, product telemetry, or analytics; the dashboard talks to your self-hosted backend, while optional live OSINT panels may contact their configured public data providers when you use them. +Built with **Next.js**, **MapLibre GL**, **FastAPI**, and **Python**. 40+ toggleable data layers, including SAR ground-change detection, a **server-side recon toolkit** (DNS, WHOIS, sanctions, BGP, IP sweep, and more), supply-chain risk overlays, and malware/C2 threat feeds. Multiple visual modes (DEFAULT / SATELLITE / FLIR / NVG / CRT). Right-click any point on Earth for a country dossier, head-of-state lookup, entity-graph expansion, and the latest Sentinel-2 satellite photo. ShadowBroker has no accounts, product telemetry, or analytics; the dashboard talks to your self-hosted backend. Sensitive recon and Shodan queries never hit third-party APIs from the browser — they are proxied through the backend with SSRF guards and local-operator auth. Designed for analysts, researchers, radio operators, and anyone who wants to see what the world looks like when every public signal is on the same map. @@ -30,16 +30,18 @@ A surprising amount of global telemetry is already public — aircraft ADS-B bro The project does not introduce new surveillance capabilities — it aggregates and visualizes existing public datasets. It is fully open-source so anyone can audit exactly what data is accessed and how. ShadowBroker does not include product telemetry, analytics, or accounts. Operator-supplied keys stay in your local deployment, but live OSINT features necessarily make outbound requests to the public data providers you enable or query. -### Shodan Connector +### Shodan & Recon (security-first) -ShadowBroker includes an optional Shodan connector for operator-supplied API access. Shodan results are fetched with your own `SHODAN_API_KEY`, rendered as a local investigative overlay (not merged into core feeds), and remain subject to Shodan’s terms of service. +ShadowBroker includes an optional **Shodan connector** for operator-supplied API access (`SHODAN_API_KEY`) and a **Recon Toolkit** panel for keyless OSINT lookups. Both run **server-side only**: the browser calls your self-hosted `/api/osint/*` and `/api/tools/shodan/*` routes; outbound requests are made by the backend after SSRF validation. Recon requires **local-operator** access (same trust model as layer toggles and admin routes). Shodan results render as a separate map overlay and remain subject to Shodan’s terms of service. + +> **Not included:** embedded live-news YouTube grids or a built-in Gemini AI analyst panel — use the **OpenClaw / agent channel** for AI-assisted analysis instead. --- ## Interesting Use Cases * **Track Air Force One**, the private jets of billionaires and dictators, and every military tanker, ISR, and fighter broadcasting ADS-B. Air Force One and all of the accompanying Presidential/Vice Presidential planes are highlighted and monitored from the moment they leave the ground. -* **Connect an AI agent as a co-analyst** through ShadowBroker's HMAC-signed agentic command channel — supports OpenClaw and any other agent that speaks the protocol (Claude, GPT, LangChain, custom). The agent gets full read/write access to all 35+ data layers, pin placement, map control, SAR ground-change, mesh networking, and alert delivery. It sees everything the operator sees and can take actions on the map in real time. +* **Connect an AI agent as a co-analyst** through ShadowBroker's HMAC-signed agentic command channel — supports OpenClaw and any other agent that speaks the protocol (Claude, GPT, LangChain, custom). The agent gets full read/write access to all 40+ data layers, pin placement, map control, SAR ground-change, mesh networking, and alert delivery. It sees everything the operator sees and can take actions on the map in real time. * **Communicate on the InfoNet testnet** — The first decentralized intelligence mesh built into an OSINT tool. Obfuscated messaging with gate personas, Dead Drop peer-to-peer exchange, and a built-in terminal CLI. No accounts, no signup. Privacy is not guaranteed yet — this is an experimental testnet — but the protocol is live and being hardened. * **Right-click anywhere on Earth** for a country dossier (head of state, population, languages), Wikipedia summary, and the latest Sentinel-2 satellite photo at 10m resolution * **Click a KiwiSDR node** and tune into live shortwave radio directly in the dashboard. Click a police scanner feed and eavesdrop in one click. @@ -55,6 +57,11 @@ ShadowBroker includes an optional Shodan connector for operator-supplied API acc * **Track trains** across the US (Amtrak) and Europe (DigiTraffic) in real time * **Estimate where US aircraft carriers are** using automated GDELT news scraping — no other open tool does this * **Search internet-connected devices worldwide** via Shodan — cameras, SCADA systems, databases — plotted as a live overlay on the map +* **Run a full recon toolkit** from the left sidebar — IP geolocation, DNS, RDAP/WHOIS, certificate transparency, BGP/ASN, OFAC sanctions search, CVE lookup, Tor/OTX threat checks, and subnet sweeps (InternetDB proxied server-side) +* **Expand an entity graph** when you select an aircraft, vessel, company, or IP — Wikidata + OFAC + live store cross-links rendered in the Entity Graph panel +* **Monitor supply-chain risk** — Tier 1/2 semiconductor and battery fabs scored against nearby earthquakes, wildfires, and conflict events (SCM panel) +* **Toggle malware C2 hotspots** — abuse.ch Feodo Tracker + URLhaus feeds mapped by country (opt-in layer) +* **Overlay global submarine cables** — static TeleGeography-derived cable routes (opt-in layer) --- @@ -239,11 +246,26 @@ The first decentralized intelligence communication and governance layer built di > **Experimental Testnet — No Privacy Guarantee:** InfoNet messages are obfuscated but NOT end-to-end encrypted. The Mesh network (Meshtastic/APRS) is NOT private — radio transmissions are inherently public. The privacy primitive contracts are scaffolded but not yet wired. Do not send anything sensitive on any channel. Treat all channels as open and public for now. -### 🔍 Shodan Device Search (NEW in v0.9.6) +### 🔍 Recon Toolkit & Shodan (Osiris-derived, security-first) -* **Internet Device Search** — Query Shodan directly from ShadowBroker. Search by keyword, CVE, port, or service — results plotted as a live overlay on the map +Adapted from the [OSIRIS](https://github.com/simplifaisoul/osiris) recon stack (MIT) with ShadowBroker’s proxy model. Attribution: `backend/third_party/osiris/NOTICE.md`. + +**Recon Toolkit** (left sidebar — local operator only): + +* **IP / DNS / WHOIS** — ip-api.com geolocation, Google DNS-over-HTTPS, RDAP registrant data with optional HTTP security header scoring +* **Certificates & BGP** — crt.sh subdomain discovery, bgpview.io ASN/prefix lookups +* **Threat intel** — AlienVault OTX pulses, Tor exit-node checks, optional per-IP/domain reputation +* **Sanctions** — OpenSanctions `us_ofac_sdn` index (CC-BY); cross-checks on WHOIS entities and IP ISP/org strings +* **CVE / MAC / GitHub / leaks** — MITRE CVE API, MAC vendor lookup, GitHub profile recon, public breach checks +* **IP sweep** — `/api/osint/sweep/scan` geolocates a target /24–/32 and proxies Shodan InternetDB host discovery server-side (browser never contacts InternetDB directly) +* **SSRF guard** — Private, loopback, link-local, and metadata hostnames are blocked before any user-supplied fetch + +**Entity graph** — Select any map entity to open the Entity Graph panel (`GET /api/entity/expand`). Resolves aircraft, vessels, companies, persons, IPs, and countries into a node/link graph (Wikidata SPARQL + OFAC + in-memory flight/ship store). + +**Shodan overlay** (unchanged): + +* **Internet Device Search** — Query Shodan with your own API key; results plotted as a live overlay * **Configurable Markers** — Shape, color, and size customization for Shodan results -* **Operator-Supplied API** — Uses your own `SHODAN_API_KEY`; results rendered as a local investigative overlay ### 🛩️ Aviation Tracking @@ -331,11 +353,12 @@ The first decentralized intelligence communication and governance layer built di ### 📷 Surveillance -* **CCTV Mesh** — 11,000+ live traffic cameras from 13 sources across 6 countries: +* **CCTV Mesh** — 22,000+ live traffic cameras from 21 ingestors across 10 countries (US, UK, Canada, Australia, Austria, Spain, Singapore, Netherlands when NDW feed is up, plus OSM): * 🇬🇧 Transport for London JamCams * 🇺🇸 NYC DOT, Austin TX (TxDOT) * 🇺🇸 California (12 Caltrans districts), Washington State (WSDOT), Georgia DOT, Illinois DOT, Michigan DOT * 🇪🇸 Spain DGT National (20 cities), Madrid City (357 cameras via KML) + * 🇦🇹 Austria ASFINAG motorway webcams * 🇸🇬 Singapore LTA * 🌍 Windy Webcams * **Feed Rendering** — Automatic detection & rendering of video, MJPEG, HLS, embed, satellite tile, and image feeds @@ -356,6 +379,11 @@ The first decentralized intelligence communication and governance layer built di * **Data Center Mapping** — 2,000+ global data centers plotted from a curated dataset. Clustered purple markers with server-rack icons. Click for operator, location, and automatic internet outage cross-referencing by country. * **Military Bases** — Global military installation and missile facility database (NEW) * **Power Plants** — 35,000+ global power plants from the WRI database (NEW) +* **Submarine Cables** — Global undersea cable routes from static TeleGeography-derived GeoJSON (`frontend/public/data/submarine-cables.json`). Opt-in line overlay. +* **Malware C2 Layer** — Botnet C2 servers (Feodo Tracker) and recent malware URLs (URLhaus) from abuse.ch, refreshed on the slow tier when the layer is enabled. +* **SCM Supplier Risk** — Tier 1/2 fabs and battery plants (TSMC, Samsung, CATL, etc.) cross-referenced against earthquakes, FIRMS fires, and GDELT conflict proximity. Alerts in the SCM panel; optional map layer. +* **Cyber Threats Feed** — Recent CISA Known Exploited Vulnerabilities (KEV) entries exposed via `/api/cyber-threats` and the layer toggle. +* **Country Risk Index** — Static geopolitical risk scores with USGS earthquake enrichment via `/api/country-risk`. ### 🌐 Additional Layers & Tools @@ -381,7 +409,7 @@ v0.9.7 turns ShadowBroker from a dashboard a human watches into an intelligence **Capabilities:** -* **Full Telemetry Access** — The agent queries all 35+ data layers: flights, ships, satellites, SIGINT, conflict events, earthquakes, fires, wastewater, prediction markets, and more. Fast and slow tier endpoints return enriched data with geographic coordinates, timestamps, and source attribution. +* **Full Telemetry Access** — The agent queries all 40+ data layers: flights, ships, satellites, SIGINT, conflict events, earthquakes, fires, wastewater, malware/C2, SCM overlays, prediction markets, and more. Fast and slow tier endpoints return enriched data with geographic coordinates, timestamps, and source attribution. * **AI Intel Pins** — Place color-coded investigation markers directly on the operator's map. 14 pin categories (threat, anomaly, military, maritime, aviation, SIGINT, infrastructure, etc.) with confidence scores, TTL expiry, source URLs, and batch placement up to 100 pins at once. * **Map Control** — Fly the operator's map view to any coordinate, trigger satellite imagery lookups, and open region dossiers. The agent can direct the operator's attention to specific locations in real time. * **SAR Ground-Change** — Query SAR anomaly feeds, inspect pin details, manage AOIs, and fly the map to watch areas. The agent can monitor for ground deformation, flood extent, or damage and promote anomalies to pins. @@ -543,9 +571,19 @@ ShadowBroker v0.9.7 is composed of three vertically-stacked planes — the **Ope | [GDELT Project](https://www.gdeltproject.org) | Global conflict events | ~6h | No | | [DeepState Map](https://deepstatemap.live) | Ukraine frontline | ~30min | No | | [Shodan](https://www.shodan.io) | Internet-connected device search | On-demand | **Yes** | +| [OpenSanctions](https://www.opensanctions.org) | OFAC SDN sanctions index (recon + entity graph) | 24h cache | No | +| [abuse.ch Feodo + URLhaus](https://abuse.ch) | Malware C2 / distribution URLs | ~5min (opt-in layer) | No | +| [CISA KEV](https://www.cisa.gov/known-exploited-vulnerabilities-catalog) | Known exploited CVEs | ~5min (opt-in layer) | No | +| [ip-api.com](https://ip-api.com) | IP geolocation (recon, entity graph) | On-demand | No | +| [Google Public DNS](https://dns.google) | DNS-over-HTTPS lookups (recon) | On-demand | No | +| [RDAP.org](https://rdap.org) | Domain registration data (recon) | On-demand | No | +| [crt.sh](https://crt.sh) | Certificate transparency (recon) | On-demand | No | +| [bgpview.io](https://bgpview.io) | BGP/ASN routing (recon) | On-demand | No | +| TeleGeography (static) | Submarine cable routes | Static | No | +| [ASFINAG](https://www.asfinag.at) | Austria motorway webcams | ~10min | No | | [Amtrak](https://www.amtrak.com) | US train positions | ~60s | No | | [DigiTraffic](https://www.digitraffic.fi) | European rail positions | ~60s | No | -| [Global Fishing Watch](https://globalfishingwatch.org) | Fishing vessel activity events | ~10min | No | +| [Global Fishing Watch](https://globalfishingwatch.org) | Fishing vessel activity events | ~1hr | **Yes** (`GFW_API_TOKEN`) | | Transport for London, NYC DOT, TxDOT | CCTV cameras (UK, US) | ~10min | No | | Caltrans, WSDOT, GDOT, IDOT, MDOT | CCTV cameras (5 US states) | ~10min | No | | Spain DGT, Madrid City | CCTV cameras (Spain) | ~10min | No | @@ -821,7 +859,7 @@ AIS-catcher decodes VHF radio signals on 161.975 MHz and 162.025 MHz and POSTs d ## 🎛️ Data Layers -All 37 layers are independently toggleable from the left panel: +All 41 layers are independently toggleable from the left panel: | Layer | Default | Description | |---|---|---| @@ -863,6 +901,20 @@ All 37 layers are independently toggleable from the left panel: | VIIRS Nightlights | ❌ OFF | Night-time light change detection | | Power Plants | ❌ OFF | 35,000+ global power plants | | Shodan Overlay | ❌ OFF | Internet device search results | +| Road Freight Trends | ❌ OFF | Sentinel-2 truck-motion trends on major highways (Analyze Here) | +| Submarine Cables | ❌ OFF | Global undersea cable routes (static GeoJSON) | +| Malware C2 | ❌ OFF | abuse.ch Feodo + URLhaus threat points | +| SCM Suppliers | ❌ OFF | Tier 1/2 supply-chain risk markers + panel alerts | +| Cyber Threats | ❌ OFF | Recent CISA KEV entries (stats in slow-tier payload) | +| SAR | ✅ ON | Synthetic aperture radar catalog + anomaly alerts | + +**Recon & entity tools** (not map layers — left sidebar / selection): + +| Tool | Access | Description | +|---|---|---| +| Recon Toolkit | Local operator | DNS, WHOIS, sanctions, BGP, CVE, sweep, etc. via `/api/osint/*` | +| SCM Risk panel | Local operator | Live supplier threat rollup via `/api/scm-suppliers` | +| Entity Graph | Local operator | Graph expansion on selected entities via `/api/entity/expand` | --- @@ -895,7 +947,16 @@ Shadowbroker/ │ │ ├── data_fetcher.py # Core scheduler — orchestrates all data sources │ │ ├── ais_stream.py # AIS WebSocket client (25K+ vessels) │ │ ├── carrier_tracker.py # OSINT carrier position estimator (GDELT news scraping) -│ │ ├── cctv_pipeline.py # 13-source CCTV camera ingestion pipeline +│ │ ├── cctv_pipeline.py # 14-source CCTV camera ingestion pipeline +│ │ ├── ssrf_guard.py # SSRF validation for operator recon fetches +│ │ ├── sanctions/ofac.py # OpenSanctions OFAC SDN index +│ │ ├── osint/lookups.py # Server-side recon lookups (Osiris port) +│ │ ├── osint_intel/resolve.py # Entity graph resolver (Wikidata + OFAC) +│ │ ├── scm/suppliers.py # Supply-chain risk overlay +│ │ ├── intel_feeds/ # Country risk index helpers +│ │ ├── fetchers/malware.py # abuse.ch Feodo + URLhaus +│ │ ├── fetchers/cyber_status.py # CISA KEV feed +│ │ ├── third_party/osiris/ # MIT attribution for Osiris-derived code │ │ ├── geopolitics.py # GDELT + Ukraine frontline + air alerts │ │ ├── region_dossier.py # Right-click country/city intelligence │ │ ├── radio_intercept.py # Police scanner feeds + OpenMHZ @@ -933,7 +994,14 @@ Shadowbroker/ │ │ ├── mesh_reputation.py # Node reputation scoring │ │ ├── mesh_oracle.py # Oracle consensus protocol │ │ └── mesh_secure_storage.py # Secure credential storage +│ ├── routers/ +│ │ ├── osint.py # /api/osint/* recon routes (local operator) +│ │ ├── entity_graph.py # /api/entity/expand +│ │ ├── scm.py # /api/scm-suppliers +│ │ └── intel_feeds.py # /api/malware, /api/cyber-threats, /api/country-risk ├── frontend/ +│ ├── public/data/ +│ │ └── submarine-cables.json # Static undersea cable GeoJSON │ ├── src/ │ │ ├── app/ │ │ │ └── page.tsx # Main dashboard — state, polling, layout @@ -942,7 +1010,11 @@ Shadowbroker/ │ │ ├── MeshChat.tsx # InfoNet / Mesh / Dead Drop chat panel │ │ ├── MeshTerminal.tsx # Draggable CLI terminal │ │ ├── NewsFeed.tsx # SIGINT feed + entity detail panels -│ │ ├── WorldviewLeftPanel.tsx # Data layer toggles (35+ layers) +│ │ ├── WorldviewLeftPanel.tsx # Data layer toggles (40+ layers) +│ │ ├── ShodanPanel.tsx # Shodan device search overlay +│ │ ├── ReconPanel.tsx # Server-side OSINT recon toolkit +│ │ ├── ScmPanel.tsx # Supply-chain risk command panel +│ │ ├── EntityGraphPanel.tsx # Entity graph on map selection │ │ ├── WorldviewRightPanel.tsx # Search + filter sidebar │ │ ├── AdvancedFilterModal.tsx # Airport/country/owner filtering │ │ ├── MapLegend.tsx # Dynamic legend with all icons diff --git a/backend/.env.example b/backend/.env.example index c536109..0d9d8d5 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -100,6 +100,19 @@ AIS_API_KEY= # https://aisstream.io/ — free tier WebSocket key # configured news feeds (kill switch for the news layer). # NEWS_ENABLED=true +# Global Fishing Watch — fishing vessel activity events (Fishing Activity map layer). +# Free API token from https://globalfishingwatch.org/our-apis/tokens +# Without this the fishing_activity layer stays empty. +# GFW_API_TOKEN= +# Optional tuning — GFW can return 40k+ global events; defaults cap fetch for map paint. +# GFW_EVENTS_PAGE_SIZE=500 +# GFW_EVENTS_MAX_PAGES=10 +# GFW_EVENTS_LOOKBACK_DAYS=7 +# GFW_EVENTS_TIMEOUT_S=90 + +# Windy Webcams global CCTV layer — free key from https://api.windy.com/webcams/docs +# WINDY_API_KEY= + # LTA Singapore traffic cameras — leave blank to skip this data source. # LTA_ACCOUNT_KEY= diff --git a/backend/main.py b/backend/main.py index ede77be..88eb351 100644 --- a/backend/main.py +++ b/backend/main.py @@ -366,6 +366,10 @@ ai_intel_router = _load_optional_router("routers.ai_intel") sar_router = _load_optional_router("routers.sar") infonet_router = _load_optional_router("routers.infonet") road_corridors_router = _load_optional_router("routers.road_corridors") +osint_router = _load_optional_router("routers.osint") +scm_router = _load_optional_router("routers.scm") +entity_graph_router = _load_optional_router("routers.entity_graph") +intel_feeds_router = _load_optional_router("routers.intel_feeds") # --------------------------------------------------------------------------- @@ -3643,6 +3647,10 @@ app.include_router(ai_intel_router) app.include_router(sar_router) app.include_router(infonet_router) app.include_router(road_corridors_router) +app.include_router(osint_router) +app.include_router(scm_router) +app.include_router(entity_graph_router) +app.include_router(intel_feeds_router) from services.data_fetcher import update_all_data @@ -3774,6 +3782,8 @@ async def update_layers(update: LayerUpdate, request: Request): old_mesh = is_any_active("sigint_meshtastic") old_aprs = is_any_active("sigint_aprs") old_viirs = is_any_active("viirs_nightlights") + old_datacenters = is_any_active("datacenters") + old_fishing = is_any_active("fishing_activity") # Update only known keys changed = False @@ -3792,6 +3802,8 @@ async def update_layers(update: LayerUpdate, request: Request): new_mesh = is_any_active("sigint_meshtastic") new_aprs = is_any_active("sigint_aprs") new_viirs = is_any_active("viirs_nightlights") + new_datacenters = is_any_active("datacenters") + new_fishing = is_any_active("fishing_activity") # Start/stop AIS stream on transition if old_ships and not new_ships: @@ -3847,6 +3859,18 @@ async def update_layers(update: LayerUpdate, request: Request): _queue_viirs_change_refresh() logger.info("VIIRS change refresh queued (layer enabled)") + if not old_datacenters and new_datacenters: + from services.fetchers.infrastructure import fetch_datacenters + + fetch_datacenters() + logger.info("Datacenters loaded (layer enabled)") + + if not old_fishing and new_fishing: + from services.fetchers.geo import fetch_fishing_activity + + fetch_fishing_activity() + logger.info("Fishing activity refresh queued (layer enabled)") + return {"status": "ok"} @@ -7834,6 +7858,8 @@ _CCTV_PROXY_ALLOWED_HOSTS = { "www.tripcheck.com", "infocar.dgt.es", # Spain DGT "informo.madrid.es", # Madrid + "webcams2.asfinag.at", # Austria ASFINAG motorway cameras + "odo.asfinag.at", # ASFINAG catalog API host "www.windy.com", "imgproxy.windy.com", # Windy preview image CDN "www.lakecountypassage.com", # Illinois Lake County PASSAGE snapshots @@ -7842,6 +7868,14 @@ _CCTV_PROXY_ALLOWED_HOSTS = { "www.nps.gov", # WSDOT-linked Mount Rainier camera "home.lewiscounty.com", # WSDOT partner public camera "www.seattle.gov", # Seattle traffic camera media linked from WSDOT + "511on.ca", # Ontario 511 cameras + "511.alberta.ca", # Alberta 511 cameras + "fl511.com", # Florida 511 cameras + "www.fl511.com", + "webcams.transport.nsw.gov.au", # NSW Live Traffic camera snapshots + "www.livetraffic.com", + "livetraffic.com", + "opendata.ndw.nu", # Netherlands RWS legacy open-data host } @@ -7937,7 +7971,7 @@ def _cctv_proxy_profile_for_url(target_url: str) -> _CCTVProxyProfile: cache_seconds=15, headers={ "Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8", - "Referer": "http://navigator-c2c.dot.ga.gov/", + "Referer": "https://navigator-c2c.dot.ga.gov/", }, ) if host == "511ga.org": @@ -7957,7 +7991,7 @@ def _cctv_proxy_profile_for_url(target_url: str) -> _CCTVProxyProfile: cache_seconds=10, headers={ "Accept": "application/vnd.apple.mpegurl,application/x-mpegURL,video/*,*/*;q=0.8", - "Referer": "http://navigator-c2c.dot.ga.gov/", + "Referer": "https://navigator-c2c.dot.ga.gov/", }, ) if host in {"gettingaroundillinois.com", "cctv.travelmidwest.com"}: @@ -8039,6 +8073,16 @@ def _cctv_proxy_profile_for_url(target_url: str) -> _CCTVProxyProfile: "Referer": "https://informo.madrid.es/", }, ) + if host in {"webcams2.asfinag.at", "odo.asfinag.at"}: + return _CCTVProxyProfile( + name="asfinag-austria", + timeout=(5.0, 15.0), + cache_seconds=60, + headers={ + "Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8", + "Referer": "https://www.asfinag.at/", + }, + ) if host in {"www.windy.com", "imgproxy.windy.com"}: return _CCTVProxyProfile( name="windy-webcams", @@ -8049,6 +8093,56 @@ def _cctv_proxy_profile_for_url(target_url: str) -> _CCTVProxyProfile: "Referer": "https://www.windy.com/", }, ) + if host == "511on.ca": + return _CCTVProxyProfile( + name="ontario-511", + timeout=(5.0, 15.0), + cache_seconds=30, + headers={ + "Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8", + "Referer": "https://511on.ca/", + }, + ) + if host == "511.alberta.ca": + return _CCTVProxyProfile( + name="alberta-511", + timeout=(5.0, 15.0), + cache_seconds=30, + headers={ + "Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8", + "Referer": "https://511.alberta.ca/", + }, + ) + if host in {"fl511.com", "www.fl511.com"}: + return _CCTVProxyProfile( + name="florida-511", + timeout=(5.0, 15.0), + cache_seconds=30, + headers={ + "Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8", + "Referer": "https://fl511.com/", + }, + ) + if host == "webcams.transport.nsw.gov.au": + return _CCTVProxyProfile( + name="nsw-live-traffic", + timeout=(5.0, 12.0), + cache_seconds=60, + headers={ + "Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8", + "Referer": "https://www.livetraffic.com/", + }, + ) + if host in {"opendata.ndw.nu", "www.ndw.nu"}: + return _CCTVProxyProfile( + name="ndw-netherlands", + timeout=(5.0, 12.0), + cache_seconds=120, + headers={ + "Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8", + "Referer": "https://www.ndw.nu/", + }, + ) if host in { "webcam.forkswa.com", "webcam.sunmountainlodge.com", diff --git a/backend/routers/cctv.py b/backend/routers/cctv.py index 599b84c..644cc79 100644 --- a/backend/routers/cctv.py +++ b/backend/routers/cctv.py @@ -47,6 +47,8 @@ _CCTV_PROXY_ALLOWED_HOSTS = { "www.tripcheck.com", "infocar.dgt.es", "informo.madrid.es", + "webcams2.asfinag.at", + "odo.asfinag.at", "www.windy.com", "imgproxy.windy.com", "www.lakecountypassage.com", @@ -55,6 +57,14 @@ _CCTV_PROXY_ALLOWED_HOSTS = { "www.nps.gov", "home.lewiscounty.com", "www.seattle.gov", + "511on.ca", + "511.alberta.ca", + "fl511.com", + "www.fl511.com", + "webcams.transport.nsw.gov.au", + "www.livetraffic.com", + "livetraffic.com", + "opendata.ndw.nu", } @@ -120,7 +130,7 @@ def _cctv_proxy_profile_for_url(target_url: str) -> _CCTVProxyProfile: read_timeout = 18.0 if "/snapshots/" in path else 12.0 return _CCTVProxyProfile(name="gdot-snapshot", timeout=(_CCTV_PROXY_CONNECT_TIMEOUT_S, read_timeout), cache_seconds=15, headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8", - "Referer": "http://navigator-c2c.dot.ga.gov/"}) + "Referer": "https://navigator-c2c.dot.ga.gov/"}) if host == "511ga.org": return _CCTVProxyProfile(name="gdot-511ga-image", timeout=(_CCTV_PROXY_CONNECT_TIMEOUT_S, 12.0), cache_seconds=15, headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8", @@ -128,7 +138,7 @@ def _cctv_proxy_profile_for_url(target_url: str) -> _CCTVProxyProfile: if host.startswith("vss") and host.endswith("dot.ga.gov"): return _CCTVProxyProfile(name="gdot-hls", timeout=(_CCTV_PROXY_CONNECT_TIMEOUT_S, 20.0), cache_seconds=10, headers={"Accept": "application/vnd.apple.mpegurl,application/x-mpegURL,video/*,*/*;q=0.8", - "Referer": "http://navigator-c2c.dot.ga.gov/"}) + "Referer": "https://navigator-c2c.dot.ga.gov/"}) if host in {"gettingaroundillinois.com", "cctv.travelmidwest.com"}: return _CCTVProxyProfile(name="illinois-dot", timeout=(_CCTV_PROXY_CONNECT_TIMEOUT_S, 12.0), cache_seconds=30, headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8"}) @@ -156,10 +166,34 @@ def _cctv_proxy_profile_for_url(target_url: str) -> _CCTVProxyProfile: return _CCTVProxyProfile(name="madrid-city", timeout=(_CCTV_PROXY_CONNECT_TIMEOUT_S, 12.0), cache_seconds=30, headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8", "Referer": "https://informo.madrid.es/"}) + if host in {"webcams2.asfinag.at", "odo.asfinag.at"}: + return _CCTVProxyProfile(name="asfinag-austria", timeout=(_CCTV_PROXY_CONNECT_TIMEOUT_S, 15.0), cache_seconds=60, + headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8", + "Referer": "https://www.asfinag.at/"}) if host in {"www.windy.com", "imgproxy.windy.com"}: return _CCTVProxyProfile(name="windy-webcams", timeout=(_CCTV_PROXY_CONNECT_TIMEOUT_S, 12.0), cache_seconds=60, headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8", "Referer": "https://www.windy.com/"}) + if host == "511on.ca": + return _CCTVProxyProfile(name="ontario-511", timeout=(_CCTV_PROXY_CONNECT_TIMEOUT_S, 15.0), cache_seconds=30, + headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8", + "Referer": "https://511on.ca/"}) + if host == "511.alberta.ca": + return _CCTVProxyProfile(name="alberta-511", timeout=(_CCTV_PROXY_CONNECT_TIMEOUT_S, 15.0), cache_seconds=30, + headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8", + "Referer": "https://511.alberta.ca/"}) + if host in {"fl511.com", "www.fl511.com"}: + return _CCTVProxyProfile(name="florida-511", timeout=(_CCTV_PROXY_CONNECT_TIMEOUT_S, 15.0), cache_seconds=30, + headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8", + "Referer": "https://fl511.com/"}) + if host == "webcams.transport.nsw.gov.au": + return _CCTVProxyProfile(name="nsw-live-traffic", timeout=(_CCTV_PROXY_CONNECT_TIMEOUT_S, 12.0), cache_seconds=60, + headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8", + "Referer": "https://www.livetraffic.com/"}) + if host in {"opendata.ndw.nu", "www.ndw.nu"}: + return _CCTVProxyProfile(name="ndw-netherlands", timeout=(_CCTV_PROXY_CONNECT_TIMEOUT_S, 12.0), cache_seconds=120, + headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8", + "Referer": "https://www.ndw.nu/"}) return _CCTVProxyProfile(name="generic-cctv", timeout=(_CCTV_PROXY_CONNECT_TIMEOUT_S, 8.0), cache_seconds=30, headers={"Accept": "*/*"}) diff --git a/backend/routers/data.py b/backend/routers/data.py index febe068..6593f5d 100644 --- a/backend/routers/data.py +++ b/backend/routers/data.py @@ -502,6 +502,8 @@ async def update_layers(update: LayerUpdate, request: Request): old_mesh = is_any_active("sigint_meshtastic") old_aprs = is_any_active("sigint_aprs") old_viirs = is_any_active("viirs_nightlights") + old_datacenters = is_any_active("datacenters") + old_fishing = is_any_active("fishing_activity") changed = False for key, value in update.layers.items(): if key in active_layers: @@ -514,6 +516,8 @@ async def update_layers(update: LayerUpdate, request: Request): new_mesh = is_any_active("sigint_meshtastic") new_aprs = is_any_active("sigint_aprs") new_viirs = is_any_active("viirs_nightlights") + new_datacenters = is_any_active("datacenters") + new_fishing = is_any_active("fishing_activity") if old_ships and not new_ships: from services.ais_stream import stop_ais_stream stop_ais_stream() @@ -557,6 +561,16 @@ async def update_layers(update: LayerUpdate, request: Request): if not old_viirs and new_viirs: _queue_viirs_change_refresh() logger.info("VIIRS change refresh queued (layer enabled)") + if not old_datacenters and new_datacenters: + from services.fetchers.infrastructure import fetch_datacenters + + fetch_datacenters() + logger.info("Datacenters loaded (layer enabled)") + if not old_fishing and new_fishing: + from services.fetchers.geo import fetch_fishing_activity + + fetch_fishing_activity() + logger.info("Fishing activity refresh queued (layer enabled)") return {"status": "ok"} @@ -759,6 +773,7 @@ async def live_data_slow( "scanners", "weather_alerts", "ukraine_alerts", "air_quality", "volcanoes", "fishing_activity", "psk_reporter", "correlations", "uap_sightings", "wastewater", "crowdthreat", "threat_level", "trending_markets", "road_corridor_trends", + "malware_threats", "cyber_threats", "scm_suppliers", "telegram_osint", ) freshness = get_source_timestamps_snapshot() payload = { @@ -804,6 +819,26 @@ async def live_data_slow( ) if active_layers.get("road_corridor_trends", False) else {"updated_at": None, "corridors": []}, + "malware_threats": ( + d.get("malware_threats") or {"threats": [], "total": 0} + ) + if active_layers.get("malware_c2", False) + else {"threats": [], "total": 0}, + "cyber_threats": ( + d.get("cyber_threats") or {"threats": [], "stats": {}} + ) + if active_layers.get("cyber_threats", False) + else {"threats": [], "stats": {}}, + "scm_suppliers": ( + d.get("scm_suppliers") or {"suppliers": [], "total": 0, "critical_count": 0} + ) + if active_layers.get("scm_suppliers", False) + else {"suppliers": [], "total": 0, "critical_count": 0}, + "telegram_osint": ( + d.get("telegram_osint") or {"posts": [], "total": 0, "geolocated": 0} + ) + if active_layers.get("telegram_osint", True) + else {"posts": [], "total": 0, "geolocated": 0}, "freshness": freshness, } # Issue #288: bbox filter heavy/dense layers only when all four bounds diff --git a/backend/routers/entity_graph.py b/backend/routers/entity_graph.py new file mode 100644 index 0000000..96ad54d --- /dev/null +++ b/backend/routers/entity_graph.py @@ -0,0 +1,30 @@ +"""Entity graph expansion (intel layer).""" +from __future__ import annotations + +from fastapi import APIRouter, Depends, HTTPException, Query, Request + +from auth import require_local_operator +from limiter import limiter +from services.osint_intel.resolve import resolve_entity + +router = APIRouter() + + +@router.get("/api/entity/expand") +@limiter.limit("30/minute") +async def entity_expand( + request: Request, + _: None = Depends(require_local_operator), + type: str = Query(..., min_length=3, max_length=32), + id: str = Query(..., min_length=2, max_length=200), + registration: str | None = Query(default=None, max_length=32), + model: str | None = Query(default=None, max_length=64), + icao24: str | None = Query(default=None, max_length=16), +) -> dict: + props = {"label": id, "registration": registration, "model": model, "icao24": icao24} + try: + return resolve_entity(type, id, props) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + except Exception as exc: + raise HTTPException(status_code=502, detail="Intelligence layer unavailable") from exc diff --git a/backend/routers/intel_feeds.py b/backend/routers/intel_feeds.py new file mode 100644 index 0000000..e1c0899 --- /dev/null +++ b/backend/routers/intel_feeds.py @@ -0,0 +1,122 @@ +"""Malware, cyber threats, and country risk feeds.""" +from __future__ import annotations + +import logging +from urllib.parse import urlparse + +import requests +from fastapi import APIRouter, HTTPException, Query, Request +from fastapi.responses import StreamingResponse +from starlette.background import BackgroundTask + +from limiter import limiter +from services.fetchers._store import get_latest_data_subset_refs +from services.fetchers.telegram_osint import telegram_media_host_allowed +from services.intel_feeds.country_risk import build_country_risk_payload +from services.network_utils import outbound_user_agent + +logger = logging.getLogger(__name__) + +router = APIRouter() + + +@router.get("/api/malware") +@limiter.limit("60/minute") +async def malware_feed(request: Request) -> dict: + snap = get_latest_data_subset_refs("malware_threats") + payload = snap.get("malware_threats") + if isinstance(payload, dict) and payload.get("threats") is not None: + return payload + return {"threats": [], "total": 0, "timestamp": None, "source": "abuse.ch"} + + +@router.get("/api/cyber-threats") +@limiter.limit("60/minute") +async def cyber_threats(request: Request) -> dict: + snap = get_latest_data_subset_refs("cyber_threats") + return snap.get("cyber_threats") or {"threats": [], "stats": {}} + + +@router.get("/api/country-risk") +@limiter.limit("30/minute") +async def country_risk(request: Request) -> dict: + return build_country_risk_payload() + + +@router.get("/api/telegram-feed") +@limiter.limit("30/minute") +async def telegram_feed(request: Request) -> dict: + snap = get_latest_data_subset_refs("telegram_osint") + payload = snap.get("telegram_osint") + if isinstance(payload, dict) and payload.get("posts") is not None: + return payload + return {"posts": [], "total": 0, "geolocated": 0, "timestamp": None} + + +def _infer_telegram_media_type(target_url: str, content_type: str) -> str: + clean_type = str(content_type or "").split(";", 1)[0].strip().lower() + if clean_type and clean_type not in {"application/octet-stream", "binary/octet-stream"}: + return content_type + path = str(urlparse(target_url).path or "").lower() + if path.endswith((".jpg", ".jpeg")): + return "image/jpeg" + if path.endswith(".png"): + return "image/png" + if path.endswith(".webp"): + return "image/webp" + if path.endswith(".gif"): + return "image/gif" + if path.endswith(".mp4"): + return "video/mp4" + if path.endswith(".webm"): + return "video/webm" + return content_type or "application/octet-stream" + + +@router.get("/api/telegram/media") +@limiter.limit("60/minute") +async def telegram_media_proxy(request: Request, url: str = Query(...)) -> StreamingResponse: + """Stream Telegram CDN media for in-app playback (host allowlist only).""" + parsed = urlparse(url) + if parsed.scheme not in ("http", "https"): + raise HTTPException(status_code=400, detail="Invalid scheme") + if not telegram_media_host_allowed(parsed.hostname): + raise HTTPException(status_code=403, detail="Host not allowed") + + headers = { + "User-Agent": ( + f"Mozilla/5.0 (compatible; {outbound_user_agent('telegram-media')}) " + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + ), + "Accept": "*/*", + } + if range_header := request.headers.get("range"): + headers["Range"] = range_header + + try: + resp = requests.get(url, stream=True, timeout=(3, 45), headers=headers) + except requests.RequestException as exc: + logger.warning("Telegram media upstream failure %s: %s", url, exc) + raise HTTPException(status_code=502, detail="Upstream fetch failed") from exc + + if resp.status_code >= 400: + resp.close() + raise HTTPException(status_code=int(resp.status_code), detail=f"Upstream returned {resp.status_code}") + + media_type = _infer_telegram_media_type(url, resp.headers.get("Content-Type", "application/octet-stream")) + response_headers = { + "Cache-Control": "private, max-age=300", + "Accept-Ranges": resp.headers.get("Accept-Ranges", "bytes"), + } + if content_length := resp.headers.get("Content-Length"): + response_headers["Content-Length"] = content_length + if content_range := resp.headers.get("Content-Range"): + response_headers["Content-Range"] = content_range + + return StreamingResponse( + resp.iter_content(chunk_size=65536), + status_code=resp.status_code, + media_type=media_type, + headers=response_headers, + background=BackgroundTask(resp.close), + ) diff --git a/backend/routers/osint.py b/backend/routers/osint.py new file mode 100644 index 0000000..d1f819f --- /dev/null +++ b/backend/routers/osint.py @@ -0,0 +1,151 @@ +"""Operator OSINT recon routes (server-side proxies, SSRF guarded).""" +from __future__ import annotations + +from fastapi import APIRouter, Depends, HTTPException, Query, Request +from pydantic import BaseModel, Field + +from auth import require_local_operator +from limiter import limiter +from services.osint import lookups + +router = APIRouter(dependencies=[Depends(require_local_operator)]) + +_ALLOWED_SCHEMAS = { + "Person", + "Organization", + "Company", + "Vessel", + "Airplane", + "LegalEntity", +} + + +class SweepScanRequest(BaseModel): + ip: str = Field(min_length=7, max_length=45) + cidr: int = Field(default=24, ge=24, le=32) + + +def _bad_request(exc: ValueError) -> HTTPException: + return HTTPException(status_code=400, detail=str(exc)) + + +@router.get("/api/osint/ip") +@limiter.limit("20/minute") +async def osint_ip(request: Request, ip: str = Query(..., min_length=7, max_length=45)) -> dict: + try: + return lookups.lookup_ip(ip) + except ValueError as exc: + raise _bad_request(exc) from exc + + +@router.get("/api/osint/dns") +@limiter.limit("20/minute") +async def osint_dns(request: Request, domain: str = Query(..., min_length=4, max_length=253)) -> dict: + try: + return lookups.lookup_dns(domain) + except ValueError as exc: + raise _bad_request(exc) from exc + + +@router.get("/api/osint/whois") +@limiter.limit("20/minute") +async def osint_whois(request: Request, domain: str = Query(..., min_length=4, max_length=253)) -> dict: + try: + return lookups.lookup_whois(domain) + except ValueError as exc: + raise _bad_request(exc) from exc + + +@router.get("/api/osint/certs") +@limiter.limit("20/minute") +async def osint_certs(request: Request, domain: str = Query(..., min_length=4, max_length=253)) -> dict: + try: + return lookups.lookup_certs(domain) + except ValueError as exc: + raise _bad_request(exc) from exc + + +@router.get("/api/osint/threats") +@limiter.limit("20/minute") +async def osint_threats(request: Request, query: str | None = Query(default=None, max_length=253)) -> dict: + return lookups.lookup_threats(query) + + +@router.get("/api/osint/bgp") +@limiter.limit("20/minute") +async def osint_bgp(request: Request, query: str = Query(..., min_length=2, max_length=64)) -> dict: + try: + return lookups.lookup_bgp(query) + except ValueError as exc: + raise _bad_request(exc) from exc + + +@router.get("/api/osint/sanctions") +@limiter.limit("20/minute") +async def osint_sanctions( + request: Request, + query: str = Query(..., min_length=4, max_length=200), + schema: str | None = Query(default=None), + limit: int = Query(default=25, ge=1, le=100), +) -> dict: + if schema and schema not in _ALLOWED_SCHEMAS: + raise HTTPException(status_code=400, detail=f"Invalid schema. Allowed: {', '.join(sorted(_ALLOWED_SCHEMAS))}") + return lookups.lookup_sanctions(query, schema=schema, limit=limit) + + +@router.get("/api/osint/cve") +@limiter.limit("30/minute") +async def osint_cve(request: Request, cve: str = Query(..., min_length=10, max_length=32)) -> dict: + try: + return lookups.lookup_cve(cve) + except ValueError as exc: + raise HTTPException(status_code=404 if "not found" in str(exc).lower() else 400, detail=str(exc)) from exc + + +@router.get("/api/osint/mac") +@limiter.limit("20/minute") +async def osint_mac(request: Request, mac: str = Query(..., min_length=5, max_length=32)) -> dict: + return lookups.lookup_mac(mac) + + +@router.get("/api/osint/github") +@limiter.limit("20/minute") +async def osint_github(request: Request, username: str = Query(..., min_length=1, max_length=64)) -> dict: + try: + return lookups.lookup_github(username) + except ValueError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + + +@router.get("/api/osint/leaks") +@limiter.limit("10/minute") +async def osint_leaks(request: Request, email: str = Query(..., min_length=5, max_length=254)) -> dict: + try: + return lookups.lookup_leaks(email) + except ValueError as exc: + raise _bad_request(exc) from exc + + +@router.get("/api/osint/sweep") +@limiter.limit("5/minute") +async def osint_sweep_init( + request: Request, + ip: str = Query(..., min_length=7, max_length=45), + cidr: int = Query(default=24, ge=24, le=32), +) -> dict: + try: + return lookups.sweep_init(ip, cidr) + except ValueError as exc: + raise _bad_request(exc) from exc + + +@router.post("/api/osint/sweep/scan") +@limiter.limit("3/minute") +async def osint_sweep_scan(request: Request, payload: SweepScanRequest) -> dict: + try: + subnet = lookups.subnet_start_for(payload.ip, payload.cidr) + scan = lookups.sweep_scan(subnet, payload.cidr) + init = lookups.sweep_init(payload.ip, payload.cidr) + return {**init, **scan, "subnet": f"{subnet}/{payload.cidr}"} + except ValueError as exc: + raise _bad_request(exc) from exc diff --git a/backend/routers/scm.py b/backend/routers/scm.py new file mode 100644 index 0000000..43b6ed8 --- /dev/null +++ b/backend/routers/scm.py @@ -0,0 +1,16 @@ +"""Supply-chain risk overlay.""" +from __future__ import annotations + +from fastapi import APIRouter, Depends, Request + +from auth import require_local_operator +from limiter import limiter +from services.scm.suppliers import build_scm_payload + +router = APIRouter() + + +@router.get("/api/scm-suppliers") +@limiter.limit("30/minute") +async def scm_suppliers(request: Request, _: None = Depends(require_local_operator)) -> dict: + return build_scm_payload() diff --git a/backend/services/api_settings.py b/backend/services/api_settings.py index 8e5265b..49b2ed6 100644 --- a/backend/services/api_settings.py +++ b/backend/services/api_settings.py @@ -51,6 +51,15 @@ API_REGISTRY = [ "url": "https://aisstream.io/", "required": True, }, + { + "id": "gfw_api_token", + "env_key": "GFW_API_TOKEN", + "name": "Global Fishing Watch", + "description": "Bearer token for Global Fishing Watch fishing-vessel activity events (Fishing Activity map layer). Free registration at globalfishingwatch.org.", + "category": "Maritime", + "url": "https://globalfishingwatch.org/our-apis/", + "required": False, + }, { "id": "adsb_lol", "env_key": None, diff --git a/backend/services/cctv_pipeline.py b/backend/services/cctv_pipeline.py index 380b0a0..3f2d352 100644 --- a/backend/services/cctv_pipeline.py +++ b/backend/services/cctv_pipeline.py @@ -17,6 +17,9 @@ _KNOWN_CCTV_MEDIA_HOST_ALIASES = { # Trusted upstream occasionally publishes a typo for this Georgia camera # host. Normalize it at ingest so the proxy and client stay consistent. "navigatos-c2c.dot.ga.gov": "navigator-c2c.dot.ga.gov", + # TravelIQ staging hosts occasionally appear in 511 catalog metadata. + "on.stage.traveliq.co": "511on.ca", + "ab.stage.traveliq.co": "511.alberta.ca", } _POINT_WKT_RE = re.compile( @@ -40,6 +43,17 @@ def _normalize_cctv_media_url(raw_url: str) -> str: return urlunparse(parsed._replace(netloc=netloc)) +def _ensure_https_url(raw_url: str) -> str: + """Upgrade http:// media/catalog URLs to https:// at ingest time.""" + candidate = _normalize_cctv_media_url(str(raw_url or "").strip()) + if not candidate: + return "" + parsed = urlparse(candidate) + if parsed.scheme.lower() == "http": + return urlunparse(parsed._replace(scheme="https")) + return candidate + + def _looks_like_direct_cctv_media_url(url: str) -> bool: candidate = str(url or "").strip().lower() if not candidate.startswith(("http://", "https://")): @@ -93,6 +107,165 @@ def _parse_wkt_point(raw_point: str) -> tuple[float | None, float | None]: return lat, lon +def _fetch_traveliq_v2_cameras( + *, + api_url: str, + base_url: str, + id_prefix: str, + source_agency: str, +) -> List[Dict[str, Any]]: + """Parse TravelIQ-style GET /api/v2/get/cameras feeds (Ontario, Alberta).""" + resp = fetch_with_curl( + api_url, + timeout=30, + headers={"Accept": "application/json"}, + ) + if not resp or resp.status_code != 200: + logger.error( + "%s CCTV fetch failed: HTTP %s", + source_agency, + resp.status_code if resp else "no response", + ) + return [] + + data = resp.json() + if not isinstance(data, list): + return [] + + cameras: List[Dict[str, Any]] = [] + for cam in data: + if not isinstance(cam, dict): + continue + try: + lat = float(cam.get("Latitude")) + lon = float(cam.get("Longitude")) + except (TypeError, ValueError): + continue + + site_id = cam.get("Id") + location = str(cam.get("Location") or cam.get("Roadway") or "Camera")[:120] + views = cam.get("Views") or [] + if not views: + continue + + for view in views: + if not isinstance(view, dict): + continue + status = str(view.get("Status") or "enabled").strip().lower() + if status and status not in {"enabled", "active"}: + continue + media_url = _ensure_https_url( + urljoin(base_url, str(view.get("Url") or "").strip()) + ) + if not media_url: + continue + view_id = view.get("Id") or site_id + if site_id is None or view_id is None: + continue + label = str(view.get("Description") or location or "Camera")[:120] + cameras.append( + { + "id": f"{id_prefix}-{site_id}-{view_id}", + "source_agency": source_agency, + "lat": lat, + "lon": lon, + "direction_facing": label, + "media_url": media_url, + "media_type": "image", + "refresh_rate_seconds": 60, + } + ) + return cameras + + +def _fetch_511_datatables_cameras( + *, + list_url: str, + base_url: str, + id_prefix: str, + source_agency: str, + referer: str, + page_size: int = 500, +) -> List[Dict[str, Any]]: + """Parse 511 DataTables POST /List/GetData/Cameras feeds (Georgia, Florida).""" + cameras: List[Dict[str, Any]] = [] + start = 0 + draw = 1 + while True: + resp = fetch_with_curl( + list_url, + method="POST", + json_data={"draw": draw, "start": start, "length": page_size}, + timeout=30, + headers={ + "Accept": "application/json", + "Referer": referer, + "Origin": base_url.rstrip("/"), + }, + ) + if not resp or resp.status_code != 200: + logger.error( + "%s CCTV fetch failed: HTTP %s", + source_agency, + resp.status_code if resp else "no response", + ) + break + + data = resp.json() + rows = data.get("data") or [] + if not rows: + break + + for row in rows: + if not isinstance(row, dict): + continue + site_id = row.get("id") or row.get("DT_RowId") + location = row.get("location") or row.get("roadway") or source_agency + lat_lng = row.get("latLng") or {} + geography = lat_lng.get("geography") if isinstance(lat_lng, dict) else {} + lat, lon = _parse_wkt_point( + geography.get("wellKnownText") if isinstance(geography, dict) else "" + ) + images = row.get("images") or [] + image = next( + ( + candidate + for candidate in images + if str(candidate.get("imageUrl") or "").strip() + and not bool(candidate.get("blocked")) + ), + None, + ) + if not (site_id and image and lat is not None and lon is not None): + continue + media_url = _ensure_https_url( + urljoin(base_url, str(image.get("imageUrl") or "").strip()) + ) + if not media_url: + continue + cameras.append( + { + "id": f"{id_prefix}-{site_id}", + "source_agency": source_agency, + "lat": lat, + "lon": lon, + "direction_facing": str(location)[:120], + "media_url": media_url, + "media_type": "image", + "refresh_rate_seconds": 60, + } + ) + + start += len(rows) + draw += 1 + total = int(data.get("recordsTotal") or 0) + if total and start >= total: + break + if not total and len(rows) < page_size: + break + return cameras + + def init_db(): DB_PATH.parent.mkdir(parents=True, exist_ok=True) conn = sqlite3.connect(str(DB_PATH)) @@ -169,7 +342,7 @@ class BaseCCTVIngestor(ABC): cam.get("lat"), cam.get("lon"), cam.get("direction_facing", "Unknown"), - cam.get("media_url"), + _ensure_https_url(cam.get("media_url", "")), cam.get("media_type", _detect_media_type(cam.get("media_url", ""))), cam.get("refresh_rate_seconds", 60), ), @@ -454,77 +627,14 @@ class WSDOTIngestor(BaseCCTVIngestor): class GeorgiaDOTIngestor(BaseCCTVIngestor): """Georgia cameras via the public 511GA list feed.""" - URL = "https://511ga.org/List/GetData/Cameras" - BASE_URL = "https://511ga.org" - PAGE_SIZE = 500 - def fetch_data(self) -> List[Dict[str, Any]]: - cameras = [] - start = 0 - draw = 1 - while True: - resp = fetch_with_curl( - self.URL, - method="POST", - json_data={"draw": draw, "start": start, "length": self.PAGE_SIZE}, - timeout=30, - headers={ - "Accept": "application/json", - "Referer": "https://511ga.org/cctv", - "Origin": "https://511ga.org", - }, - ) - if not resp or resp.status_code != 200: - logger.error( - "Georgia CCTV fetch failed: HTTP %s", - resp.status_code if resp else "no response", - ) - break - data = resp.json() - rows = data.get("data") or [] - if not rows: - break - for row in rows: - site_id = row.get("id") or row.get("DT_RowId") - location = row.get("location") or row.get("roadway") or "GA Camera" - lat_lng = row.get("latLng") or {} - geography = lat_lng.get("geography") if isinstance(lat_lng, dict) else {} - lat, lon = _parse_wkt_point(geography.get("wellKnownText") if isinstance(geography, dict) else "") - images = row.get("images") or [] - image = next( - ( - candidate - for candidate in images - if str(candidate.get("imageUrl") or "").strip() - and not bool(candidate.get("blocked")) - ), - None, - ) - if not (site_id and image and lat is not None and lon is not None): - continue - media_url = _normalize_cctv_media_url( - urljoin(self.BASE_URL, str(image.get("imageUrl") or "").strip()) - ) - cameras.append( - { - "id": f"GDOT-{site_id}", - "source_agency": "Georgia DOT", - "lat": lat, - "lon": lon, - "direction_facing": str(location)[:120], - "media_url": media_url, - "media_type": "image", - "refresh_rate_seconds": 60, - } - ) - start += len(rows) - draw += 1 - total = int(data.get("recordsTotal") or 0) - if total and start >= total: - break - if not total and len(rows) < self.PAGE_SIZE: - break - return cameras + return _fetch_511_datatables_cameras( + list_url="https://511ga.org/List/GetData/Cameras", + base_url="https://511ga.org", + id_prefix="GDOT", + source_agency="Georgia DOT", + referer="https://511ga.org/cctv", + ) class IllinoisDOTIngestor(BaseCCTVIngestor): @@ -1009,30 +1119,66 @@ def _extract_img_src(html_fragment: str): return None +class AsfinagIngestor(BaseCCTVIngestor): + """Austria ASFINAG motorway webcams (Osiris port).""" + + API_URL = "https://odo.asfinag.at/odo/rest/sec/resource/001/json/webcams?language=atDE" + HEADERS = { + "User-Agent": "Shadowbroker-CCTV/1.0", + "Accept": "application/json", + "Referer": "https://www.asfinag.at/", + "Authorization": "Basic bWFwX3dpZGdldDp0ZWdkaXc=", + } + + def fetch_data(self) -> List[Dict[str, Any]]: + try: + response = fetch_with_curl(self.API_URL, timeout=15, headers=self.HEADERS) + response.raise_for_status() + payload = response.json() + except Exception as exc: + logger.error("AsfinagIngestor: fetch failed: %s", exc) + return [] + if not isinstance(payload, list): + return [] + cameras: List[Dict[str, Any]] = [] + for cam in payload: + cam_id = cam.get("wcs_id") + lat = cam.get("wgs84_lat") + lon = cam.get("wgs84_lon") + image_url = cam.get("url_campic") + if not cam_id or lat is None or lon is None or not image_url: + continue + if str(cam_id).startswith("Utinform"): + continue + label = cam.get("position_txt") or cam.get("direction_txt") or "ASFINAG Webcam" + secure_url = _ensure_https_url(image_url) + if not secure_url: + continue + cameras.append( + { + "id": f"ASFINAG-{cam_id}", + "source_agency": "ASFINAG Austria", + "lat": float(lat), + "lon": float(lon), + "direction_facing": label, + "media_url": secure_url, + "media_type": "image", + "refresh_rate_seconds": 300, + } + ) + logger.info("AsfinagIngestor: parsed %s cameras", len(cameras)) + return cameras + + class MadridCityIngestor(BaseCCTVIngestor): """Madrid City Hall traffic cameras from datos.madrid.es KML feed.""" - KML_URL_HTTPS = "https://datos.madrid.es/egob/catalogo/202088-0-trafico-camaras.kml" - KML_URL_HTTP = "http://datos.madrid.es/egob/catalogo/202088-0-trafico-camaras.kml" + KML_URL = "https://datos.madrid.es/egob/catalogo/202088-0-trafico-camaras.kml" def _fetch_kml(self): - """Prefer HTTPS; fall back to legacy HTTP if the catalog is HTTP-only (#363).""" - last_error: Exception | None = None - for url in (self.KML_URL_HTTPS, self.KML_URL_HTTP): - try: - response = fetch_with_curl(url, timeout=20) - response.raise_for_status() - if url == self.KML_URL_HTTP: - logger.warning( - "MadridCityIngestor: HTTPS KML unavailable, using HTTP catalog feed" - ) - return response - except Exception as e: - last_error = e - logger.debug("MadridCityIngestor: KML fetch failed for %s: %s", url, e) - if last_error is not None: - raise last_error - raise RuntimeError("Madrid KML fetch failed") + response = fetch_with_curl(self.KML_URL, timeout=20) + response.raise_for_status() + return response def fetch_data(self) -> List[Dict[str, Any]]: import defusedxml.ElementTree as ET @@ -1074,6 +1220,9 @@ class MadridCityIngestor(BaseCCTVIngestor): if desc_el is not None and desc_el.text: image_url = _extract_img_src(desc_el.text) + if not image_url: + continue + image_url = _ensure_https_url(image_url) if not image_url: continue @@ -1095,6 +1244,153 @@ class MadridCityIngestor(BaseCCTVIngestor): return cameras +class Ontario511Ingestor(BaseCCTVIngestor): + """Ontario highway cameras via 511on.ca TravelIQ API.""" + + def fetch_data(self) -> List[Dict[str, Any]]: + return _fetch_traveliq_v2_cameras( + api_url="https://511on.ca/api/v2/get/cameras", + base_url="https://511on.ca", + id_prefix="ON511", + source_agency="511 Ontario", + ) + + +class Alberta511Ingestor(BaseCCTVIngestor): + """Alberta highway cameras via 511 Alberta TravelIQ API.""" + + def fetch_data(self) -> List[Dict[str, Any]]: + return _fetch_traveliq_v2_cameras( + api_url="https://511.alberta.ca/api/v2/get/cameras", + base_url="https://511.alberta.ca", + id_prefix="AB511", + source_agency="511 Alberta", + ) + + +class Florida511Ingestor(BaseCCTVIngestor): + """Florida cameras via FL511 DataTables feed (~4,800 sites).""" + + def fetch_data(self) -> List[Dict[str, Any]]: + return _fetch_511_datatables_cameras( + list_url="https://fl511.com/List/GetData/Cameras", + base_url="https://fl511.com", + id_prefix="FL511", + source_agency="Florida 511", + referer="https://fl511.com/", + ) + + +class AustraliaLiveTrafficIngestor(BaseCCTVIngestor): + """NSW / Australia live traffic cameras via Transport for NSW JSON feed.""" + + URL = "https://www.livetraffic.com/datajson/all-feeds-web.json" + + def fetch_data(self) -> List[Dict[str, Any]]: + resp = fetch_with_curl(self.URL, timeout=35, headers={"Accept": "application/json"}) + if not resp or resp.status_code != 200: + logger.error( + "Australia Live Traffic CCTV fetch failed: HTTP %s", + resp.status_code if resp else "no response", + ) + return [] + + data = resp.json() + if not isinstance(data, list): + return [] + + cameras: List[Dict[str, Any]] = [] + for item in data: + if not isinstance(item, dict) or item.get("eventType") != "liveCams": + continue + geometry = item.get("geometry") if isinstance(item.get("geometry"), dict) else {} + coords = geometry.get("coordinates") if isinstance(geometry.get("coordinates"), list) else [] + if len(coords) < 2: + continue + try: + lon = float(coords[0]) + lat = float(coords[1]) + except (TypeError, ValueError): + continue + + props = item.get("properties") if isinstance(item.get("properties"), dict) else {} + media_url = _ensure_https_url(str(props.get("href") or "").strip()) + if not media_url: + continue + + cam_id = str(item.get("path") or props.get("id") or len(cameras)).strip("/") + label = str(props.get("title") or props.get("headline") or "Australia Camera")[:120] + cameras.append( + { + "id": f"AUS-{cam_id}", + "source_agency": "NSW Live Traffic", + "lat": lat, + "lon": lon, + "direction_facing": label, + "media_url": media_url, + "media_type": "image", + "refresh_rate_seconds": 120, + } + ) + logger.info("AustraliaLiveTrafficIngestor: parsed %s cameras", len(cameras)) + return cameras + + +class NetherlandsRWSIngestor(BaseCCTVIngestor): + """Netherlands Rijkswaterstaat cameras from legacy NDW open-data JSON. + + The opendata.ndw.nu/cameras.json feed Osiris used is often offline; when + unavailable this ingestor returns an empty set and logs a warning. + """ + + URL = "https://opendata.ndw.nu/cameras.json" + MAX_CAMERAS = 1200 + + def fetch_data(self) -> List[Dict[str, Any]]: + resp = fetch_with_curl(self.URL, timeout=25, headers={"Accept": "application/json"}) + if not resp or resp.status_code != 200: + logger.warning( + "Netherlands RWS cameras.json unavailable (HTTP %s) — " + "NDW retired this open-data endpoint; no cameras ingested", + resp.status_code if resp else "no response", + ) + return [] + + data = resp.json() + if not isinstance(data, list): + return [] + + cameras: List[Dict[str, Any]] = [] + for i, cam in enumerate(data[: self.MAX_CAMERAS]): + if not isinstance(cam, dict): + continue + lat = cam.get("lat") if cam.get("lat") is not None else cam.get("latitude") + lon = cam.get("lng") if cam.get("lng") is not None else cam.get("longitude") + media_url = _ensure_https_url( + str(cam.get("imageUrl") or cam.get("feed_url") or cam.get("url") or "").strip() + ) + if lat is None or lon is None or not media_url: + continue + try: + lat_f, lon_f = float(lat), float(lon) + except (TypeError, ValueError): + continue + cameras.append( + { + "id": f"NLRWS-{cam.get('id') or i}", + "source_agency": "Rijkswaterstaat", + "lat": lat_f, + "lon": lon_f, + "direction_facing": str(cam.get("name") or "Netherlands Camera")[:120], + "media_url": media_url, + "media_type": "image", + "refresh_rate_seconds": 120, + } + ) + logger.info("NetherlandsRWSIngestor: parsed %s cameras", len(cameras)) + return cameras + + def _detect_media_type(url: str) -> str: """Detect the media type from a camera URL for proper frontend rendering.""" if not url: @@ -1113,29 +1409,40 @@ def _detect_media_type(url: str) -> str: return "image" +def scheduled_cctv_ingestors() -> List[tuple["BaseCCTVIngestor", str]]: + """Canonical list of CCTV ingestors for startup, scheduler, and DB seeding.""" + return [ + (TFLJamCamIngestor(), "cctv_tfl"), + (LTASingaporeIngestor(), "cctv_lta"), + (AustinTXIngestor(), "cctv_atx"), + (NYCDOTIngestor(), "cctv_nyc"), + (CaltransIngestor(), "cctv_caltrans"), + (ColoradoDOTIngestor(), "cctv_codot"), + (WSDOTIngestor(), "cctv_wsdot"), + (GeorgiaDOTIngestor(), "cctv_gdot"), + (IllinoisDOTIngestor(), "cctv_idot"), + (MichiganDOTIngestor(), "cctv_mdot"), + (WindyWebcamsIngestor(), "cctv_windy"), + (DGTNationalIngestor(), "cctv_dgt"), + (MadridCityIngestor(), "cctv_madrid"), + (OSMTrafficCameraIngestor(), "cctv_osm"), + (AsfinagIngestor(), "cctv_asfinag"), + (OSMALPRCameraIngestor(), "cctv_osm_alpr"), + (Ontario511Ingestor(), "cctv_on511"), + (Alberta511Ingestor(), "cctv_ab511"), + (Florida511Ingestor(), "cctv_fl511"), + (AustraliaLiveTrafficIngestor(), "cctv_australia"), + (NetherlandsRWSIngestor(), "cctv_nl_rws"), + ] + + def run_all_ingestors(): """Run all CCTV ingestors synchronously. Used for first-run DB seeding.""" - ingestors = [ - TFLJamCamIngestor(), - LTASingaporeIngestor(), - AustinTXIngestor(), - NYCDOTIngestor(), - CaltransIngestor(), - ColoradoDOTIngestor(), - WSDOTIngestor(), - GeorgiaDOTIngestor(), - IllinoisDOTIngestor(), - MichiganDOTIngestor(), - WindyWebcamsIngestor(), - OSMTrafficCameraIngestor(), - DGTNationalIngestor(), - MadridCityIngestor(), - ] - for ing in ingestors: + for ingestor, _name in scheduled_cctv_ingestors(): try: - ing.ingest() + ingestor.ingest() except Exception as e: - logger.warning(f"Ingestor {ing.__class__.__name__} failed during seed: {e}") + logger.warning(f"Ingestor {ingestor.__class__.__name__} failed during seed: {e}") def get_all_cameras() -> List[Dict[str, Any]]: diff --git a/backend/services/data_fetcher.py b/backend/services/data_fetcher.py index 5a04c26..9e205d5 100644 --- a/backend/services/data_fetcher.py +++ b/backend/services/data_fetcher.py @@ -101,6 +101,10 @@ from services.fetchers.crowdthreat import fetch_crowdthreat # noqa: F401 from services.fetchers.wastewater import fetch_wastewater # noqa: F401 from services.fetchers.sar_catalog import fetch_sar_catalog # noqa: F401 from services.fetchers.sar_products import fetch_sar_products # noqa: F401 +from services.fetchers.malware import fetch_malware_threats # noqa: F401 +from services.fetchers.telegram_osint import fetch_telegram_osint # noqa: F401 +from services.fetchers.cyber_status import fetch_cyber_threats # noqa: F401 +from services.scm.suppliers import fetch_scm_suppliers # noqa: F401 from services.ais_stream import prune_stale_vessels # noqa: F401 logger = logging.getLogger(__name__) @@ -480,6 +484,9 @@ def update_slow_data(): fetch_fishing_activity, fetch_power_plants, fetch_ukraine_air_raid_alerts, + fetch_malware_threats, + fetch_cyber_threats, + fetch_scm_suppliers, ] _run_tasks("slow-tier", slow_funcs) # Run correlation engine after all data is fresh @@ -523,6 +530,15 @@ def _load_cctv_cache_for_startup() -> None: logger.warning("Startup CCTV cache load failed (non-fatal): %s", e) +def _load_static_infrastructure_for_startup() -> None: + """Disk-backed reference layers — instant, no network.""" + for func in (fetch_datacenters, fetch_military_bases, fetch_power_plants): + try: + func() + except Exception as e: + logger.warning("Startup static infrastructure load failed for %s: %s", func.__name__, e) + + def _run_delayed_startup_heavy_refresh() -> None: if _STARTUP_HEAVY_REFRESH_DELAY_S > 0: logger.info( @@ -535,6 +551,7 @@ def _run_delayed_startup_heavy_refresh() -> None: "startup-heavy", [ update_slow_data, + fetch_telegram_osint, fetch_volcanoes, fetch_viirs_change_nodes, fetch_unusual_whales, @@ -573,6 +590,7 @@ def update_all_data(*, startup_mode: bool = False): logger.info("Full data update starting (parallel)...") # Preload Meshtastic map cache immediately (instant, from disk) seed_startup_caches() + _load_static_infrastructure_for_startup() with _data_lock: meshtastic_seeded = bool(latest_data.get("meshtastic_map_nodes")) if startup_mode: @@ -649,22 +667,9 @@ def update_all_data(*, startup_mode: bool = False): # (the scheduled job also runs every 10 min for ongoing refresh). if startup_mode: try: - from services.cctv_pipeline import ( - TFLJamCamIngestor, LTASingaporeIngestor, AustinTXIngestor, - NYCDOTIngestor, CaltransIngestor, ColoradoDOTIngestor, - WSDOTIngestor, GeorgiaDOTIngestor, IllinoisDOTIngestor, - MichiganDOTIngestor, WindyWebcamsIngestor, DGTNationalIngestor, - MadridCityIngestor, OSMTrafficCameraIngestor, get_all_cameras, - ) - from services.cctv_pipeline import OSMALPRCameraIngestor - _startup_ingestors = [ - TFLJamCamIngestor(), LTASingaporeIngestor(), AustinTXIngestor(), - NYCDOTIngestor(), CaltransIngestor(), ColoradoDOTIngestor(), - WSDOTIngestor(), GeorgiaDOTIngestor(), IllinoisDOTIngestor(), - MichiganDOTIngestor(), WindyWebcamsIngestor(), DGTNationalIngestor(), - MadridCityIngestor(), OSMTrafficCameraIngestor(), - OSMALPRCameraIngestor(), - ] + from services.cctv_pipeline import get_all_cameras, scheduled_cctv_ingestors + + _startup_ingestors = [ing for ing, _name in scheduled_cctv_ingestors()] logger.info("Running CCTV ingest at startup (%d ingestors)...", len(_startup_ingestors)) ingest_futures = { _SHARED_EXECUTOR.submit(ing.ingest): ing.__class__.__name__ @@ -800,6 +805,18 @@ def start_scheduler(): misfire_grace_time=120, ) + # Telegram OSINT — hourly t.me/s channel scrape (kept off the 5-minute slow tier). + _telegram_interval_m = max(15, int(os.environ.get("TELEGRAM_OSINT_INTERVAL_MINUTES", "60"))) + _scheduler.add_job( + lambda: _run_task_with_health(fetch_telegram_osint, "fetch_telegram_osint"), + "interval", + minutes=_telegram_interval_m, + next_run_time=datetime.utcnow() + timedelta(seconds=45), + id="telegram_osint", + max_instances=1, + misfire_grace_time=600, + ) + # Prediction markets — own jittered cadence (Polymarket/Kalshi clearnet egress). # Kept off the fixed 5-minute slow tier so poll timing is less fingerprintable. from services.fetchers.prediction_markets import fetch_prediction_markets @@ -938,39 +955,9 @@ def start_scheduler(): # CCTV pipeline refresh — runs all ingestors, then refreshes in-memory data. # Delay the first run slightly so startup serves cached/DB-backed data first. - from services.cctv_pipeline import ( - TFLJamCamIngestor, - LTASingaporeIngestor, - AustinTXIngestor, - NYCDOTIngestor, - CaltransIngestor, - ColoradoDOTIngestor, - WSDOTIngestor, - GeorgiaDOTIngestor, - IllinoisDOTIngestor, - MichiganDOTIngestor, - WindyWebcamsIngestor, - DGTNationalIngestor, - MadridCityIngestor, - OSMTrafficCameraIngestor, - ) + from services.cctv_pipeline import scheduled_cctv_ingestors - _cctv_ingestors = [ - (TFLJamCamIngestor(), "cctv_tfl"), - (LTASingaporeIngestor(), "cctv_lta"), - (AustinTXIngestor(), "cctv_atx"), - (NYCDOTIngestor(), "cctv_nyc"), - (CaltransIngestor(), "cctv_caltrans"), - (ColoradoDOTIngestor(), "cctv_codot"), - (WSDOTIngestor(), "cctv_wsdot"), - (GeorgiaDOTIngestor(), "cctv_gdot"), - (IllinoisDOTIngestor(), "cctv_idot"), - (MichiganDOTIngestor(), "cctv_mdot"), - (WindyWebcamsIngestor(), "cctv_windy"), - (DGTNationalIngestor(), "cctv_dgt"), - (MadridCityIngestor(), "cctv_madrid"), - (OSMTrafficCameraIngestor(), "cctv_osm"), - ] + _cctv_ingestors = scheduled_cctv_ingestors() def _run_cctv_ingest_cycle(): from services.fetchers._store import is_any_active diff --git a/backend/services/env_check.py b/backend/services/env_check.py index e758dbc..d7e4fbb 100644 --- a/backend/services/env_check.py +++ b/backend/services/env_check.py @@ -46,6 +46,7 @@ _CRITICAL_WARN = { _OPTIONAL = { "AIS_API_KEY": "AIS vessel streaming (ships layer will be empty without it)", + "GFW_API_TOKEN": "Global Fishing Watch fishing-vessel activity (fishing_activity layer)", "LTA_ACCOUNT_KEY": "Singapore LTA traffic cameras (CCTV layer)", "PUBLIC_API_KEY": "Optional client auth for public endpoints (recommended for exposed deployments)", } diff --git a/backend/services/fetchers/_store.py b/backend/services/fetchers/_store.py index 7691110..81eacf1 100644 --- a/backend/services/fetchers/_store.py +++ b/backend/services/fetchers/_store.py @@ -70,6 +70,10 @@ class DashboardData(TypedDict, total=False): sar_anomalies: List[Dict[str, Any]] sar_aoi_coverage: List[Dict[str, Any]] road_corridor_trends: Dict[str, Any] + malware_threats: Dict[str, Any] + cyber_threats: Dict[str, Any] + scm_suppliers: Dict[str, Any] + telegram_osint: Dict[str, Any] # In-memory store @@ -121,6 +125,10 @@ latest_data: DashboardData = { "sar_anomalies": [], "sar_aoi_coverage": [], "road_corridor_trends": {"updated_at": None, "corridors": []}, + "malware_threats": {"threats": [], "total": 0, "timestamp": None}, + "cyber_threats": {"threats": [], "stats": {}}, + "scm_suppliers": {"suppliers": [], "total": 0, "critical_count": 0}, + "telegram_osint": {"posts": [], "total": 0, "geolocated": 0, "timestamp": None}, } # Per-source freshness timestamps @@ -331,6 +339,11 @@ active_layers: dict[str, bool] = { "crowdthreat": False, "sar": True, "road_corridor_trends": False, + "malware_c2": False, + "submarine_cables": False, + "scm_suppliers": False, + "cyber_threats": False, + "telegram_osint": True, } diff --git a/backend/services/fetchers/cyber_status.py b/backend/services/fetchers/cyber_status.py new file mode 100644 index 0000000..5562437 --- /dev/null +++ b/backend/services/fetchers/cyber_status.py @@ -0,0 +1,62 @@ +"""CISA KEV + cyber threat stats (Osiris port).""" +from __future__ import annotations + +import logging +from datetime import datetime, timezone +from typing import Any + +from services.fetchers._store import _data_lock, _mark_fresh, is_any_active, latest_data +from services.network_utils import fetch_with_curl + +logger = logging.getLogger(__name__) + + +def fetch_cyber_threats() -> dict[str, Any]: + if not is_any_active("cyber_threats"): + return latest_data.get("cyber_threats") or {"threats": [], "stats": {}} + + results: dict[str, Any] = {"threats": [], "stats": {}, "timestamp": datetime.now(timezone.utc).isoformat()} + try: + resp = fetch_with_curl( + "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json", + timeout=15, + ) + if resp.status_code == 200: + data = resp.json() + vulns = data.get("vulnerabilities") or [] + results["stats"]["cisa_total"] = len(vulns) + now = datetime.now(timezone.utc) + recent = [] + for v in vulns: + try: + added = datetime.fromisoformat(v.get("dateAdded", "").replace("Z", "+00:00")) + days = (now - added).total_seconds() / 86400 + except Exception: + continue + if days <= 30: + recent.append(v) + recent = recent[:10] + results["threats"] = [ + { + "id": v.get("cveID"), + "name": v.get("vulnerabilityName"), + "vendor": v.get("vendorProject"), + "product": v.get("product"), + "severity": "CRITICAL", + "date": v.get("dateAdded"), + "due": v.get("dueDate"), + "source": "CISA KEV", + } + for v in recent + ] + except Exception as exc: + logger.warning("CISA KEV fetch failed: %s", exc) + + count = len(results["threats"]) + results["stats"]["active_cves"] = count + results["stats"]["threat_level"] = "CRITICAL" if count >= 8 else "HIGH" if count >= 4 else "ELEVATED" + + with _data_lock: + latest_data["cyber_threats"] = results + _mark_fresh("cyber_threats") + return results diff --git a/backend/services/fetchers/geo.py b/backend/services/fetchers/geo.py index 57078db..eae79f9 100644 --- a/backend/services/fetchers/geo.py +++ b/backend/services/fetchers/geo.py @@ -278,6 +278,16 @@ _FISHING_FETCH_INTERVAL_S = 3600 # once per hour — GFW data has ~5 day lag _last_fishing_fetch_ts: float = 0.0 +def _gfw_int_env(name: str, default: int, *, minimum: int = 1, maximum: int | None = None) -> int: + try: + value = int(os.environ.get(name, str(default)) or default) + except (TypeError, ValueError): + value = default + if maximum is not None: + value = min(maximum, value) + return max(minimum, value) + + @with_retry(max_retries=1, base_delay=5) def fetch_fishing_activity(): """Fetch recent fishing events from Global Fishing Watch (~5 day lag).""" @@ -300,10 +310,16 @@ def fetch_fishing_activity(): try: import datetime as _dt + # GFW publishes with ~5 day lag; windows shorter than ~7 days often return 0 events. + lookback_days = _gfw_int_env("GFW_EVENTS_LOOKBACK_DAYS", 7, minimum=1, maximum=14) + max_pages = _gfw_int_env("GFW_EVENTS_MAX_PAGES", 10, minimum=1, maximum=100) + timeout_s = _gfw_int_env("GFW_EVENTS_TIMEOUT_S", 90, minimum=30, maximum=180) _end = _dt.date.today().isoformat() - _start = (_dt.date.today() - _dt.timedelta(days=7)).isoformat() - page_size = max(1, int(os.environ.get("GFW_EVENTS_PAGE_SIZE", "500") or "500")) + _start = (_dt.date.today() - _dt.timedelta(days=lookback_days)).isoformat() + page_size = _gfw_int_env("GFW_EVENTS_PAGE_SIZE", 500, minimum=1, maximum=1000) offset = 0 + pages_fetched = 0 + total_available: int | None = None seen_offsets: set[int] = set() seen_ids: set[str] = set() headers = {"Authorization": f"Bearer {token}"} @@ -324,7 +340,7 @@ def fetch_fishing_activity(): } ) url = f"https://gateway.api.globalfishingwatch.org/v3/events?{query}" - response = fetch_with_curl(url, timeout=30, headers=headers) + response = fetch_with_curl(url, timeout=timeout_s, headers=headers) if response.status_code != 200: logger.warning( "Fishing activity fetch failed at offset=%s: HTTP %s", @@ -334,10 +350,16 @@ def fetch_fishing_activity(): break payload = response.json() or {} + if total_available is None: + try: + total_available = int(payload.get("total")) if payload.get("total") is not None else None + except (TypeError, ValueError): + total_available = None entries = payload.get("entries", []) if not entries: break + pages_fetched += 1 added_this_page = 0 for e in entries: pos = e.get("position", {}) @@ -372,6 +394,15 @@ def fetch_fishing_activity(): if len(entries) < page_size: break + if pages_fetched >= max_pages: + logger.info( + "Fishing activity: capped at %s pages (%s events fetched; GFW total=%s)", + max_pages, + len(events), + total_available if total_available is not None else "unknown", + ) + break + next_offset = payload.get("nextOffset") if next_offset is None: next_offset = (payload.get("pagination") or {}).get("nextOffset") diff --git a/backend/services/fetchers/infrastructure.py b/backend/services/fetchers/infrastructure.py index 0372f8d..93565ee 100644 --- a/backend/services/fetchers/infrastructure.py +++ b/backend/services/fetchers/infrastructure.py @@ -235,11 +235,11 @@ _DC_GEOCODED_PATH = Path(__file__).parent.parent.parent / "data" / "datacenters_ def fetch_datacenters(): - """Load geocoded data centers (5K+ street-level precise locations).""" - from services.fetchers._store import is_any_active + """Load geocoded data centers (5K+ street-level precise locations). - if not is_any_active("datacenters"): - return + Always loads from disk; /api/live-data/slow gates the payload on the + datacenters layer toggle so enabling the layer can render immediately. + """ dcs = [] try: if not _DC_GEOCODED_PATH.exists(): diff --git a/backend/services/fetchers/malware.py b/backend/services/fetchers/malware.py new file mode 100644 index 0000000..3c83ff3 --- /dev/null +++ b/backend/services/fetchers/malware.py @@ -0,0 +1,107 @@ +"""Malware C2 / URLhaus feed (abuse.ch, Osiris port).""" +from __future__ import annotations + +import logging +from datetime import datetime, timezone +from typing import Any + +from services.fetchers._store import _data_lock, _mark_fresh, is_any_active, latest_data +from services.network_utils import fetch_with_curl + +logger = logging.getLogger(__name__) + +COUNTRY_CENTROIDS: dict[str, tuple[float, float]] = { + "AF": (65, 33), "AL": (20, 41), "DZ": (3, 28), "AR": (-64, -34), "AU": (134, -25), + "AT": (14, 47.5), "BE": (4, 50.8), "BR": (-51, -10), "CA": (-96, 62), "CN": (105, 35), + "DE": (10, 51), "FR": (2, 46), "GB": (-2, 54), "IN": (79, 22), "IR": (53, 32), + "IT": (12.5, 42.8), "JP": (138, 36), "KR": (128, 36), "MX": (-102, 23.5), "NL": (5.5, 52.5), + "PL": (19.5, 52), "RU": (100, 60), "SG": (103.8, 1.35), "TW": (121, 23.7), "UA": (32, 49), + "US": (-97, 38), "VN": (106, 16), +} + + +def fetch_malware_threats() -> list[dict[str, Any]]: + if not is_any_active("malware_c2"): + return latest_data.get("malware_threats") or [] + + threats: list[dict[str, Any]] = [] + threat_id = 0 + + try: + resp = fetch_with_curl( + "https://feodotracker.abuse.ch/downloads/ipblocklist.json", + timeout=10, + headers={"User-Agent": "Shadowbroker/1.0", "Accept": "application/json"}, + ) + if resp.status_code == 200: + entries = resp.json() + if not isinstance(entries, list): + entries = [] + for entry in entries[:200]: + cc = entry.get("country") + if not cc or cc not in COUNTRY_CENTROIDS: + continue + lng, lat = COUNTRY_CENTROIDS[cc] + j_lng = ((threat_id * 173.7) % 200 - 100) / 100 * 4 + j_lat = ((threat_id * 293.1) % 200 - 100) / 100 * 4 + threats.append( + { + "id": f"feodo-{threat_id}", + "lat": lat + j_lat, + "lng": lng + j_lng, + "ip": entry.get("ip_address") or "unknown", + "port": entry.get("dst_port") or 0, + "malware": entry.get("malware") or "unknown", + "status": entry.get("status") or "active", + "first_seen": entry.get("first_seen"), + "last_online": entry.get("last_online"), + "country": cc, + "threat_type": "botnet_c2", + } + ) + threat_id += 1 + except Exception as exc: + logger.warning("Feodo fetch failed: %s", exc) + + try: + resp = fetch_with_curl( + "https://urlhaus-api.abuse.ch/v1/urls/recent/limit/100/", + timeout=8, + ) + if resp.status_code == 200: + urls = (resp.json() or {}).get("urls") or [] + for u in urls: + cc = u.get("country") + if not cc or cc not in COUNTRY_CENTROIDS: + cc = next(iter(COUNTRY_CENTROIDS)) + lng, lat = COUNTRY_CENTROIDS[cc] + j_lng = ((threat_id * 137.3) % 200 - 100) / 100 * 5 + j_lat = ((threat_id * 211.7) % 200 - 100) / 100 * 5 + threats.append( + { + "id": f"urlhaus-{threat_id}", + "lat": lat + j_lat, + "lng": lng + j_lng, + "ip": u.get("host") or "unknown", + "port": 0, + "malware": ", ".join(u.get("tags") or []) or u.get("threat") or "malware", + "status": u.get("url_status") or "online", + "first_seen": u.get("dateadded"), + "country": cc, + "threat_type": "malware_url", + } + ) + threat_id += 1 + except Exception as exc: + logger.debug("URLhaus supplement failed: %s", exc) + + payload = { + "threats": threats, + "total": len(threats), + "timestamp": datetime.now(timezone.utc).isoformat(), + "source": "abuse.ch Feodo Tracker + URLhaus", + } + with _data_lock: + latest_data["malware_threats"] = payload + _mark_fresh("malware_threats") + return threats diff --git a/backend/services/fetchers/news.py b/backend/services/fetchers/news.py index b4618ec..43d2679 100644 --- a/backend/services/fetchers/news.py +++ b/backend/services/fetchers/news.py @@ -158,21 +158,26 @@ _KEYWORD_COORDS = { _SORTED_KEYWORDS = sorted(_KEYWORD_COORDS.items(), key=lambda x: len(x[0]), reverse=True) +def resolve_coords_match(text: str) -> tuple[tuple[float, float], str] | None: + """Return ((lat, lng), matched_keyword) for the most specific keyword hit.""" + padded_text = f" {text} " + for kw, coords in _SORTED_KEYWORDS: + if kw.startswith(" ") or kw.endswith(" "): + if kw in padded_text: + return coords, kw + elif re.search(r"\b" + re.escape(kw) + r"\b", text): + return coords, kw + return None + + def _resolve_coords(text: str) -> tuple[float, float] | None: """Return (lat, lng) for the most specific keyword match, or None. Longer keywords are tried first. Space-padded keywords (" us ", " uk ") use substring matching on padded text; all others use word-boundary regex. """ - padded_text = f" {text} " - for kw, coords in _SORTED_KEYWORDS: - if kw.startswith(" ") or kw.endswith(" "): - if kw in padded_text: - return coords - else: - if re.search(r'\b' + re.escape(kw) + r'\b', text): - return coords - return None + match = resolve_coords_match(text) + return match[0] if match else None @with_retry(max_retries=1, base_delay=2) diff --git a/backend/services/fetchers/telegram_osint.py b/backend/services/fetchers/telegram_osint.py new file mode 100644 index 0000000..5ac4668 --- /dev/null +++ b/backend/services/fetchers/telegram_osint.py @@ -0,0 +1,381 @@ +"""Telegram OSINT — public channel web previews (t.me/s) with keyword geoparsing.""" +from __future__ import annotations + +import hashlib +import logging +import os +import re +from datetime import datetime, timezone +from typing import Any + +from services.fetchers._store import _data_lock, _mark_fresh, is_any_active, latest_data +from services.fetchers.news import resolve_coords_match +from services.network_utils import fetch_with_curl, outbound_user_agent + +logger = logging.getLogger(__name__) + +_DEFAULT_CHANNELS = ( + "osintdefender", + "insiderpaper", + "aljazeeraenglish", + "nexta_live", + "war_monitor", + "OSINTtechnical", + "Liveuamap", +) + +_MESSAGE_BLOCK_RE = re.compile( + r'
\s*
\s*', + re.IGNORECASE, +) +_TEXT_RE = re.compile( + r'
]+src="([^"]+)"', re.IGNORECASE) +_BG_IMAGE_RE = re.compile(r"background-image:url\('([^']+)'\)", re.IGNORECASE) + +_TELEGRAM_MEDIA_HOST_SUFFIXES = (".telesco.pe", ".telegram-cdn.org") + +# Cyrillic / Arabic aliases for war-reporting channels (merged after English resolver). +_EXTRA_PLACE_KEYWORDS: dict[str, tuple[float, float]] = { + "киев": (50.450, 30.523), + "київ": (50.450, 30.523), + "харьков": (49.993, 36.231), + "харків": (49.993, 36.231), + "одесса": (46.482, 30.724), + "одеса": (46.482, 30.724), + "донецк": (48.015, 37.803), + "донецьк": (48.015, 37.803), + "луганск": (48.574, 39.307), + "луганськ": (48.574, 39.307), + "москва": (55.755, 37.617), + "крым": (45.000, 34.000), + "крим": (45.000, 34.000), + "бахмут": (48.595, 38.000), + "запорожье": (47.838, 35.139), + "запоріжжя": (47.838, 35.139), + "غزة": (31.416, 34.333), + "دمشق": (33.513, 36.276), + "بيروت": (33.893, 35.501), + "tel aviv": (32.085, 34.781), + "תל אביב": (32.085, 34.781), +} + +# Country-level news geocodes sit on national centroids that stack with threat alerts. +# Telegram uses major metro anchors so pins land on a different map cell than news. +_TELEGRAM_ANCHOR_OVERRIDES: dict[str, tuple[float, float]] = { + "israel": (32.085, 34.781), # Tel Aviv (news uses central Israel ~Jerusalem corridor) + "middle east": (32.085, 34.781), + "china": (39.904, 116.407), # Beijing (news uses country centroid) + "united states": (40.712, -74.006), # New York (news uses Washington DC) + "usa": (40.712, -74.006), + "us": (40.712, -74.006), + "america": (40.712, -74.006), + "uk": (51.507, -0.127), # London + "iran": (35.689, 51.389), # Tehran + "russia": (55.755, 37.617), # Moscow + "ukraine": (50.450, 30.523), # Kyiv + "france": (48.856, 2.352), # Paris + "germany": (52.520, 13.405), # Berlin + "lebanon": (34.433, 35.844), # Tripoli (news uses Beirut corridor) +} + +_RISK_KEYWORDS = ( + "war", + "missile", + "strike", + "attack", + "crisis", + "tension", + "military", + "conflict", + "defense", + "clash", + "nuclear", + "invasion", + "bomb", + "drone", + "weapon", + "sanctions", + "ceasefire", + "escalation", + "killed", + "destroyed", + "operation", + "casualty", + "frontline", + "threat", + "explosion", + "shelling", +) + + +def telegram_osint_enabled() -> bool: + return str(os.environ.get("TELEGRAM_OSINT_ENABLED", "true")).strip().lower() not in { + "0", + "false", + "no", + "off", + "", + } + + +def _configured_channels() -> list[str]: + raw = str(os.environ.get("TELEGRAM_OSINT_CHANNELS", "")).strip() + if raw: + return [part.strip().lstrip("@") for part in raw.split(",") if part.strip()] + return list(_DEFAULT_CHANNELS) + + +def telegram_media_host_allowed(hostname: str | None) -> bool: + host = str(hostname or "").strip().lower() + if not host: + return False + return any(host.endswith(suffix) for suffix in _TELEGRAM_MEDIA_HOST_SUFFIXES) + + +def _extract_media(block: str, link: str) -> dict[str, Any]: + has_video = bool(_HAS_VIDEO_RE.search(block)) + has_photo = bool(_HAS_PHOTO_RE.search(block)) + media_type: str | None = None + media_url: str | None = None + if has_video: + media_type = "video" + video_match = _VIDEO_SRC_RE.search(block) + if video_match: + media_url = video_match.group(1).strip() + elif has_photo: + media_type = "photo" + photo_match = _BG_IMAGE_RE.search(block) + if photo_match: + media_url = photo_match.group(1).strip() + + embed_url: str | None = None + if media_type and link: + embed_url = f"{link}?embed=1" + + return { + "media_type": media_type, + "media_url": media_url, + "embed_url": embed_url, + } + + +def _strip_html(text: str) -> str: + cleaned = re.sub(r"", "\n", text, flags=re.IGNORECASE) + cleaned = re.sub(r"<[^>]+>", "", cleaned) + return ( + cleaned.replace(""", '"') + .replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .strip() + ) + + +def _score_risk(text: str) -> int: + lower = text.lower() + score = 1 + for kw in _RISK_KEYWORDS: + if kw in lower: + score += 2 + return min(10, score) + + +def _refresh_post_coords(post: dict[str, Any]) -> dict[str, Any]: + """Re-apply geoparsing so stored posts pick up anchor updates.""" + text = "\n".join( + str(part).strip() + for part in (post.get("title"), post.get("description")) + if part and str(part).strip() + ) + if not text: + return post + coords = _resolve_telegram_coords(text) + if not coords: + return post + updated = dict(post) + updated["coords"] = [coords[0], coords[1]] + return updated + + +def _resolve_telegram_coords(text: str) -> tuple[float, float] | None: + lower = text.lower() + match = resolve_coords_match(lower) + if match: + _coords, keyword = match + anchor = _TELEGRAM_ANCHOR_OVERRIDES.get(keyword.strip().lower()) + if anchor: + return anchor + return _coords + for keyword, coords in sorted(_EXTRA_PLACE_KEYWORDS.items(), key=lambda x: len(x[0]), reverse=True): + if keyword in lower: + return coords + return None + + +def _post_link(post: dict[str, Any]) -> str: + return str(post.get("link") or "").strip() + + +def _extract_new_channel_posts( + html: str, + channel: str, + known_links: set[str], + *, + bootstrap_limit: int = 12, +) -> list[dict[str, Any]]: + """Return unseen posts from a channel page; stop once we hit a stored link.""" + parsed = parse_telegram_channel_html(html, channel) + if not parsed: + return [] + if not known_links: + return parsed[-bootstrap_limit:] + + fresh: list[dict[str, Any]] = [] + for post in reversed(parsed): + link = _post_link(post) + if not link: + continue + if link in known_links: + break + fresh.append(post) + fresh.reverse() + return fresh + + +def _merge_telegram_posts( + existing: list[dict[str, Any]], + incoming: list[dict[str, Any]], + *, + max_posts: int = 120, +) -> tuple[list[dict[str, Any]], int]: + known_links = {_post_link(post) for post in existing if _post_link(post)} + added = 0 + for post in incoming: + link = _post_link(post) + if not link or link in known_links: + continue + known_links.add(link) + existing.append(post) + added += 1 + existing.sort(key=lambda p: str(p.get("published") or ""), reverse=True) + return existing[:max_posts], added + + +def parse_telegram_channel_html(html: str, channel: str) -> list[dict[str, Any]]: + """Parse public t.me/s channel preview HTML into post dicts.""" + posts: list[dict[str, Any]] = [] + for block in _MESSAGE_BLOCK_RE.findall(html or ""): + text_match = _TEXT_RE.search(block) + if not text_match: + continue + text = _strip_html(text_match.group(1)) + if len(text) < 10: + continue + + date_match = _DATE_RE.search(block) + link = date_match.group(1) if date_match else f"https://t.me/{channel}" + published = date_match.group(2) if date_match else datetime.now(timezone.utc).isoformat() + title = text.split("\n", 1)[0][:160] + risk_score = _score_risk(text) + coords = _resolve_telegram_coords(text) + post_id = hashlib.sha1(f"{link}|{published}".encode("utf-8")).hexdigest()[:16] + + media = _extract_media(block, link) + posts.append( + { + "id": post_id, + "title": title, + "description": text[:1200], + "link": link, + "published": published, + "source": f"t.me/{channel}", + "channel": channel, + "risk_score": risk_score, + "coords": [coords[0], coords[1]] if coords else None, + **media, + } + ) + return posts + + +def fetch_telegram_osint() -> dict[str, Any]: + if not is_any_active("telegram_osint"): + return latest_data.get("telegram_osint") or {"posts": [], "total": 0, "timestamp": None} + + if not telegram_osint_enabled(): + with _data_lock: + latest_data["telegram_osint"] = {"posts": [], "total": 0, "timestamp": None, "disabled": True} + _mark_fresh("telegram_osint") + return latest_data["telegram_osint"] + + headers = { + "User-Agent": ( + f"Mozilla/5.0 (compatible; {outbound_user_agent('telegram-osint')}) " + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + ), + "Accept": "text/html,application/xhtml+xml", + } + + with _data_lock: + prior = latest_data.get("telegram_osint") or {} + existing_posts = list(prior.get("posts") or []) + + known_links = {_post_link(post) for post in existing_posts if _post_link(post)} + incoming: list[dict[str, Any]] = [] + + for channel in _configured_channels(): + url = f"https://t.me/s/{channel}" + try: + resp = fetch_with_curl(url, timeout=15, headers=headers) + if not resp or resp.status_code != 200: + logger.warning( + "Telegram channel %s fetch failed: HTTP %s", + channel, + resp.status_code if resp else "no response", + ) + continue + channel_new = _extract_new_channel_posts(resp.text, channel, known_links) + for post in channel_new: + link = _post_link(post) + if not link or link in known_links: + continue + known_links.add(link) + incoming.append(post) + except Exception as exc: + logger.warning("Telegram channel %s parse failed: %s", channel, exc) + + merged_posts, added = _merge_telegram_posts(existing_posts, incoming) + merged_posts = [_refresh_post_coords(post) for post in merged_posts] + geolocated = sum(1 for p in merged_posts if p.get("coords")) + + payload = { + "posts": merged_posts, + "total": len(merged_posts), + "geolocated": geolocated, + "timestamp": datetime.now(timezone.utc).isoformat(), + "channels": _configured_channels(), + "last_fetch_new": added, + } + + with _data_lock: + latest_data["telegram_osint"] = payload + _mark_fresh("telegram_osint") + logger.info( + "Telegram OSINT: +%s new, %s retained (%s geolocated)", + added, + len(merged_posts), + geolocated, + ) + return payload diff --git a/backend/services/intel_feeds/__init__.py b/backend/services/intel_feeds/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/services/intel_feeds/country_risk.py b/backend/services/intel_feeds/country_risk.py new file mode 100644 index 0000000..cb24501 --- /dev/null +++ b/backend/services/intel_feeds/country_risk.py @@ -0,0 +1,94 @@ +"""Country risk index (static scores + USGS quake enrichment).""" +from __future__ import annotations + +from datetime import datetime, timezone +from typing import Any +from zoneinfo import ZoneInfo + +from services.network_utils import fetch_with_curl + +RISK_FACTORS: dict[str, dict[str, Any]] = { + "UA": {"base": 85, "tags": ["active_conflict", "infrastructure_damage"]}, + "RU": {"base": 72, "tags": ["sanctions", "military_mobilization"]}, + "IL": {"base": 78, "tags": ["active_conflict", "regional_instability"]}, + "PS": {"base": 90, "tags": ["active_conflict", "humanitarian_crisis"]}, + "SY": {"base": 82, "tags": ["post_conflict", "infrastructure_damage"]}, + "YE": {"base": 88, "tags": ["active_conflict", "humanitarian_crisis"]}, + "MM": {"base": 76, "tags": ["civil_unrest", "military_junta"]}, + "SD": {"base": 84, "tags": ["active_conflict", "humanitarian_crisis"]}, + "AF": {"base": 80, "tags": ["post_conflict", "governance_collapse"]}, + "KP": {"base": 70, "tags": ["nuclear_risk", "isolation"]}, + "IR": {"base": 68, "tags": ["sanctions", "nuclear_program", "regional_proxy"]}, + "CN": {"base": 35, "tags": ["strategic_competition", "taiwan_tensions"]}, + "TW": {"base": 45, "tags": ["invasion_risk", "semiconductor_dependency"]}, + "VE": {"base": 60, "tags": ["economic_collapse", "political_instability"]}, + "HT": {"base": 85, "tags": ["gang_violence", "governance_collapse"]}, + "LB": {"base": 65, "tags": ["economic_crisis", "political_deadlock"]}, + "PK": {"base": 55, "tags": ["terrorism", "political_instability"]}, + "SO": {"base": 82, "tags": ["terrorism", "state_fragility"]}, + "LY": {"base": 72, "tags": ["divided_government", "militia_control"]}, + "ET": {"base": 62, "tags": ["ethnic_tensions", "regional_conflicts"]}, +} + +EXCHANGES = [ + {"name": "NYSE", "tz": "America/New_York", "open": 9.5, "close": 16, "country": "US"}, + {"name": "NASDAQ", "tz": "America/New_York", "open": 9.5, "close": 16, "country": "US"}, + {"name": "LSE", "tz": "Europe/London", "open": 8, "close": 16.5, "country": "GB"}, + {"name": "TSE", "tz": "Asia/Tokyo", "open": 9, "close": 15, "country": "JP"}, + {"name": "SSE", "tz": "Asia/Shanghai", "open": 9.5, "close": 15, "country": "CN"}, + {"name": "HKEX", "tz": "Asia/Hong_Kong", "open": 9.5, "close": 16, "country": "HK"}, + {"name": "FRA", "tz": "Europe/Berlin", "open": 8, "close": 20, "country": "DE"}, + {"name": "TSX", "tz": "America/Toronto", "open": 9.5, "close": 16, "country": "CA"}, + {"name": "MOEX", "tz": "Europe/Moscow", "open": 10, "close": 18.5, "country": "RU"}, +] + + +def _exchange_open(ex: dict[str, Any]) -> bool: + try: + now = datetime.now(ZoneInfo(ex["tz"])) + if now.weekday() >= 5: + return False + decimal = now.hour + now.minute / 60 + return ex["open"] <= decimal < ex["close"] + except Exception: + return False + + +def build_country_risk_payload() -> dict[str, Any]: + quake_risks: dict[str, float] = {} + try: + resp = fetch_with_curl( + "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/4.5_day.geojson", + timeout=5, + ) + if resp.status_code == 200: + for f in resp.json().get("features") or []: + place = (f.get("properties") or {}).get("place") or "" + mag = (f.get("properties") or {}).get("mag") or 0 + for code in RISK_FACTORS: + if code.lower() in place.lower(): + quake_risks[code] = quake_risks.get(code, 0) + mag + except Exception: + pass + + countries = [] + for code, data in RISK_FACTORS.items(): + base = data["base"] + score = min(100, base + quake_risks.get(code, 0)) + countries.append( + { + "code": code, + "risk_score": score, + "risk_level": "CRITICAL" if base >= 80 else "HIGH" if base >= 60 else "ELEVATED" if base >= 40 else "LOW", + "tags": data["tags"], + } + ) + countries.sort(key=lambda c: c["risk_score"], reverse=True) + exchanges = [{"name": e["name"], "country": e["country"], "open": _exchange_open(e)} for e in EXCHANGES] + return { + "countries": countries, + "exchanges": exchanges, + "open_exchanges": sum(1 for e in exchanges if e["open"]), + "total_exchanges": len(exchanges), + "timestamp": datetime.now(timezone.utc).isoformat(), + } diff --git a/backend/services/osint/__init__.py b/backend/services/osint/__init__.py new file mode 100644 index 0000000..d500f29 --- /dev/null +++ b/backend/services/osint/__init__.py @@ -0,0 +1 @@ +"""Operator-initiated OSINT lookups (server-side proxies).""" diff --git a/backend/services/osint/lookups.py b/backend/services/osint/lookups.py new file mode 100644 index 0000000..3a78211 --- /dev/null +++ b/backend/services/osint/lookups.py @@ -0,0 +1,492 @@ +"""Server-side OSINT lookups (Osiris port, HTTPS outbound only).""" +from __future__ import annotations + +import ipaddress +import json +import logging +import re +import socket +import time +from concurrent.futures import ThreadPoolExecutor, as_completed +from datetime import datetime, timezone +from typing import Any +from urllib.parse import quote + +from services.network_utils import fetch_with_curl +from services.sanctions.ofac import match_exact, search_sanctions +from services.ssrf_guard import safe_get, validate_domain, validate_host + +logger = logging.getLogger(__name__) + +_IPV4_RE = re.compile(r"^(\d{1,3}\.){3}\d{1,3}$") +_IPV6_RE = re.compile(r"^[0-9a-fA-F:]+$") +_CVE_RE = re.compile(r"^CVE-\d{4}-\d{4,}$", re.I) +_ASN_RE = re.compile(r"^(AS)?\d+$", re.I) + + +def _now_iso() -> str: + return datetime.now(timezone.utc).isoformat() + + +def _json_get(url: str, *, timeout: float = 8.0, headers: dict[str, str] | None = None) -> Any: + resp = fetch_with_curl(url, timeout=timeout, headers=headers or {"Accept": "application/json"}) + if resp.status_code != 200: + return None + try: + return resp.json() + except Exception: + return None + + +def _sanctions_hits(*values: str) -> list[dict[str, Any]] | None: + hits: list[dict[str, Any]] = [] + seen: set[str] = set() + for value in values: + if not value or value in seen: + continue + seen.add(value) + entries = match_exact(value) + if entries: + hits.append({"matched_value": value, "entries": entries}) + return hits or None + + +def lookup_ip(ip: str) -> dict[str, Any]: + if not _IPV4_RE.match(ip) and not _IPV6_RE.match(ip): + raise ValueError("Invalid IP format") + check = validate_host(ip.strip("[]")) + if not check.get("ok"): + raise ValueError(check.get("reason", "blocked IP")) + + results: dict[str, Any] = {"ip": ip, "timestamp": _now_iso()} + fields = ( + "status,message,continent,country,countryCode,region,regionName,city,zip," + "lat,lon,timezone,isp,org,as,asname,mobile,proxy,hosting,query" + ) + geo = _json_get(f"https://ip-api.com/json/{quote(ip)}?fields={fields}", timeout=5) + if isinstance(geo, dict) and geo.get("status") == "success": + results["geo"] = { + "country": geo.get("country"), + "country_code": geo.get("countryCode"), + "region": geo.get("regionName"), + "city": geo.get("city"), + "lat": geo.get("lat"), + "lon": geo.get("lon"), + "timezone": geo.get("timezone"), + "isp": geo.get("isp"), + "org": geo.get("org"), + "as_number": geo.get("as"), + "as_name": geo.get("asname"), + "is_mobile": geo.get("mobile"), + "is_proxy": geo.get("proxy"), + "is_hosting": geo.get("hosting"), + } + results["reputation"] = { + "is_proxy": bool(geo.get("proxy")), + "is_hosting": bool(geo.get("hosting")), + "is_mobile": bool(geo.get("mobile")), + "risk_level": "HIGH" if geo.get("proxy") else "MEDIUM" if geo.get("hosting") else "LOW", + } + sm = _sanctions_hits(geo.get("org") or "", geo.get("isp") or "", geo.get("asname") or "") + if sm: + results["sanctions_match"] = {"source": "OFAC SDN", "hits": sm} + return results + + +def lookup_dns(domain: str) -> dict[str, Any]: + if not validate_domain(domain): + raise ValueError("Invalid domain format") + results: dict[str, Any] = {"domain": domain, "records": {}, "timestamp": _now_iso()} + for rtype in ("A", "AAAA", "MX", "NS", "TXT", "CNAME", "SOA"): + data = _json_get( + f"https://dns.google/resolve?name={quote(domain)}&type={rtype}", + timeout=5, + ) + answers = [] + if isinstance(data, dict): + for ans in data.get("Answer") or []: + answers.append( + { + "name": ans.get("name"), + "type": ans.get("type"), + "ttl": ans.get("TTL"), + "data": ans.get("data"), + } + ) + results["records"][rtype] = answers + a_records = results["records"].get("A") or [] + mx_records = results["records"].get("MX") or [] + ns_records = results["records"].get("NS") or [] + results["summary"] = { + "ip_addresses": [r["data"] for r in a_records if r.get("data")], + "mail_servers": [r["data"] for r in mx_records if r.get("data")], + "nameservers": [r["data"] for r in ns_records if r.get("data")], + "total_records": sum(len(v) for v in results["records"].values()), + } + return results + + +def lookup_whois(domain: str) -> dict[str, Any]: + if not validate_domain(domain): + raise ValueError("Invalid domain format") + results: dict[str, Any] = {"domain": domain, "timestamp": _now_iso()} + rdap = _json_get(f"https://rdap.org/domain/{quote(domain)}", timeout=8) + if isinstance(rdap, dict): + entities = [] + for ent in rdap.get("entities") or []: + vcard = ent.get("vcardArray") + name = org = None + if isinstance(vcard, list) and len(vcard) > 1: + for row in vcard[1]: + if row[0] == "fn": + name = row[3] + if row[0] == "org": + org = row[3] + if name or org: + entities.append({"handle": ent.get("handle"), "roles": ent.get("roles"), "name": name, "org": org}) + events = [ + {"action": e.get("eventAction"), "date": e.get("eventDate")} + for e in (rdap.get("events") or []) + ] + results["rdap"] = { + "handle": rdap.get("handle"), + "name": rdap.get("ldhName"), + "status": rdap.get("status"), + "events": events, + "nameservers": [ns.get("ldhName") for ns in (rdap.get("nameservers") or [])], + "entities": entities, + } + results["registration"] = next((e["date"] for e in events if e["action"] == "registration"), None) + results["expiration"] = next((e["date"] for e in events if e["action"] == "expiration"), None) + results["last_changed"] = next((e["date"] for e in events if e["action"] == "last changed"), None) + sm = _sanctions_hits(*(e.get("name") or "" for e in entities), *(e.get("org") or "" for e in entities)) + if sm: + results["sanctions_match"] = {"source": "OFAC SDN", "hits": sm} + + try: + res = safe_get(f"https://{domain}", timeout=5, headers={"User-Agent": "Shadowbroker-OSINT/1.0"}) + headers = {} + for h in ( + "server", + "x-powered-by", + "x-frame-options", + "strict-transport-security", + "content-security-policy", + "x-content-type-options", + "x-xss-protection", + "referrer-policy", + "permissions-policy", + ): + val = res.headers.get(h) + if val: + headers[h] = val + score = sum( + 1 + for k in ( + "strict-transport-security", + "content-security-policy", + "x-frame-options", + "x-content-type-options", + "referrer-policy", + ) + if k in headers + ) + (2 if "strict-transport-security" in headers else 0) + (2 if "content-security-policy" in headers else 0) + results["http"] = {"status": res.status_code, "headers": headers, "final_url": res.url} + results["security_score"] = { + "score": score, + "max": 7, + "grade": "A" if score >= 5 else "B" if score >= 3 else "C" if score >= 1 else "F", + } + except Exception as exc: + logger.debug("WHOIS header probe failed for %s: %s", domain, exc) + return results + + +def lookup_certs(domain: str) -> dict[str, Any]: + if not validate_domain(domain): + raise ValueError("Invalid domain format") + resp = fetch_with_curl( + f"https://crt.sh/?q=%25.{quote(domain)}&output=json", + timeout=10, + headers={"User-Agent": "Shadowbroker-OSINT/1.0"}, + ) + if resp.status_code != 200: + return {"domain": domain, "certificates": [], "error": "crt.sh unavailable"} + try: + certs = resp.json() + except Exception: + certs = [] + seen: set[str] = set() + subdomains: set[str] = set() + unique: list[dict[str, Any]] = [] + for cert in (certs or [])[:200]: + key = f"{cert.get('common_name')}-{cert.get('serial_number')}" + if key in seen: + continue + seen.add(key) + for name in (cert.get("name_value") or "").split("\n"): + clean = name.strip().replace("*.", "") + if clean.endswith(domain): + subdomains.add(clean) + unique.append( + { + "id": cert.get("id"), + "issuer": cert.get("issuer_name"), + "common_name": cert.get("common_name"), + "not_before": cert.get("not_before"), + "not_after": cert.get("not_after"), + } + ) + return { + "domain": domain, + "certificates": unique[:50], + "subdomains": sorted(subdomains)[:100], + "total_found": len(certs or []), + "timestamp": _now_iso(), + } + + +def lookup_threats(query: str | None = None) -> dict[str, Any]: + results: dict[str, Any] = {"timestamp": _now_iso()} + pulses = _json_get("https://otx.alienvault.com/api/v1/pulses/activity?limit=10", timeout=8) + if isinstance(pulses, dict): + results["pulses"] = [ + { + "name": p.get("name"), + "description": (p.get("description") or "")[:200], + "created": p.get("created"), + "tags": (p.get("tags") or [])[:5], + "adversary": p.get("adversary"), + "indicators_count": p.get("indicator_count"), + } + for p in (pulses.get("results") or [])[:10] + ] + if query: + if _IPV4_RE.match(query): + try: + tor_resp = fetch_with_curl("https://check.torproject.org/torbulkexitlist", timeout=5) + results["tor_exit_node"] = query in (tor_resp.text or "").splitlines() if tor_resp.status_code == 200 else None + except Exception: + results["tor_exit_node"] = None + otx = _json_get(f"https://otx.alienvault.com/api/v1/indicators/IPv4/{quote(query)}/general", timeout=5) + if isinstance(otx, dict): + results["otx"] = { + "reputation": otx.get("reputation"), + "pulse_count": (otx.get("pulse_info") or {}).get("count", 0), + "country": otx.get("country_name"), + "asn": otx.get("asn"), + } + elif validate_domain(query): + otx = _json_get(f"https://otx.alienvault.com/api/v1/indicators/domain/{quote(query)}/general", timeout=5) + if isinstance(otx, dict): + results["otx"] = {"pulse_count": (otx.get("pulse_info") or {}).get("count", 0)} + pulse_count = (results.get("otx") or {}).get("pulse_count", 0) + results["threat_level"] = "HIGH" if pulse_count > 5 else "MEDIUM" if pulse_count > 0 else "LOW" + return results + + +def lookup_bgp(query: str) -> dict[str, Any]: + results: dict[str, Any] = {"query": query, "timestamp": _now_iso()} + if _IPV4_RE.match(query): + data = _json_get(f"https://api.bgpview.io/ip/{quote(query)}", timeout=8) + if isinstance(data, dict) and data.get("status") == "ok": + results["ip"] = data.get("data") + results["type"] = "ip" + return results + if _ASN_RE.match(query): + asn_num = re.sub(r"^AS", "", query, flags=re.I) + asn = _json_get(f"https://api.bgpview.io/asn/{asn_num}", timeout=8) + prefixes = _json_get(f"https://api.bgpview.io/asn/{asn_num}/prefixes", timeout=8) + peers = _json_get(f"https://api.bgpview.io/asn/{asn_num}/peers", timeout=8) + if isinstance(asn, dict) and asn.get("status") == "ok": + results["asn"] = asn.get("data") + if isinstance(prefixes, dict) and prefixes.get("status") == "ok": + pdata = prefixes.get("data") or {} + results["prefixes"] = { + "ipv4": (pdata.get("ipv4_prefixes") or [])[:20], + "ipv6": (pdata.get("ipv6_prefixes") or [])[:10], + "total_v4": len(pdata.get("ipv4_prefixes") or []), + "total_v6": len(pdata.get("ipv6_prefixes") or []), + } + if isinstance(peers, dict) and peers.get("status") == "ok": + pdata = peers.get("data") or {} + results["peers"] = { + "upstream": (pdata.get("ipv4_peers") or [])[:10], + "total": len(pdata.get("ipv4_peers") or []), + } + results["type"] = "asn" + return results + raise ValueError("Unrecognized query format. Use IP address or AS number.") + + +def lookup_sanctions(query: str, *, schema: str | None = None, limit: int = 25) -> dict[str, Any]: + matches = search_sanctions(query, schema=schema, limit=limit) + return { + "query": query, + "schema": schema, + "total": len(matches), + "matches": matches, + "source": "OpenSanctions / US OFAC SDN", + "timestamp": _now_iso(), + } + + +def lookup_cve(cve: str) -> dict[str, Any]: + if not _CVE_RE.match(cve): + raise ValueError("Invalid CVE format") + cve_id = cve.upper() + data = _json_get(f"https://cveawg.mitre.org/api/cve/{quote(cve_id)}", timeout=8) + if isinstance(data, dict) and data.get("cveMetadata"): + meta = data["cveMetadata"] + desc = "" + for block in (data.get("containers") or {}).get("cna", {}).get("descriptions") or []: + if block.get("lang") == "en": + desc = block.get("value") or desc + return {"id": meta.get("cveId", cve_id), "description": desc or "No description.", "timestamp": _now_iso()} + fallback = _json_get(f"https://cve.circl.lu/api/cve/{quote(cve_id)}", timeout=8) + if isinstance(fallback, dict): + return { + "id": fallback.get("id", cve_id), + "description": fallback.get("summary") or "No description.", + "cvss": fallback.get("cvss"), + "references": (fallback.get("references") or [])[:5], + "timestamp": _now_iso(), + } + raise ValueError("CVE not found") + + +def lookup_mac(mac: str) -> dict[str, Any]: + clean = mac.strip().upper() + clean = re.sub(r"[^A-F0-9:-]", "", clean) + data = _json_get(f"https://api.macvendors.com/{quote(clean)}", timeout=8) + if isinstance(data, dict): + return {"mac": clean, "vendor": data.get("company") or data.get("organization") or "Not Found"} + if isinstance(data, str) and data: + return {"mac": clean, "vendor": data} + return {"mac": clean, "vendor": "Not Found"} + + +def lookup_github(username: str) -> dict[str, Any]: + user = _json_get(f"https://api.github.com/users/{quote(username)}", timeout=8) + if not isinstance(user, dict) or user.get("message") == "Not Found": + raise ValueError("GitHub user not found") + repos = _json_get(f"https://api.github.com/users/{quote(username)}/repos?per_page=10&sort=updated", timeout=8) + return { + "username": username, + "profile": { + "name": user.get("name"), + "bio": user.get("bio"), + "company": user.get("company"), + "location": user.get("location"), + "public_repos": user.get("public_repos"), + "followers": user.get("followers"), + "created_at": user.get("created_at"), + "html_url": user.get("html_url"), + }, + "repos": [ + {"name": r.get("name"), "language": r.get("language"), "stars": r.get("stargazers_count")} + for r in (repos or [])[:10] + if isinstance(r, dict) + ], + "timestamp": _now_iso(), + } + + +def lookup_leaks(email: str) -> dict[str, Any]: + if "@" not in email or len(email) < 5: + raise ValueError("Invalid email") + # HIBP requires API key for v3; use public breach directory style via leak-lookup (rate limited) + data = _json_get(f"https://leakcheck.io/api/public?check={quote(email)}", timeout=8) + if isinstance(data, dict): + return { + "email": email, + "found": bool(data.get("found")), + "sources": data.get("sources") or [], + "timestamp": _now_iso(), + } + return {"email": email, "found": False, "sources": [], "timestamp": _now_iso()} + + +def sweep_init(ip: str, cidr: int = 24) -> dict[str, Any]: + try: + addr = ipaddress.IPv4Address(ip) + except ValueError as exc: + raise ValueError("Invalid IPv4 address format") from exc + if addr.is_private or addr.is_loopback or addr.is_link_local or addr.is_reserved: + raise ValueError("Private and reserved IP ranges are not allowed") + if cidr < 24 or cidr > 32: + raise ValueError("CIDR must be between 24 and 32") + + fields = "status,message,country,countryCode,region,regionName,city,lat,lon,isp,org,as,proxy,hosting" + geo = _json_get(f"https://ip-api.com/json/{quote(ip)}?fields={fields}", timeout=5) + if not isinstance(geo, dict) or geo.get("status") != "success": + raise ValueError(f"Geolocation failed: {(geo or {}).get('message', 'unknown')}") + return { + "center": { + "lat": geo.get("lat"), + "lng": geo.get("lon"), + "city": geo.get("city"), + "region": geo.get("regionName"), + "country": geo.get("country"), + "countryCode": geo.get("countryCode"), + "isp": geo.get("isp"), + "asn": geo.get("as") or "", + "org": geo.get("org") or "", + }, + "target_ip": ip, + "cidr": cidr, + } + + +def _internetdb_lookup(ip: str) -> dict[str, Any] | None: + try: + resp = fetch_with_curl( + f"https://internetdb.shodan.io/{quote(ip)}", + timeout=4, + headers={"Accept": "application/json"}, + ) + if resp.status_code == 404: + return None + if resp.status_code != 200: + return None + return resp.json() + except Exception: + return None + + +def sweep_scan(subnet_start: str, cidr: int, *, max_workers: int = 12) -> dict[str, Any]: + """Scan a /24-/32 via Shodan InternetDB (server-side proxy).""" + base = int(ipaddress.IPv4Address(subnet_start)) + host_count = 2 ** (32 - cidr) + if host_count > 256: + raise ValueError("Subnet too large") + ips = [str(ipaddress.IPv4Address(base + i)) for i in range(host_count)] + devices: list[dict[str, Any]] = [] + t0 = time.time() + with ThreadPoolExecutor(max_workers=max_workers) as pool: + futures = {pool.submit(_internetdb_lookup, ip): ip for ip in ips} + for fut in as_completed(futures): + ip = futures[fut] + data = fut.result() + if not data: + continue + devices.append( + { + "ip": data.get("ip") or ip, + "ports": data.get("ports") or [], + "hostnames": data.get("hostnames") or [], + "cpes": data.get("cpes") or [], + "vulns": data.get("vulns") or [], + "tags": data.get("tags") or [], + } + ) + return { + "devices": devices, + "summary": {"total_hosts": host_count, "total_responsive": len(devices)}, + "sweep_time_ms": int((time.time() - t0) * 1000), + } + + +def subnet_start_for(ip: str, cidr: int) -> str: + net = ipaddress.IPv4Network(f"{ip}/{cidr}", strict=False) + return str(net.network_address) diff --git a/backend/services/osint_intel/__init__.py b/backend/services/osint_intel/__init__.py new file mode 100644 index 0000000..5dfca1a --- /dev/null +++ b/backend/services/osint_intel/__init__.py @@ -0,0 +1 @@ +"""Entity graph resolution (Osiris intel layer port).""" diff --git a/backend/services/osint_intel/resolve.py b/backend/services/osint_intel/resolve.py new file mode 100644 index 0000000..954a838 --- /dev/null +++ b/backend/services/osint_intel/resolve.py @@ -0,0 +1,268 @@ +"""Entity graph resolver (Python port of Osiris intel/server.js).""" +from __future__ import annotations + +import logging +import re +import threading +import time +from typing import Any +from urllib.parse import quote + +from services.network_utils import fetch_with_curl +from services.sanctions.ofac import match_exact, search_sanctions + +logger = logging.getLogger(__name__) + +ALLOWED_TYPES = frozenset({"aircraft", "vessel", "company", "person", "ip", "country"}) +_WD_CACHE: dict[str, tuple[float, dict[str, Any]]] = {} +_WD_LOCK = threading.Lock() +_WD_TTL = 24 * 60 * 60 +_WD_UA = "Shadowbroker-Intel/1.0 (ontology engine)" + + +def _dedup(nodes: list[dict], links: list[dict]) -> dict[str, Any]: + node_map: dict[str, dict] = {} + for n in nodes: + node_map[n["id"]] = n + seen_links: set[str] = set() + out_links: list[dict] = [] + for link in links: + key = f"{link['source']}→{link['target']}→{link['label']}" + if key in seen_links: + continue + seen_links.add(key) + out_links.append(link) + return {"nodes": list(node_map.values()), "links": out_links} + + +def _wd_cache_get(key: str) -> dict[str, Any] | None: + with _WD_LOCK: + entry = _WD_CACHE.get(key) + if not entry: + return None + ts, data = entry + if time.time() - ts > _WD_TTL: + _WD_CACHE.pop(key, None) + return None + return data + + +def _wd_cache_set(key: str, data: dict[str, Any]) -> None: + with _WD_LOCK: + if len(_WD_CACHE) > 5000: + oldest = next(iter(_WD_CACHE)) + _WD_CACHE.pop(oldest, None) + _WD_CACHE[key] = (time.time(), data) + + +def _add_sanctions(id_label: str, root_id: str, nodes: list, links: list) -> None: + for hit in search_sanctions(id_label, limit=3): + sid = f"sanction:{hit['id']}" + nodes.append( + { + "id": sid, + "label": hit["name"], + "type": "sanction", + "properties": {"programs": hit.get("programs"), "source": "OFAC SDN"}, + } + ) + links.append({"source": root_id, "target": sid, "label": "SANCTIONS MATCH"}) + + +def _sparql(query: str) -> list[dict[str, Any]]: + url = f"https://query.wikidata.org/sparql?query={quote(query)}&format=json" + resp = fetch_with_curl(url, timeout=10, headers={"User-Agent": _WD_UA, "Accept": "application/sparql-results+json"}) + if resp.status_code != 200: + return [] + try: + data = resp.json() + except Exception: + return [] + return data.get("results", {}).get("bindings", []) + + +def _wd_search(label: str) -> str | None: + url = ( + "https://www.wikidata.org/w/api.php?action=wbsearchentities" + f"&search={quote(label)}&language=en&limit=1&format=json" + ) + resp = fetch_with_curl(url, timeout=5, headers={"User-Agent": _WD_UA}) + if resp.status_code != 200: + return None + try: + hits = resp.json().get("search") or [] + except Exception: + return None + return hits[0]["id"] if hits else None + + +def _resolve_ip(id_value: str) -> dict[str, Any]: + cache_key = f"ip:{id_value}" + cached = _wd_cache_get(cache_key) + if cached: + return cached + + root_id = f"ip:{id_value}" + nodes: list[dict] = [{"id": root_id, "label": id_value, "type": "ip", "properties": {}}] + links: list[dict] = [] + + geo = fetch_with_curl( + f"https://ip-api.com/json/{quote(id_value)}" + "?fields=status,country,countryCode,city,lat,lon,isp,org,as,asname,proxy,hosting,mobile", + timeout=8, + ) + if geo.status_code == 200: + try: + data = geo.json() + except Exception: + data = {} + if data.get("status") == "success": + nodes[0]["properties"] = { + "proxy": bool(data.get("proxy")), + "hosting": bool(data.get("hosting")), + "mobile": bool(data.get("mobile")), + "source": "ip-api.com", + } + if data.get("isp"): + iid = f"company:{data['isp']}" + nodes.append({"id": iid, "label": data["isp"], "type": "company", "properties": {"role": "ISP"}}) + links.append({"source": root_id, "target": iid, "label": "HOSTED_BY"}) + if data.get("country"): + cid = f"country:{data['country']}" + nodes.append( + { + "id": cid, + "label": data["country"], + "type": "country", + "properties": {"code": data.get("countryCode")}, + } + ) + links.append({"source": root_id, "target": cid, "label": "LOCATED_IN"}) + for val in (data.get("isp"), data.get("org"), data.get("asname")): + if val: + for entry in match_exact(val): + sid = f"sanction:{entry['id']}" + nodes.append({"id": sid, "label": entry["name"], "type": "sanction", "properties": {}}) + links.append({"source": root_id, "target": sid, "label": "SANCTIONS MATCH"}) + + whois = fetch_with_curl( + f"https://stat.ripe.net/data/whois/data.json?resource={quote(id_value)}", + timeout=8, + ) + if whois.status_code == 200: + try: + records = whois.json().get("data", {}).get("records") or [] + except Exception: + records = [] + for record in records: + for field in record: + if field.get("key") in ("netname", "NetName"): + nid = f"company:{field['value']}" + nodes.append({"id": nid, "label": field["value"], "type": "company", "properties": {"role": "Network"}}) + links.append({"source": root_id, "target": nid, "label": "HOSTED_BY"}) + + result = _dedup(nodes, links) + _wd_cache_set(cache_key, result) + return result + + +def _resolve_company(id_value: str) -> dict[str, Any]: + cache_key = f"company:{id_value}" + cached = _wd_cache_get(cache_key) + if cached: + return cached + root_id = f"company:{id_value}" + nodes = [{"id": root_id, "label": id_value, "type": "company", "properties": {}}] + links: list[dict] = [] + safe = re.sub(r'[^a-zA-Z0-9 \-._]', '', id_value).strip() + qid = _wd_search(safe) + filt = f"VALUES ?item {{ wd:{qid} }}" if qid else f'?item rdfs:label "{safe}"@en . ?item wdt:P31/wdt:P279* wd:Q4830453 .' + rows = _sparql( + f""" + SELECT ?countryLabel ?parentLabel ?ceoLabel WHERE {{ + {filt} + OPTIONAL {{ ?item wdt:P17 ?country . }} + OPTIONAL {{ ?item wdt:P749 ?parent . }} + OPTIONAL {{ ?item wdt:P169 ?ceo . }} + SERVICE wikibase:label {{ bd:serviceParam wikibase:language "en" . }} + }} LIMIT 10 + """ + ) + for row in rows: + if row.get("countryLabel", {}).get("value"): + cid = f"country:{row['countryLabel']['value']}" + nodes.append({"id": cid, "label": row["countryLabel"]["value"], "type": "country", "properties": {}}) + links.append({"source": root_id, "target": cid, "label": "HEADQUARTERED"}) + if row.get("parentLabel", {}).get("value"): + pid = f"company:{row['parentLabel']['value']}" + nodes.append({"id": pid, "label": row["parentLabel"]["value"], "type": "company", "properties": {}}) + links.append({"source": root_id, "target": pid, "label": "PARENT ORG"}) + if row.get("ceoLabel", {}).get("value"): + pid = f"person:{row['ceoLabel']['value']}" + nodes.append({"id": pid, "label": row["ceoLabel"]["value"], "type": "person", "properties": {"role": "CEO"}}) + links.append({"source": root_id, "target": pid, "label": "CEO"}) + _add_sanctions(id_value, root_id, nodes, links) + result = _dedup(nodes, links) + _wd_cache_set(cache_key, result) + return result + + +def _resolve_from_store(entity_type: str, id_value: str, props: dict[str, Any]) -> dict[str, Any]: + from services.fetchers._store import get_latest_data_subset_refs + + root_id = f"{entity_type}:{id_value}" + nodes = [{"id": root_id, "label": props.get("label") or id_value, "type": entity_type, "properties": props}] + links: list[dict] = [] + data = get_latest_data_subset_refs("flights", "ships", "military_flights", "tracked_flights") + + if entity_type == "aircraft": + icao = (props.get("icao24") or id_value).lower() + for bucket in ("military_flights", "tracked_flights", "flights"): + for f in data.get(bucket) or []: + if str(f.get("icao24", "")).lower() == icao: + if f.get("country"): + cid = f"country:{f['country']}" + nodes.append({"id": cid, "label": f["country"], "type": "country", "properties": {}}) + links.append({"source": root_id, "target": cid, "label": "REGISTERED_IN"}) + if f.get("registration"): + nodes[0]["properties"]["registration"] = f["registration"] + break + elif entity_type == "vessel": + mmsi = str(props.get("mmsi") or id_value) + for ship in data.get("ships") or []: + if str(ship.get("mmsi")) == mmsi: + if ship.get("country"): + cid = f"country:{ship['country']}" + nodes.append({"id": cid, "label": ship["country"], "type": "country", "properties": {}}) + links.append({"source": root_id, "target": cid, "label": "FLAG"}) + break + _add_sanctions(id_value, root_id, nodes, links) + return _dedup(nodes, links) + + +def resolve_entity(entity_type: str, id_value: str, properties: dict[str, Any] | None = None) -> dict[str, Any]: + etype = (entity_type or "").lower().strip() + eid = (id_value or "").strip() + if etype not in ALLOWED_TYPES: + raise ValueError(f"Invalid type. Allowed: {', '.join(sorted(ALLOWED_TYPES))}") + if len(eid) < 2 or len(eid) > 200: + raise ValueError("Invalid id (2-200 chars)") + props = properties or {} + + if etype == "ip": + return _resolve_ip(eid) + if etype in ("company", "person", "country"): + if etype == "company": + return _resolve_company(eid) + if etype == "person": + root_id = f"person:{eid}" + nodes = [{"id": root_id, "label": eid, "type": "person", "properties": {}}] + links: list[dict] = [] + _add_sanctions(eid, root_id, nodes, links) + return _dedup(nodes, links) + root_id = f"country:{eid}" + nodes = [{"id": root_id, "label": eid, "type": "country", "properties": {}}] + links = [] + _add_sanctions(eid, root_id, nodes, links) + return _dedup(nodes, links) + return _resolve_from_store(etype, eid, props) diff --git a/backend/services/sanctions/__init__.py b/backend/services/sanctions/__init__.py new file mode 100644 index 0000000..f3bc3f6 --- /dev/null +++ b/backend/services/sanctions/__init__.py @@ -0,0 +1 @@ +"""Sanctions screening (OpenSanctions OFAC SDN).""" diff --git a/backend/services/sanctions/ofac.py b/backend/services/sanctions/ofac.py new file mode 100644 index 0000000..663ae34 --- /dev/null +++ b/backend/services/sanctions/ofac.py @@ -0,0 +1,154 @@ +"""OFAC SDN index via OpenSanctions (adapted from Osiris sanctions.ts).""" +from __future__ import annotations + +import csv +import io +import logging +import re +import threading +import time +from dataclasses import dataclass, field +from typing import Any + +from services.network_utils import fetch_with_curl + +logger = logging.getLogger(__name__) + +SDN_CSV_URL = "https://data.opensanctions.org/datasets/latest/us_ofac_sdn/targets.simple.csv" +TTL_S = 24 * 60 * 60 + +_lock = threading.Lock() +_cache: dict[str, Any] | None = None +_cache_at: float = 0.0 +_inflight: threading.Event | None = None + + +@dataclass +class SanctionEntry: + id: str + schema: str + name: str + aliases: list[str] = field(default_factory=list) + countries: list[str] = field(default_factory=list) + programs: list[str] = field(default_factory=list) + sanctions: str = "" + first_seen: str | None = None + last_seen: str | None = None + + def to_dict(self) -> dict[str, Any]: + return { + "id": self.id, + "schema": self.schema, + "name": self.name, + "aliases": self.aliases, + "countries": self.countries, + "programs": self.programs, + "sanctions": self.sanctions, + "first_seen": self.first_seen, + "last_seen": self.last_seen, + } + + +def norm_name(s: str) -> str: + s = re.sub(r"[^\w\s]+", " ", s.lower(), flags=re.UNICODE) + return re.sub(r"\s+", " ", s).strip() + + +def _split_semi(val: str) -> list[str]: + return [x.strip() for x in (val or "").split(";") if x.strip()] + + +def _load_list() -> dict[str, Any]: + global _cache, _cache_at + with _lock: + if _cache and (time.time() - _cache_at) < TTL_S: + return _cache + + try: + resp = fetch_with_curl(SDN_CSV_URL, timeout=45, headers={"Accept": "text/csv"}) + if resp.status_code != 200: + raise RuntimeError(f"OpenSanctions HTTP {resp.status_code}") + text = resp.text + reader = csv.DictReader(io.StringIO(text)) + entries: list[SanctionEntry] = [] + by_norm: dict[str, list[SanctionEntry]] = {} + for row in reader: + name = (row.get("name") or "").strip() + if not name: + continue + entry = SanctionEntry( + id=row.get("id") or "", + schema=row.get("schema") or "LegalEntity", + name=name, + aliases=_split_semi(row.get("aliases") or ""), + countries=_split_semi(row.get("countries") or ""), + programs=_split_semi(row.get("program_ids") or ""), + sanctions=row.get("sanctions") or "", + first_seen=row.get("first_seen") or None, + last_seen=row.get("last_seen") or None, + ) + entries.append(entry) + for key in {norm_name(name), *(norm_name(a) for a in entry.aliases)}: + if not key: + continue + by_norm.setdefault(key, []).append(entry) + loaded = {"entries": entries, "by_norm": by_norm, "fetched_at": time.time()} + with _lock: + _cache = loaded + _cache_at = time.time() + logger.info("OFAC SDN index loaded: %s entries", len(entries)) + return loaded + except Exception as exc: + logger.error("OFAC SDN load failed: %s", exc) + with _lock: + if _cache: + return _cache + raise + + +def match_exact(query: str) -> list[dict[str, Any]]: + if not query or len(query) < 3: + return [] + data = _load_list() + hits = data["by_norm"].get(norm_name(query), []) + return [e.to_dict() for e in hits] + + +def search_sanctions(query: str, *, schema: str | None = None, limit: int = 50) -> list[dict[str, Any]]: + if not query or len(query) < 4: + return [] + data = _load_list() + q = norm_name(query) + exact_name: list[SanctionEntry] = [] + exact_alias: list[SanctionEntry] = [] + sub_name: list[SanctionEntry] = [] + sub_alias: list[SanctionEntry] = [] + seen: set[str] = set() + + def push(bucket: list[SanctionEntry], entry: SanctionEntry) -> None: + if entry.id in seen: + return + if schema and entry.schema != schema: + return + seen.add(entry.id) + bucket.append(entry) + + for entry in data["entries"]: + name_norm = norm_name(entry.name) + if name_norm == q: + push(exact_name, entry) + elif any(norm_name(a) == q for a in entry.aliases): + push(exact_alias, entry) + elif q in name_norm: + push(sub_name, entry) + elif any(q in norm_name(a) for a in entry.aliases): + push(sub_alias, entry) + if len(seen) >= limit * 4: + break + + ordered = exact_name + exact_alias + sub_name + sub_alias + return [e.to_dict() for e in ordered[:limit]] + + +def index_size() -> int: + return len(_load_list()["entries"]) diff --git a/backend/services/scm/__init__.py b/backend/services/scm/__init__.py new file mode 100644 index 0000000..e39c635 --- /dev/null +++ b/backend/services/scm/__init__.py @@ -0,0 +1 @@ +"""Supply-chain risk overlay.""" diff --git a/backend/services/scm/suppliers.py b/backend/services/scm/suppliers.py new file mode 100644 index 0000000..3695023 --- /dev/null +++ b/backend/services/scm/suppliers.py @@ -0,0 +1,154 @@ +"""SCM supplier risk overlay (Osiris port, uses in-memory dashboard data).""" +from __future__ import annotations + +import math +from datetime import datetime, timezone +from typing import Any + +from services.fetchers._store import _data_lock, _mark_fresh, get_latest_data_subset_refs, is_any_active, latest_data +from services.network_utils import fetch_with_curl + +SUPPLIERS: list[dict[str, Any]] = [ + {"id": "sup-tsmc-hsinchu", "name": "TSMC Fab 12 (Tier 1)", "city": "Hsinchu", "country": "Taiwan", "lat": 24.774, "lng": 120.992, "category": "Semiconductor"}, + {"id": "sup-tsmc-tainan", "name": "TSMC Fab 14 (Tier 1)", "city": "Tainan", "country": "Taiwan", "lat": 23.111, "lng": 120.273, "category": "Semiconductor"}, + {"id": "sup-sec-giheung", "name": "Samsung Electronics (Tier 1)", "city": "Giheung", "country": "South Korea", "lat": 37.221, "lng": 127.098, "category": "Semiconductor"}, + {"id": "sup-sk-icheon", "name": "SK Hynix (Tier 1)", "city": "Icheon", "country": "South Korea", "lat": 37.256, "lng": 127.483, "category": "Semiconductor"}, + {"id": "sup-sony-kumamoto", "name": "Sony Semiconductor (Tier 2)", "city": "Kikuyo", "country": "Japan", "lat": 32.883, "lng": 130.825, "category": "Electronics"}, + {"id": "sup-mlcc-murata", "name": "Murata MLCC (Tier 2)", "city": "Izumo", "country": "Japan", "lat": 35.361, "lng": 132.756, "category": "Electronics"}, + {"id": "sup-bosch-stuttgart", "name": "Bosch Auto Parts (Tier 1)", "city": "Stuttgart", "country": "Germany", "lat": 48.815, "lng": 9.176, "category": "Automotive"}, + {"id": "sup-zf-bavaria", "name": "ZF Friedrichshafen (Tier 1)", "city": "Friedrichshafen", "country": "Germany", "lat": 47.662, "lng": 9.489, "category": "Automotive"}, + {"id": "sup-valeo-paris", "name": "Valeo R&D (Tier 2)", "city": "Paris", "country": "France", "lat": 48.878, "lng": 2.308, "category": "Automotive"}, + {"id": "sup-magna-celaya", "name": "Magna Assembly (Tier 2)", "city": "Celaya", "country": "Mexico", "lat": 20.525, "lng": -100.814, "category": "Automotive"}, + {"id": "sup-denso-monterrey", "name": "Denso Corp (Tier 1)", "city": "Monterrey", "country": "Mexico", "lat": 25.772, "lng": -100.174, "category": "Automotive"}, + {"id": "sup-catl-ningde", "name": "CATL Battery HQ (Tier 1)", "city": "Ningde", "country": "China", "lat": 26.666, "lng": 119.544, "category": "Battery"}, + {"id": "sup-byd-shenzhen", "name": "BYD Gigafactory (Tier 1)", "city": "Shenzhen", "country": "China", "lat": 22.684, "lng": 114.341, "category": "Battery"}, + {"id": "sup-panasonic-nevada", "name": "Panasonic Giga (Tier 1)", "city": "Sparks", "country": "US", "lat": 39.539, "lng": -119.439, "category": "Battery"}, +] + + +def _distance_km(lat1: float, lng1: float, lat2: float, lng2: float) -> float: + dx = (lng1 - lng2) * math.cos(math.radians((lat1 + lat2) / 2)) + dy = lat1 - lat2 + return math.sqrt(dx * dx + dy * dy) * 111.32 + + +def _seismic_risk_level(distance_km: float, magnitude: float) -> str | None: + """Meaningful fab impact only — ignore routine micro-quakes (e.g. Taiwan M3.x).""" + if magnitude < 4.5: + return None + if magnitude >= 6.0 and distance_km <= 200: + return "CRITICAL" + if magnitude >= 5.5 and distance_km <= 75: + return "CRITICAL" + if magnitude >= 5.0 and distance_km <= 100: + return "HIGH" + if magnitude >= 4.5 and distance_km <= 40: + return "HIGH" + return None + + +def _apply_seismic_threats(suppliers: list[dict[str, Any]], earthquakes: list[dict[str, Any]]) -> None: + for sup in suppliers: + best: tuple[str, float] | None = None + for eq in earthquakes: + lat = eq.get("lat") + lng = eq.get("lng") or eq.get("lon") + mag = float(eq.get("mag") or eq.get("magnitude") or 0) + if lat is None or lng is None or mag < 4.5: + continue + dist = _distance_km(sup["lat"], sup["lng"], float(lat), float(lng)) + level = _seismic_risk_level(dist, mag) + if not level: + continue + severity = {"HIGH": 1, "CRITICAL": 2} + if best is None: + best = (level, mag) + else: + cur = severity[level] + prev = severity[best[0]] + if cur > prev or (cur == prev and mag > best[1]): + best = (level, mag) + if best: + level, mag = best + if sup["risk_level"] == "NORMAL" or ( + level == "CRITICAL" and sup["risk_level"] != "CRITICAL" + ): + sup["risk_level"] = level + elif level == "CRITICAL" and sup["risk_level"] == "HIGH": + sup["risk_level"] = "CRITICAL" + sup["active_threats"].append(f"SEISMIC PROXIMITY (M{mag:.1f})") + + +def build_scm_payload() -> dict[str, Any]: + suppliers = [{**s, "risk_level": "NORMAL", "active_threats": []} for s in SUPPLIERS] + refs = get_latest_data_subset_refs("earthquakes", "firms_fires", "gdelt") + + earthquakes = refs.get("earthquakes") or [] + _apply_seismic_threats(suppliers, earthquakes) + + fires = refs.get("firms_fires") or [] + for sup in suppliers: + count = 0 + for fire in fires: + lat = fire.get("lat") or fire.get("latitude") + lng = fire.get("lng") or fire.get("lon") or fire.get("longitude") + if lat is None or lng is None: + continue + if _distance_km(sup["lat"], sup["lng"], float(lat), float(lng)) < 50: + count += 1 + if count: + if sup["risk_level"] == "NORMAL": + sup["risk_level"] = "HIGH" + sup["active_threats"].append(f"WILDFIRE PROXIMITY ({count} hotspots)") + + conflicts = refs.get("gdelt") or [] + for sup in suppliers: + for event in conflicts: + lat = event.get("lat") + lng = event.get("lng") or event.get("lon") + if lat is None or lng is None: + continue + if _distance_km(sup["lat"], sup["lng"], float(lat), float(lng)) < 100: + sup["risk_level"] = "CRITICAL" + sup["active_threats"].append("ARMED CONFLICT / RIOT") + break + + # USGS fallback if earthquakes empty + if not earthquakes: + try: + resp = fetch_with_curl( + "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/4.5_day.geojson", + timeout=5, + ) + if resp.status_code == 200: + features = resp.json().get("features") or [] + usgs_quakes = [ + { + "lat": f.get("geometry", {}).get("coordinates", [None, None])[1], + "lng": f.get("geometry", {}).get("coordinates", [None, None])[0], + "mag": f.get("properties", {}).get("mag") or 0, + } + for f in features + if len(f.get("geometry", {}).get("coordinates") or []) >= 2 + ] + _apply_seismic_threats(suppliers, usgs_quakes) + except Exception: + pass + + critical = sum(1 for s in suppliers if s["risk_level"] == "CRITICAL") + return { + "suppliers": suppliers, + "total": len(suppliers), + "critical_count": critical, + "timestamp": datetime.now(timezone.utc).isoformat(), + } + + +def fetch_scm_suppliers() -> dict[str, Any]: + if not is_any_active("scm_suppliers"): + return latest_data.get("scm_suppliers") or {} + payload = build_scm_payload() + with _data_lock: + latest_data["scm_suppliers"] = payload + _mark_fresh("scm_suppliers") + return payload diff --git a/backend/services/ssrf_guard.py b/backend/services/ssrf_guard.py new file mode 100644 index 0000000..6c1855d --- /dev/null +++ b/backend/services/ssrf_guard.py @@ -0,0 +1,141 @@ +"""SSRF guard for operator-initiated recon (ported from Osiris ssrf-guard.ts).""" +from __future__ import annotations + +import ipaddress +import re +import socket +from typing import Any +from urllib.parse import urljoin, urlparse + +import requests + +_IPV4_BLOCKS = [ + ipaddress.ip_network("0.0.0.0/8"), + ipaddress.ip_network("10.0.0.0/8"), + ipaddress.ip_network("100.64.0.0/10"), + ipaddress.ip_network("127.0.0.0/8"), + ipaddress.ip_network("169.254.0.0/16"), + ipaddress.ip_network("172.16.0.0/12"), + ipaddress.ip_network("192.0.0.0/24"), + ipaddress.ip_network("192.0.2.0/24"), + ipaddress.ip_network("192.168.0.0/16"), + ipaddress.ip_network("198.18.0.0/15"), + ipaddress.ip_network("198.51.100.0/24"), + ipaddress.ip_network("203.0.113.0/24"), + ipaddress.ip_network("224.0.0.0/4"), + ipaddress.ip_network("240.0.0.0/4"), +] + +_NAME_BLOCKLIST = ( + re.compile(r"^localhost$", re.I), + re.compile(r"\.localhost$", re.I), + re.compile(r"^host\.docker\.internal$", re.I), + re.compile(r"\.local$", re.I), + re.compile(r"\.internal$", re.I), + re.compile(r"^metadata\.google\.internal$", re.I), +) + +_HOSTNAME_RE = re.compile( + r"^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$" +) + + +def _ipv4_blocked(ip: str) -> bool: + try: + addr = ipaddress.ip_address(ip) + except ValueError: + return True + if not isinstance(addr, ipaddress.IPv4Address): + return False + return any(addr in net for net in _IPV4_BLOCKS) + + +def _ip_blocked(ip: str) -> bool: + try: + addr = ipaddress.ip_address(ip) + except ValueError: + return True + if isinstance(addr, ipaddress.IPv4Address): + return _ipv4_blocked(ip) + return ( + addr.is_loopback + or addr.is_private + or addr.is_link_local + or addr.is_multicast + or addr.is_reserved + or addr.is_unspecified + ) + + +def validate_host(host: str) -> dict[str, Any]: + trimmed = (host or "").strip() + if not trimmed: + return {"ok": False, "reason": "empty host"} + bracketed = trimmed.strip("[]") + lower = trimmed.lower() + if any(p.search(lower) for p in _NAME_BLOCKLIST): + return {"ok": False, "reason": "hostname matches reserved name pattern"} + + try: + ipaddress.ip_address(bracketed) + is_literal = True + except ValueError: + is_literal = False + + if is_literal: + if _ip_blocked(bracketed): + return {"ok": False, "reason": "IP in reserved range"} + return {"ok": True, "resolved": [bracketed]} + + if not _HOSTNAME_RE.match(trimmed): + return {"ok": False, "reason": "invalid hostname syntax"} + + try: + infos = socket.getaddrinfo(trimmed, None, proto=socket.IPPROTO_TCP) + except OSError as exc: + return {"ok": False, "reason": f"DNS lookup failed: {exc}"} + if not infos: + return {"ok": False, "reason": "hostname has no A/AAAA records"} + + resolved: list[str] = [] + for info in infos: + addr = info[4][0] + if _ip_blocked(addr): + return {"ok": False, "reason": f"hostname resolves to reserved IP {addr}"} + resolved.append(addr) + return {"ok": True, "resolved": resolved} + + +def safe_get( + url: str, + *, + timeout: float = 8.0, + headers: dict[str, str] | None = None, + max_redirects: int = 3, +) -> requests.Response: + current = url + for _ in range(max_redirects + 1): + parsed = urlparse(current) + if parsed.scheme not in ("http", "https"): + raise ValueError(f"blocked protocol {parsed.scheme}") + check = validate_host(parsed.hostname or "") + if not check.get("ok"): + raise ValueError(f"blocked target — {check.get('reason')}") + res = requests.get( + current, + timeout=timeout, + headers=headers or {}, + allow_redirects=False, + ) + if 300 <= res.status_code < 400: + loc = res.headers.get("location") + if not loc: + return res + current = urljoin(current, loc) + continue + return res + raise ValueError("too many redirects") + + +def validate_domain(domain: str) -> bool: + return bool(re.match(r"^[a-zA-Z0-9][a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$", domain or "")) diff --git a/backend/tests/test_cctv_pipeline.py b/backend/tests/test_cctv_pipeline.py index 0f435af..09d87d9 100644 --- a/backend/tests/test_cctv_pipeline.py +++ b/backend/tests/test_cctv_pipeline.py @@ -77,3 +77,62 @@ def test_ingest_updates_existing_rows_in_persistent_data_dir(tmp_path, monkeypat assert len(cameras) == 1 assert cameras[0]["media_url"] == "https://example.com/live.m3u8" assert cameras[0]["media_type"] == "hls" + + +def test_scheduled_cctv_ingestors_include_asfinag_and_alpr(): + names = {ing.__class__.__name__ for ing, _ in cctv_pipeline.scheduled_cctv_ingestors()} + assert "AsfinagIngestor" in names + assert "OSMALPRCameraIngestor" in names + assert "OSMTrafficCameraIngestor" in names + assert "Ontario511Ingestor" in names + assert "Alberta511Ingestor" in names + assert "Florida511Ingestor" in names + assert "AustraliaLiveTrafficIngestor" in names + assert "NetherlandsRWSIngestor" in names + assert len(names) == 21 + + +def test_fetch_traveliq_v2_cameras_parses_views(monkeypatch): + class FakeResp: + status_code = 200 + + @staticmethod + def json(): + return [ + { + "Id": 9, + "Latitude": 45.0, + "Longitude": -75.0, + "Location": "Test Highway", + "Views": [ + { + "Id": 42, + "Url": "/map/Cctv/42", + "Status": "Enabled", + "Description": "Northbound", + } + ], + } + ] + + monkeypatch.setattr(cctv_pipeline, "fetch_with_curl", lambda *a, **k: FakeResp()) + cameras = cctv_pipeline._fetch_traveliq_v2_cameras( + api_url="https://511on.ca/api/v2/get/cameras", + base_url="https://511on.ca", + id_prefix="ON511", + source_agency="511 Ontario", + ) + assert len(cameras) == 1 + assert cameras[0]["id"] == "ON511-9-42" + assert cameras[0]["media_url"] == "https://511on.ca/map/Cctv/42" + + +def test_ensure_https_upgrades_http_media_urls(): + assert ( + cctv_pipeline._ensure_https_url("http://example.com/camera.jpg") + == "https://example.com/camera.jpg" + ) + assert ( + cctv_pipeline._ensure_https_url("https://secure.example.com/live.m3u8") + == "https://secure.example.com/live.m3u8" + ) diff --git a/backend/tests/test_datacenters_fetch.py b/backend/tests/test_datacenters_fetch.py new file mode 100644 index 0000000..76d67fd --- /dev/null +++ b/backend/tests/test_datacenters_fetch.py @@ -0,0 +1,10 @@ +"""Datacenters load from static JSON regardless of layer toggle.""" +from services.fetchers import _store +from services.fetchers.infrastructure import fetch_datacenters + + +def test_fetch_datacenters_populates_store_when_layer_disabled(monkeypatch): + monkeypatch.setitem(_store.active_layers, "datacenters", False) + _store.latest_data["datacenters"] = [] + fetch_datacenters() + assert len(_store.latest_data.get("datacenters") or []) > 0 diff --git a/backend/tests/test_geo_fetchers.py b/backend/tests/test_geo_fetchers.py index 0d386af..c36e6b0 100644 --- a/backend/tests/test_geo_fetchers.py +++ b/backend/tests/test_geo_fetchers.py @@ -113,3 +113,52 @@ def test_fetch_fishing_activity_dedupes_to_latest_event_per_vessel(monkeypatch): assert latest_data["fishing_activity"][0]["vessel_ssvid"] == "ssvid-1" finally: latest_data["fishing_activity"] = original + + +def test_fetch_fishing_activity_respects_max_pages(monkeypatch): + from services.fetchers import geo + from services.fetchers._store import latest_data + + original = list(latest_data.get("fishing_activity") or []) + requests: list[str] = [] + + def fake_fetch(url, timeout=30, headers=None): + requests.append(url) + offset = 0 + if "offset=500" in url: + offset = 500 + payload = { + "total": 5000, + "entries": [ + { + "id": f"evt-{offset + i}", + "position": {"lat": 10.0 + i, "lon": 20.0 + i}, + "event": {"duration": 3600}, + "vessel": { + "id": f"v-{offset + i}", + "ssvid": f"ssvid-{offset + i}", + "name": f"Vessel-{offset + i}", + "flag": "US", + }, + } + for i in range(500) + ], + "nextOffset": offset + 500, + } + return SimpleNamespace(status_code=200, json=lambda p=payload: p) + + monkeypatch.setenv("GFW_API_TOKEN", "test-token") + monkeypatch.setenv("GFW_EVENTS_PAGE_SIZE", "500") + monkeypatch.setenv("GFW_EVENTS_MAX_PAGES", "2") + monkeypatch.setattr("services.fetchers._store.is_any_active", lambda *args: True) + monkeypatch.setattr(geo, "fetch_with_curl", fake_fetch) + monkeypatch.setattr(geo, "_mark_fresh", lambda *args, **kwargs: None) + monkeypatch.setattr(geo, "_last_fishing_fetch_ts", 0.0) + + try: + geo.fetch_fishing_activity() + assert len(latest_data["fishing_activity"]) == 1000 + assert len(requests) == 2 + assert all("offset=0" in url or "offset=500" in url for url in requests) + finally: + latest_data["fishing_activity"] = original diff --git a/backend/tests/test_osiris_port.py b/backend/tests/test_osiris_port.py new file mode 100644 index 0000000..af6fe05 --- /dev/null +++ b/backend/tests/test_osiris_port.py @@ -0,0 +1,43 @@ +"""Tests for Osiris-ported security and sanctions modules.""" +from __future__ import annotations + +import pytest + +from services.ssrf_guard import validate_host, validate_domain +from services.sanctions.ofac import norm_name, search_sanctions + + +def test_ssrf_blocks_localhost(): + result = validate_host("localhost") + assert result["ok"] is False + + +def test_ssrf_blocks_private_ip(): + result = validate_host("192.168.1.1") + assert result["ok"] is False + + +def test_ssrf_blocks_metadata_endpoint(): + result = validate_host("metadata.google.internal") + assert result["ok"] is False + + +def test_validate_domain_rejects_garbage(): + assert validate_domain("not a domain") is False + assert validate_domain("example.com") is True + + +def test_norm_name_strips_punctuation(): + assert norm_name("ACME, Inc.") == norm_name("acme inc") + + +def test_search_sanctions_requires_min_length(): + assert search_sanctions("ab") == [] + + +@pytest.mark.parametrize("query", ["127.0.0.1", "10.0.0.1"]) +def test_sweep_init_rejects_private(query: str): + from services.osint.lookups import sweep_init + + with pytest.raises(ValueError, match="Private|reserved|Invalid"): + sweep_init(query, 24) diff --git a/backend/tests/test_scm_suppliers.py b/backend/tests/test_scm_suppliers.py new file mode 100644 index 0000000..3499ccc --- /dev/null +++ b/backend/tests/test_scm_suppliers.py @@ -0,0 +1,13 @@ +from services.scm.suppliers import _seismic_risk_level + + +def test_micro_quakes_ignored(): + assert _seismic_risk_level(10.0, 3.9) is None + assert _seismic_risk_level(10.0, 4.4) is None + + +def test_meaningful_quake_thresholds(): + assert _seismic_risk_level(30.0, 4.6) == "HIGH" + assert _seismic_risk_level(80.0, 5.2) == "HIGH" + assert _seismic_risk_level(50.0, 5.6) == "CRITICAL" + assert _seismic_risk_level(150.0, 6.1) == "CRITICAL" diff --git a/backend/tests/test_telegram_osint.py b/backend/tests/test_telegram_osint.py new file mode 100644 index 0000000..57c8273 --- /dev/null +++ b/backend/tests/test_telegram_osint.py @@ -0,0 +1,103 @@ +"""Telegram OSINT HTML parsing and geoparsing.""" + +from services.fetchers import telegram_osint + + +SAMPLE_HTML = """ +
+
Missile strike reported near Kyiv overnight.
+ + + +
+
+ +""" + +SAMPLE_VIDEO_HTML = """ +
+
Drone footage from Kharkiv.
+ + + + +
+ + +""" + + +def test_parse_telegram_channel_html_extracts_geolocated_post(): + posts = telegram_osint.parse_telegram_channel_html(SAMPLE_HTML, "osintdefender") + assert len(posts) == 1 + post = posts[0] + assert "Kyiv" in post["title"] + assert post["coords"] == [50.45, 30.523] + assert post["risk_score"] >= 3 + assert post["link"].startswith("https://t.me/") + + +def test_resolve_telegram_coords_handles_cyrillic(): + coords = telegram_osint._resolve_telegram_coords("Обстріл біля Харкова") + assert coords == (49.993, 36.231) + + +def test_resolve_telegram_coords_uses_metro_anchors_for_country_tags(): + assert telegram_osint._resolve_telegram_coords("#Israel #Iran") == (32.085, 34.781) + assert telegram_osint._resolve_telegram_coords("China announces policy") == (39.904, 116.407) + assert telegram_osint._resolve_telegram_coords("#USA response") == (40.712, -74.006) + + +def test_resolve_telegram_coords_keeps_specific_cities_over_country_anchor(): + assert telegram_osint._resolve_telegram_coords("Strike near Gaza") == (31.416, 34.333) + assert telegram_osint._resolve_telegram_coords("Missile strike reported near Kyiv overnight") == ( + 50.45, + 30.523, + ) + + +def test_parse_telegram_channel_html_extracts_video_media(): + posts = telegram_osint.parse_telegram_channel_html(SAMPLE_VIDEO_HTML, "osintdefender") + assert len(posts) == 1 + post = posts[0] + assert post["media_type"] == "video" + assert post["media_url"].startswith("https://cdn4.telesco.pe/") + assert post["embed_url"] == "https://t.me/osintdefender/99999?embed=1" + + +def test_telegram_media_host_allowed(): + assert telegram_osint.telegram_media_host_allowed("cdn4.telesco.pe") + assert telegram_osint.telegram_media_host_allowed("cdn4.telegram-cdn.org") + assert not telegram_osint.telegram_media_host_allowed("evil.example.com") + + +def test_extract_new_channel_posts_stops_at_known_links(): + known = {"https://t.me/osintdefender/12345"} + fresh = telegram_osint._extract_new_channel_posts(SAMPLE_HTML, "osintdefender", known) + assert fresh == [] + + +def test_merge_telegram_posts_keeps_existing_and_adds_only_new(): + existing = [ + { + "id": "old", + "link": "https://t.me/osintdefender/111", + "published": "2026-06-01T12:00:00+00:00", + } + ] + incoming = [ + { + "id": "dup", + "link": "https://t.me/osintdefender/111", + "published": "2026-06-02T12:00:00+00:00", + }, + { + "id": "new", + "link": "https://t.me/osintdefender/222", + "published": "2026-06-03T12:00:00+00:00", + }, + ] + merged, added = telegram_osint._merge_telegram_posts(existing, incoming) + assert added == 1 + assert len(merged) == 2 + assert merged[0]["link"] == "https://t.me/osintdefender/222" diff --git a/backend/third_party/osiris/NOTICE.md b/backend/third_party/osiris/NOTICE.md new file mode 100644 index 0000000..fadab0a --- /dev/null +++ b/backend/third_party/osiris/NOTICE.md @@ -0,0 +1,14 @@ +# Osiris-derived components — third-party notice + +Portions of the recon toolkit, sanctions index, SCM overlay, entity graph, +malware feeds, and related UI were adapted from: + +- **OSIRIS** — MIT License — Copyright (c) 2026 simplifaisoul + https://github.com/simplifaisoul/osiris + +Additional data attribution: + +- **OpenSanctions** `us_ofac_sdn` dataset — CC-BY 4.0 + https://www.opensanctions.org/ +- **TeleGeography** submarine cable map data (static GeoJSON) +- **abuse.ch** Feodo Tracker / URLhaus (malware feeds) diff --git a/docker-compose.yml b/docker-compose.yml index 08db5a2..67976ed 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,6 +17,12 @@ services: - OPENSKY_CLIENT_ID=${OPENSKY_CLIENT_ID:-} - OPENSKY_CLIENT_SECRET=${OPENSKY_CLIENT_SECRET:-} - LTA_ACCOUNT_KEY=${LTA_ACCOUNT_KEY:-} + - GFW_API_TOKEN=${GFW_API_TOKEN:-} + - GFW_EVENTS_PAGE_SIZE=${GFW_EVENTS_PAGE_SIZE:-500} + - GFW_EVENTS_MAX_PAGES=${GFW_EVENTS_MAX_PAGES:-10} + - GFW_EVENTS_LOOKBACK_DAYS=${GFW_EVENTS_LOOKBACK_DAYS:-7} + - GFW_EVENTS_TIMEOUT_S=${GFW_EVENTS_TIMEOUT_S:-90} + - WINDY_API_KEY=${WINDY_API_KEY:-} - ADMIN_KEY=${ADMIN_KEY:-} - FINNHUB_API_KEY=${FINNHUB_API_KEY:-} # Override allowed CORS origins (comma-separated). Auto-detects LAN IPs if empty. @@ -77,6 +83,9 @@ services: - FIMI_ENABLED=${FIMI_ENABLED:-false} - NUFORC_ENABLED=${NUFORC_ENABLED:-false} - NEWS_ENABLED=${NEWS_ENABLED:-true} + - TELEGRAM_OSINT_ENABLED=${TELEGRAM_OSINT_ENABLED:-true} + - TELEGRAM_OSINT_CHANNELS=${TELEGRAM_OSINT_CHANNELS:-} + - TELEGRAM_OSINT_INTERVAL_MINUTES=${TELEGRAM_OSINT_INTERVAL_MINUTES:-60} volumes: - backend_data:/app/data restart: unless-stopped diff --git a/frontend/public/data/submarine-cables.json b/frontend/public/data/submarine-cables.json new file mode 100644 index 0000000..691936d --- /dev/null +++ b/frontend/public/data/submarine-cables.json @@ -0,0 +1 @@ +{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"id":"fea","name":"FEA","color":"#97b93c","feature_id":"fea-0","coordinates":[48.28492053481918,12.648196968516562],"length_km":11897.835610330192},"geometry":{"type":"MultiLineString","coordinates":[[[32.52993119143143,29.972545436050364],[32.48440122368525,29.63833609362628],[32.653276104052736,29.344566989489813],[32.65318110412008,29.113614162980063],[33.412950565897326,28.161052262220792],[34.14380004815155,27.364667993860262],[34.931299490279585,26.562513149236715],[35.55004905195155,25.75470426341523],[36.56254833468653,24.12261698700344],[37.80004745803156,22.05298561667754],[38.5875469001596,20.375041253465433],[39.93754594380764,18.251816319028222],[40.950045226544624,16.534196198259725],[42.07504442958354,14.801154224791581],[42.58129407095158,13.92930384327183],[43.03129375216765,13.054150695298627],[43.22816861269962,12.834868817846521],[43.31254355292765,12.615395567393394],[43.65004331383962,12.395734000022975],[44.55004267627159,11.900822861999712],[45.450042038703735,11.955858207114732],[48.60003980721571,12.72515592356304],[55.35003502545574,14.801154224791581],[58.95003247518379,16.534196198259725],[61.65003056247987,18.251816319028222],[66.60002705585578,19.104405475930452],[70.20002450558383,19.104405475930452],[72.87590260996693,19.07607425728523]],[[56.33372432860119,25.121690004958644],[56.92503390971164,24.83931282559271],[58.50003279396771,24.353428099494565],[59.85003183761575,23.71125814248539],[61.65003056247987,22.469443964829516],[61.65003056247987,18.251816319028222]],[[99.67500362523216,5.510071711803246],[97.42500521915215,6.908035999527216]],[[72.87590260996693,19.07607425728523],[72.67502275227187,16.965102599435927],[73.74513399127409,13.492128176464083],[75.09513303492214,9.52441134501949],[77.29024347696435,6.852191098754328],[78.69513048465001,5.061986954416114],[81.00001685476798,4.389285926050993],[85.500013666928,5.510071711803246],[90.00001047908802,6.405200795356032],[92.70000856638391,6.405200795356032],[94.27500745064,6.628746603597807],[95.40000665368001,6.628746603597807],[97.42500521915215,6.908035999527216],[99.00000410340806,6.852191098754328],[99.67500362523216,6.740481724921185]],[[35.005095000000225,29.581033231032787],[34.71137964666887,29.08904402302273],[34.51762978332711,28.313851747133643],[34.46137982257925,28.115586087218034],[34.44732233253752,27.991483447904724],[34.44732233253752,27.916953178205596],[34.76254960982351,26.964304734562898],[35.55004905195155,25.75470426341523]],[[39.18783647490891,21.48407381384701],[37.80004745803156,22.05298561667754]]]}},{"type":"Feature","properties":{"id":"apx-east","name":"APX East","color":"#939597","feature_id":"apx-east-0","coordinates":[-158.27113338492862,19.750197618517358],"length_km":12751.85370956038},"geometry":{"type":"MultiLineString","coordinates":[[[-158.08325,21.33416151999962],[-157.612209709934,20.89386327374593]],[[-179.9997982511102,-18.061131292991963],[-179.42038154188631,-17.39087476566283],[-178.74461906750656,-15.006817032918805],[-174.85607630903357,-6.488084689915104],[-170.61177896620688,2.034307151732652],[-164.88359854945884,10.466900881405515],[-158.3545375332775,19.60543660356217],[-157.612209709934,20.89386327374593],[-157.1766943014276,21.31382923952174],[-153.02181855404697,23.579250199271907],[-147.51596477447862,25.207427175713182],[-138.5998275786419,26.642332131240735],[-127.7443269907507,30.931712269623308],[-117.59240577997376,32.74410431204178]],[[178.43744782917764,-18.123810943537187],[178.73193115309707,-18.559310645747416]],[[151.20699711948467,-33.86955173177822],[152.0782218382938,-34.02550502281781],[154.79996457419256,-32.7971829776099],[160.65491043157013,-30.92010189533491],[166.46655787137254,-26.350166054761747],[171.08750007969573,-23.120800845543467],[178.93328068350618,-18.437353927623583],[179.99916547224635,-18.061134844662394]]]}},{"type":"Feature","properties":{"id":"i-am-cable","name":"I-AM Cable","color":"#939597","feature_id":"i-am-cable-0","coordinates":[118.75874804835738,16.793480780830787],"length_km":7672.500442220848},"geometry":{"type":"MultiLineString","coordinates":[[[136.7199507151282,30.53294284406713],[137.36641060200284,32.785366191258156],[136.87399727311598,34.33682825203173]],[[128.52566678030564,30.091027829845515],[130.674395726876,29.025552386567824],[131.5275258053045,28.952812528511625],[135.08525859472044,30.05919859667051],[137.66282478514836,30.80514961102188],[138.97867305155404,31.561902368184647],[139.72426021939998,33.43328037624602],[139.9190731999997,35.013343599999885]],[[127.32517934949227,31.656682201919793],[127.89442654126425,32.27726247137304],[129.67643493815459,33.95373326203758],[129.98803709203204,33.96159035645912],[130.40164185819341,33.59022724332908]],[[104.94056871477744,1.736802757529686],[105.2363763727327,1.570164819342774],[108.77548545901945,1.892657008485546],[109.12499082666056,2.250177622979053],[109.83178358835691,2.952621888152617],[111.19765176138824,3.455890703299274],[114.40336233155375,5.789850566899969],[116.49540191466217,8.708813834577057],[119.08615923318736,12.264847835707236],[118.6940271732684,15.19740563655082],[118.79999007691224,17.81054639660834],[119.5708391396146,18.516117231747454],[120.14998912056028,19.025415878074014],[121.17742979836576,19.649517415292912],[122.9440789374799,20.680547036291784],[124.97763468202892,22.0068603894298],[125.68750771631929,23.606545664540995],[126.39045223093163,25.57323317368417],[126.97650517696137,26.30123502337342],[127.70079480862088,28.509888502156297],[127.95091132809287,29.540507745394493],[128.52566678030564,30.091027829845515],[127.68084378563216,31.39196231991538],[127.32517934949227,31.656682201919793],[128.48826446305196,34.32871402630493],[128.99949285148878,35.17037876180022]],[[104.94056871477744,1.736802757529686],[104.11414047991015,1.925884465105483]]]}},{"type":"Feature","properties":{"id":"dhivaru","name":"Dhivaru","color":"#939597","feature_id":"dhivaru-0","coordinates":[74.54454111498403,-1.4353149100485663],"length_km":7223.875190353672},"geometry":{"type":"MultiLineString","coordinates":[[[73.08918245887737,-0.605519711481727],[72.96721004468763,-0.856036275299538]],[[59.12890734816923,23.95691876049908],[59.85003265596724,23.613306477496128],[61.43184006201105,22.443137450517234],[61.43184006201105,17.70450691214013],[66.58928706346421,4.200837482124073],[72.96721004468763,-0.856036275299538],[95.01270223995415,-8.952296383106004],[105.697069844276,-10.437358645528692]]]}},{"type":"Feature","properties":{"id":"fastnet","name":"Fastnet","color":"#939597","feature_id":"fastnet-0","coordinates":[-42.130994975499426,45.9247091404965],"length_km":5399.552175568134},"geometry":{"type":"LineString","coordinates":[[-75.0868362216179,38.333973036424055],[-74.15954119809128,38.98903709691993],[-71.20833234451034,40.384331936905745],[-68.39383801346253,40.87581011427994],[-61.11086880126824,41.90275121351782],[-50.3984584393863,43.90604356235435],[-39.60007920054038,46.54268254653617],[-23.399918393327315,50.45145951678565],[-16.199914287888834,50.32426129232395],[-10.759631602539102,50.4569444653093],[-8.963843834000034,51.557783405563]]}},{"type":"Feature","properties":{"id":"asia-united-gateway-east-aug-east","name":"Asia United Gateway East (AUG East)","color":"#939597","feature_id":"asia-united-gateway-east-aug-east-0","coordinates":[120.5229491866214,21.194716626934056],"length_km":7871.082545924348},"geometry":{"type":"MultiLineString","coordinates":[[[111.39672165701015,6.171501577529976],[113.00481498190783,5.487590881986131],[114.23759482610686,4.588647828574142]],[[104.11414047991015,1.925884465105483],[104.87449000000015,1.892657008485546]],[[118.6425127475267,17.11527658709149],[120.35483897484683,16.830716773770945]],[[120.88977859589048,22.340450000000153],[121.56833222233168,21.924689276263475]],[[121.81483169057424,24.644044899818205],[122.54589674250994,23.362789999999872]],[[126.73662929999966,35.96767720000027],[125.87119947589126,35.24748606079355],[125.29998547165854,34.21298798248237],[127.09744513582127,30.889576581749527],[128.20623341284528,29.582151688140414]],[[104.36205904297623,1.270050569926827],[104.87449000000015,1.892657008485546],[105.01938569859679,2.694535330212616],[105.6249994591074,3.122369746724294],[108.12382576441128,4.52214502236528],[110.04937200000029,5.3874206225493],[110.77665596587559,5.851759500300678],[114.12509490580292,7.582140257363066],[116.33876816197802,8.922093709519404],[118.95653880706887,12.3233643030216],[118.54461916347348,15.204850542914642],[118.68094913578804,17.860172049184143],[119.63564587191297,18.712816516277243],[120.07376012753181,19.097338103155888],[120.15077449851793,19.766307370739014],[120.60995966425898,21.52866349547806],[121.00227992030457,21.515456423470585],[122.27260117306696,22.41379429784117],[122.81360897983394,24.28586318606878],[125.35516121382157,27.94597135692341],[128.9423827125133,30.00041066943345],[130.46892712243186,29.23580178344556],[135.04811304345853,29.018742247699468],[137.7291107270958,29.776436679647418],[138.86036096921643,30.49131312259013],[139.68430240954356,31.888856797764863],[140.59357815315917,32.88889579845178],[140.00231880638776,35.01763482425761]],[[103.98701057056589,1.389451396800233],[104.36205904297623,1.270050569926827]]]}},{"type":"Feature","properties":{"id":"candle","name":"Candle","color":"#939597","feature_id":"candle-0","coordinates":[124.65330659048438,15.00880984412805],"length_km":7183.16626193893},"geometry":{"type":"MultiLineString","coordinates":[[[121.80145279439779,24.863504254255204],[123.10327422030367,23.605964017138188],[125.13103838718632,22.026048616021743]],[[124.70338539586164,16.18456011212011],[123.51328398507665,15.788534760565602],[122.2022204659374,15.763215182399467],[121.56019319908759,15.761538854943385]],[[104.43613306430733,1.371386341516657],[104.38095732214448,1.745889155817368],[104.1120592000004,1.934672203856039]],[[104.79111328158616,1.389440999999453],[108.80828174287835,1.736802757529686],[109.83178358835691,2.732428351392954],[111.39530787136668,3.27891318606882],[114.51459277101094,5.678236514249047],[116.08153497084551,7.767275218496221],[116.69308743865093,8.771383773025718],[119.59989185340848,12.602955744095647],[120.18280159671971,13.135750445218061]],[[120.60765565086099,13.572496884445817],[121.2985234625819,13.6150212673131],[122.39559299790758,12.75504864040869],[123.44753124604064,12.714337493644035]],[[124.09520116888906,12.319162730128816],[124.5097019530358,12.899450499230491],[125.13103838718632,22.026048616021743],[125.82482710987084,23.605964017138188],[127.32790884999972,24.81255270367667],[136.02580082175842,29.171402207908102],[137.7369866617662,29.63361253688773],[138.99232836452646,30.3690092136078],[140.17497493467252,32.43331330641721],[139.9766352308324,34.98004652018856]]]}},{"type":"Feature","properties":{"id":"sol","name":"Sol","color":"#939597","feature_id":"sol-0","coordinates":[-40.91502897305539,34.55949833109139],"length_km":7598.782645514151},"geometry":{"type":"MultiLineString","coordinates":[[[-25.676797574971758,37.74037606999961],[-25.456507920164015,37.35549021792695]],[[-64.02577349475438,31.895737453957153],[-64.65917995948635,32.36157723537831]],[[-81.22094335999995,29.5685423804529],[-73.34993072903801,30.97027945496055],[-64.02577349475438,31.895737453957153],[-61.85481927288275,31.74295976790169],[-50.00000020746783,33.13361886908059],[-39.99999508453427,34.70311221769831],[-25.456507920164015,37.35549021792695],[-16.199914288484827,44.694829089578164],[-9.899918750864702,45.646541495187385],[-5.810956804369809,45.16903272799839],[-4.871846141519585,44.66863115657076],[-3.833321486595139,43.45394044407752]]]}},{"type":"Feature","properties":{"id":"project-waterworth","name":"Project Waterworth","color":"#939597","feature_id":"project-waterworth-0","coordinates":[5.863543126433965,-27.98841784639247],"length_km":47460.87916657867},"geometry":{"type":"MultiLineString","coordinates":[[[33.490096931155925,-32.120588991728155],[31.980099654439186,-30.22719186196053],[30.88039235938437,-30.066210326831214]],[[87.72300880490236,-1.761851807844764],[90.09946213585373,-1.292098430153732],[92.73770385158454,3.460265883431271],[94.3164380847574,4.500452898358986],[95.47224819004255,5.742729330133516],[97.42500521915215,6.160631743962457],[99.6847692427181,5.86388465224445],[100.40982310408316,5.368393581488473]],[[76.85773463235368,0.847399963023893],[80.97374466123836,1.869852037928076],[85.500013666928,-1.270676198168406],[87.29076711246154,-0.856753963754112]],[[-179.9997982511102,3.097077087813343],[-169.96832199026696,16.17794031173706],[-161.61408917916899,23.0378173020752],[-147.45057458707294,30.255702942039875],[-127.79047240201581,33.529146127585946],[-122.38731473421552,33.477440406239914],[-120.6195929927509,33.45053234181001],[-118.90292030393906,33.82651440184361]],[[80.23727325388181,13.061502540930007],[81.52150148534334,11.831105221838026],[83.03675798088666,9.555359258837743],[83.42909910532154,7.869634986459248],[84.12223509182334,5.770024991079929],[90.77895616973278,-8.135430886528464],[94.72510478744105,-10.344819867074083],[123.06805345903014,-12.77887632491396],[129.87005785386256,-12.379679526898897]],[[131.6807426702204,-11.66969729826832],[131.72527388867405,-10.733304228874694],[132.75207980190007,-10.238639981123713],[138.5484135863478,-10.53440199037786],[142.13312784377766,-9.85125498416662],[144.89997158744055,-11.017428674932855],[147.26153671946165,-11.66969729826832],[153.48673308202717,-12.4774587840296],[160.98814378358247,-12.4774587840296],[170.00000263387216,-10.000034051545839],[179.99994672169302,3.097072904518241]],[[-78.88266988343256,33.69355790837514],[-74.2367384090291,31.895737453957153],[-56.93747717996609,23.619667749347347],[-52.10709095043794,16.57776764106849],[-38.292979177102396,1.433940786388127],[-37.82901999752645,-1.243529466953686],[-38.54877540946694,-3.720856079754825],[-35.947227022705135,-3.059997375917503],[-34.024755512974004,-3.469028991515528],[-31.65109171014376,-4.861397229447735],[13.886634799254555,-32.93449308708872],[16.894583419921453,-34.322821702626335],[18.043255079695783,-34.230964637690946]],[[18.171981177101898,-34.845022128572865],[18.74576413017278,-35.773488145151255],[19.623400227197045,-35.98012665125699],[23.413985104279334,-35.98012665125699],[27.97749587157728,-35.20204261409993],[38.62374269486827,-29.15403653793159],[45.29070355814658,-29.16507618637023],[47.34269069793371,-28.66595753524189],[56.973482479608386,-23.083727842143475],[62.53039521042826,-14.511535911566813],[65.57437726936688,-10.963562200511431],[74.66037634553088,-5.454560702582534],[76.26954099999939,-2.786733343456576],[76.85773463235368,0.847399963023893],[76.05002036139189,4.093063841323328],[75.63713069157988,8.283573814251765],[73.9070597935403,11.529557268608945],[73.90074005700309,11.538453807348953],[73.89027785728716,11.55119668203278],[73.87584899910533,11.56762651126271],[73.85762928717041,11.587583913641023],[73.83579452619524,11.610909507770005],[73.81052052089265,11.637443912251937],[73.78198307597543,11.667027745689104],[73.75035799615642,11.699501626683789],[73.71582108614845,11.734706173838275],[73.67854815066434,11.772482005754842],[73.6387149944169,11.812669741035775],[73.59649742211894,11.855109998283359],[73.55207123848332,11.899643396099872],[73.50561224822283,11.946110553087603],[73.45729625605031,11.994352087848831],[73.40729906667858,12.04420861898584],[73.35579648482046,12.095520765100915],[73.30296431518876,12.148129144796334],[73.2489783624963,12.201874376674384],[73.19401443145593,12.256597079337348],[73.13824832678046,12.312137871387508],[73.08185585318269,12.368337371427147],[73.02501281537546,12.425036198058548],[72.9678950180716,12.482074969883993],[72.91067826598392,12.539294305505766],[72.85353836382525,12.596534823526152],[72.7966511163084,12.653637142547431],[72.74019232814621,12.710441881171887],[72.68433780405148,12.766789658001803],[72.62926334873704,12.822521091639462],[72.57514476691573,12.877476800687148],[72.52215786330034,12.93149740374714],[72.47047844260372,12.984423519421728],[72.42028230953868,13.036095766313188],[72.37174526881803,13.086354763023808],[72.32504312515462,13.13504112815587],[72.28035168326124,13.181995480311652],[72.23784674785075,13.227058438093444],[72.19770412363594,13.270070620103526],[72.16009961532964,13.31087264494418],[72.12520902764467,13.34930513121769],[72.09320816529387,13.38520869752634],[72.06427283299004,13.418423962472412],[72.03857883544602,13.448791544658189],[72.01630197737461,13.476152062685951],[71.99761806348866,13.500346135157987],[71.98270289850096,13.521214380676577],[71.97173228712435,13.538597417844004],[71.9623279440557,13.562270341534498],[71.9623279440557,13.562270341534498],[71.9623279440557,16.965102599435927],[72.87590260996693,19.07607425728523]]]}},{"type":"Feature","properties":{"id":"e2a","name":"E2A","color":"#939597","feature_id":"e2a-0","coordinates":[150.20483591605063,35.3395606281797],"length_km":13436.13327126687},"geometry":{"type":"MultiLineString","coordinates":[[[141.60319903282527,42.6503368858653],[142.46989931401754,41.2387523289666],[144.5591158016331,40.046427062568554],[149.37742517921646,35.09752950393123]],[[130.1400211054319,33.59415838270747],[129.92752125596886,34.2343858923205],[128.99949285148878,35.17037876180022],[127.22759354455998,31.045550176016285],[129.44637107969632,28.381232304422188]],[[139.97546699999984,35.005433000000174],[140.5353756278244,34.73523695239605],[142.46336029527683,32.882826065515154],[142.8306622911177,32.40623097592082]],[[-179.99981351108062,41.930618376160524],[-151.17708002193328,41.930618376160524],[-138.88446003691308,39.45836886627482],[-129.6106746966534,35.73186715949322],[-122.84983873608158,34.6306903658982],[-120.85160406826154,35.34417937462789]],[[121.80144795065142,24.863504112487785],[122.87827437969564,25.925523115093068],[129.44637107969632,28.381232304422188],[135.05465206220015,28.81841452402809],[137.79450091450283,29.48716901807319],[143.5499725437925,32.81579602205538],[149.37742517921646,35.09752950393123],[172.73711131950242,41.930618376160524],[179.99992719256971,41.93061956425144]]]}},{"type":"Feature","properties":{"id":"orca","name":"ORCA","color":"#939597","feature_id":"orca-0","coordinates":[150.24960320115872,30.59792888376766],"length_km":12107.000489906517},"geometry":{"type":"MultiLineString","coordinates":[[[-129.5998339543217,36.87321951208928],[-122.3998390548656,33.565491482352044],[-120.59984033000157,33.51860823841915],[-118.39534899999987,33.86403999999962]],[[-123.687,38.96962000000024],[-124.64983746154178,38.651811712711336],[-129.5998339543217,36.87321951208928],[-139.00520928888923,37.327605702523414],[-151.1998186532859,35.90725015614043],[-163.79980972733406,34.43595690575565],[-179.9998012760825,33.31515395812905]],[[179.99994672169302,33.31515395812905],[172.79992073307184,32.81233785755136],[149.39996839960057,30.5144959597591],[137.24997700676838,26.36108632539156],[132.74998019460836,24.32780311165181],[128.69998306366443,23.91710129093513],[125.83455068834256,23.79499635869734],[125.08449263258076,23.798418307988157],[123.07498704846441,24.007054825363046],[121.8014540000001,24.863576000000307]]]}},{"type":"Feature","properties":{"id":"tasman-ring-network","name":"Tasman Ring Network","color":"#939597","feature_id":"tasman-ring-network-0","coordinates":[163.1243917769377,-37.4755324388374],"length_km":5795.6650310769255},"geometry":{"type":"MultiLineString","coordinates":[[[151.7987929508469,-34.82698199055459],[159.29996138635258,-37.222658379725665],[167.39995564824065,-37.758235764746985],[173.69995118407297,-37.04328040742407]],[[144.8048516542285,-38.52352352301163],[145.34997126806056,-39.2547415615617],[146.24997063108842,-39.861878679516124],[149.39996839960057,-38.99291515860618],[150.72496746155474,-37.942453525696855],[151.7987929508469,-34.82698199055459],[151.20704000000026,-33.869695999999635]],[[149.39996839960057,-38.99291515860618],[164.99995734782604,-45.7951245820919],[166.3249564091843,-46.33285525519881],[167.67495545283234,-46.487989748417824],[168.34750000000054,-46.413056000000275]],[[171.19999999999968,-42.46666700000018],[171.22495293798085,-41.454827610945536],[173.34995143201604,-38.73986711592515],[173.69995118407297,-37.04328040742407]],[[173.79995111323194,-38.91515016713438],[173.34995143201604,-38.73986711592515]]]}},{"type":"Feature","properties":{"id":"talaylink","name":"TalayLink","color":"#939597","feature_id":"talaylink-0","coordinates":[110.26326781057011,-26.766476642591662],"length_km":8864.339194037308},"geometry":{"type":"MultiLineString","coordinates":[[[100.06611334816643,6.613518860854109],[96.09214483515096,5.72580268887697],[95.50021180355195,5.708127481172063]],[[94.39121736831608,4.30521657626247],[94.4988798791227,3.367668509872753],[99.97532568091725,-4.151795635971226],[101.90529164924416,-5.681554559384467],[102.59094710926541,-7.138190551514161],[105.41361374274392,-10.565783729977944],[105.94799918019127,-15.265358169534869],[107.9205544240356,-20.433922197637408],[110.4658162928233,-27.31398249584017],[112.85917397234627,-30.67295608460977],[113.84999358353615,-32.042386559186966],[113.60321110091725,-33.06182889373477],[113.60321110091725,-34.611299785451145],[115.44676979611502,-36.28083847394901],[119.55018485796727,-36.44205411672081],[127.09256232737636,-37.45923113608103],[134.85085761257704,-38.106390221946036],[140.39997477528055,-39.16757423638764],[142.19997350014447,-39.51559387611211],[143.9999722250084,-39.51559387611211],[144.57985181362045,-38.52352352301163],[144.75060169265976,-38.25898836030261]],[[105.65751366522899,-10.4318730000001],[105.41361374274392,-10.565783729977944]],[[115.74022000000038,-32.537020895712736],[113.84999358353615,-32.042386559186966]]]}},{"type":"Feature","properties":{"id":"asia-connect-cable-1-acc-1","name":"Asia Connect Cable-1 (ACC-1)","color":"#939597","feature_id":"asia-connect-cable-1-acc-1-0","coordinates":[143.91185695233523,12.797443250456062],"length_km":19079.60615827567},"geometry":{"type":"MultiLineString","coordinates":[[[128.24998338125656,5.061986954416114],[126.33748473549242,5.957818681088533],[125.61287587559997,7.079988883160643]],[[107.77499788652415,-3.778666580061055],[107.77499788712005,-4.60145376483711],[107.32499820590417,-5.273944363641298],[106.82782855810404,-6.171876390816321]],[[117.37991922838094,-5.945707155070644],[118.79999007691224,-5.273944363641298],[119.41238964308275,-5.152180217334703]],[[123.74998656969242,-6.914561059201749],[125.32498545454442,-7.955717094334652],[125.58071527278696,-8.570689999999674]],[[126.8999843382044,2.068137876964541],[125.99998497636834,2.143087178471855],[124.87498577273243,1.805788280129153]],[[127.79998370063637,-4.676208028751072],[123.74998656969242,-6.914561059201749],[120.14998911996437,-6.914561059201749],[117.37991922838094,-5.945707155070644],[116.58310164737706,-5.525204085835],[114.46970572643436,-5.721872747834119],[112.0499948586722,-4.825692499217419],[109.34999676839627,-3.479268678970064],[107.77499788652415,-3.778666580061055],[106.9874984438013,-3.029995968008661],[106.87499852468808,-2.130918480960333],[106.4249988434722,-0.331409329660265],[105.29999964043219,0.793562652607196],[104.8499999592161,0.906050180869095]],[[179.99992672230314,19.95262290516439],[151.1999671244645,16.10232559580297],[146.24997063108842,14.256644994553485],[145.34997126865645,13.601498202276586],[144.89997158744055,13.3827080361257]],[[143.9999722250084,12.834868817846521],[137.24997700676838,9.967915186974132],[133.19997987582445,8.190543417795496],[128.24998338125656,5.061986954416114],[126.8999843382044,2.068137876964541],[126.44998465698852,0.118588418888312],[126.44998465698852,-1.231315750217412],[126.5744945687844,-1.751747198540771],[127.23748409911637,-2.580536704984131],[127.46248393972441,-3.254657364797681],[127.79998370063637,-4.676208028751072],[127.79998370063637,-6.467627592690688],[127.46248393972441,-8.252720521974979],[127.79998370063637,-9.586362493293953],[128.92498290367638,-10.620064860363238],[129.9375821869376,-12.108990934944092],[130.50000678851072,-12.219087219884248],[130.84314154543083,-12.467474336203543]],[[-179.99979825051412,19.95262290516439],[-172.79980335105813,21.635297384859552],[-163.799809726738,21.635297384859552],[-161.44056423563913,22.469372680574537],[-152.99981737755394,26.964304734562898],[-147.6030982105313,29.738014316088],[-138.60310458621126,31.288652857283093],[-127.79983522945767,33.189714664600466],[-122.3998390548656,33.37780603565933],[-120.59984033000157,33.37780603565933],[-118.79984160513764,33.89296086026743],[-118.39945344493313,33.862474868985494]]]}},{"type":"Feature","properties":{"id":"barat-timur-indonesia-2-bti-2","name":"Barat Timur Indonesia-2 (BTI-2)","color":"#939597","feature_id":"barat-timur-indonesia-2-bti-2-0","coordinates":[115.06424459648615,-6.058897973817234],"length_km":5322.397337514359},"geometry":{"type":"MultiLineString","coordinates":[[[107.92934120914595,0.125144305293085],[108.79413778955596,0.062743022966006]],[[108.0255698871077,-5.556836232143262],[107.5700525897694,-5.716442612332668],[106.96892712282748,-5.790569544810861],[106.791457,-6.108548339473468]],[[115.71425007478364,-6.112135256061153],[115.63417200651152,-6.971419999999669],[115.10901907969586,-7.785727122839621],[115.07594712344931,-8.115525289526817]],[[110.27177103540173,-6.048780990968187],[110.51371472880342,-6.689455323276428],[110.42549900000049,-6.971419999999669]],[[114.41254491570477,-3.739870959183627],[114.3103932573849,-4.424721793215856],[114.13221994550848,-4.838842846816813],[113.97217748075913,-6.048780990968187]],[[119.64813400673333,-5.679276766569264],[119.27840770615076,-5.945707155070644],[119.01248992577939,-6.690552320245617]],[[123.25408848349132,-3.577436557815043],[124.15161121973277,-3.585934037759635]],[[123.93724143733724,-1.188784534490496],[123.61629525815061,-0.887871730847793]],[[123.12391279505485,-1.0174889466678],[122.79275724839854,-0.938788738945164]],[[104.23671914307417,0.718701163999102],[107.92934120914595,0.125144305293085],[108.518987560062,-3.308304570300115],[107.9205544240356,-3.92832730414264],[107.8752206669785,-4.421462021257835],[108.028612707457,-5.57979326526674],[108.98720405963194,-6.200644751046103],[111.39530787136668,-5.881175967910585],[112.47060007836957,-6.110368702679498],[114.55431437305602,-6.017142230629707],[117.2863535895726,-6.240855834298438],[119.13788046195164,-6.723203526046124],[120.14039440019944,-6.656981736799576],[123.70604128832369,-6.656981736799576],[124.24565408402098,-5.020341303484515],[124.19467123697362,-4.066822301947936],[123.93724143733724,-1.188784534490496],[125.00091074855803,0.147235879809841],[125.14944846892539,0.786495458405646],[125.12195825402485,1.023645803504536]]]}},{"type":"Feature","properties":{"id":"umoja","name":"Umoja","color":"#939597","feature_id":"umoja-0","coordinates":[73.3691361580669,-30.194272617562497],"length_km":8133.562328984948},"geometry":{"type":"LineString","coordinates":[[115.74022000000038,-32.537020895712736],[113.84999358353615,-31.85146566557725],[89.10001111606014,-30.568602759492247],[45.450041666655096,-29.52991296614913],[38.70004682046353,-29.52991296614913],[33.30005064587154,-30.115516969775936],[32.40005128343957,-30.115516969775936],[30.88039235938437,-30.05771707645661]]}},{"type":"Feature","properties":{"id":"taihei","name":"Taihei","color":"#939597","feature_id":"taihei-0","coordinates":[160.28836810524842,32.02917547451569],"length_km":6163.663351855345},"geometry":{"type":"MultiLineString","coordinates":[[[-158.08325,21.33416151999962],[-158.3998135047771,21.216397899942],[-158.8498131859927,21.216397899942],[-161.44056418827003,21.84429407917369],[-179.9997982353205,27.896238989528694]],[[179.99994672169302,27.896238989528694],[149.39996839960057,34.31215165223547],[141.97497365894054,36.33133835588799],[141.07497429650857,36.542522130541435],[140.7182700000002,36.71307999999942]]]}},{"type":"Feature","properties":{"id":"honomoana","name":"Honomoana","color":"#939597","feature_id":"honomoana-0","coordinates":[-140.30285250410262,12.5325883545074],"length_km":15172.672808487649},"geometry":{"type":"MultiLineString","coordinates":[[[152.0999664863006,-37.995035579083044],[149.39996839960057,-39.34180065396819],[147.59996967473646,-39.68895338487733],[146.24997063108842,-40.03436927637599],[145.23747134537265,-39.2547415615617],[144.69235173392437,-38.52352352301163],[144.8048516542285,-38.25898836030261]],[[174.59995054710086,-31.340410556277746],[175.28071006484436,-33.34801975644684],[174.82499288827472,-36.140033391295425],[174.93749280857884,-36.59297842795038],[174.77046042690606,-36.88418050095063]],[[-149.3081207740364,-17.723342219804366],[-149.25614786794546,-17.131277756723552],[-149.22482005239348,-15.97048661114128],[-148.949820247206,-13.698987269610743],[-148.04982088417785,-9.586362493293953],[-138.59982757864174,17.395022634700517],[-127.79983522945767,26.964304734562898],[-119.6998409675696,31.670513047087127],[-117.44984256148977,32.62301664000789]],[[-179.9997982511102,-29.790604899288688],[-163.79980972733406,-23.49392244589784],[-156.5998148272819,-21.414661827960504],[-150.97481881208188,-17.597998996155503],[-149.84981960963796,-16.953454989809906],[-149.22482005239348,-15.97048661114128]],[[-149.84981960963796,-16.953454989809906],[-149.62481976783775,-17.168553094226155],[-149.44108067057505,-17.51215245847912]],[[179.99994678470878,-29.790605028386853],[174.59995054710086,-31.340410556277746],[166.94995596702458,-32.80208389299158],[159.29996138635258,-34.67300361524697],[152.0999664863006,-37.995035579083044]],[[152.0999664863006,-37.995035579083044],[152.023790917052,-34.82698199055459],[151.20704000000026,-33.869695999999635]]]}},{"type":"Feature","properties":{"id":"tabua","name":"Tabua","color":"#939597","feature_id":"tabua-0","coordinates":[-158.6775256239548,19.76826141245578],"length_km":13651.37041471297},"geometry":{"type":"MultiLineString","coordinates":[[[178.42494704980945,-18.559310645747416],[178.43744782917764,-18.123810943537187]],[[178.0874480765248,-18.77365906052572],[177.52494847500478,-18.347065591453177],[177.31889862097242,-18.100389418495688]],[[160.64997402527118,-30.697669744492647],[155.316576355408,-27.936291840641456],[153.96657731175995,-27.138263161649714],[153.5165776281601,-26.937854887542912],[153.40407770964404,-26.837516818959585],[153.08943909999996,-26.651840200000382]],[[-158.08325,21.33416151999962],[-157.90008311233407,21.128587431864354],[-157.80225453681595,20.89386327374593]],[[179.9999049246783,-17.81234135340585],[178.8749611145193,-18.240251410711533],[178.42494704980945,-18.559310645747416],[178.0874480765248,-18.77365906052572],[170.9999666932391,-22.665969967794794],[166.49996988107907,-25.94623071841455],[160.64997402527118,-30.697669744492647],[154.8000717646766,-32.61276000573574],[152.047927014282,-33.93716236175066],[151.20704000000026,-33.869695999999635]],[[-118.24533600000012,34.05348099999984],[-120.59984033000157,32.90681902852468],[-127.79983522945767,31.286738814391754],[-138.60310458621126,27.366657115363733],[-147.6030982105313,25.75672152517522],[-152.99981737755394,24.12261698700344],[-157.27481434970198,21.356164482330126],[-157.49981418971396,21.047336588294048],[-157.80225453681595,20.89386327374593],[-159.52481275518593,18.678647022154717],[-165.59980845160203,10.41081650540272],[-170.999804626194,2.367912558705407],[-175.4998014389502,-6.616650693475355],[-178.8747990480702,-15.006817032918805],[-179.54979856929816,-17.383402005942457],[-179.99979825051412,-17.81234135340585]]]}},{"type":"Feature","properties":{"id":"bulikula","name":"Bulikula","color":"#939597","feature_id":"bulikula-0","coordinates":[159.83186028836406,-3.531667448778883],"length_km":20353.497629486214},"geometry":{"type":"MultiLineString","coordinates":[[[-179.99979733681937,-18.220645596860603],[-177.99980191490647,-15.97048661114128]],[[-179.9997982511102,-16.054802311227153],[-177.99980191490647,-15.97048661114128],[-169.64981232407783,-16.522522203974884],[-156.59981974082785,-16.522522203974884],[-151.8748249160457,-16.953454989809906],[-150.03446255149993,-17.837289372768534],[-149.72150385007072,-17.78028785386549],[-149.72966799010806,-17.664652292845517],[-149.69106782289776,-17.538318795988708],[-149.5776818542037,-17.43919664767101],[-149.44108067057505,-17.51215245847912]],[[175.99994955532844,-15.657788279357506],[179.14994732384042,-16.090625820394795],[179.99994672169302,-16.054802405935337]],[[-158.08325,21.33416151999962],[-157.7973234078978,20.5096446349069],[-157.04981450969004,18.251816319028222],[-155.69981546544602,5.957818681088533],[-151.24899954602645,-13.539755839687075],[-149.53372862921304,-17.03242725695966],[-149.44108067057505,-17.51215245847912]],[[-149.30591250000003,-17.71856760999951],[-149.17482008781394,-17.383402005942457],[-149.16770301754153,-17.079257710327674],[-149.53372862921304,-17.03242725695966]],[[-149.72149710853895,-17.78028785386549],[-149.62897692964813,-17.799987092276584],[-149.49562930903915,-17.81785908364595],[-149.32858584802543,-17.734063000000265]],[[179.99994672169302,-18.220645596860603],[178.8749611145193,-18.34587912918448],[178.43744600000022,-18.123634]],[[-157.7973234078978,20.5096446349069],[-158.73731331305797,20.58581909604039],[-163.799809726738,19.104405475930452],[-172.79980335105813,19.104405475930452],[-179.99979825051412,17.395022634700517]],[[179.99994675357007,17.395022634700517],[151.1999671244645,13.492128176464083],[148.49996903716843,13.492128176464083],[147.14996999352056,13.710817738179635],[146.24997063108842,14.692360031374392],[145.637512,15.011117]],[[147.14996999352056,13.710817738179635],[146.92497015172074,12.615395567393394],[146.24997063108842,11.735650161405832]],[[144.69470173285575,13.464772962370143],[146.24997063108842,11.735650161405832],[149.39996839960057,9.08033076823294],[152.99996584873273,2.367912558705407],[159.07496154574454,-3.029995968008661],[167.84995532945672,-8.846050186819125],[173.69996478053517,-13.698987269610743],[175.99994955532844,-15.657788279357506],[177.0749487937889,-18.026426383713453],[177.31889862097242,-18.100389418495688]]]}},{"type":"Feature","properties":{"id":"halaihai","name":"Halaihai","color":"#939597","feature_id":"halaihai-0","coordinates":[-128.78716876816586,-20.883657118412998],"length_km":16619.522147051288},"geometry":{"type":"MultiLineString","coordinates":[[[-148.92678359570436,-17.282577603673964],[-149.3081207740364,-17.723342219804366]],[[-71.59739837597036,-32.95696151932608],[-72.87683746960369,-32.33383519609184],[-106.97681331345684,-24.721012464905712],[-143.9767871023282,-18.211166830631022],[-148.92678359570436,-17.282577603673964],[-149.7142830378323,-17.06378750083079],[-150.0517827981482,-16.852397237765768],[-156.57677634779455,-13.377553880887607],[-168.27676988739648,-9.186173588259289],[-179.9997982511102,3.141849579307493]],[[146.27300728259013,12.937829392787588],[146.27300728259013,13.92262683581919],[145.66050771649057,15.11350037881627]],[[-149.44108067057505,-17.51215245847912],[-149.7142830378323,-17.06378750083079]],[[179.99992672230314,3.141849579307493],[167.3118697725457,8.14543454452957],[151.2058440342044,10.149281786817761],[149.45504648498684,11.060179356238443],[147.1730066450221,12.279111812101094],[146.27300728259013,12.937829392787588],[144.809541651502,13.549094363148988]]]}},{"type":"Feature","properties":{"id":"tam-1","name":"TAM-1","color":"#939597","feature_id":"tam-1-0","coordinates":[-73.10356090767246,23.77858245135987],"length_km":6422.412128369371},"geometry":{"type":"MultiLineString","coordinates":[[[-79.64986933934534,9.967915186974132],[-82.34986742664132,10.07869800665097],[-83.03765938887457,9.988597517410145]],[[-78.29987029569729,11.955858207114732],[-75.37487236838926,11.33148066218366]],[[-70.19987599438629,22.145638852308416],[-71.5498749986113,21.425997872385402],[-73.3498738023213,20.796306105108872],[-74.13737324444934,19.95262290516439],[-74.69987284596934,19.316876111628712],[-75.14987252718532,18.251816319028222],[-75.93737196931336,17.395022634700517],[-78.29987029569729,11.955858207114732],[-79.64986933934534,9.967915186974132],[-79.7534792659471,9.437721984870015]],[[-87.97486344184153,16.534196198259725],[-87.86236352213342,16.10232559580297],[-87.9461557876504,15.844981598742601]],[[-80.09986902056141,25.348717422116714],[-80.77486854238535,24.73717827217609],[-80.99986838299355,24.53265756616073],[-83.2498667890733,24.53265756616073],[-84.82486567332928,23.50508968095737],[-85.94986487636929,21.635297384859552],[-86.28736463787752,20.375041253465433],[-87.0748640794093,18.251816319028222],[-87.97486344184153,16.534196198259725],[-88.3123632027534,16.10232559580297]],[[-85.94986487636929,21.635297384859552],[-86.51236447788929,21.268825931479064],[-86.767566,21.095663483773514]],[[-66.10666893347558,18.46610423294742],[-66.03732898259665,18.678647022154717],[-65.92487906344923,18.838433217733183]],[[-65.92487906344923,18.838433217733183],[-65.32398553479186,18.62408068875806],[-65.19362958028117,18.251816319028222],[-64.8805299999999,17.753367672551875]],[[-80.3942513282677,27.638731771078668],[-79.64986933934534,27.264711877833996],[-79.19986965991733,26.86399017396059],[-78.86236989721739,26.763586569619914],[-77.84987061448132,27.164665812813517],[-77.39987093326533,27.064530023167972],[-76.94987125324133,26.562513149236715],[-76.6061714961255,26.116791262410242],[-76.24064175447413,25.70052610903888],[-73.3498738023213,23.91710129093513],[-70.19987599438629,22.145638852308416],[-68.84987695073825,21.309590385318035],[-67.49987794710923,20.375041253465433],[-66.48737866377725,19.104405475930452],[-65.92487906344923,18.838433217733183]]]}},{"type":"Feature","properties":{"id":"nuvem","name":"Nuvem","color":"#939597","feature_id":"nuvem-0","coordinates":[-52.48579468172721,33.29578855556337],"length_km":6625.932188700297},"geometry":{"type":"MultiLineString","coordinates":[[[-78.88266988343256,33.69355790837514],[-78.26653698657397,33.47169957086474],[-69.29987667197325,32.17975358978957],[-64.46238009890118,32.052708023486204],[-62.09988177192116,32.052708023486204],[-50.39989006030513,33.565491482355505],[-39.54989774713741,35.215791334874076],[-26.22490718668583,37.569973991598665]],[[-26.22490718668583,37.569973991598665],[-25.87490743403289,37.50058844605323],[-25.668707580106854,37.739604626314694]],[[-26.22490718668583,37.569973991598665],[-25.87490743462888,38.29952060596925],[-25.199907914592835,38.827311095266374],[-23.399909187344846,38.827311095266374],[-13.94991588359671,38.29952060596925],[-10.349918432080775,38.122730108392204],[-8.869597215129223,37.95721527519206]],[[-64.65917995948635,32.36157723537831],[-64.5748800192052,32.243210016262736],[-64.46238009890118,32.052708023486204]]]}},{"type":"Feature","properties":{"id":"tpu","name":"TPU","color":"#939597","feature_id":"tpu-0","coordinates":[150.20026301046514,16.574275964936565],"length_km":12461.449831250802},"geometry":{"type":"MultiLineString","coordinates":[[[145.20625803709336,14.576853976609653],[145.42186455104067,14.850056076332436]],[[122.8730238593579,20.789874953693833],[121.74802465572199,19.34577048503237],[121.07814990718249,18.601843893825777]],[[-179.97676374562144,25.579505481562034],[-172.776773138787,28.781327858545534],[-163.77677307583247,32.648802835102664],[-151.17678200178437,36.35600267515968],[-124.15955921950356,40.803253108521126]],[[145.5437577986014,14.46794848402804],[145.37300792015816,13.758776740709814],[144.9230082389421,13.540131566252656]],[[120.88201985138694,22.34569808206579],[122.8730238593579,20.789874953693833],[131.42301780246206,18.352094170308565],[138.62301270191816,16.635422895611335],[144.47300855772602,14.68570566481149],[145.20625803709336,14.576853976609653],[145.5437577986014,14.46794848402804],[146.27300728259013,14.68570566481149],[151.22300377596605,17.066099764630273],[160.22299739969037,20.614481930232845],[179.99994672169302,25.579505481562034]]]}},{"type":"Feature","properties":{"id":"anjana","name":"Anjana","color":"#939597","feature_id":"anjana-0","coordinates":[-40.72402000723348,38.72436239362389],"length_km":6753.640647014426},"geometry":{"type":"LineString","coordinates":[[-78.88266988343256,33.69355790837514],[-78.26653698657397,33.565491482352044],[-76.94987125204935,33.565491482352044],[-62.09988177192116,34.683017659857974],[-50.39989006030513,36.33133835588799],[-39.59989771112098,39.00237890905839],[-23.399909187344846,43.401144973153954],[-16.199914287888834,45.0138336439531],[-9.899918750864702,45.96024524125342],[-5.849921619920758,45.331071073324864],[-4.724922416284671,44.694829089578164],[-3.810013065606984,43.46149931727051]]}},{"type":"Feature","properties":{"id":"asia-link-cable-alc","name":"Asia Link Cable (ALC)","color":"#939597","feature_id":"asia-link-cable-alc-0","coordinates":[112.41647789726676,10.043700582907936],"length_km":5759.970071021623},"geometry":{"type":"MultiLineString","coordinates":[[[114.20292333351767,22.22205041973683],[114.41249318505615,20.796306105108872],[114.7499929459683,19.104405475930452],[114.97499278657615,18.265170800514795],[114.7499929453724,17.12195969020027],[114.07499342354828,14.801154224791581],[113.39999390232026,12.615395567393394],[112.38749461958417,9.967915186974132],[111.14999549624025,7.744889052551447],[107.99999772772827,5.286069860821008],[107.09999836529612,4.725718053703611],[105.74999932164808,3.940475772228814],[104.90624991936826,2.803404866588448],[104.66056009282137,1.961481175550864],[104.47058772799548,1.468426767331968],[104.28790035741287,1.299801162778933],[104.19209042528557,1.26242303552454],[103.98701057056589,1.389451396800233]],[[104.11414047991015,1.925884465105483],[104.32889093295785,2.027683761259223],[104.66056009282137,1.961481175550864]],[[114.07499342354828,14.801154224791581],[109.79999645259221,15.994209911785974],[108.89999709016024,16.10232559580297],[108.19247759137373,16.043393005208348]],[[111.14999549624025,7.744889052551447],[112.94999422050827,5.883218793719735],[114.29999326475222,5.174038327226071],[114.88563284987971,4.926762452886689]],[[114.7499929459683,19.104405475930452],[113.84999358353615,18.678647022154717],[111.09966159348336,18.505614628693614],[110.0493762753333,18.389897000000236]],[[114.7499929453724,17.12195969020027],[116.99999135204831,16.857467609772335],[119.92498927995224,16.749771315644697],[120.35557897432248,16.51402516186766]],[[114.97499278657615,18.265170800514795],[116.99999135204831,18.038005439608753],[119.92498927995224,16.857467609772335],[120.35483897484683,16.830716773770945]]]}},{"type":"Feature","properties":{"id":"manta","name":"MANTA","color":"#939597","feature_id":"manta-0","coordinates":[-80.95313552313398,16.0271021935348],"length_km":5134.297118260956},"geometry":{"type":"MultiLineString","coordinates":[[[-78.74986997750936,12.542195817724492],[-79.4248694987373,9.967915186974132],[-79.7535392665006,9.437546999999826]],[[-75.50568227572235,10.387005000000206],[-76.0498718896173,10.963556857789316],[-78.74986997750936,12.542195817724492],[-79.64986934053732,13.92930384327183],[-80.99986838299328,16.10232559580297],[-82.34986742664132,17.395022634700517],[-85.04986551393732,19.104405475930452],[-86.0623647972694,20.375041253465433],[-86.17486471816949,21.635297384859552]],[[-86.01795306402694,25.56262900538288],[-84.59986583331732,27.896238989528694],[-84.11536453807635,29.953701800771658]],[[-86.01795306402694,25.56262900538288],[-88.19986328304547,23.848523186487938],[-91.79986073277334,23.022776999376084],[-94.49985882006952,21.356164482330126],[-96.14075765764133,19.195461502894283]],[[-84.82486567332928,24.94136317175375],[-84.82486567332928,23.91710129093513],[-86.17486471816949,21.635297384859552],[-86.51236447788929,21.32123529551186],[-86.76758665233304,21.09572879236739]],[[-80.21236894086552,25.348717422116714],[-80.99986838299365,24.73717827217609],[-83.2498667890733,24.73717827217609],[-84.82486567332928,24.94136317175375],[-86.01795306402694,25.56262900538288]]]}},{"type":"Feature","properties":{"id":"juno","name":"JUNO","color":"#cd5628","feature_id":"juno-0","coordinates":[-149.7385266899431,42.4219179059235],"length_km":9555.796724909233},"geometry":{"type":"MultiLineString","coordinates":[[[-179.99981351108062,42.74035917563463],[-151.19983391146832,42.74035917563463],[-138.82500430186852,40.04369389177534],[-129.5998339543217,36.1498667868178],[-122.84983873608158,34.867831005273345],[-120.62152181466479,35.12094936772415]],[[143.09997286257644,33.93964008831966],[141.2999741377125,34.683017659857974],[139.9609750856761,34.97407819999975]],[[179.99992719256971,42.740357181754895],[172.79992073307184,42.74369770072915],[160.19996074878455,39.00237890905839],[149.39996839960057,35.419780517080355],[143.09997286257644,33.93964008831966],[140.39997477528055,33.189714664600466],[138.59997605041642,32.43331330641721],[137.69997668798445,33.001218522654476],[136.87399727311598,34.33682825203173]]]}},{"type":"Feature","properties":{"id":"sea-h2x","name":"SEA-H2X","color":"#939597","feature_id":"sea-h2x-0","coordinates":[111.3682205674424,10.244365025006168],"length_km":5139.021144757745},"geometry":{"type":"MultiLineString","coordinates":[[[113.84999358294024,17.10851996079568],[116.99999135204831,16.749771315644697],[119.92498927995224,16.642014062854003],[120.320938998862,16.61589414713403]],[[107.99999772772827,5.845915088460266],[109.12499693017237,4.164912849976942],[109.79999645259221,2.817450442654169],[110.36249605411221,1.918228780215599]],[[114.07499342414418,18.251816319028222],[111.14999549564435,18.287425986594243],[110.0493762753333,18.389897000000236]],[[105.74999932164808,4.501447394015217],[104.8359699685594,5.398081130463647],[103.04883123518101,7.005165564456285],[101.70000219070414,7.075530930004602],[100.5951029728293,7.198818071264419]],[[103.64609081207688,1.338585852071497],[103.83750067588413,1.168506749040978]],[[104.26002537715979,1.468426767331968],[104.62500011860807,2.817450442654169],[105.74999932164808,4.501447394015217],[107.09999836529612,5.174038327226071],[107.99999772772827,5.845915088460266],[110.02499629320025,7.744889052551447],[111.26249541654417,9.967915186974132],[112.27499469928026,12.615395567393394],[113.84999358294024,17.10851996079568],[114.07499342414418,18.251816319028222],[113.96249350384026,20.796306105108872],[114.2586832940168,22.31829267897149]]]}},{"type":"Feature","properties":{"id":"seamewe-6","name":"SeaMeWe-6","color":"#939597","feature_id":"seamewe-6-0","coordinates":[67.71027967765008,15.063283874132395],"length_km":22701.927257926807},"geometry":{"type":"MultiLineString","coordinates":[[[60.30003151883183,15.669513225155248],[60.30003151883183,19.104405475930452],[60.30003151883183,22.469443964829516],[59.85003183761575,23.09178547692239],[58.95003247518379,23.65974644119216],[58.61253271367574,23.81422051502533],[57.661372759507515,24.420846844473278],[57.15003374972377,25.348717422116714],[56.812533989407704,26.1593079707739],[56.58753414879967,26.512189502051797],[56.362534308191634,26.512189502051797],[55.80003470667163,26.1593079707739],[55.35003502545574,25.855985466072205],[54.34014573908288,24.872582666687173],[52.87503677817179,25.450342946923914],[52.20003725694376,26.05828756029904],[51.637537655423756,26.36108632539156],[51.187537974207686,26.461843796188983],[50.57595273852649,26.229438]],[[54.419075684956134,24.443964572625426],[54.34014573908288,24.872582666687173]],[[51.5193877385264,25.294536999999895],[52.20003725694376,26.05828756029904]],[[85.500013666928,1.018588540518982],[83.70001494146798,5.510071711803246],[83.02501541964405,7.744876956882131],[82.57501573842798,9.52441134501949],[81.45001653598388,11.515266158038768],[80.24298739105474,13.06385310188338]],[[65.75491565746178,16.45525448044254],[65.47502785162398,20.375041253465433],[66.15002737344808,23.298598065875897],[67.02854675228855,24.889731701235817]],[[70.2549124696218,13.25182881322956],[71.1000238680158,16.965102599435927],[72.87590260996693,19.07607425728523]],[[74.70002131714794,5.659359572411489],[74.25002163652778,4.837826391986557],[73.54027200000027,4.211916943627916]],[[90.00001047908802,1.91828308225627],[90.90000984092408,5.061986954416114],[91.35000952273606,9.52441134501949],[92.70000856638391,14.801154224791581],[92.70000856578802,17.82393441253792],[92.02500904336819,19.95262290516439],[91.99482906593992,21.42927456664916]],[[101.25000250948806,2.03066189047467],[101.4750023495002,2.592701464601932],[101.44360237174425,2.751228763607222]],[[79.65001781052403,2.967259208499635],[79.87501765113208,3.191933974144809],[80.550017172956,5.435413643888211],[80.53985718074962,5.940820740520149]],[[44.55004267627159,11.018774999640526],[43.65004331383962,11.399928123027385],[43.14799366949638,11.594869371447825]],[[33.08276564295245,28.365936333863583],[33.35680060388853,28.161052262220792]],[[33.80630028723958,27.364667993860262],[34.42504984950763,26.562513149236715],[34.87504953131979,25.75470426341523],[35.88754881405568,24.12261698700344],[37.12504793739979,22.05298561667754],[37.91254737952765,20.375041253465433],[39.26254642257961,18.251816319028222],[40.27504570531769,16.534196198259725],[41.68129470911569,14.801154224791581],[42.18754435048373,13.92930384327183],[42.83441889223159,13.054150695298627],[43.14379367306766,12.834868817846521],[43.2281686132957,12.615395567393394],[43.50941841345962,12.395734000022975],[44.55004267627159,11.018774999640526],[45.450042037511935,10.963556857789316],[48.60003980661981,11.735650161405832],[52.65003693696785,12.72515592356304],[54.00003598299986,13.054150695298627],[55.35003502307177,13.273238157547594],[58.9500324733959,15.018578573757472],[60.30003151883183,15.669513225155248],[65.75491565746178,16.45525448044254],[70.2549124696218,13.25182881322956],[71.71468744134651,9.199470523025733],[73.90979788338855,6.525105259563287],[74.70002131714794,5.659359572411489],[78.3000187674719,3.491423322320592],[79.65001781052403,2.967259208499635],[81.00001685476798,2.592701464601932],[85.500013666928,1.018588540518982],[90.00001047908802,1.91828308225627],[92.70000856638391,4.389285926050993],[94.27500745064,5.385636447723476],[95.42107397406397,6.138771009008069],[97.42500521915215,5.510071711803246],[97.87500490036805,5.286069860821008],[98.66250434249609,4.613591578862773],[99.90000346584002,3.266814816815666]],[[102.68279723997186,1.41768673166663],[103.34065102845322,1.074949758433191]],[[103.50000091556807,1.144999057563372],[103.64609081207688,1.338585852071497]],[[5.372530429989069,43.29362778902908],[5.287570490175359,41.74435878948223],[5.850070091695361,38.651811712711336],[6.750069455319493,37.94551049545967],[9.000067860207336,37.411283634923244],[10.34861723036536,37.411283634923244],[11.137566345387336,37.23235432155614],[11.812565868412525,35.419780517080355],[14.400064035395404,33.565491482352044],[16.650062441475413,32.62301664000789],[19.350060528771497,33.001218522654476],[22.050058614875596,33.174022096718055],[25.200056384579554,32.43331330641721],[27.900054471875638,31.766210259727007],[31.050052239195633,31.798087367585257],[32.06255152193171,31.510798430049064]],[[35.88754881405568,24.12261698700344],[36.90004809500369,24.361968609765935]]]}},{"type":"Feature","properties":{"id":"topaz","name":"Topaz","color":"#33499e","feature_id":"topaz-1","coordinates":[-152.30356639946402,49.58728674004685],"length_km":8450.596134764663},"geometry":{"type":"MultiLineString","coordinates":[[[-123.97483793971784,49.355787080150606],[-123.114034,49.260440000000195]],[[142.19997350014447,36.87321951208928],[142.4249733401566,36.1498667868178],[142.19997350014447,35.05222991093673],[140.84997445649643,33.93964008831966],[138.59997605041642,33.189714664600466],[137.69997668798445,33.565491482352044],[136.87399727311598,34.33682825203173]],[[179.99994672169302,49.58728674004685],[172.7999518228328,49.58728674004685],[160.19996074878455,46.5823550820958],[149.39996839960057,40.04369219283004],[142.19997350014447,36.87321951208928],[141.07497429650857,36.632853424489355],[140.71827454919813,36.71305911738493]],[[-125.09983714275776,48.951106020783016],[-125.54983682397375,48.803129141654416],[-138.59982757864174,49.000334389463426],[-151.1998186526899,49.58728674004685],[-179.99979825051412,49.58728674004685]]]}},{"type":"Feature","properties":{"id":"medusa-submarine-cable-system","name":"Medusa Submarine Cable System","color":"#939597","feature_id":"medusa-submarine-cable-system-0","coordinates":[10.856379406776762,37.62522787291101],"length_km":8163.49230646117},"geometry":{"type":"MultiLineString","coordinates":[[[32.047198751652715,34.36126232841875],[34.200050008303506,34.36126232841875],[35.89779880560243,34.89170328553848]],[[14.350100000000367,35.95239999999964],[13.589266586762104,35.50152744443852]],[[20.060997550718024,32.114846773024304],[19.54833292295356,32.37254680331547],[19.270752161685323,32.80214707225284],[19.24898112158607,34.860996398864586]],[[14.47077136061674,34.99040862562433],[15.28032546762509,33.68969836184677],[15.088930733703902,32.37254680331547]],[[31.050052239791533,32.90681902852468],[31.610642393614338,33.937310785298685],[32.46668800000052,34.76662920076821]],[[22.50654831394805,34.53907554907902],[22.713297877407687,35.39334285607203],[23.903670583009415,36.290409440352754],[23.903670583009415,37.09588010976437],[23.73052617441654,37.97446102487642]],[[9.000067859611436,37.94551049545967],[9.562567461131438,37.589786573603064],[9.867357245811593,37.276816253475154]],[[11.812565866615547,37.23235432155614],[12.060515691561484,37.546826221282146],[12.591375316091606,37.65058617278613]],[[8.10006849777537,38.21117903702318],[6.975069294139464,37.44106375352233],[6.557241714000397,37.00295935602444]],[[2.25007264196731,37.411283634923244],[2.812572243487313,37.23235432155614],[3.035712085413061,36.76212778211003]],[[4.950070728667312,41.1823303464796],[2.700072322587302,41.1823303464796],[2.168725042748786,41.38560270176812]],[[6.075069932303216,38.651811712711336],[5.400070410479286,41.74435878948223],[5.372530429989069,43.29362778902908],[5.062570649567322,41.74435878948223],[4.950070728667312,41.1823303464796],[4.837570808959284,40.04369219283004],[4.725070888059456,38.651811712711336]],[[-2.249924170192707,35.876870570092834],[-2.474924011396645,35.450334489671505],[-2.933034999999599,35.170020000000214]],[[-4.499922576272716,35.78566189952622],[-4.724922416880753,35.602930322906126]],[[-9.33155915349599,38.690161972355526],[-9.449919069648717,38.122730108392204],[-9.674918909064772,37.23235432155614],[-9.562418989356745,36.69301553274438],[-8.999919388432733,36.33133835588799],[-5.843848196000099,36.13475690999979]],[[-5.250523729785807,36.37610614625084],[-4.499922576272716,35.78566189952622],[-2.249924170192707,35.876870570092834],[2.25007264196731,37.411283634923244],[4.725070888059456,38.651811712711336],[6.075069932303216,38.651811712711336],[8.10006849777537,38.21117903702318],[9.000067859611436,37.94551049545967],[10.348617229769278,37.94551049545967],[10.91256650537538,37.589786573603064],[11.812565866615547,37.23235432155614],[12.946265012802193,35.87231864098554],[14.47077136061674,34.99040862562433],[16.5602576693071,34.484997549332334],[19.24898112158607,34.860996398864586],[22.050058614875596,34.63291490532071],[23.64128846164546,34.305351542954675],[25.252400096306765,33.894592669629816],[27.000055108251505,33.87739543625145],[31.050052239791533,32.90681902852468],[31.9500516016276,31.798087367585257],[32.28755136253957,31.510798430049064],[32.28445136473581,31.25927814644905]]]}},{"type":"Feature","properties":{"id":"hawaiki-nui-1","name":"Hawaiki Nui 1","color":"#939597","feature_id":"hawaiki-nui-1-0","coordinates":[115.5138499091612,-7.8829131023718695],"length_km":11273.540898244566},"geometry":{"type":"MultiLineString","coordinates":[[[129.59998242550049,-9.586362493293953],[128.6999870359665,-8.920150538056042],[128.3038833436693,-7.801903059182855],[127.34998401942048,-7.385865390978736],[125.99998497577243,-7.385865390978736],[125.54998529396046,-7.992854458460687]],[[152.99996584932845,-16.30669306561827],[155.24996425540846,-15.441023659568087],[157.49996266089255,-11.943944931746815],[159.41246130606078,-10.17745743036107],[159.52496122636472,-9.73423534230066],[159.46876126617727,-9.29042430103552],[159.5249612269606,-9.123848597823338],[159.74996106756865,-9.123848597823338],[159.94976092602863,-9.42905364643845]],[[147.2624699138245,-11.062032109909483],[147.14996999352056,-10.17745743036107],[147.1885196909836,-9.479589292697288]],[[109.34999676839627,-3.92832730414264],[108.22499756833612,-4.60145376483711],[107.77499788712005,-5.273944363641298]],[[131.39998115036443,-9.142360877005329],[129.59998242550049,-9.586362493293953],[129.14998274428442,-10.620064860363238],[129.0374828245764,-11.833858275879866],[129.8751578176926,-12.194543031162238],[130.50000678851072,-12.329005823921618],[130.84315500000028,-12.46750362874273]],[[154.3499648929765,-27.553513996438145],[153.56246544906074,-27.353852936231306]],[[152.99996584932845,-16.30669306561827],[153.44996553054452,-18.880139975101173],[153.8999652117606,-25.540896076259312],[154.3499648929765,-27.553513996438145],[154.3499648929765,-28.743810281149894],[153.76076054297755,-31.85146566557725],[152.09996648689665,-33.17952345666914],[151.2070371188603,-33.869695999999635]],[[152.99996584932845,-16.30669306561827],[151.6499668050847,-14.571726491332546],[148.49996903716843,-12.383840433185572],[147.2624699138245,-11.062032109909483],[144.89997158684466,-10.47259892498978],[142.19997349954858,-9.586362493293953],[138.59997605041642,-9.955921616229002],[134.5499789188764,-9.586362493293953],[131.39998115036443,-9.142360877005329]],[[129.0374828245764,-11.833858275879866],[127.34998401942048,-11.772679839911943],[126.44998465698852,-11.772679839911943],[120.99998851781679,-11.55232519729577],[118.68749015601242,-10.804297138907085],[116.54999167023632,-9.586362493293953],[115.8749921484124,-9.142360877005329],[115.81874218826026,-8.86457667788879],[115.8749921490083,-8.401139048122838],[115.76249222810829,-8.178490278944933],[115.19999262658828,-7.509810688339549],[114.46970572643436,-6.616650693475355],[112.0499948586722,-5.497950688314882],[109.34999676839627,-3.92832730414264],[108.89999708777627,-3.029995968008661],[106.08749908256004,-0.331409329660265],[104.8499999592161,0.624825760007394],[104.40000027800004,0.906050180869095],[104.2875003576961,1.018534216615524]],[[104.1778504347774,1.18452336036332],[103.85310700000069,1.293877684611663]]]}},{"type":"Feature","properties":{"id":"raman","name":"Raman","color":"#939597","feature_id":"raman-0","coordinates":[50.51187004053453,14.013272070190846],"length_km":6905.885419012751},"geometry":{"type":"MultiLineString","coordinates":[[[62.775029765519875,20.375041253465433],[62.775029765519875,22.469443964829516],[59.85003183761575,24.225251377403733],[58.50003279396771,23.89138876125301]],[[55.35003502545574,15.886035719079029],[54.78753542333983,16.534196198259725]],[[44.55004267627159,12.285833556268383],[43.63637028333588,11.94650543254203],[43.14799366949638,11.594869371447825]],[[72.87590260996693,19.07607425728523],[70.20002450558383,19.635064099942493],[66.60002705585578,20.163975031975873],[62.775029765519875,20.375041253465433],[58.95003247518379,17.609605913224996],[55.35003502545574,15.886035719079029],[48.60003980721571,13.273238157547594],[45.450042038703735,12.505588131780646],[44.55004267627159,12.285833556268383],[43.881959133154325,12.458770746871922],[43.42504347323158,12.615395567393394],[43.340668533003544,12.834868817846521],[43.256293592775506,13.054150695298627],[43.03129375216765,13.92930384327183],[42.52504411079961,14.801154224791581],[41.8500445889755,16.534196198259725],[39.487546262591565,20.375041253465433],[39.1500465010837,20.936468000149056],[38.70004682046353,22.05298561667754],[37.46254769711778,24.12261698700344],[36.45004841438352,25.75470426341523],[35.696548947573824,27.354010300438862]]]}},{"type":"Feature","properties":{"id":"apricot","name":"Apricot","color":"#939597","feature_id":"apricot-1","coordinates":[119.96497042769207,1.1841040607806506],"length_km":5951.134353094223},"geometry":{"type":"MultiLineString","coordinates":[[[104.17500043679628,1.018409236452146],[104.11887547655554,1.018409236452146]],[[109.12499692898021,-3.029995968008661],[107.88749780742415,-4.60145376483711],[107.43749812620827,-5.273944363641298],[107.12099835041957,-5.981154260263285]],[[126.57346403647036,5.404676658378629],[126.3374847366844,5.659359572411489],[126.11248489607637,5.957818681088533],[125.61287587559997,7.079988883160643]],[[125.54998529455636,16.24638821747719],[123.52498672908439,15.597287859114385],[121.94998784542422,15.669513225155248],[121.56018812156209,15.761539465842137]],[[103.64609081207688,1.338585852071497],[103.83750067588413,1.182753739294242],[104.02421554420971,1.202092036773615],[104.14960045538582,1.18452336036332],[104.17500043679628,1.018409236452146],[104.2875003576961,0.962292662396848],[104.40000027800004,0.849806826211382],[104.8499999592161,0.68107206531244],[106.19999900286416,-0.331409329660265],[109.12499692898021,-3.029995968008661],[109.79999645080431,-3.479268678970064],[112.0499948586722,-4.60145376483711],[114.60984933620142,-5.273944363641298],[116.80810148738884,-4.179995582158629],[117.7124908479029,-2.730375485267853],[117.96440181128685,-1.51533365197483],[118.76628430204579,-0.810555324740758],[119.47498959873617,0.568578852526193],[119.69998943934421,1.018534216615524],[120.59998880177636,1.580886840914131],[124.19998625090851,3.41655961832325],[126.57346403647036,5.404676658378629],[128.24998338185245,7.447522319872199],[127.34998401942048,11.882475268284509],[125.54998529455636,16.24638821747719],[126.44998465639262,18.251816319028222]]]}},{"type":"Feature","properties":{"id":"polar-express","name":"Polar Express","color":"#939597","feature_id":"polar-express-1","coordinates":[100.75766056091868,77.59274941227683],"length_km":11385.658441773896},"geometry":{"type":"MultiLineString","coordinates":[[[133.19997987522854,41.85617959436374],[133.19997987463265,42.329239699665536],[132.89127009392124,42.83453574149161]],[[131.9107473900959,43.154615407914534],[132.2999805127964,42.35695685335537],[133.19997987522854,41.85617959436374],[134.5499789188764,41.74435878948223],[137.9249765279966,43.073310783003215],[140.84997445590054,45.59408578894718],[141.97497365894054,45.75130568537152],[143.09997286198055,45.75130568537152],[144.67497174444856,46.11643477220242],[145.12497142745252,47.04430546733287]],[[143.43747262348842,47.120911503379745],[145.12497142745252,47.04430546733287],[147.5999696729486,46.8394840627337],[153.44996552994863,48.35656994073399],[158.8499617045408,50.83511113795267],[159.74996106697276,52.50929095964713]],[[159.74996106697276,52.50929095964713],[161.99995947305277,52.78232285776401],[168.29995501007673,53.857473440304716],[174.02495095443587,55.90629872677893],[179.97797408100845,60.528451203358905]],[[-179.9997982511102,60.528451203358905],[-179.52782592920985,60.8695351663294],[-172.7998033516541,63.99042806051585]],[[177.5021457567835,64.72719091772642],[178.64994767804478,64.4794309101756],[179.99994672169302,64.18706730674532]],[[-179.9997982511102,64.18706730674532],[-172.7998033516541,63.99042806051585],[-170.0998052643581,64.76870273314248],[-169.19980590192614,66.0785049157679],[-170.54980494557392,67.66857826204087],[-175.94980112016617,69.47590081058141],[-179.9997967228899,70.1742290900765]],[[179.99994824991296,70.1742290900765],[177.7499483156128,70.55228024514132],[171.89995245980478,70.70155250919251],[169.64995405372477,70.25039973315269]],[[170.30700000000036,69.7029],[169.64995405372477,70.25039973315269]],[[169.64995405372477,70.25039973315269],[159.74996106697276,72.13481545023681],[148.9499687177886,72.8123099710512],[141.7499738183325,73.07627580961623],[134.99089662947918,72.81107196658]],[[128.8645,71.6375],[131.39998115036443,71.9962481654091],[134.99997860009248,72.8123099710512],[135.00070515758972,72.81107196658],[130.94998146914853,74.09330000483304],[123.74998656969242,76.27568108311124],[112.94999422050827,77.49983814503032],[104.84999995862022,77.88351717807191],[99.45000378402823,77.49983814503032],[94.50000729065214,77.20425577257384],[86.85001270998013,75.9517011642262],[81.00001685417209,74.45913348599115],[78.75001844809208,73.96950097861448],[71.55002354863598,73.96950097861448],[65.70002769282792,72.8123099710512],[63.00002960553183,70.99679695986222],[60.75003119945182,70.40189838789871]],[[78.75001844809208,73.96950097861448],[80.10001749174012,73.59244957584484],[80.531,73.5077]]]}},{"type":"Feature","properties":{"id":"india-asia-xpress-iax","name":"India Asia Xpress (IAX)","color":"#ced528","feature_id":"india-asia-xpress-iax-1","coordinates":[82.91557519892766,2.4346698251038394],"length_km":7508.31863759256},"geometry":{"type":"MultiLineString","coordinates":[[[85.500013666928,1.918228780215599],[83.92501477969205,5.510071711803246],[83.2500152596562,7.744876956882131],[82.80001557844011,9.52441134501949],[81.45001653598388,11.735650161405832],[80.24298739105474,13.06385310188338]],[[75.37502083897188,5.659359572411489],[74.25002163652778,4.613591578862773],[73.54035213926461,4.212345781871782]],[[101.25000250948806,2.143087178471855],[101.41875238934823,2.592701464601932],[101.44360237174425,2.751228763607222]],[[98.10000474097609,5.286069860821008],[99.00000410340806,5.957818681088533]],[[79.65001781052403,3.191933974144809],[80.43751725265209,5.435413643888211],[80.53985718074962,5.940820740520149]],[[72.87590151562159,19.066189490208128],[71.32502370862383,16.965102599435927],[71.04513590397801,13.492128176464083],[72.50491087570272,9.361978911833972],[74.70002131774494,6.688675551202349],[75.37502083897188,5.659359572411489],[78.3000187674719,3.715978119298069],[79.65001781052403,3.191933974144809],[81.00001685476798,2.817450442654169],[85.500013666928,1.918228780215599],[90.00001047908802,2.817450442654169],[92.70000856638391,4.613591578862773],[94.27500745064,5.609601184516913],[95.40635270497349,6.218295306499518],[97.42500521915215,5.622041180883233],[98.10000474097609,5.286069860821008],[98.88750418310413,4.613591578862773],[100.01250338614413,3.266814816815666],[101.25000250948806,2.143087178471855]],[[102.15000187192003,1.805788280129153],[102.68279723997186,1.558264177552469],[103.34065102845322,1.383978004154865],[103.50000091556807,1.327518855965853],[103.64609081207688,1.338585852071497]]]}},{"type":"Feature","properties":{"id":"india-europe-xpress-iex","name":"India Europe Xpress (IEX)","color":"#939597","feature_id":"india-europe-xpress-iex-0","coordinates":[48.479840860047226,12.256429919428616],"length_km":10282.730603026666},"geometry":{"type":"MultiLineString","coordinates":[[[8.662568098699472,44.21300917863173],[8.325068338383225,43.8084564391072],[7.875068657167333,43.564400497117596],[7.200069135343221,43.237448352440914],[6.412569692619463,42.908732427720366],[5.962570011403388,42.908732427720366]],[[24.9202225972494,34.242187316462925],[24.90279096897315,34.76016729562049]],[[35.521924071875475,26.562513149236715],[35.21254929044368,27.497802509202188]],[[37.34051417738748,24.138084599448355],[37.80004745803156,24.12261698700344]],[[44.55004267627159,11.570378484364811],[43.65004331383962,11.620598816364184],[43.14799366949638,11.594869371447825]],[[54.00003598419185,13.601498202276586],[54.11253590151572,14.5835116451186],[54.173383948448546,16.54803642952232]],[[32.65318110412008,29.113614162980063],[33.0750508052635,28.95155473219332]],[[33.53928797639455,28.161052262220792],[34.537549769215474,27.364667993860262],[35.521924071875475,26.562513149236715],[36.33754849407959,25.75470426341523],[37.35004777681367,24.12261698700344],[38.5875469001596,22.05298561667754],[39.03754658077959,20.936468000149056],[39.37504634228764,20.375041253465433],[41.737544668671575,16.534196198259725],[42.46879415064765,14.801154224791581],[42.975043792015505,13.92930384327183],[43.22816861269962,13.054150695298627],[43.3266060429656,12.834868817846521],[43.41098098319364,12.615395567393394],[43.87504315444765,12.395734000022975],[44.55004267627159,11.570378484364811],[45.450042038703735,11.515266158038768],[48.60003980781179,12.285833556268383],[52.65003693815965,13.273238157547594],[54.00003598419185,13.601498202276586],[55.35003502426394,13.710817738179635],[58.950032474587886,15.452760959322058],[60.97503103946396,16.534196198259725],[66.60002705585578,18.678647022154717],[70.20002450558383,18.891661584303154],[72.87590151562159,19.066189490208128]],[[8.48375822596588,44.30574823054251],[8.662568098699472,44.21300917863173],[9.225067700219473,44.05151922873524],[9.562567461131438,43.401144973153954],[9.675067381435365,43.073310783003215],[9.900067221447502,42.41235450073586],[10.462566823563405,41.52013202089327],[11.475066106299483,39.6983233549332],[11.700065947503239,37.85673997565852],[12.26256554783162,37.23235432155614],[13.082092624606803,36.025097835273904],[14.492544437439456,35.24657290391401],[16.516715589108777,34.74883374222028],[19.24898112158607,35.09735393311572],[22.050058614875596,34.89090355336368],[23.518709918219088,34.56509456203117],[25.289696105851924,34.1568536124284],[27.900054471279375,32.52821504536491],[29.67235321517045,31.047641997876443]]]}},{"type":"Feature","properties":{"id":"firmina","name":"Firmina","color":"#c52159","feature_id":"firmina-0","coordinates":[-34.347349102592155,1.407257351190094],"length_km":12789.221868560446},"geometry":{"type":"MultiLineString","coordinates":[[[-41.849896117201084,-27.95174728521976],[-45.22489372632109,-25.134186547061336],[-46.4124928850147,-24.00886839636483]],[[-50.84988974152112,-35.95811819864919],[-53.99988751003309,-35.40985639492525],[-54.949996837562736,-34.96666536873363]],[[-78.88266988343256,33.69355790837514],[-78.3790402402086,33.189714664600466],[-77.39987093386132,32.36998992114339],[-74.69987284596934,30.901396088515508],[-73.3498738023213,30.320465424761444],[-69.29987667137726,28.359233526108557],[-63.4498808155692,25.75470426341523],[-48.59989133544111,16.534196198259725],[-39.152117353648734,8.152046688785141],[-33.74990185531301,0.568578852526193],[-30.48740414473091,-3.816084200032395],[-27.89990599950586,-9.29042430103552],[-28.349731034615626,-13.697820288632505],[-30.14972975947965,-18.025284192896695],[-36.33740002230498,-24.41918455361521],[-39.59989771171697,-25.94623071841455],[-41.849896117201084,-27.95174728521976],[-44.09989452328109,-32.61276000573574],[-50.84988974152112,-35.95811819864919],[-53.99988751003309,-36.68325067019043],[-56.695445600474535,-36.47095527632105]]]}},{"type":"Feature","properties":{"id":"bifrost","name":"Bifrost","color":"#f5ae1a","feature_id":"bifrost-1","coordinates":[-152.38305336538838,37.36629963591011],"length_km":17591.569771024144},"geometry":{"type":"MultiLineString","coordinates":[[[-123.97340935607642,45.14659036063931],[-125.09983714216159,44.53466416326733],[-129.5998339543217,43.073310783003215],[-138.5998275786419,40.38732029077508],[-151.1998186532859,37.70855130533492],[-163.79980972733406,34.063992930126034],[-172.79980979028863,30.255702942039875],[-179.99980039712295,27.097918575215974]],[[109.34999677018433,-3.029995968008661],[107.99999772772827,-4.60145376483711],[107.5499980465122,-5.273944363641298],[106.82782855810404,-6.171876390816321]],[[126.44998465698852,5.659359572411489],[126.2249848157844,5.957818681088533],[125.61287587559997,7.079988883160643]],[[124.19998625090851,3.865649782482034],[124.64998593331639,1.805788280129153],[124.8396357983706,1.490779296094715]],[[179.99994672169302,27.097918575215974],[173.6999511846689,25.484199086872454],[160.19996074818866,22.191942630775465],[151.1999671244645,18.678647022154717],[145.79997094927663,16.24638821747719],[144.67497174623662,14.365653759228442],[144.5624718259325,13.92930384327183],[144.77675167413477,13.490037504527912]],[[144.5624718259325,13.92930384327183],[143.9999722250084,13.273238157547594],[137.24997700676838,11.735650161405832],[133.19997987582445,9.967915186974132],[129.59998242550049,7.893494252945166],[126.44998465698852,5.659359572411489],[124.19998625090851,3.865649782482034],[120.59998880177636,2.367912558705407],[119.24998975693651,1.018534216615524],[119.02498991513632,0.568578852526193],[118.31629235168833,-0.810555324740758],[117.85190631946367,-1.290401396141605],[117.73941082764067,-1.51533365197483],[117.48749100610306,-2.730375485267853],[116.58310164737706,-4.179995582158629],[114.67992114108486,-5.049857167366764],[112.0499948586722,-4.37714437553184],[110.02499629200844,-3.479268678970064],[109.34999677018433,-3.029995968008661],[106.31249892316808,-0.331409329660265],[105.63749940134416,0.118588418888312],[104.8499999592161,0.568578852526193],[104.17500043739219,0.685071778220519],[103.89375063663218,0.793562652607196],[103.89375063663218,1.018534216615524],[103.64609081207688,1.338585852071497]],[[-120.62152181466479,35.12094936772415],[-122.84983873608158,35.05222991093673],[-129.5998339543217,38.29952060596925],[-138.5998275786419,40.38732029077508]]]}},{"type":"Feature","properties":{"id":"echo","name":"Echo","color":"#939597","feature_id":"echo-1","coordinates":[127.3711135694469,-4.619987344659122],"length_km":6449.246440438224},"geometry":{"type":"MultiLineString","coordinates":[[[144.65753175859138,13.385305518498077],[143.9999722250084,13.3827080361257],[143.54997254319662,13.419186961310027]],[[133.64997955704033,7.744889052551447],[134.09997923706462,7.521883237406507],[134.5609408257699,7.531746239289517]],[[109.57499661138827,-3.029995968008661],[108.1124976480322,-4.60145376483711],[107.66249796681612,-5.273944363641298],[107.12099835041957,-5.981154260263285]],[[143.88747230410857,13.965698203819535],[143.54997254319662,13.419186961310027],[137.24997700676838,9.52441134501949],[133.64997955704033,7.744889052551447],[133.19997987582445,7.29876275445952],[130.49998178793246,3.865649782482034],[129.59998242550049,1.618372199773176],[128.69998306306852,-1.981015190984684],[128.0249835412444,-2.880195580251407],[127.34998401942048,-4.676208028751072],[123.74998656969242,-6.467627592690688],[120.14998911996437,-6.467627592690688],[116.58310164737706,-4.852974874840906],[114.7499929459683,-4.825692499217419],[112.0499948586722,-4.152767748013638],[110.24999613321238,-3.479268678970064],[109.57499661138827,-3.029995968008661],[106.53749876377611,-0.331409329660265],[105.29999964043219,0.906050180869095],[104.8499999592161,1.187252773694101],[104.62500011860807,1.250557147797234],[104.28790035741287,1.299826156329162],[104.18975042694323,1.31015097035863],[103.98701057056589,1.389451396800233]]]}},{"type":"Feature","properties":{"id":"maroc-telecom-west-africa","name":"Maroc Telecom West Africa","color":"#b27032","feature_id":"maroc-telecom-west-africa-0","coordinates":[-16.68894148387257,8.72531679447526],"length_km":7143.674492523389},"geometry":{"type":"MultiLineString","coordinates":[[[-16.199914288484827,23.848523186487938],[-16.08741436818081,23.745587983103654]],[[-3.712423134144677,3.266814816815666],[-4.026242911831877,5.323508791824841]],[[1.350073278939443,4.164912849976942],[1.350073279535343,5.061986954416114],[1.227803366152544,6.126307297218732]],[[2.25007264196731,4.0527020972683],[2.25007264196731,5.061986954416114],[2.440112507341202,6.356673335458259]],[[-7.631920358132023,33.60539511325584],[-8.999919389028726,33.31515395812905],[-12.1499171569448,30.126049846722832],[-13.022832188573513,28.161052262220792],[-14.399915563024809,26.964304734562898],[-16.199914288484827,23.848523186487938],[-16.64991396910482,22.884654113882444],[-17.99991301275286,19.95262290516439],[-17.999913013348852,16.534196198259725],[-17.99991301275286,11.735650161405832],[-16.64991396910482,8.635699417327467],[-15.299914926648759,7.744889052551447],[-14.399915564216792,6.852191098754328],[-12.1499171569448,5.061986954416114],[-10.799918113296759,3.715978119298069],[-6.299921301732732,3.715978119298069],[-3.712423134144677,3.266814816815666],[-0.899925126544665,3.279837005484997],[1.350073278939443,4.164912849976942],[2.25007264196731,4.0527020972683],[5.400070410479286,1.918228780215599],[7.200069135343221,0.793562652607196],[8.550068178991262,0.568578852526193],[9.454267538448212,0.394465191855477]]]}},{"type":"Feature","properties":{"id":"africa-1","name":"Africa-1","color":"#939597","feature_id":"africa-1-0","coordinates":[48.51132784674715,11.824040700245183],"length_km":14207.188584350148},"geometry":{"type":"MultiLineString","coordinates":[[[42.95452380595619,14.797809010241023],[42.75004395140765,14.637942589496117],[41.79379462882354,14.801154224791581]],[[7.425068975355357,37.94551049545967],[5.625070250491423,37.35168786972502],[5.055680653852241,36.75152814511764]],[[33.08276564295245,28.365936333863583],[32.7320868096055,28.91576488954453]],[[35.696548947573824,27.354010300438862],[35.10004937013957,27.097918575215974],[34.59379972936762,26.562513149236715]],[[56.33993432360578,25.0514826710929],[56.92503390911573,24.711631506331194],[58.50003279396771,24.045587109255923],[59.850031837019856,23.40188413185254],[60.97503104005986,22.469443964829516],[60.97503104005986,16.749771315644697]],[[32.28445136473581,31.25927814644905],[32.118801482083676,31.510798430049064],[31.275052079803668,31.798087367585257],[27.900054470683475,31.957307911004964],[25.200056383387572,32.7177179367584],[22.050058614875596,33.45605770170512],[19.35006052757951,33.37780603565933],[16.65006244028343,33.001218522654476],[14.400064034203421,33.846256070003854],[12.150065628130324,35.419780517080355],[11.925065787515376,35.78566189952622],[11.700065946907337,36.24065523321488],[11.36256618480339,37.23235432155614],[10.348617229173378,37.589786573603064],[9.000067859015536,37.589786573603064],[7.425068975355357,37.94551049545967],[6.750069453531427,38.651811712711336],[5.737570170795351,41.74435878948223],[5.372530429392986,43.29362778902908]],[[39.6728961306921,-4.052924364763054],[42.30004427019158,-3.254657364797681],[45.225042198095515,-1.006358951224796],[47.70004044418784,1.468426767331968],[50.85003821329572,5.510071711803246],[53.55003630059162,9.52441134501949],[54.67503550363163,12.615395567393394],[54.00003598419185,13.163718917913586],[52.650036937563755,12.834868817846521],[48.60003980721571,11.84577637362577],[45.45004203810783,11.073982781226615],[44.550042675675684,11.12918015324408],[43.50941841286372,12.395734000022975],[43.25629359217961,12.615395567393394],[43.17191865195175,12.834868817846521],[42.89066885178765,13.054150695298627],[42.30004427019158,13.92930384327183],[41.79379462882354,14.801154224791581],[40.500045545329826,16.534196198259725],[39.487546262591565,18.251816319028222],[38.137547218943524,20.375041253465433],[37.35004777681549,22.05298561667754],[36.11254865347155,24.12261698700344],[35.100049370735476,25.75470426341523],[34.59379972877172,26.562513149236715],[33.91880020694761,27.364667993860262],[33.38487558519097,28.161052262220792],[33.08276564295245,28.365936333863583],[32.76239392030568,28.935360665915493]],[[44.550042675675684,11.12918015324408],[43.65004331324372,11.489461709300535],[43.1479936689003,11.594869371447825]],[[45.45004203810783,11.073982781226615],[45.235802189877134,10.705441883254426]],[[54.00003598359576,13.163718917913586],[55.350035024859835,13.92930384327183],[58.95003247518379,15.669513225155248],[60.97503104005986,16.749771315644697],[65.70002769282792,20.375041253465433],[66.26252729434792,23.298598065875897],[67.02854675169264,24.889731701235817]]]}},{"type":"Feature","properties":{"id":"amitie","name":"Amitie","color":"#4bb748","feature_id":"amitie-0","coordinates":[-35.6110060180745,47.22990849577579],"length_km":6194.387094222405},"geometry":{"type":"MultiLineString","coordinates":[[[-10.799918113296759,49.44120372312804],[-7.199920663568708,50.88245364291024],[-5.399921938704683,51.02419288763878],[-4.544402544762735,50.82820142743812]],[[-70.95027550281526,42.46364601310954],[-69.29987667137726,42.41235450073586],[-61.19988240948919,41.74435878948223],[-50.39989006030513,43.564400497117596],[-39.99999508453427,46.17414676370763],[-23.399909187344846,50.167261162927154],[-16.199914287888834,50.167261162927154],[-10.799918113296759,49.44120372312804],[-5.849921620516659,46.99317357497891],[-3.149923533220666,45.1197757996118],[-1.211824906187882,44.89381187034425]]]}},{"type":"Feature","properties":{"id":"grace-hopper","name":"Grace Hopper","color":"#28b08c","feature_id":"grace-hopper-0","coordinates":[-38.34293272068773,42.07147147019439],"length_km":7081.464150732298},"geometry":{"type":"MultiLineString","coordinates":[[[-16.199914287888834,46.272182853813646],[-9.899918750864702,46.890762878622326],[-5.849921619920758,45.96024524125342],[-4.27492273626067,44.694829089578164],[-2.949193674823563,43.274220252000646]],[[-72.93786409419282,40.75584308487114],[-71.09987539624129,39.78482855593699],[-68.39987730894529,39.524987333511675],[-61.19988240948919,38.651811712711336],[-50.39989006030513,39.00237890905839],[-39.59989771112098,41.74435878948223],[-23.399909187344846,45.96024524125342],[-16.199914287888834,46.272182853813646],[-10.799918113296759,49.29468421942562],[-8.099920026000767,49.73293362369082],[-4.544402544762735,50.82820142743812]]]}},{"type":"Feature","properties":{"id":"asia-direct-cable-adc","name":"Asia Direct Cable (ADC)","color":"#ed1b2c","feature_id":"asia-direct-cable-adc-0","coordinates":[120.88988111749366,20.05833455139623],"length_km":9471.109003441663},"geometry":{"type":"MultiLineString","coordinates":[[[114.29999326356042,14.801154224791581],[116.99999135204831,13.710817738179635],[118.79999007691224,13.492128176464083],[120.14998912056028,13.492128176464083],[121.06600847164388,13.762418337904428]],[[116.99999135204831,19.316876111628712],[117.22499119206026,19.95262290516439],[117.22499119206026,20.796306105108872],[116.99999135204831,22.469443964829516],[116.67753158048176,23.355006811273547]],[[115.19999262718419,18.251816319028222],[114.52499310536027,20.796306105108872],[114.20292333351767,22.22205041973683]],[[113.17499406171221,12.615395567393394],[112.94999422110418,12.834868817846521],[111.59999517745614,13.492128176464083],[110.02499629320025,13.820086409698062],[109.21959686375268,13.782910441432074]],[[107.99999772772827,5.398081130463647],[105.29999964043219,6.405200795356032],[103.27500107496003,7.744889052551447],[100.80000282767627,9.52441134501949],[100.12500330585216,11.294709319565477],[100.23750322675217,12.175887185507976],[100.57500298647233,12.834868817846521],[100.93057273577533,13.174371211662239]],[[140.1749749340766,34.89859296336222],[140.3437248145325,34.69072647741027],[140.51247469558447,34.405022750715936],[140.84997445649643,33.93964008831966],[140.84997445649643,32.43331330641721],[140.39997477528055,30.901396088515508],[138.82497589102445,28.161052262220792],[137.24997700676838,26.562513149236715],[132.74998019460836,23.50508968095737],[130.94998146974444,22.677206196582915],[125.99998497636834,21.111485983488812],[122.84998720785636,20.269544035929588],[121.04998848299225,20.05833455139623],[120.14998912056028,20.05833455139623],[116.99999135204831,19.316876111628712],[115.19999262718419,18.251816319028222],[114.29999326356042,14.801154224791581],[113.17499406171221,12.615395567393394],[112.16249477897614,9.967915186974132],[110.92499565563222,7.744889052551447],[107.99999772772827,5.398081130463647],[105.74999932164808,4.389285926050993],[104.68125007876004,2.817450442654169],[104.3161753373827,1.468426767331968]],[[104.20615041532531,1.341894944206377],[103.91973061822782,1.228443589211858]]]}},{"type":"Feature","properties":{"id":"2africa","name":"2Africa","color":"#b5258f","feature_id":"2africa-1","coordinates":[-21.144285882711586,16.45536331058273],"length_km":39064.907439805844},"geometry":{"type":"MultiLineString","coordinates":[[[25.198689196856183,34.014861549780676],[24.99488074748585,34.89090355336368]],[[45.450042038703735,11.184367066436712],[45.17955222972517,10.705441883254426]],[[34.67817466959547,26.562513149236715],[35.21254929044368,27.097918575215974],[35.696548947573824,27.354010300438862]],[[37.4625476971196,22.05298561667754],[39.18275647850768,21.481533475502996]],[[41.8500445889755,-11.943944931746815],[42.07504442958354,-12.053986862571566],[42.75004395140765,-12.053986862571566],[43.24330360197787,-11.700589282272533]],[[-18.67491253517278,31.286738814391754],[-16.64991396970081,30.255702942039875],[-15.862414526976778,28.95155473219332]],[[55.45302495190069,-4.565748350533772],[55.350035024859835,-3.92832730414264],[54.900035343643765,-1.081346446098796],[54.000035981211795,1.168506749040978],[51.75003757632377,5.510071711803246]],[[5.400070410479286,2.367912558705407],[7.200069135343221,3.154491498099848],[7.650068816559295,3.491423322320592],[7.999287318573677,4.541590692825097]],[[1.800072960155335,-11.356308769000893],[11.700065947503239,-8.846050186819125],[12.600065309935387,-8.830631937053012],[13.235024860124117,-8.812561807472813]],[[27.000055108251505,-34.73466151270862],[25.875055905211504,-34.36402589452668],[25.6232560835889,-33.9624706667599]],[[5.372530429989069,43.29362778902908],[5.175070569275348,42.85377505385472],[4.050071366235344,41.85617959436374],[2.700072322587302,41.35145028689676],[2.168725042748786,41.38560270176812]],[[8.938867903561944,44.41035752885385],[8.775068019003399,44.05151922873524],[8.325068338383225,43.72721479104982],[7.200069135343221,43.073310783003215],[6.412569692619463,42.743713464436695],[5.962570011403388,42.743713464436695],[5.175070569275348,42.85377505385472]],[[32.28445136473581,31.25927814644905],[32.175051442235635,31.510798430049064],[31.72505176101956,31.798087367585257],[30.60005255857546,32.243210016262736],[28.68266219511189,32.58881770832659],[25.198689196856183,34.014861549780676],[24.04673688790992,34.33537907402818],[22.050058614875596,34.76657169708598],[19.24898112158607,34.96588061426259],[16.538486629207664,34.62626157775406],[14.485430000000347,35.114981288071654],[13.014410566350236,35.96055382761562],[12.369303754063077,36.753951849626404],[11.587566027795393,37.85673997565852],[11.250066265691444,39.6983233549332],[10.350066903259478,41.52013202089327],[9.787567301739475,42.41235450073586],[9.562567461727339,43.073310783003215],[9.450067540827328,43.401144973153954],[9.112567779915365,44.05151922873524],[8.938867903561944,44.41035752885385]],[[33.412950565897326,28.161052262220792],[33.30005064527564,28.29321405801615],[33.08276564295245,28.365936333863583]],[[36.22504857377548,24.12261698700344],[36.90004809500369,24.259444485784776],[37.80004745803156,24.225251377401914]],[[48.60003980721571,1.468426767331968],[46.80004108235159,1.918228780215599],[45.344182113695986,2.041205223228781]],[[41.17504506655567,-4.900422453147402],[40.7250453853396,-4.676208028751072],[39.7450660795661,-3.946125186873873]],[[40.95004522654354,-5.273944363641298],[40.50004554473174,-4.676208028751072],[39.67289769319088,-4.053024114605376]],[[39.269676416932725,-6.823132108349236],[39.82504602290763,-6.467627592690688],[40.387545625023535,-6.169450529574503]],[[42.30004426959567,-14.861883917661954],[45.00004235689176,-15.29638776062193],[46.315441425050686,-15.713729798684179]],[[42.30004427019158,-15.224032284647373],[41.40004490716371,-14.64430197712377]],[[35.32504921134351,-26.551620801657084],[33.750050327087614,-26.350174904573713],[32.58062115552212,-25.968268155407962]],[[30.88039235938437,-30.05771707645661],[32.40005128343957,-29.8231438437626],[32.850050964655466,-29.431979622206125],[33.750050327087614,-28.348517239947288],[35.32504921134351,-26.551620801657084],[41.17504506715157,-23.287413403488653],[42.30004427019158,-20.574419057276128],[42.30004427019158,-15.224032284647373],[42.30004426959567,-14.861883917661954],[41.8500445889755,-11.943944931746815],[41.40004490775961,-9.29042430103552],[40.387545625023535,-6.169450529574503],[40.95004522654354,-5.273944363641298],[41.17504506655567,-4.900422453147402]],[[41.17504506655567,-4.900422453147402],[42.30004427019158,-3.479268678970064],[45.450042038703735,-1.231315750217412],[48.60003980721571,1.468426767331968],[51.75003757632377,5.510071711803246],[54.00003598180769,9.52441134501949],[54.90003534423966,12.615395567393394],[54.00003598419185,13.273238157547594],[52.65003693815965,12.944533868662969],[48.60003980781179,11.955858207114732],[45.450042038703735,11.184367066436712],[44.55004267627159,11.23954347159687]],[[38.252175044777196,20.36041343679164],[37.4625476971196,22.05298561667754]],[[37.4625476971196,22.05298561667754],[36.22504857377548,24.12261698700344],[35.212549291039586,25.75470426341523],[34.67817466959547,26.562513149236715],[33.918800207543505,27.364667993860262],[33.412950565897326,28.161052262220792],[32.906450924701375,28.95155473219332],[32.65318110412008,29.113614162980063]],[[32.65318110412008,29.113614162980063],[32.62520112394137,29.344566989489813],[32.45622624364466,29.63833609362628],[32.52993119143143,29.972545436050364]],[[18.44992116524676,-33.69332014378617],[17.775061643323433,-34.11602012163193],[17.32506196210754,-34.85783936223576],[18.00006148393147,-36.3215277599179],[19.80006020939149,-36.68325067019043],[23.40005765911936,-36.68325067019043],[26.10005574581954,-35.83660803749216],[27.000055108251505,-34.73466151270862]],[[27.000055108251505,-34.73466151270862],[31.500051920411707,-31.340410556277746],[32.40005128343957,-29.8231438437626]],[[10.80006658447537,-5.124561675456293],[11.250066265691444,-5.572600905016464],[12.349965487108514,-5.933373731471328]],[[7.423529131e-05,-6.467627592690688],[10.80006658447537,-5.124561675456293],[11.863635831629022,-4.77878776891936]],[[-3.599923213840749,-2.580536704984131],[7.423529131e-05,-6.467627592690688],[1.800072960155335,-11.356308769000893],[6.750069454127329,-18.026426383713453],[8.10006849777537,-23.49392244589784],[13.05006499115128,-30.30995334464681],[15.30006339723147,-32.61276000573574],[16.65006244087933,-33.74264465652956],[17.55006180331148,-33.929536992458964],[18.15553137439108,-33.348058456467676]],[[3.423511810692114,6.439066911484626],[3.825071526223208,4.164912849976942],[2.25007264196731,1.131014326431719],[1.575073120143199,0.34358628488916],[0.450073917103194,-0.631398185258107],[-3.599923213840749,-2.580536704984131],[-10.799918113296759,-2.580536704984131],[-15.299914925456777,1.918228780215599],[-19.799911737616885,8.635699417327467],[-21.149910781264836,11.735650161405832],[-21.140924849904593,14.365653759228442],[-21.149910781264836,19.95262290516439],[-20.299911384604414,22.884654113882444],[-18.67491253517278,31.286738814391754],[-17.0999136503208,35.05222991093673],[-16.246065123519113,39.467127190190496],[-16.199914287888834,39.6983233549332],[-16.199914288484827,44.694829089578164],[-11.249917795108734,48.70423463096067],[-7.199920663568708,50.740281893948165],[-5.399921938704683,50.95337730033451],[-4.544402544762735,50.82820142743812]],[[-0.204315619320974,5.558285889905858],[0.450073917103194,3.279837005484997],[0.450073917103194,-0.631398185258107]],[[-3.599923213840749,-2.580536704984131],[-4.374922664823806,1.468426767331968],[-4.499922576272716,2.367912558705407],[-4.274922735664679,3.266814816815666],[-4.026242911831877,5.323508791824841]],[[-21.140924849904593,14.365653759228442],[-18.449912693968844,14.365653759228442],[-17.445713405352947,14.686594841994992]],[[-16.199914287888834,39.6983233549332],[-13.949915882404726,38.76885922455357],[-10.799918113296759,38.475881348138756],[-9.337919149586595,38.68882888776792]],[[3.825071526223208,4.164912849976942],[5.400070410479286,2.367912558705407],[7.200069135343221,1.243490076978041],[8.550068178991262,1.018534216615524],[9.454267538448212,0.394465191855477]]]}},{"type":"Feature","properties":{"id":"mist","name":"MIST","color":"#a84c9c","feature_id":"mist-0","coordinates":[83.14307882739791,2.9351430719707547],"length_km":6690.199498371039},"geometry":{"type":"MultiLineString","coordinates":[[[101.25000250948806,2.19929675402769],[101.36250242919627,2.592701464601932],[101.44360237174425,2.751228763607222]],[[97.42500521915215,5.845915088460266],[99.00000410340806,6.405200795356032],[100.06611334816643,6.613518860854109]],[[72.87590260996693,19.07607425728523],[71.55002354923187,16.965102599435927],[71.49513558519409,13.492128176464083],[72.900022592881,9.443204702286902],[75.09513303492322,6.770440250104527],[78.69513048465001,3.940475772228814],[79.65001781052403,3.41655961832325],[81.00001685476798,3.042156042425856],[85.500013666928,2.817450442654169],[90.00001047908802,3.715978119298069],[92.70000856638391,4.837826391986557],[94.27500745064,5.833479966632704],[95.40635270497349,6.311016478693225],[97.42500521915215,5.845915088460266],[98.32500458158412,5.286069860821008],[99.11250402371216,4.613591578862773],[100.12500330644806,3.266814816815666],[101.25000250948806,2.19929675402769]],[[102.15000187192003,1.918228780215599],[102.68279723997186,1.614492576237963],[103.34065102845322,1.412069619499378]],[[81.00001685476798,3.042156042425856],[82.57501573902388,5.510047410567148],[82.80001557963192,7.744876956882131],[82.35001589841602,9.52441134501949],[81.45001653598388,11.294709319565477],[80.24298739105474,13.06385310188338]]]}},{"type":"Feature","properties":{"id":"oman-australia-cable-oac","name":"Oman Australia Cable (OAC)","color":"#41b878","feature_id":"oman-australia-cable-oac-1","coordinates":[77.83897821383535,-11.776885531285858],"length_km":10497.29064625741},"geometry":{"type":"MultiLineString","coordinates":[[[72.11252315015598,-7.732812885336029],[72.33752299076401,-7.509800774121521],[72.41667293469345,-7.333331811022597]],[[89.10001111784821,-19.729525450021],[95.4000066530841,-13.99027116703797],[96.83231563842303,-12.19311300919909]],[[115.85731216153303,-31.953441330324313],[113.84999358353615,-31.468437004267024],[111.59999517745614,-30.30995334464681],[89.10001111784821,-19.729525450021],[72.11252315015598,-7.732812885336029],[65.25002801220774,4.164912849976942],[61.20003088126379,15.669513225155248],[60.75003120004772,16.534196198259725],[60.75003120004772,19.104405475930452],[60.75003120004772,22.469443964829516],[59.85003183761575,23.298598065875897],[58.95003247518379,23.7112581424843],[58.162533033055745,23.91710129093513]]]}},{"type":"Feature","properties":{"id":"equiano","name":"Equiano","color":"#7e2c18","feature_id":"equiano-0","coordinates":[-20.69991110004885,12.483262075762921],"length_km":14504.781322569803},"geometry":{"type":"MultiLineString","coordinates":[[[1.575073120143199,0.793562652607196],[1.687573039851407,3.82823430332105],[1.575073120143199,5.061986954416114],[1.227803366152544,6.126307297218732]],[[7.650068815963395,-13.99027116703797],[-3.599923214436649,-15.29638776062193],[-5.711521717964474,-15.918564131427317]],[[1.575073120143199,0.793562652607196],[2.25007264196731,1.580886840914131],[3.600071685615352,4.164912849976942],[3.423511810692114,6.439066911484626]],[[9.000067860207336,-23.49392244589784],[13.05006499115128,-22.665969967794794],[14.533463940297649,-22.68542973223072]],[[18.445861168718828,-33.72721819637743],[17.55006180331148,-33.74264465652956],[16.65006244087933,-33.55534420877606],[15.525063237839326,-32.61276000573574],[13.500064672367355,-30.30995334464681],[9.000067860207336,-23.49392244589784],[7.650068816559295,-18.026426383713453],[7.650068815963395,-13.99027116703797],[8.325068338383225,-10.620064860363238],[7.650068816559295,-6.616650693475355],[7.200069135343221,-4.825692499217419],[4.050071366831244,-1.231315750217412],[2.25007264196731,1.580886840914131]],[[1.575073120143199,0.793562652607196],[0.225074076495339,-0.093411304877754],[-3.599923213840749,-1.681168935904995],[-10.799918113296759,-1.681168935904995],[-14.849915244240792,2.367912558705407],[-19.34991205640081,8.635699417327467],[-20.69991110004885,11.735650161405832],[-20.69991110004885,19.95262290516439],[-19.849911702196447,22.884654113882444],[-19.124912215792865,27.76358852605777],[-17.999913013348852,30.255702942039875],[-17.0999136503208,31.670513047087127],[-16.312414208788844,32.43331330641721],[-13.949915881808826,33.93964008831966],[-11.699917475728817,36.51238821239364],[-10.349918432080775,38.03417390064187],[-9.562418989952736,38.29952060596925],[-9.102749315587026,38.4430794831419]]]}},{"type":"Feature","properties":{"id":"south-pacific-cable-system-spcsmistral","name":"South Pacific Cable System (SPCS)/Mistral","color":"#d18e29","feature_id":"south-pacific-cable-system-spcsmistral-0","coordinates":[-83.51924740133119,-9.940940045674754],"length_km":7803.308813761213},"geometry":{"type":"MultiLineString","coordinates":[[[-86.39986455818145,-2.430680261964474],[-84.14986615150535,-2.580536704984131],[-81.00906837647595,-2.228447783119022]],[[-76.49987157083336,-18.45381377577717],[-72.89987412110531,-19.305384072361306],[-70.30675595809456,-18.473543073651214]],[[-82.79986710785731,-11.503333845984299],[-79.19986965812936,-12.60351210497128],[-76.87428130559793,-12.278420041799619]],[[-90.8222236775613,13.934797333208856],[-91.57486089156932,13.054150695298627],[-92.24986041339342,9.52441134501949],[-90.89986137034147,3.715978119298069],[-87.74986360123404,0.568578852526193],[-86.39986455818145,-2.430680261964474],[-85.04986551393732,-6.616650693475355],[-82.79986710785731,-11.503333845984299],[-79.19986965812936,-15.441023659568087],[-76.49987157083336,-18.45381377577717],[-74.02487332414532,-27.15383128539156],[-72.22487459928129,-31.85146566557725],[-71.62043502747198,-33.04554123247811]]]}},{"type":"Feature","properties":{"id":"dunant","name":"Dunant","color":"#ab7a2b","feature_id":"dunant-0","coordinates":[-38.8058960245367,39.91168430026707],"length_km":6366.320595134595},"geometry":{"type":"LineString","coordinates":[[-1.968324369680654,46.69399663348963],[-2.699923851408691,46.5823550820958],[-5.399921938704683,46.5823550820958],[-9.899918750864702,46.272182853813646],[-16.199914287888834,45.331071073324864],[-23.399909187344846,44.05151922873524],[-39.59989771112098,39.6983233549332],[-50.39989006030513,37.411283634923244],[-61.19988240948919,37.589786573603064],[-72.44987443988924,37.23235432155614],[-74.69987284596934,36.723078949445465],[-76.05919805488554,36.755008440642534]]}},{"type":"Feature","properties":{"id":"coral-sea-cable-system-cs","name":"Coral Sea Cable System (CS\u00b2)","color":"#3c4b9f","feature_id":"coral-sea-cable-system-cs-0","coordinates":[155.4046202241159,-21.16930979043273],"length_km":5009.197266782298},"geometry":{"type":"MultiLineString","coordinates":[[[157.19068553683346,-8.24208943896252],[157.2749628208806,-7.955717094334652],[157.38746274118455,-7.732822794391767]],[[156.3958618811439,-6.708991470564002],[156.59996329905667,-7.28668409428739],[157.38746274118455,-7.732822794391767],[158.3999620239206,-8.067119032529211],[159.74996106756865,-8.901626855396449],[159.94976092602863,-9.42905364643845]],[[160.70132758111177,-8.7746372976857],[160.64996043000062,-9.179382545871277],[160.5374605096967,-9.29042430103552],[160.19996074878455,-9.29042430103552],[159.94976092602863,-9.42905364643845]],[[154.79996457419256,-15.441023659568087],[152.09996648689665,-14.135775375064666],[148.49996903716843,-11.503333845984299],[147.2624699138245,-10.17745743036107],[147.1885196909836,-9.479589292697288]],[[151.20699836948359,-33.86955536437545],[152.09996648689665,-33.3676367639474],[154.2803625585856,-31.85146566557725],[155.69996393662453,-28.743810281149894],[155.69996393662453,-25.540896076259312],[155.24996425540846,-18.880139975101173],[154.79996457419256,-15.441023659568087],[157.04996298027257,-11.943944931746815],[159.18746146604866,-10.17745743036107],[159.29996138635258,-9.73423534230066],[159.24376142616532,-9.29042430103552],[159.5249612269606,-9.012754814881783],[159.74996106756865,-9.012754814881783],[159.94976092602863,-9.42905364643845]]]}},{"type":"Feature","properties":{"id":"southeast-asia-japan-cable-2-sjc2","name":"Southeast Asia-Japan Cable 2 (SJC2)","color":"#cfc12a","feature_id":"southeast-asia-japan-cable-2-sjc2-0","coordinates":[120.24622973092585,20.280825284596364],"length_km":9906.280909339477},"geometry":{"type":"MultiLineString","coordinates":[[[126.22498481697637,25.55188275942587],[123.74998657028833,25.653336613276053],[122.84998720785636,25.75470426341523],[121.94998784542422,25.653336613276053]],[[128.24998338244836,28.359233526108557],[127.12498417940834,29.540507745394493],[125.7749851357603,30.320465424761444],[124.6499859327203,30.901396088515508],[122.84998720785636,31.286738814391754],[122.17498768603225,31.142418511463656],[121.89608788360773,30.935661747314708]],[[129.5999824260964,28.75448641587171],[129.37498258548837,30.901396088515508],[129.1499827448803,31.670513047087127],[129.26248266518442,32.8123187832876],[129.37498258548837,34.31215165223547]],[[138.59997605041642,32.052708023486204],[137.69997668798445,33.09551711711581],[136.87399727311598,34.33682825203173]],[[100.5951029728293,7.198818071264419],[101.70000219070414,7.410337121715135],[103.04883123518101,7.563123274145237],[105.29999964043219,6.18155703253704],[107.09999836529612,4.837826391986557]],[[112.94999422110418,12.615395567393394],[111.59999517745614,13.273238157547594],[110.02499629320025,13.710817738179635],[109.21959686375268,13.782910441432074]],[[120.14998912056028,20.269544035929588],[120.48748888147225,21.635297384859552],[120.66208875778415,22.249168196123776]],[[114.7499929459683,18.251816319028222],[114.29999326475222,20.796306105108872],[114.20309333339704,22.22214038855274]],[[103.9870122893147,1.389451396800233],[104.19209042528557,1.276482074072847],[104.28790035741287,1.327893755020881],[104.45655023793971,1.468426767331968],[104.8499999592161,2.817450442654169],[105.74999932164808,4.0527020972683],[107.09999836529612,4.837826391986557],[107.99999772772827,5.510071711803246],[110.69999581502417,7.744889052551447],[111.93749493836829,9.967915186974132],[112.94999422110418,12.615395567393394],[114.52499310476436,17.10851996079568],[114.7499929459683,18.251816319028222],[116.99999135204831,19.529070924350908],[120.14998912056028,20.269544035929588],[121.04998848299225,20.375041253465433],[122.84998720785636,20.90143978523765],[124.87498577332833,22.05298561667754],[125.7749851357603,24.12261698700344],[126.22498481697637,25.55188275942587],[126.67498449819227,26.1593079707739],[128.24998338244836,28.359233526108557],[129.5999824260964,28.75448641587171],[131.39998115096031,29.049948644465697],[134.99997860068837,30.126049846722832],[136.7999773255523,30.901396088515508],[138.59997605041642,32.052708023486204],[139.04997573163232,32.43331330641721],[139.72497525345642,33.93964008831966],[140.23122489482446,34.405022750715936],[140.2030999141525,34.69072647741027],[140.0343500336966,34.852445708846155]]]}},{"type":"Feature","properties":{"id":"southern-cross-next","name":"Southern Cross NEXT","color":"#b61e51","feature_id":"southern-cross-next-0","coordinates":[-152.82053849664103,11.385632586825267],"length_km":14619.550989052997},"geometry":{"type":"MultiLineString","coordinates":[[[179.34974953238174,-16.80801177359569],[179.09994735985677,-17.168553094226155],[179.09994735985677,-17.70520217268605],[179.3249472004648,-18.880139975101173],[179.54994704107284,-19.305384072361306]],[[178.4382278286252,-18.123069640992355],[178.6499476786407,-18.880139975101173],[179.09994735985677,-19.729525450021]],[[-171.44980430741026,-9.29042430103552],[-171.674804148018,-9.29042430103552],[-171.8115040511786,-9.174626307490298]],[[-157.42781424071939,1.872154031030243],[-157.724814030322,2.817450442654169],[-158.849813233362,4.164912849976942],[-160.19981227700995,5.061986954416114]],[[-179.99979825051412,-18.880139975101173],[-177.29980016321815,-16.738110438702464],[-175.49980143835413,-14.571726491332546],[-173.92480255409802,-11.943944931746815],[-172.79980335105822,-11.062032109909483],[-171.674804148018,-10.620064860363238],[-171.44980430741026,-9.29042430103552],[-169.19980590133008,-4.825692499217419],[-162.89981036430612,2.367912558705407],[-160.19981227700995,5.061986954416114],[-159.29981291457798,5.957818681088533],[-138.59982757864174,23.298598065875897],[-127.79983522945767,28.55704546571133],[-120.59984033000157,33.09551711711581],[-118.79984160513764,33.799525734581415],[-118.39955344545581,33.86223405937197]],[[173.69995118526478,-24.72611802920699],[175.04995022891282,-30.30995334464681],[175.5057116247971,-33.34801975644684],[174.9374503086087,-36.140033391295425],[174.99370026876068,-36.59297842795038],[174.77324144056075,-36.78413802465393]],[[151.19625712709274,-33.913571605570375],[152.0920653907799,-34.2066327368011],[154.8286447337135,-33.129107644117234],[160.64996043000062,-31.468437004267024],[166.49995628580868,-27.95174728521976],[173.69995118526478,-24.72611802920699],[178.1999479974248,-20.574419057276128],[179.09994735985677,-19.729525450021],[179.54994704107284,-19.305384072361306],[179.9999467222889,-18.880139975101173]]]}},{"type":"Feature","properties":{"id":"havfrueaec-2","name":"Havfrue/AEC-2","color":"#30aa9f","feature_id":"havfrueaec-2-0","coordinates":[-32.13340756110129,50.01928620984207],"length_km":7205.926689474144},"geometry":{"type":"MultiLineString","coordinates":[[[7.996258571315225,58.15106571642484],[7.987568576875359,57.932056586951404],[6.300069772911254,57.57189027900508],[4.500071048047319,57.57189027900508]],[[-74.06286329723282,40.15283384719588],[-71.09987539624129,40.55848045058698],[-68.39987730894529,41.2387523289666],[-61.19988240948919,42.41235450073586],[-50.39989006030513,44.694829089578164],[-39.59989771112098,47.50228998113266],[-23.399909187344846,52.96339810559356],[-16.199914287888834,55.07780072164767],[-8.999919388432733,58.99117670269853],[-5.399921938704683,59.679663707208995],[-1.799924488976633,59.45171731890513],[1.800072960751236,58.52439396084473],[4.500071048047319,57.57189027900508],[5.400070410479286,56.717468482041156],[6.300069772911254,56.22032688484507],[7.200069135343221,55.84318584148108],[7.650068816559295,55.77997032709834]],[[-16.199914287888834,55.07780072164767],[-12.599916838160784,54.81936191424915],[-10.799918113296759,54.16597178715178],[-10.349918432080775,53.90168472607427],[-9.696518894955075,53.77080186175808]]]}},{"type":"Feature","properties":{"id":"japan-guam-australia-south-jga-s","name":"Japan-Guam-Australia South (JGA-S)","color":"#944b9d","feature_id":"japan-guam-australia-south-jga-s-0","coordinates":[162.3847454355713,-8.271835486920656],"length_km":6915.151784876782},"geometry":{"type":"MultiLineString","coordinates":[[[157.94996234270454,-25.540896076259312],[153.8999652117606,-26.551620801657084],[153.08946578592602,-26.651853879370247]],[[144.81564664717723,13.273238157547594],[145.34997126865645,12.834868817846521],[146.24997063108842,12.175887185507976],[147.14996999352056,11.735650161405832],[149.39996839960057,10.85308969074528],[151.1999671244645,9.52441134501949],[152.99996584932845,8.190543417795496],[155.24996425540846,4.164912849976942],[156.59996329905667,-0.331409329660265],[157.72496250209667,-3.029995968008661],[159.29996138635258,-4.825692499217419],[161.5499597924326,-6.616650693475355],[162.44995915486456,-8.401139048122838],[162.89995883608063,-10.17745743036107],[161.99995947364866,-13.698987269610743],[159.29996138635258,-18.880139975101173],[157.94996234270454,-25.540896076259312],[156.59996329905667,-28.743810281149894],[154.60976590078016,-31.85146566557725],[152.09996648689665,-33.46154129054857],[151.273987072028,-33.76116106060912]]]}},{"type":"Feature","properties":{"id":"curie","name":"Curie","color":"#b2692e","feature_id":"curie-0","coordinates":[-88.45134022131478,6.516022999881251],"length_km":10891.727293958897},"geometry":{"type":"MultiLineString","coordinates":[[[-85.94986487636929,0.568578852526193],[-82.34986742664142,2.367912558705407],[-80.09986902056123,5.061986954416114],[-79.4248694987373,7.29876275445952],[-79.64986933934534,8.190543417795496],[-79.56661939832034,8.950317108800572]],[[-71.62043502747198,-33.04554123247811],[-72.67487428049728,-31.85146566557725],[-79.64986933934534,-18.45381377577717],[-84.14986615150535,-11.503333845984299],[-85.4998651951533,-6.616650693475355],[-85.94986487636929,0.568578852526193],[-87.06475643430802,5.534179292238662],[-92.69986009460932,9.52441134501949],[-98.09985626920157,12.615395567393394],[-104.39985180622544,16.10232559580297],[-108.44984893716946,18.678647022154717],[-114.29984479297735,22.469443964829516],[-118.79984160513736,27.76358852605777],[-119.6998409675696,31.286738814391754],[-119.24984128635361,32.8123187832876],[-118.79984160513764,33.75276987113061],[-118.41596126184768,33.91992001851462]]]}},{"type":"Feature","properties":{"id":"djibouti-africa-regional-express-1-dare-1","name":"Djibouti Africa Regional Express 1 (DARE 1)","color":"#939597","feature_id":"djibouti-africa-regional-express-1-dare-1-0","coordinates":[42.07504442898764,-19.10292196513146],"length_km":5528.835633400022},"geometry":{"type":"MultiLineString","coordinates":[[[34.833447996502606,-19.82010899999981],[42.025949814008925,-19.82010899999981]],[[39.63257107969674,-6.306132261225844],[40.12927201041025,-5.927942387053361]],[[40.174850462603004,-10.25488665702017],[41.28961680273296,-10.030240541416509]],[[41.5379072360397,-14.45669497006816],[42.07504442898764,-14.80743804437604]],[[46.315441425050686,-15.713729798684179],[45.04301107645226,-15.039497695402932],[42.025949814008925,-14.511535911566813]],[[43.66314330455965,-23.354724804059778],[42.72697695458166,-23.42087339119824],[40.971261031231286,-23.12506902069823]],[[32.58062115552212,-25.968268155407962],[33.79140784557965,-26.192699081406893],[35.223462368650665,-26.361156827463727]],[[39.700143767639595,-4.050296575426323],[39.999997070845666,-5.506103863356715],[41.17504506655567,-9.326623361059807],[42.07504442898764,-14.80743804437604],[42.07504442898764,-20.51958710633204],[40.971261031231286,-23.12506902069823],[35.223462368650665,-26.361156827463727],[33.30005064527564,-28.51533365786017],[31.757961738301827,-28.950559666538012]]]}},{"type":"Feature","properties":{"id":"peace-cable","name":"PEACE Cable","color":"#0090c5","feature_id":"peace-cable-0","coordinates":[81.18159047227279,2.025087667012478],"length_km":21263.263525059505},"geometry":{"type":"MultiLineString","coordinates":[[[65.25002801220774,22.884654113882444],[63.22180684167684,23.91710129093513],[60.00000731671387,24.85391243144087],[58.71855992030421,25.0514826710929],[56.33993432360578,25.0514826710929]],[[45.450042038703735,11.625479959569855],[45.33754211780372,11.073982781226615],[45.01088234980864,10.43511874899288]],[[37.237547857107636,22.05298561667754],[39.18275647850768,21.481533475502996]],[[57.00003385598526,13.248904801212989],[58.000033147576175,12.273619553801206],[63.00002960553183,11.735650161405832],[67.00002677189639,10.85308969074528],[71.3195757241684,9.11818824277241],[73.22502236205099,7.013502779332166],[73.59708669534523,6.355510376771536],[74.25002163593187,5.659359572411489],[78.3000187674719,3.042156042425856],[79.65001781052403,2.51777609524721],[81.00001685476798,2.143087178471855],[85.500013666928,-0.781332308789108],[90.00001047908802,0.118642751260435],[92.70000856638391,3.940475772228814],[94.27500745064,4.937462677928599],[95.45328225426326,5.909674833582863],[97.42500521915215,5.286069860821008],[97.65000505976,5.286069860821008],[98.21250466128001,4.613591578862773],[99.7875035455361,3.266814816815666]],[[102.15000187192003,1.74956539407541],[102.68279723997186,1.502034277710852],[103.34065102845322,1.159233669139442],[103.6462108113958,1.338645835654649]],[[73.07152247079179,6.622458884185137],[73.11252244174707,6.790133539110704],[73.22502236205099,7.013502779332166]],[[11.981315748271243,35.419780517080355],[13.668814552823427,35.69434844652369],[14.350104069595588,35.95240101739213]],[[27.900054471279375,31.86180860227073],[31.500051920411707,34.25018044028598],[32.466651236259686,34.76657169708598]],[[50.40003853207964,1.468426767331968],[52.20003725694376,-2.130918480960333],[54.00003598180769,-3.92832730414264],[55.44505495814286,-4.617611322442136]],[[44.55004267627159,11.680570534838436],[43.24223110273756,12.615395567393394],[43.15785616250971,12.834868817846521],[42.86254387230766,13.054150695298627],[42.24379431063569,13.92930384327183],[41.737544669267656,14.801154224791581],[40.38754562562161,16.534196198259725],[39.37504634288372,18.251816319028222],[38.02504729923568,20.375041253465433],[37.237547857107636,22.05298561667754],[36.00004873376353,24.12261698700344],[34.98754945102763,25.75470426341523],[34.50942478913958,26.562513149236715],[33.862550247391546,27.364667993860262]],[[33.37083809573128,28.161052262220792],[32.65318110412008,29.113614162980063]],[[29.70093319552029,31.072270031660306],[27.900054471279375,31.86180860227073],[25.200056383983473,32.575628370353215],[22.050058614875596,33.31515395812905],[19.350060528175415,33.189714664600466],[16.65006244087933,32.8123187832876],[14.400064034799321,33.70598849685854],[11.981315748271243,35.419780517080355],[11.812565867807347,35.78566189952622],[11.250066265095544,37.23235432155614],[10.348617229769278,37.50058844605323],[9.450067540827328,37.50058844605323],[9.000067859611436,37.50058844605323],[7.200069135343221,37.94551049545967],[6.525069613519291,38.651811712711336],[5.625070251087323,41.74435878948223],[5.372530429989069,43.29362778902908]],[[58.000033147576175,12.273619553801206],[57.00003385598526,11.294709319565477],[54.90003534423966,9.52441134501949],[53.55003630118789,5.510071711803246],[50.40003853207964,1.468426767331968],[45.90004171991963,-1.681168935904995],[42.30004427019158,-3.92832730414264],[39.672896131288006,-4.052924364763054]],[[67.02854675228855,24.889731701235817],[65.25002801220774,22.884654113882444],[64.35002864977577,20.375041253465433],[60.97503104065576,16.965102599435927],[58.95003247279982,14.801154224791581],[57.00003385598526,13.248904801212989],[55.35003502545574,14.147583506948735]],[[55.35003502545574,14.147583506948735],[48.60003980781179,12.395734000022975],[45.450042038703735,11.625479959569855],[44.55004267627159,11.680570534838436]],[[9.450067540827328,37.50058844605323],[9.675067381435365,37.35168786972502]]]}},{"type":"Feature","properties":{"id":"jupiter","name":"JUPITER","color":"#63c5b9","feature_id":"jupiter-0","coordinates":[-148.28713581206685,43.443614889598344],"length_km":13750.627556868796},"geometry":{"type":"MultiLineString","coordinates":[[[-123.95633795222729,45.231085444465165],[-125.09983714216159,44.694829089578164],[-129.5998339543217,43.401144973153954],[-138.4872392170287,41.40772623743595]],[[141.97497365953643,34.405022750715936],[141.07497429710446,32.052708023486204],[140.6249746158884,30.901396088515508],[138.82497589102445,27.76358852605777],[137.24997700676838,26.1593079707739],[133.19997987582445,22.469443964829516],[127.79998370123228,17.395022634700517],[124.6499859327203,15.23578178303578],[123.5249867296803,14.256644994553485],[122.95008713694455,14.11652289884896]],[[140.84997445649643,34.405022750715936],[138.59997605041642,33.75276987113061],[137.69997668798445,33.846256070003854],[136.87399727311598,34.33682825203173]],[[179.99992719256971,44.051501534601925],[172.79992073307184,44.051501534601925],[160.19996074878455,40.72920412488655],[149.39996839960057,35.78566189952622],[143.09997286257644,34.59045588265237],[141.97497365953643,34.405022750715936],[140.84997445649643,34.405022750715936],[140.39997477468464,34.69072647741027],[140.18903742411456,34.89859296336222]],[[-118.39955344545581,33.86223405937197],[-120.59984033000157,33.70598849685854],[-122.3998390548656,33.93964008831966],[-129.5998339543217,37.589786573603064],[-138.4872392170287,41.40772623743595],[-151.19983391146832,44.048716071048425],[-179.99981350929238,44.048716071048425]]]}},{"type":"Feature","properties":{"id":"kumul-domestic-submarine-cable-system","name":"Kumul Domestic Submarine Cable System","color":"#a76c35","feature_id":"kumul-domestic-submarine-cable-system-0","coordinates":[150.11278431665937,-9.549626145302685],"length_km":5568.708913353526},"geometry":{"type":"MultiLineString","coordinates":[[[145.34997126865645,-3.479268678970064],[145.79997094987252,-2.130918480960333],[146.6999703123045,-1.681168935904995],[147.14996999352056,-1.793617120354896]],[[152.2124664072006,-3.92832730414264],[152.2124664072006,-4.040555289062099],[152.2745757382018,-4.342382000415339]],[[151.1999671244645,-3.92832730414264],[152.2124664072006,-3.92832730414264],[152.43746624780846,-4.152767748013638],[152.54996616811255,-4.37714437553184],[152.6624660884165,-4.825692499217419],[152.99996584932845,-5.049857167366764],[153.8999652117606,-4.825692499217419],[154.79996457419256,-4.825692499217419],[155.24996425540846,-5.273944363641298],[155.47496409601666,-5.721872747834119],[155.56783512397587,-6.225662994684213]],[[150.52496760264057,-4.825692499217419],[150.41246768233648,-5.273944363641298]],[[146.24997063108842,-4.825692499217419],[146.6999703123045,-4.60145376483711],[149.39996839960057,-4.60145376483711],[150.52496760264057,-4.825692499217419],[151.1999671244645,-3.92832730414264],[149.84996808081647,-2.580536704984131],[149.84996808081647,-2.130918480960333],[150.52496760264057,-2.130918480960333],[150.8085611517402,-2.578139138209948]],[[143.6583709045021,-3.580053610579215],[143.9999722250084,-3.479268678970064],[144.44997190622448,-3.029995968008661],[144.44997190622448,-2.580536704984131]],[[141.2999741377125,-2.130918480960333],[141.29987648153164,-2.689698402599833]],[[145.7847409606618,-5.23368424614946],[146.02497079048055,-5.049857167366764],[146.24997063108842,-4.825692499217419],[146.24997063108842,-4.37714437553184],[145.34997126865645,-3.479268678970064],[144.44997190622448,-2.580536704984131],[142.19997350014447,-2.130918480960333],[141.2999741377125,-2.130918480960333],[140.84997445649643,-2.35574573664619]],[[146.99293885476283,-6.739375698979019],[147.37496983412842,-7.063446338991064],[148.04996935595253,-7.062898168976873]],[[148.2935238709162,-8.604445748342727],[148.9499687183845,-8.401139048122838],[149.39996839960057,-8.401139048122838]],[[150.45875671204502,-10.315743405478468],[150.74996744324844,-10.398839577127402],[151.1999671244645,-10.398839577127402]],[[147.1885196909836,-9.479589292697288],[147.37496983412842,-10.17745743036107],[149.39996839960057,-11.062032109909483],[150.74996744324844,-11.062032109909483],[151.1999671244645,-10.841130095525688],[151.1999671244645,-10.398839577127402],[150.97496728385667,-10.17745743036107],[150.29996776203254,-9.73423534230066],[149.84996808081647,-9.29042430103552],[149.39996839960057,-8.846050186819125],[149.39996839960057,-8.401139048122838],[148.04996935595253,-7.063446338991064],[148.04996935595253,-6.393099497823911],[147.59996967473646,-5.945707155070644],[146.90630490718817,-5.585376278266893],[146.02497079048055,-5.273944363641298],[145.7847409606618,-5.23368424614946]],[[145.77174829265064,-7.96374446783501],[145.57497110926448,-8.401139048122838]],[[143.20993372217916,-9.07814229926296],[143.9999722250084,-8.846050186819125],[144.72573462481674,-8.332313959673433],[145.57497110926448,-8.401139048122838],[146.6999703123045,-9.29042430103552]]]}},{"type":"Feature","properties":{"id":"marea","name":"MAREA","color":"#288da3","feature_id":"marea-0","coordinates":[-38.88408857801272,40.57765092802222],"length_km":6396.536654458328},"geometry":{"type":"LineString","coordinates":[[-76.05920188300784,36.75500543613895],[-74.69987284596934,36.813198605777224],[-72.44987443988924,37.411283634923244],[-61.19988240948919,37.94551049545967],[-50.39989006030513,37.94551049545967],[-39.59989771112098,40.38732029077508],[-23.399909187344846,44.694829089578164],[-16.199914287888834,45.646541495187385],[-9.899918750864702,46.5823550820958],[-5.849921619920758,45.646541495187385],[-4.499922576272716,44.694829089578164],[-2.949193674823563,43.274220252000646]]}},{"type":"Feature","properties":{"id":"south-atlantic-inter-link-sail","name":"South Atlantic Inter Link (SAIL)","color":"#c62a26","feature_id":"south-atlantic-inter-link-sail-0","coordinates":[-13.859932011165128,-2.614200652258127],"length_km":5556.943493751821},"geometry":{"type":"LineString","coordinates":[[-38.542968459859594,-3.718735129291092],[-35.99990026139302,-2.580536704984131],[-34.199901536529,-2.130918480960333],[-25.19990791220887,-1.906058394384765],[-10.799918113296759,-2.805287932307917],[-3.599923213840749,-2.805287932307917],[0.450073917103194,-1.081346446098796],[6.300069772911254,1.918228780215599],[8.10006849777537,2.480311786858737],[9.000067860207336,2.592701464601932],[9.91022721544214,2.933124533518602]]}},{"type":"Feature","properties":{"id":"brusa","name":"BRUSA","color":"#c86d28","feature_id":"brusa-0","coordinates":[-42.08907980888387,8.420314618733522],"length_km":10947.297469583124},"geometry":{"type":"MultiLineString","coordinates":[[[-63.4498808155692,24.94136317175375],[-65.69987922164921,20.375041253465433],[-66.14987890286528,19.104405475930452],[-66.1066660428526,18.46610541858561]],[[-35.549900580176946,0.568578852526193],[-36.899899623824986,-0.781386636225587],[-38.542964866112136,-3.718736532579084]],[[-43.209563122750176,-22.903495209373933],[-42.29989579841706,-23.905969261790265],[-40.94989675476902,-24.316706749469176],[-37.34989930504097,-23.49392244589784],[-32.84990249288096,-18.026426383713453],[-31.04990376801693,-13.698987269610743],[-29.924904564976924,-9.29042430103552],[-31.49990343679494,-4.825692486823558],[-35.549900580176946,0.568578852526193],[-41.399896435985006,7.744889052551447],[-48.59989133544111,14.801154224791581],[-57.59988495976114,21.216397899942],[-63.4498808155692,24.94136317175375],[-69.29987667137726,29.73606949729215],[-73.3498738023213,33.565491482352044],[-75.59987220840131,35.78566189952622]]]}},{"type":"Feature","properties":{"id":"ellalink","name":"EllaLink","color":"#c22c75","feature_id":"ellalink-1","coordinates":[-22.96085384459116,17.041489374928894],"length_km":6771.006697976689},"geometry":{"type":"MultiLineString","coordinates":[[[-20.430336961483764,21.674407102148695],[-17.035703999999548,20.947172000000176]],[[-13.72491604179678,33.93964008831966],[-12.374916998148738,33.93964008831966],[-9.899918750864702,33.93964008831966],[-7.631920358132023,33.60539511325584]],[[-38.542964866112136,-3.718736532579084],[-36.44989994260901,-0.781386636225587],[-35.09990089896096,0.568578852526193],[-31.04990376801693,3.715978119298069],[-27.85950075469042,7.716632622882817],[-25.649907593424945,11.294709319565477],[-24.2999085497769,14.365653759228442],[-24.074908709168866,14.801154224791581],[-23.399909187344846,16.10232559580297],[-21.59991046248082,19.95262290516439],[-19.59991187929863,22.884654113882444],[-18.449912693968844,24.94136317175375],[-17.662413251840803,27.76358852605777],[-17.47038457928175,28.275606244082983],[-17.0999136503208,28.95155473219332],[-17.19854858529393,29.690264251007914],[-16.64991396970081,31.670513047087127],[-16.08741436818081,32.43331330641721],[-13.72491604179678,33.93964008831966],[-11.47491763512078,36.51238821239364],[-9.449919069648717,37.67887792909206],[-9.112419308736753,37.85358171958824],[-8.869597215129223,37.95721527519206]],[[-23.521209101414858,14.923035560171673],[-23.849908868560828,14.801154224791581],[-24.074908709168866,14.801154224791581]],[[-16.08741436818081,32.43331330641721],[-16.64991396910482,32.52821504536491],[-16.908898160637996,32.647276965637694]]]}},{"type":"Feature","properties":{"id":"new-cross-pacific-ncp-cable-system","name":"New Cross Pacific (NCP) Cable System","color":"#22ad97","feature_id":"new-cross-pacific-ncp-cable-system-0","coordinates":[150.1828860661257,39.04324962041344],"length_km":12325.172762088048},"geometry":{"type":"MultiLineString","coordinates":[[[131.39998115096031,29.1482487910328],[128.69998306366443,26.1593079707739],[127.34998402001638,25.55188275942587],[125.99998497636834,25.348717422116714],[124.19998625150441,24.94136317175375],[122.84998720785636,25.043329056612176],[122.17498768603225,24.99235668767365]],[[140.39997477528055,33.565491482352044],[140.17497493467252,33.93964008831966],[140.11872497452055,34.405022750715936],[140.14684995400054,34.69072647741027],[140.14684995400054,34.89859296336222]],[[128.24998338244836,30.5144959597591],[128.24998338244836,31.670513047087127],[128.4749832230562,32.8123187832876],[128.92498290427227,34.31215165223547],[128.99949285148878,35.17037876180022]],[[125.99998497636834,30.901396088515508],[124.19998625150441,31.670513047087127],[122.84998720785636,31.86180860227073],[121.94998784542422,31.718374001887323]],[[179.99995828233068,47.19740739556967],[172.7999518228328,47.19740739556967],[160.19996074878455,44.05151922873524],[149.39996839960057,38.651811712711336],[143.09997286257644,34.683017659857974],[140.39997477528055,33.565491482352044],[138.59997605041642,32.8123187832876],[136.7999773255523,31.670513047087127],[134.99997860068837,30.901396088515508],[132.74998019460836,30.5144959597591],[131.39998115096031,29.1482487910328],[130.49998178852834,29.344566989489813],[128.24998338244836,30.5144959597591],[125.99998497636834,30.901396088515508],[122.17498768603225,30.853118511880677],[121.92508786246776,30.86475026744717],[121.89393788393856,30.89950835597767],[121.89607788361477,30.935660541111577]],[[-123.96253794783507,45.202232100184204],[-125.09983714216159,45.646541495187385],[-129.5998339543217,45.96024524125342],[-138.5998275786419,46.5823550820958],[-151.1998186526899,47.19740739556967],[-179.99979825051412,47.19740739556967]]]}},{"type":"Feature","properties":{"id":"monet","name":"Monet","color":"#5bba46","feature_id":"monet-0","coordinates":[-40.0931941836019,7.245978255095368],"length_km":10643.303593967077},"geometry":{"type":"MultiLineString","coordinates":[[[-34.64990121774498,0.568578852526193],[-35.99990026139302,-0.781386636225587],[-38.542968459859594,-3.718735129291092]],[[-46.328062944825646,-23.961842897597087],[-44.54989420449707,-25.179443898921058],[-41.399896435985006,-25.33771218660113],[-37.01239954412901,-23.803079640835886],[-31.949728484343677,-18.025284192896695],[-30.14972975947965,-13.697820288632505],[-29.249905043153905,-9.29042430103552],[-31.162403672774964,-4.489307673128955],[-34.64990121774498,0.568578852526193],[-40.499897073553036,7.744889052551447],[-48.59989133544111,15.669513225155248],[-57.59988495976114,20.796306105108872],[-69.29987667137726,25.75470426341523],[-73.3498738023213,27.76358852605777],[-76.94987125204935,27.962503359972466],[-77.84987061448132,27.76358852605777],[-78.74986997691337,27.264711877833996],[-79.64986933934534,26.763586569619914],[-80.08893155227202,26.350584577319996]]]}},{"type":"Feature","properties":{"id":"sea-us","name":"SEA-US","color":"#4eb748","feature_id":"sea-us-0","coordinates":[-150.242675675476,27.388951488009898],"length_km":13759.441206456717},"geometry":{"type":"MultiLineString","coordinates":[[[144.89997158744055,13.273238157547594],[145.34997126865645,13.492128176464083],[146.24997063108842,14.038469666260218],[147.14996999352056,14.147583506948735],[151.1999671244645,14.365653759228442],[160.19996074878455,15.669513225155248],[179.99992672230314,18.251816319028222]],[[125.99998497636834,1.918228780215599],[126.44998465758441,3.266814816815666],[126.44998465758441,5.061986954416114],[133.19997987582445,9.08033076823294],[137.24997700676838,10.85308969074528],[143.9999722250084,13.054150695298627],[144.69469829535797,13.464777824933044]],[[125.61287587559997,7.079988883160643],[125.99998497636834,5.957818681088533],[126.44998465758441,5.061986954416114]],[[133.19997987582445,9.08033076823294],[133.64997955704033,8.190543417795496],[134.5609408257699,7.531746239289517]],[[137.24997700676838,10.85308969074528],[137.69997668798445,9.967915186974132],[138.06149986937797,9.443922836169385]],[[-118.39945344493313,33.862474868985494],[-120.59984033000157,33.189714664600466],[-122.3998390548656,33.189714664600466],[-127.79983522945767,32.8123187832876],[-138.60310458621126,30.516425505901374],[-147.6030982105313,28.953514579902283],[-152.99981737755394,25.75470426341523],[-157.94981387092994,22.677206196582915],[-158.39981355214593,22.05298561667754],[-158.45606295953138,21.73983373091106],[-158.456063506614,21.635297384859552],[-158.22066328843266,21.4634468234482],[-158.3998135521461,21.268825931479064],[-158.849813233362,21.006499845176737],[-163.799809726738,19.95262290516439],[-172.79980335105813,19.95262290516439],[-179.99979825051412,18.251816319028222]]]}},{"type":"Feature","properties":{"id":"faster","name":"FASTER","color":"#4bb748","feature_id":"faster-0","coordinates":[-152.11059071043016,45.96024524125342],"length_km":10610.398090120929},"geometry":{"type":"MultiLineString","coordinates":[[[138.59997605041642,32.243210016262736],[136.7999773255523,31.09426282763951],[134.99997860068837,30.320465424761444],[131.39998115096031,29.246454972180413],[129.5999824260964,28.95155473219332],[128.24998338244836,28.55704546571133],[122.84998720785636,26.461843796188983],[121.94998784542422,25.75470426341523]],[[-124.40833763202653,43.118664098550106],[-125.99983650459365,43.073310783003215],[-129.5998339543217,43.72721479104982],[-138.59982757864174,45.0138336439531],[-151.19981865269,45.96024524125342],[-179.99979825051412,45.96024524125342]],[[140.39997477528055,33.001218522654476],[140.0624750143684,33.93964008831966],[140.39997477528055,34.405022750715936],[140.28747485438052,34.69072647741027],[140.0765375038106,34.852445708846155]],[[138.59997605041642,32.243210016262736],[140.39997477528055,33.001218522654476],[143.09997286257644,34.31215165223547],[149.39996839960057,37.589786573603064],[160.19996074878455,42.743713464436695],[172.7999518228328,45.96024524125342],[179.99995828233068,45.96024524125342]],[[136.87399727311598,34.33682825203173],[137.69997668798445,33.189714664600466],[138.59997605041642,32.243210016262736]]]}},{"type":"Feature","properties":{"id":"africa-coast-to-europe-ace","name":"Africa Coast to Europe (ACE)","color":"#8cc63f","feature_id":"africa-coast-to-europe-ace-0","coordinates":[-15.412392583191341,7.410105237917242],"length_km":15982.725845369177},"geometry":{"type":"MultiLineString","coordinates":[[[-17.445713405352947,14.686594841994992],[-17.54991333213278,13.92930384327183],[-17.099913650916793,12.175887185507976],[-16.64991396910482,11.735650161405832],[-16.199914287888834,11.680570534838436],[-15.791145687384237,11.774131923775238]],[[18.449961165814393,-33.69332014378617],[17.55006180331148,-33.55534420877606],[16.20006275966344,-32.61276000573574],[14.850063716015397,-30.30995334464681],[11.700065947503239,-23.49392244589784],[10.350066903855378,-18.026426383713453],[10.350066903855378,-10.620064860363238],[9.000067860207336,-6.616650693475355],[8.550068178991262,-4.825692499217419],[6.987569285284264,0.118588418888312],[6.733269466028674,0.333286471885964]],[[7.200069135343221,1.018534216615524],[8.550068178991262,0.793562652607196],[9.454267538448212,0.394465191855477]],[[7.200069135343221,1.468426767331968],[9.000067860207336,1.468426767331968],[9.768227316036105,1.860150409321811]],[[3.423511810692114,6.439066911484626],[2.475072482575348,4.164912849976942],[2.25007264196731,3.82823430332105]],[[2.440112507341202,6.356673335458259],[1.800072960751236,5.061986954416114],[1.800072960751236,3.82823430332105]],[[-0.204315619320974,5.558285889905858],[-0.674925285936628,3.279837005484997],[-0.674925285936628,2.830478071896748]],[[-10.797188115230739,6.300378530564464],[-12.599916838160784,4.613591578862773]],[[-13.238096386068461,8.485442435793914],[-14.849915244240792,6.852191098754328]],[[-15.74991460667276,7.744889052551447],[-13.703826056141079,9.51343460136282]],[[-17.99991301275286,13.492128176464083],[-17.0999136503208,13.492128176464083],[-16.58136401766616,13.456136894896968]],[[-15.978284444893506,18.08386849170685],[-17.0999136503208,18.251816319028222],[-18.449912693968844,18.251816319028222]],[[-4.338542690595681,47.81102015174913],[-5.849921619920758,47.65407102366078],[-7.649920344784692,46.890762878622326],[-10.799918113296759,43.401144973153954],[-10.799918113296759,39.6983233549332],[-10.349918432080775,39.00237890905839],[-9.899918750864702,38.29952060596925],[-11.249917794512742,36.51238821239364],[-11.699917475728817,35.419780517080355],[-13.274916359984806,32.052708023486204],[-14.174915722416772,29.73606949729215],[-14.737415323936775,28.161052262220792],[-15.299914925456777,26.964304734562898]],[[-9.33155915349599,38.690161972355526],[-9.899918750864702,38.29952060596925]],[[2.475072482575348,4.164912849976942],[5.400070410479286,2.592701464601932],[6.300069772911254,2.367912558705407],[7.200069135343221,1.468426767331968],[7.200069135343221,1.018534216615524],[6.733269466028674,0.333286471885964]],[[-16.518014062543934,28.059088061264806],[-16.199914287888834,27.564309487941923],[-15.299914925456777,26.964304734562898],[-17.0999136503208,22.884654113882444],[-18.449912693968844,19.95262290516439],[-18.449912693968844,16.534196198259725],[-17.99991301275286,15.23578178303578]],[[-17.445713405352947,14.686594841994992],[-17.99991301275286,13.92930384327183],[-17.99991301275286,13.492128176464083],[-18.449912693968844,11.735650161405832],[-17.0999136503208,8.635699417327467],[-15.74991460667276,7.744889052551447],[-14.849915244240792,6.852191098754328],[-12.599916838160784,4.613591578862773],[-10.799918113296759,2.817450442654169],[-5.399921938704683,2.817450442654169],[-4.387422655968697,3.266814816815666],[-4.026242911831877,5.323508791824841],[-3.824923054448695,3.266814816815666],[-3.149923532624674,2.817450442654169],[-0.674925285936628,2.830478071896748],[1.575073120143199,3.82823430332105],[2.25007264196731,3.82823430332105]]]}},{"type":"Feature","properties":{"id":"apollo","name":"Apollo","color":"#d36f27","feature_id":"apollo-0","coordinates":[-38.468446590652974,41.36699126032328],"length_km":11537.741793409654},"geometry":{"type":"MultiLineString","coordinates":[[[-72.87218414072115,40.800580995045266],[-71.09987539624129,40.215724060833985],[-68.39987730894529,40.55848045058698],[-61.19988240948919,40.72920412488655],[-50.39989006030513,42.07923561816413],[-39.59989771112098,45.0138336439531],[-23.399909187344846,49.000334389463426],[-16.199914287888834,49.58728674004685],[-10.799918113296759,50.167261162927154],[-8.099920026000767,50.454639125893955],[-5.399921938704683,50.88245364291024],[-4.544402544762735,50.82820142743812]],[[-74.04709330840446,40.12349265823708],[-71.09987539624129,39.6983233549332],[-68.39987730894529,39.35121757117122],[-61.19988240948919,38.29952060596925],[-50.39989006030513,38.475881348138756],[-39.59989771112098,41.0693404382162],[-23.399909187344846,45.331071073324864],[-16.199914287888834,45.96024524125342],[-5.399921938704683,49.14772788577412],[-4.499922576272716,49.000334389463426],[-3.459883313046331,48.73055297916871]]]}},{"type":"Feature","properties":{"id":"arcos","name":"ARCOS","color":"#51489d","feature_id":"arcos-0","coordinates":[-75.07080241424389,11.272769612255498],"length_km":7860.755725306291},"geometry":{"type":"MultiLineString","coordinates":[[[-74.19504320359538,22.62989167911265],[-74.4748730053613,22.677206196582915],[-74.58737292566532,22.884654113882444],[-74.4748730053613,23.298598065875897],[-74.4748730053613,23.7112581424843],[-74.92487268657729,24.225251377401914],[-75.52590226080234,24.403328403350237],[-75.77889084339483,24.410331177006306],[-76.49987157083336,24.53265756616073],[-76.94987125204935,24.94136317175375]],[[-77.84987061448132,25.145210227401346],[-79.19986965812936,25.75470426341523],[-79.64986933934534,25.75470426341523],[-80.16222149850138,25.933206978469332]],[[-72.11940467399724,21.85108832553302],[-72.44987443988924,22.469443964829516],[-73.3498738023213,22.884654113882444],[-73.79987348353728,22.884654113882444]],[[-66.48737866377725,18.625351394064932],[-67.04987826529725,18.678647022154717],[-67.38737802620922,18.465364393137126],[-67.61237786681725,17.82393441253792],[-68.39987730894529,13.92930384327183],[-68.89264695986267,12.09043961830498],[-69.07487683076923,11.955858207114732],[-69.74987635259325,12.175887185507976],[-70.12819608458797,12.355002950040006],[-70.42487587441727,12.175887185507976]],[[-70.20431603066386,11.708782466419155],[-70.42487587441727,11.735650161405832],[-70.6498757150253,12.175887185507976],[-71.32487523684924,12.615395567393394],[-71.99987475867334,12.615395567393394],[-72.67487428049728,12.175887185507976],[-72.89987412110531,11.735650161405832]],[[-73.3498738023213,11.735650161405832],[-73.79987348353728,11.735650161405832],[-74.69987284596934,11.515266158038768],[-75.37487236779336,11.073982781226615]],[[-75.50573227509088,10.38680745163333],[-76.0498718896173,10.41081650540272],[-77.41987091969315,9.67231131777662],[-77.93346055586147,9.125435434149097],[-78.29987029629338,9.67231131777662],[-79.08736973782534,9.746236973759974]],[[-79.75352926591172,9.437623338483982],[-80.09986902056123,9.746236973759974],[-82.34986742664132,9.967915186974132],[-83.03765938887449,9.988597517410145],[-83.2498667890733,11.294709319565477],[-83.47486662968133,11.735650161405832],[-83.77154885044065,11.991681428073646],[-83.47486662968133,12.175887185507976],[-83.24986678907348,12.615395567393394],[-83.24986678907348,13.492128176464083],[-83.39644912564061,14.016107024140217]],[[-83.02486694846526,14.365653759228442],[-82.91236702816133,15.018578573757472],[-83.2498667890733,15.452760959322058],[-83.47486662968133,15.452760959322058],[-83.77681884657423,15.26131021390879]],[[-72.11940467399724,21.85108832553302],[-71.99987475867334,21.635297384859552],[-70.76237563532924,20.375041253465433],[-70.6911856857609,19.799436355797177],[-70.19987603380923,19.95262290516439],[-69.29987667137726,19.740987365524937],[-68.62487714955324,19.104405475930452],[-68.51237722924922,18.891661584303154]],[[-68.17487746833726,18.891661584303154],[-67.49987794651324,19.210675111642853],[-67.04987826529725,19.104405475930452],[-66.48737866377725,18.838433217733183],[-66.01686899709074,18.441839618642867]],[[-83.77681884657423,15.26131021390879],[-83.69986647028928,15.669513225155248],[-84.14986615150535,16.10232559580297],[-85.0498655139375,16.10232559580297]],[[-85.95469724872682,15.915243688695657],[-86.39986455758554,16.10232559580297],[-86.84986423880153,16.10232559580297]],[[-87.9461557876504,15.844981598742601],[-88.19986328244948,15.886035719079029]],[[-88.42486312305734,16.10232559580297],[-88.08736336214537,16.534196198259725],[-87.97486344184135,16.965102599435927],[-88.1821856144821,17.49638520483909],[-87.7498636012335,17.82393441253792],[-87.52486376062537,18.251816319028222],[-87.29986392001751,19.529070924350908],[-87.29986392001751,19.95262290516439]],[[-87.46353614173474,20.212733353176567],[-87.18736399971331,20.05833455139623],[-86.84986423880153,20.163975031975873],[-86.62486439819331,20.375041253465433],[-86.62486439819331,20.796306105108872],[-86.76758665233304,21.09572879236739],[-86.51236447788929,21.216397899942],[-86.17486471697732,21.425997872385402],[-85.83736495666136,21.635297384859552],[-84.82486567332928,23.298598065875897],[-83.2498667890733,24.32780311165181],[-80.99986838299347,24.32780311165181],[-80.5498687017773,24.73717827217609],[-79.9873691002574,25.348717422116714]]]}},{"type":"Feature","properties":{"id":"asia-america-gateway-aag-cable-system","name":"Asia-America Gateway (AAG) Cable System","color":"#69479c","feature_id":"asia-america-gateway-aag-cable-system-0","coordinates":[132.0158316130375,16.385960718226247],"length_km":19342.836177823483},"geometry":{"type":"MultiLineString","coordinates":[[[-179.99979825051412,19.104405475930452],[-172.79980335105813,20.796306105108872],[-163.799809726738,20.796306105108872],[-158.849813233362,21.111485983488812],[-158.3998135521461,21.478351011075993],[-158.24204999203212,21.54876173571662]],[[-158.34356194506245,21.73983373091106],[-158.2873136318419,22.05298561667754],[-157.94981387092994,22.469443964829516],[-152.99981737755394,24.94136317175375],[-147.6030982105313,28.1630268819071],[-138.60310458621126,29.738014316088],[-127.79983522945767,32.43331330641721],[-122.39983905486586,34.867831005273345],[-120.8472016490899,35.367078251717096]],[[100.93057273577533,13.174371211662239],[100.68750290737216,12.834868817846521],[100.46250306616822,12.175887185507976],[100.3500031464602,11.294709319565477],[101.02500266828413,9.52441134501949],[103.27500107496003,7.967776882259704],[105.29999964043219,6.628746603597807],[107.99999772772827,5.957818681088533]],[[179.99992672230314,19.104405475930452],[160.19996074878455,16.534196198259725],[151.1999671244645,15.23578178303578],[147.14996999352056,14.365653759228442],[146.24997063108842,14.147583506948735],[145.34997126865645,13.54681947716878],[144.89997158744055,13.327979290563553]],[[144.809541651502,13.549094363148988],[143.9999722250084,13.492128176464083],[143.09997286257644,13.710817738179635],[138.59997605041642,14.801154224791581],[131.39998115096031,16.534196198259725],[125.99998497636834,18.251816319028222],[122.39998752664029,18.891661584303154],[121.09574530038982,19.019148713619977],[120.42074577856589,18.593173653796047],[120.14998912056028,16.965102599435927]],[[119.92498927995224,16.965102599435927],[116.99999135204831,18.251816319028222],[115.3124925474883,19.529070924350908],[114.52499310536027,20.375041253465433],[113.79374362338419,21.635297384859552],[113.94911351331866,22.271493895850078],[113.73749366323221,21.635297384859552],[113.51249382262418,20.796306105108872],[113.39999390232026,18.251816319028222],[111.14999549624025,12.615395567393394],[110.69999581502417,9.967915186974132],[109.34999677137613,7.29876275445952],[107.99999772772827,5.957818681088533],[106.64999868408005,5.061986954416114],[105.74999932164808,4.613591578862773],[103.85068066714335,2.29570245694968]],[[104.40000027800004,2.255504211923801],[104.77306230253555,1.961481175550864],[104.68115007883117,1.468426767331968],[104.28790035741287,1.215621515287768],[104.15439045199251,1.197800481146747],[103.98701057056589,1.389451396800233]],[[109.34999677137613,7.29876275445952],[112.94999422050827,5.659359572411489],[114.29999326475222,5.061986954416114],[114.88563284987971,4.926762452886689]],[[107.07919838003114,10.342138429683002],[107.77499788712005,9.746236973759974],[108.6749972495522,9.52441134501949],[110.24999613380828,9.746236973759974],[110.69999581502417,9.967915186974132]]]}},{"type":"Feature","properties":{"id":"atlantic-crossing-1-ac-1","name":"Atlantic Crossing-1 (AC-1)","color":"#50429a","feature_id":"atlantic-crossing-1-ac-1-0","coordinates":[-31.533730162850908,50.793170984366824],"length_km":13196.980462076366},"geometry":{"type":"MultiLineString","coordinates":[[[-72.91227411232106,40.77352073429003],[-71.09987539624129,40.64389687373837],[-68.39987730894529,41.40772623743595],[-61.19988240948919,42.743713464436695],[-50.39989006030513,45.172673246984274],[-39.59989771112098,48.10677570919628],[-23.399909187344846,53.50209788266426],[-16.199914287888834,55.58970711313177],[-8.999919388432733,59.222223914844314],[-5.399921938704683,59.79305890746809],[-1.799924488976633,59.56588346342965],[1.800072960751236,58.641677771385005],[5.400070410479286,56.469711376547025],[7.650068816559295,55.07780072164767],[8.10006849777537,54.94878902385559]],[[-5.698461727216356,50.07870033214287],[-6.299921301136742,50.167261162927154],[-8.099920026000767,50.167261162927154],[-10.799918113296759,49.87814473780419],[-16.199914287888834,49.000334389463426],[-23.399909187344846,47.80541217589291],[-39.59989771112098,43.72721479104982],[-50.39989006030513,41.0693404382162],[-61.19988240948919,40.04369219283004],[-68.39987730894529,40.215724060833985],[-71.09987539624129,40.04369219283004],[-72.91227411232106,40.77352073429003]],[[8.10006849777537,54.81936191424915],[7.200069135343221,54.55925876578231],[5.400070410479286,53.76891056666807],[4.500071048047319,53.23359531864929],[4.275071207439282,52.827662548128515],[4.38757112774321,52.62326141852725]],[[4.275071207439282,52.48646132010029],[3.600071685615352,52.356869357572975],[3.150072004399278,52.14259270367212],[2.475072482575348,51.586833980054095],[1.46147320061859,50.9810239706491],[0.900073598319269,50.5262123614087],[7.4235887302e-05,50.167261162927154],[-2.249924170192707,50.022920456254944],[-3.599923213840749,49.87814473780419],[-4.499922576272716,49.805593628808026],[-5.399921938704683,49.87814473780419],[-5.698461727216356,50.07870033214287]]]}},{"type":"Feature","properties":{"id":"australia-japan-cable-ajc","name":"Australia-Japan Cable (AJC)","color":"#46bfb2","feature_id":"australia-japan-cable-ajc-0","coordinates":[157.2557199362583,0.48206827081266507],"length_km":11188.696365923937},"geometry":{"type":"MultiLineString","coordinates":[[[136.87399727311598,34.33682825203173],[137.69997668798445,34.12610104005753],[138.59997605041642,34.21917770495358],[139.4999754122525,34.57501887961886],[139.9218501133925,34.76007352296522],[140.06247501377248,34.89859296336222],[140.10823235209364,34.96779987634099],[140.06247501377248,35.01384769751837]],[[151.2450870925013,-33.737123050977864],[151.94493746911763,-33.62464858335051],[154.8695669085838,-31.85146566557725]],[[148.49996903716843,13.710817738179635],[147.14996999352056,13.92930384327183],[146.24997063108842,13.92930384327183],[145.34997126865645,13.710817738179635]],[[144.8081264058416,13.543635828763595],[144.8081264058416,13.543635828763595],[144.8081264058416,13.543635828763595],[145.34997126865645,13.054150695298627],[146.24997063108842,12.615395567393394],[147.14996999352056,12.615395567393394],[148.49996903716843,13.273238157547594]],[[136.87399727311598,34.33682825203173],[137.69997668798445,33.28381101905092],[138.59997605041642,32.62301664000789],[140.84997445649643,31.670513047087127],[143.5499725437925,30.901396088515508]],[[140.16091244403847,34.89859296336222],[140.1749749340766,34.69072647741027],[140.17497493467252,34.405022750715936],[140.28747485497644,33.93964008831966],[142.19997350014447,32.8123187832876],[143.5499725437925,30.901396088515508],[147.59996967473646,23.298598065875897],[148.9499687183845,16.534196198259725],[148.49996903716843,13.710817738179635],[148.49996903716843,13.273238157547594],[151.1999671244645,9.967915186974132],[153.44996553054452,8.190543417795496],[156.1499636178406,4.164912849976942],[157.49996266148847,-0.331409329660265],[158.3999620239206,-3.029995968008661],[159.97496090817668,-4.825692499217419],[161.99995947364866,-6.616650693475355],[162.89995883608063,-8.401139048122838],[163.3499585172967,-10.17745743036107],[162.89995883608063,-13.698987269610743],[160.19996074878455,-18.880139975101173],[158.3999620239206,-25.540896076259312],[157.04996298027257,-28.743810281149894],[154.8695669085838,-31.85146566557725],[151.97926731750607,-33.691323153004866],[151.22848710426095,-33.882038650285516]]]}},{"type":"Feature","properties":{"id":"apcn-2","name":"APCN-2","color":"#29b14a","feature_id":"apcn-2-0","coordinates":[124.31383246990738,23.34960115952398],"length_km":17007.58012350873},"geometry":{"type":"MultiLineString","coordinates":[[[140.75095452664317,36.79060054148884],[141.1874742174084,35.77425344590863],[141.07497429710446,35.408319788563304],[140.96247437620465,35.163436337416215],[140.3437248151284,34.97160644824122],[140.06247501377248,34.96776525378359]],[[139.95485509060742,34.965046603583694],[140.04841252373456,34.84090484813936],[140.23122489422857,34.679162981906806],[140.28747485497644,34.393419492403375],[139.83747517376037,33.927972678693564],[139.72497525345642,32.421443555350706],[139.04997573163232,30.889328974889438],[137.69997668798445,29.91906305661853],[134.99997860068837,29.135966407362506],[128.69998306366443,25.944535413791687],[127.34998402001638,25.33600821718872],[125.99998497636834,25.132479722461383],[125.09998561393637,25.132479722461383],[123.74998657028833,25.43764443864878],[122.84998720785636,25.640659590796915],[121.94998784542422,25.539194978687103]],[[121.94998784542422,25.2342865621001],[122.28748760633636,24.928611492263457],[122.62498736724832,24.007054825363046],[122.17498768603225,22.45644844059945],[121.02775554672928,21.758259099599318],[120.12788799225589,21.758259099599318],[118.79999007691224,22.19628282803044],[118.34999039569617,22.40445416506672],[117.2812411528083,22.87169786996185],[116.67753158048176,23.342095886292725],[116.77499151144026,22.45644844059945],[116.54999167083223,20.783159233732995],[115.64999230840026,18.238460810952724],[114.7499929459683,14.787557926772921],[114.29999326475222,13.040451242220165],[113.39999390232026,9.954064678408844],[111.59999517745614,7.730954611330002],[109.12499693076828,5.943831970446426],[108.44999740894416,5.496074035021858],[107.99999772772827,5.160032981867319],[107.09999836529612,4.599574515521482],[105.74999932164808,3.814203076255083],[104.96249987952004,2.803404866588448],[104.48462521805108,1.454368851373345],[104.28790035741287,1.285767245880394],[104.15918044859939,1.197018152576039],[103.89502063573258,1.295434785911349]],[[103.89502063573258,1.309493642625741],[104.19697042182851,1.315741998126226],[104.28790035741287,1.36999455336686],[104.34425031749407,1.468426767331968],[104.62500011860807,2.367912558705407],[103.95000059678415,3.491423322320592],[103.38789161939158,4.128192842398844],[103.95000059678415,4.0527020972683],[107.99999772772827,7.075530930004602],[108.44999740894416,7.744889052551447],[109.79999645259221,9.967915186974132],[110.69999581502417,12.615395567393394],[113.17499406171221,18.251816319028222],[113.39999390232026,20.796306105108872],[113.68124370308026,21.635297384859552],[113.94911351331866,22.271493895850078]],[[114.24379330456497,21.84429407917369],[115.19999262718419,21.32123529551186],[116.54999167083223,21.111485983488812],[118.79999007691224,21.425997872385402],[121.02775554672928,21.24799675992802],[122.84998720785636,21.425997872385402],[123.74998657028833,22.05298561667754],[124.6499859327203,24.12261698700344],[124.81873581317637,25.55188275942587],[124.98748569363227,26.1593079707739],[124.6499859327203,30.5144959597591],[122.84998720785636,31.670513047087127],[121.94998784542422,31.622627415989587]],[[121.94998784542422,31.81402180002269],[122.84998720785636,32.052708023486204],[124.19998625150441,32.052708023486204],[128.58279314668286,31.15762634730359],[129.1499827448803,30.901396088515508],[130.49998178852834,29.73606949729215],[132.74998019460836,29.540507745394493],[134.99997860068837,29.540507745394493],[137.69997668798445,30.320465424761444],[139.04997573163232,31.09426282763951],[140.39997477528055,32.43331330641721],[142.4249733401566,35.05222991093673],[142.42497334075233,35.78566189952622],[141.97497365894054,36.51238821239364],[140.75095452664317,36.801861372486805]],[[128.58279314668286,31.15762634730359],[128.4749832230562,31.670513047087127]],[[128.69998306366443,32.8123187832876],[129.0374828245764,34.31215165223547],[128.99949285148878,35.17037876180022],[129.26248266518442,34.31215165223547]],[[129.1499827448803,32.8123187832876],[128.92498290427227,31.670513047087127],[129.1499827448803,30.901396088515508]],[[114.7499929459683,14.801154224791581],[116.99999135204831,13.92930384327183],[118.79999007691224,13.710817738179635],[120.14998912056028,13.601498202276586],[121.06600847164388,13.762418337904428]],[[120.14998912056028,13.3827080361257],[118.79999007691224,13.273238157547594],[116.99999135204831,13.054150695298627],[115.64999230840026,12.944533868662969],[114.7499929459683,12.944533868662969],[114.29999326475222,13.054150695298627]]]}},{"type":"Feature","properties":{"id":"aec-1","name":"AEC-1","color":"#923c96","feature_id":"aec-1-0","coordinates":[-40.62232040192537,46.63726305614672],"length_km":5137.793906059852},"geometry":{"type":"LineString","coordinates":[[-9.562418989952736,54.55925876578231],[-10.799918113296759,55.07780072164767],[-12.599916838160784,55.07780072164767],[-16.199914287888834,54.29748595281839],[-23.399909187344846,52.41790126031551],[-39.59989771112098,46.890762878622326],[-50.39989006030513,44.21300917863173],[-61.19988240948919,42.07923561816413],[-68.39987730894529,41.0693404382162],[-71.09987539624129,40.47295490579834],[-72.87218414072115,40.800580995045266]]}},{"type":"Feature","properties":{"id":"flag-atlantic-1-fa-1","name":"FLAG Atlantic-1 (FA-1)","color":"#80479b","feature_id":"flag-atlantic-1-fa-1-0","coordinates":[-37.9308101244673,42.84199037340771],"length_km":11638.297325325524},"geometry":{"type":"MultiLineString","coordinates":[[[-5.674921743892284,50.06393817056209],[-6.299921301136742,50.09514516168246],[-8.099920026000767,49.87814473780419],[-10.799918113296759,49.58728674004685],[-16.199914287888834,48.40638249553803],[-23.399909187344846,47.19740739556967],[-39.59989771112098,43.073310783003215],[-50.39989006030513,40.04369219283004],[-61.19988240948919,39.35121757117122],[-68.39987730894529,39.87122513561614],[-71.6786849862071,41.09701835105607],[-72.1656446412403,41.217596026703156],[-72.36584449941695,41.217596026703156],[-72.82077417714041,41.14638300214584],[-73.19440391245767,41.022498134754876]],[[-73.65597358547731,40.60068600870845],[-71.09987539624129,39.87122513561614],[-68.39987730894529,39.6983233549332],[-61.19988240948919,39.00237890905839],[-50.39989006030513,39.524987333511675],[-39.59989771112098,42.41235450073586],[-23.399909187344846,46.5823550820958],[-16.199914287888834,46.5823550820958],[-5.399921938704683,49.29468421942562],[-4.499922576272716,49.29468421942562],[-3.599923213840749,49.14772788577412],[-2.812423771712709,48.852503408348504]]]}},{"type":"Feature","properties":{"id":"flag-north-asia-loopreach-north-asia-loop","name":"FLAG North Asia Loop/REACH North Asia Loop","color":"#c6b12e","feature_id":"flag-north-asia-loopreach-north-asia-loop-0","coordinates":[139.60237302402345,30.689317362291842],"length_km":8436.327447783384},"geometry":{"type":"MultiLineString","coordinates":[[[121.02775554672928,20.93310564677346],[118.79999007691224,21.111485983488812],[116.54999167083223,20.90143978523765],[115.19999262718419,21.111485983488812],[114.13129338426104,21.84429407917369],[113.93202352542546,22.227650940807052],[114.30004326471693,21.84429407917369],[115.19999262718419,21.42595132790301],[116.54999167083223,21.216397899942],[118.79999007691224,21.635297384859552],[120.12788799225589,21.66680565299917],[121.02775554672928,21.66680565299917],[121.94998784542422,22.469443964829516],[122.39998752664029,24.01990020343248],[122.17498768603225,24.53265756616073],[121.80144795065142,24.863504112487785],[122.17498768603225,24.94136317175375],[122.84998720785636,24.94136317175375],[125.99998497636834,24.32780311165181],[128.69998306366443,24.12261698700344],[132.74998019460836,24.53265756616073],[137.24997700676838,27.76358852605777],[138.82497589102445,29.344566989489813],[139.72497525345642,30.901396088515508],[140.17497493467252,32.43331330641721],[140.6249746158884,33.93964008831966],[140.4562247348366,34.69072647741027],[140.1785441559671,34.96779987634099],[140.09059999384857,35.01384769751837]],[[140.0765375038106,35.01384769751837],[140.16448179519193,34.96779987634099],[140.42809975476052,34.69072647741027],[140.51247469558447,33.93964008831966],[139.94997509406446,32.43331330641721],[139.04997573163232,31.478822672736147],[137.69997668798445,30.708139993541643],[134.99997860068837,29.93125070442692],[132.74998019460836,29.93125070442692],[130.49998178852834,30.126049846722832],[129.5999824260964,30.901396088515508],[129.37498258548837,31.670513047087127],[129.37498258548837,32.8123187832876],[129.4874825057923,34.31215165223547]],[[128.99949285148878,35.17037876180022],[128.81248298396835,34.31215165223547],[128.24998338244836,32.8123187832876],[128.0249835418403,31.670513047087127],[127.34998402001638,29.540507745394493],[127.12498417940834,28.55704546571133],[125.99998497636834,26.1593079707739],[125.66248521545637,25.55188275942587],[125.32498545454442,24.12261698700344],[124.42498609211226,22.05298561667754],[122.84998720785636,21.111485983488812],[121.02775554672928,20.93310564677346]]]}},{"type":"Feature","properties":{"id":"glo-1","name":"Glo-1","color":"#3665b0","feature_id":"glo-1-0","coordinates":[-18.744837688538002,20.289425974077716],"length_km":8524.081937302957},"geometry":{"type":"MultiLineString","coordinates":[[[-0.204315619320974,5.558285889905858],[-0.449925445328682,3.279837005484997],[-0.449925445328682,2.156121468705662]],[[-4.544402544762735,50.82820142743812],[-6.074881460557064,50.38295743994838],[-7.649920344784692,49.73293362369082],[-9.899918750864702,48.70423463096067],[-14.399915563024809,44.05151922873524],[-14.399915563024809,39.6983233549332],[-13.499916200592752,35.78566189952622],[-12.82491667876882,33.189714664600466],[-12.599916838160784,30.126049846722832],[-13.24783202918155,28.161052262220792],[-14.624915403632755,26.964304734562898],[-17.549913331536874,22.884654113882444],[-18.899912375184826,19.95262290516439],[-18.899912375184826,14.801154224791581],[-18.899912375184826,11.735650161405832],[-17.549913331536874,8.635699417327467],[-13.049916519376767,4.164912849976942],[-10.799918113296759,1.918228780215599],[-3.599923213840749,1.918228780215599],[-0.449925445328682,2.156121468705662],[1.575073120143199,3.266814816815666],[2.25007264196731,3.379125568249918],[2.700072323183203,4.164912849976942],[3.423511810692114,6.439066911484626]]]}},{"type":"Feature","properties":{"id":"globenet","name":"GlobeNet","color":"#7b489c","feature_id":"globenet-0","coordinates":[-39.35235425663465,2.18873777337913],"length_km":22158.927275212536},"geometry":{"type":"MultiLineString","coordinates":[[[-80.08893155227202,26.350584577319996],[-79.64986933934534,26.813799487940788],[-77.84987061448132,28.95155473219332],[-76.49987157083336,30.901396088515508],[-75.14987252718532,33.565491482352044],[-74.47487300595729,35.54192681258013],[-73.91237340443729,39.00237890905839],[-74.33781310245581,39.60388206573738],[-73.3498738023213,39.35121757117122],[-71.09987539624129,39.00237890905839],[-68.31654403464633,37.589786573603064],[-65.69987922164921,34.683017659857974],[-64.5748800186092,33.56549148238552],[-64.12488033739322,32.8123187832876],[-64.23738025769724,32.52821504536491]],[[-64.7674091796907,32.31223462116531],[-64.34988017800117,31.286738814391754],[-57.59988495976114,22.05298561667754],[-52.19988878516916,15.669513225155248],[-46.79989261057708,10.41081650540272],[-38.69989834868901,1.468426767331968],[-38.02489882686499,-1.231315750217412],[-38.542968459859594,-3.718735129291092],[-35.99990026139302,-3.479268678970064],[-34.199901536529,-4.152767748013638],[-32.174902964837905,-5.4979506821245],[-31.274903608624967,-9.29042430103552],[-32.84990249288096,-13.698987269610743],[-34.64990121774498,-18.026426383713453],[-38.02489882686499,-22.873434953546333],[-40.94989675476902,-23.905969261790265],[-42.29989579841706,-23.700175468198225],[-43.20956515399876,-22.903486555497956],[-42.29989579841706,-23.59715656726005],[-40.94989675476902,-23.70010845220312],[-38.36239858777696,-22.56211951183571],[-35.549900580176946,-18.026426383713453],[-33.74990185531301,-13.698987269610743],[-31.949903130448895,-9.29042430103552],[-32.51240272885988,-5.83380111633244],[-34.199901536529,-4.489307688629284],[-35.99990026139302,-3.70382647066824],[-38.542968459859594,-3.718735129291092]],[[-80.08893155227202,26.350584577319996],[-79.64986933934534,26.663094151095223],[-77.84987061448132,27.364667993860262],[-76.94987125204935,27.164665812813517],[-73.3498738023213,24.53265756616073],[-69.29987667137726,22.469443964829516],[-67.94987762772922,20.58581909604039],[-67.83737770742529,18.678647022154717],[-68.17487746833726,17.82393441253792],[-68.39987730894529,15.23578178303578],[-67.49987794651324,12.615395567393394],[-67.27487810590529,11.294709319565477],[-66.96042832866438,10.599588212552636],[-66.71237850438528,10.85308969074528],[-65.69987922164921,11.405009147532946],[-63.89988049678528,11.735650161405832],[-62.99988113435322,11.735650161405832],[-61.19988240948919,11.735650161405832],[-59.399883684625166,11.294709319565477],[-54.89988687246515,9.08033076823294],[-51.2998894227371,6.852191098754328],[-46.79989261057708,5.061986954416114],[-40.94989675476902,1.468426767331968],[-39.149898029904996,-1.231315750217412],[-38.542968459859594,-3.718735129291092]],[[-68.39987730894529,15.23578178303578],[-70.19987603380923,14.801154224791581],[-71.54987507745727,14.147583506948735],[-74.24987316475335,12.834868817846521],[-74.92487268657729,11.735650161405832]]]}},{"type":"Feature","properties":{"id":"hawaiki","name":"Hawaiki","color":"#3851a3","feature_id":"hawaiki-0","coordinates":[-169.40629188424063,-1.1960056555101188],"length_km":14436.487381064371},"geometry":{"type":"MultiLineString","coordinates":[[[179.9999467222889,-23.905969261790265],[173.69995118526478,-28.150316035845893],[170.9999530979687,-29.137613161609917],[166.94995596702458,-32.61276000573574],[165.5999569233767,-34.11602012163193]],[[-173.6998027134901,-11.943944931746815],[-172.07822386223177,-12.894213639363048],[-170.999804626194,-13.698987269610743],[-170.69570484162125,-14.276544564158804]],[[-158.05689434939785,21.335422205733376],[-157.8373139506259,21.134806167482292],[-157.61231411001796,21.134806167482292],[-157.49981418971396,21.169779563880702],[-157.38731427000602,21.356164482330126],[-152.99981737755394,25.348717422116714],[-147.6030982105313,30.516425505901374],[-138.60310458621126,35.05406343239751],[-128.24983491067374,42.743713464436695],[-125.09983714216159,45.172673246984274],[-124.19983777972962,45.80361417369449]],[[-158.05689434939785,21.335422205733376],[-158.2873136318419,20.796306105108872],[-159.07481307396995,18.678647022154717],[-161.99981100187398,13.054150695298627],[-167.39980717646606,2.367912558705407],[-171.4498043074101,-4.825692499217419],[-172.79980335105813,-10.17745743036107],[-173.6998027134901,-11.943944931746815],[-176.39980080078607,-18.45381377577717],[-179.99979825051412,-23.905969261790265]],[[174.59995054769675,-35.59302880961419],[174.14995086648068,-34.85783936223576],[172.7999518228328,-34.11602012163193],[165.5999569233767,-34.11602012163193],[159.29996138635258,-35.042260722865336],[155.69996393662453,-34.85783936223576],[152.09996648689665,-34.30209296887181],[151.20699711948467,-33.86955173177822]]]}},{"type":"Feature","properties":{"id":"exa-north-and-south","name":"EXA North and South","color":"#4cb96a","feature_id":"exa-north-and-south-0","coordinates":[-5.766007052696966,55.067817143282014],"length_km":10075.947541619751},"geometry":{"type":"MultiLineString","coordinates":[[[-70.95027550281526,42.46364601310954],[-69.29987667137726,42.578254086072846],[-65.69987922164921,42.743713464436695],[-63.89988049678528,44.05151922873524]],[[-63.572490728711166,44.65322870491472],[-61.19988240948919,44.53466416326733],[-50.39989006030513,46.73677946695437],[-39.59989771112098,49.87814473780419],[-23.399909187344846,54.03403825672422],[-16.199914287888834,54.81936191424915],[-12.599916838160784,55.33458061322904],[-8.999919388432733,55.58970711313177],[-6.676191034583779,55.415759313161054],[-6.074921460528704,55.33458061322904],[-4.9499222574887,54.36308597431902],[-4.9499222574887,54.10005748241058],[-4.837372337220154,53.967914030873956],[-3.599923213840749,53.70236555668246],[-3.006373634316763,53.647933398832954],[-3.599923213840749,53.56895929103051],[-5.624921779312721,53.368058136502164]],[[-5.624921779312721,53.23359531864929],[-5.399921938704683,52.69150159464696],[-5.624921779312721,52.14259270367212],[-7.199920663568708,51.44682015166956],[-10.799918113296759,50.740281893948165],[-16.199914287888834,50.740281893948165],[-23.399909187344846,51.30637567738274],[-39.59989771112098,49.29468421942562],[-50.39989006030513,46.11643477220242],[-61.19988240948919,44.37405751055857],[-63.572490728711166,44.65322870491472]]]}},{"type":"Feature","properties":{"id":"mainone","name":"MainOne","color":"#603f98","feature_id":"mainone-0","coordinates":[-19.280078359401543,10.541976821281889],"length_km":8288.725452035173},"geometry":{"type":"MultiLineString","coordinates":[[[-3.599923213840749,0.118588418888312],[-3.924922983607823,1.468426767331968],[-4.049922895056732,2.367912558705407],[-4.049922895056732,3.266814816815666],[-4.026242911831877,5.323508791824841]],[[-19.803965545293075,15.23578178303578],[-18.449912693968844,15.23578178303578],[-17.445713405352947,14.686594841994992]],[[-0.204315619320974,5.558285889905858],[7.4235887302e-05,3.279837005484997],[7.4235887302e-05,0.806604849908682]],[[-9.449919069648717,38.21117903702318],[-9.787418830560773,37.23235432155614],[-10.124918591472738,35.78566189952622],[-10.574918272688812,33.93964008831966],[-13.949915881808826,29.73606949729215],[-14.399915563024809,28.95155473219332],[-14.624915403632755,28.161052262220792],[-15.074915084848831,26.964304734562898],[-18.449912693968844,22.884654113882444],[-19.799911737616885,19.95262290516439],[-19.804120576691208,15.23578178303578],[-19.799911737616885,11.735650161405832],[-18.449912693968844,8.635699417327467],[-13.949915881808826,3.266814816815666],[-10.799918113296759,0.118588418888312],[-3.599923213840749,0.118588418888312],[7.4235887302e-05,0.806604849908682],[1.575073120143199,1.918228780215599],[2.25007264196731,2.480311786858737],[3.150072004399278,4.164912849976942],[3.423511810692114,6.439066911484626]]]}},{"type":"Feature","properties":{"id":"mid-atlantic-crossing-mac","name":"Mid-Atlantic Crossing (MAC)","color":"#5bba46","feature_id":"mid-atlantic-crossing-mac-0","coordinates":[-65.34523376495947,19.101212129833563],"length_km":6730.032235685565},"geometry":{"type":"LineString","coordinates":[[-74.24987316475335,33.565491482352044],[-75.59987220840131,30.901396088515508],[-77.39987093326533,28.95155473219332],[-79.19986965812936,26.964304734562898],[-79.64986933934534,26.36108632539156],[-80.16016897784432,26.010548668010795],[-79.64986933934534,26.260240971577822],[-78.97486981752132,26.964304734562898],[-77.84987061448132,27.663994423747],[-76.94987125204935,27.76358852605777],[-73.3498738023213,26.964304734562898],[-69.29987667137726,24.94136317175375],[-67.04987826529725,22.05298561667754],[-65.92487906225725,20.375041253465433],[-65.24987954043323,18.891661584303154],[-65.13737962012921,18.251816319028222],[-64.81925984548825,17.773909269375704],[-64.5748800186092,18.091482652425203],[-64.23738025769724,18.358623372153332],[-64.23738025769724,18.891661584303154],[-64.59988000089899,20.913116766319394],[-65.24987954043323,24.94136317175375],[-66.59987858408127,28.161052262220792],[-72.44987443988924,38.651811712711336],[-72.91227411232106,40.77352073429003],[-72.67487428049728,38.651811712711336],[-74.24987316475335,33.565491482352044]]}},{"type":"Feature","properties":{"id":"pacific-caribbean-cable-system-pccs","name":"Pacific Caribbean Cable System (PCCS)","color":"#44b97a","feature_id":"pacific-caribbean-cable-system-pccs-0","coordinates":[-64.74486945319327,20.485860645421145],"length_km":6264.827431011018},"geometry":{"type":"MultiLineString","coordinates":[[[-66.10666893347558,18.46610423294742],[-65.69987922164921,18.572039052566783],[-64.83573983381369,18.521952917911317]],[[-76.0498718896173,10.85308969074528],[-75.50573227509088,10.38680745163333]],[[-70.04992614003515,12.616917900441425],[-69.74987635259325,13.92930384327183]],[[-70.04992614003515,12.616917900441425],[-69.29987667137726,12.615395567393394],[-68.9623769104653,12.395734000022975]],[[-80.5498687017773,30.320465424761444],[-79.19986965812936,30.126049846722832],[-73.3498738023213,29.540507745394493],[-69.29987667137726,27.564309487941923],[-66.37487874347323,23.7112581424843],[-64.79987985921724,20.796306105108872],[-64.46238009830519,18.891661584303154],[-64.59715000283296,18.41441211576626],[-64.34988017800117,18.305228078976267],[-64.23738025769724,18.251816319028222],[-64.23738025769724,17.82393441253792],[-66.14987890286528,16.10232559580297],[-69.74987635259325,13.92930384327183],[-71.54987507745727,13.92930384327183],[-74.24987316475335,12.615395567393394],[-76.0498718896173,10.85308969074528],[-78.29987029569729,10.85308969074528],[-79.31236957843338,9.967915186974132]],[[-79.56661939832034,8.950317108800572],[-79.31236957843338,8.190543417795496],[-78.74986997691337,7.29876275445952],[-78.74986997691337,5.061986954416114],[-80.99986838299328,2.367912558705407],[-81.1215619945354,0.553867752345412],[-80.88736846268927,-0.331409329660265],[-80.71613109211354,-0.949727245539768]]]}},{"type":"Feature","properties":{"id":"pacific-crossing-1-pc-1","name":"Pacific Crossing-1 (PC-1)","color":"#05a5d7","feature_id":"pacific-crossing-1-pc-1-0","coordinates":[-126.73328748926824,39.94054027582586],"length_km":19577.446208428762},"geometry":{"type":"MultiLineString","coordinates":[[[-179.99979825051412,49.000334389463426],[-151.1998186526899,49.000334389463426],[-138.59982757864174,48.40638249553803],[-125.09983714216186,48.40638249553803],[-124.6498374609456,48.48100994210292],[-123.74983809851364,48.256798568947445],[-122.84983873608158,48.21933401420262],[-122.66605031524853,48.11006090495571],[-122.63793033587935,48.034889597039694],[-122.58168037714837,47.959608461901695],[-122.48324649637513,47.9219300503356],[-122.44105402626471,47.90307887010544]],[[-122.44105402626471,47.89364735216821],[-122.49731043904814,47.9219300503356],[-122.59574141668266,47.959608461901695],[-122.65198637683804,48.034889597039694],[-122.6801063569178,48.11006090495571],[-122.84983873608158,48.20059144803915],[-123.74983809851364,48.21933401420262],[-124.6498374609456,48.40638249553803],[-125.09983714216186,48.10677570919628],[-127.34983554824169,42.743713464436695],[-126.44983618580963,38.651811712711336],[-122.84983873608158,35.602930322906126],[-120.62152181466479,35.12094936772415],[-122.84983873608158,35.419780517080355],[-129.5998339543217,38.651811712711336],[-138.5998275786419,42.743713464436695],[-151.19983391146832,45.32832917028219],[-179.99981350929238,45.32832917028219]],[[179.99992719256971,45.3310537658534],[172.79992073307184,45.3310537658534],[160.19996074878455,42.07923561816413],[143.09997286257644,34.12610104005753],[138.59997605041642,33.001218522654476],[137.69997668798445,33.47169957086474],[136.87399727311598,34.33682825203173],[137.69997668798445,33.75276987113061],[138.59997605041642,33.565491482352044],[140.84997445649643,34.31215165223547],[141.52497397832056,35.05222991093673],[141.52497397832056,35.78566189952622],[140.6124746247436,36.383483735312474],[142.19997350014447,36.1498667868178],[149.39996839960057,39.6983233549332],[160.19996074878455,45.96024524125342],[172.7999518228328,49.000334389463426],[179.99995828233068,49.000334389463426]]]}},{"type":"Feature","properties":{"id":"pan-american-crossing-pac","name":"Pan-American Crossing (PAC)","color":"#36a9b7","feature_id":"pan-american-crossing-pac-0","coordinates":[-102.8012887970355,16.23046473604046],"length_km":6652.755623349848},"geometry":{"type":"MultiLineString","coordinates":[[[-85.04986551393732,7.744889052551447],[-84.59986583272143,8.635699417327467],[-84.4522083510602,9.525851215144279]],[[-118.79984160513764,31.286738814391754],[-117.44984256148977,32.43331330641721]],[[-106.42187223254336,23.199717218127276],[-107.54984957473768,22.05298561667754],[-107.99984925595349,20.375041253465433]],[[-79.54654941253821,8.934106573765973],[-79.76236925964936,8.190543417795496],[-79.64986933934534,7.29876275445952],[-80.09986902056141,6.852191098754328],[-80.99986838299347,6.852191098754328],[-85.04986551393732,7.744889052551447],[-87.29986392001733,9.52441134501949],[-98.09985626920157,14.365653759228442],[-102.5998530813615,16.10232559580297],[-106.64985021230544,18.678647022154717],[-107.99984925595349,20.375041253465433],[-111.59984670568154,22.469443964829516],[-116.09984351784173,27.76358852605777],[-118.79984160513764,31.286738814391754],[-120.59984033000175,32.8123187832876],[-121.04984001121782,34.31215165223547],[-120.62152181466479,35.12094936772415]]]}},{"type":"Feature","properties":{"id":"pipe-pacific-cable-1-ppc-1","name":"PIPE Pacific Cable-1 (PPC-1)","color":"#8ac53f","feature_id":"pipe-pacific-cable-1-ppc-1-0","coordinates":[153.65779191562947,-9.050310915716352],"length_km":6094.378379172447},"geometry":{"type":"MultiLineString","coordinates":[[[145.7847409606618,-5.23368424614946],[146.02497079048055,-5.161910662113067],[146.6749703300147,-5.137011581064615],[146.90776526532372,-4.838945214956163]],[[151.20699711948467,-33.86955173177822],[152.09996648689665,-33.27363077142247],[154.02056155078103,-31.85146566557725],[154.79996457419256,-28.743810281149894],[154.79996457419256,-25.540896076259312],[154.3499648929765,-18.880139975101173],[153.8999652117606,-15.441023659568087],[154.79996457419256,-11.943944931746815],[154.79996457419256,-10.17745743036107],[152.99996584932845,-8.401139048122838],[149.39996839960057,-7.063446338991064],[148.04996935595253,-6.225371753845629],[147.59996967473646,-5.777839699209677],[147.31871987397665,-5.273944363641298],[146.90776526532372,-4.838945214956163],[145.89997087903166,-2.3307666592315],[146.65530736297208,1.519276295401345],[146.24997063108842,7.744889052551447],[145.34997126865645,12.175887185507976],[145.1249714280484,12.615395567393394],[144.78747166713646,13.273238157547594]]]}},{"type":"Feature","properties":{"id":"safe","name":"SAFE","color":"#a9499b","feature_id":"safe-0","coordinates":[76.9722034007659,-2.8525966839583687],"length_km":13017.303423608786},"geometry":{"type":"MultiLineString","coordinates":[[[31.757961738301827,-28.950559666538012],[32.62505112404761,-29.52991296614913],[33.30005064587154,-30.30995334464681]],[[76.2695502058749,9.93838642489319],[76.05002036139189,9.52441134501949],[76.50002004260797,4.164912849976942],[77.40001940503993,0.568578852526193]],[[54.45003566302377,-20.574419057276128],[54.90003534423966,-20.784921868991052],[55.27923507561119,-21.000051587769306]],[[55.35003502545574,-20.784921868991052],[55.80003470667163,-20.574419057276128],[56.70003406910378,-20.784921868991052],[57.15003375031967,-20.995131543025785],[57.825033272143784,-20.995131543025785],[58.275032953359855,-20.574419057276128],[60.30003151883183,-18.880139975101173],[63.00002960612773,-14.571726491332546],[65.70002769342382,-11.943944931746815],[72.90002259287992,-8.401139048122838],[75.15002099895992,-6.616650693475355],[76.95001972382386,-3.029995968008661],[77.40001940503993,0.568578852526193],[81.00001685476798,1.468426767331968],[85.500013666928,-1.68111462680301],[90.00001047908802,-0.781332308789108],[92.70000856638391,3.715978119298069],[94.27500745064,4.713260444333049],[95.45328225426326,5.818423043687448],[97.42500521915215,5.733989114150127],[99.67500362523216,5.398081130463647]],[[18.445861168718828,-33.72721819637743],[18.00006148452737,-34.11602012163193],[17.775061643919337,-34.85783936223576],[18.450061165743445,-36.3215277599179],[19.80006020939149,-37.401610748143824],[23.40005765911936,-37.401610748143824],[26.10005574641544,-36.3215277599179],[33.30005064587154,-30.30995334464681],[38.70004682046353,-28.743810281149894],[41.40004490775961,-28.743810281149894],[45.450041666655096,-28.743810607354384],[47.25004076356767,-27.95174728521976],[52.65003693815965,-23.08058350574764],[54.00003598180769,-20.995131543025785],[54.45003566302377,-20.574419057276128],[54.90003534423966,-20.36362554441106],[55.80003470667163,-20.36362554441106],[56.70003406910378,-20.574419057276128],[57.15003375031967,-20.574419057276128]],[[57.60003343153574,-20.679706953509093],[57.825033272143784,-20.995131543025785]]]}},{"type":"Feature","properties":{"id":"sat-3wasc","name":"SAT-3/WASC","color":"#04a6c7","feature_id":"sat-3wasc-0","coordinates":[-19.078983202670177,11.113526682380897],"length_km":14779.185020794564},"geometry":{"type":"MultiLineString","coordinates":[[[-10.349918432080775,33.93964008831966],[-8.54991970721675,35.419780517080355],[-7.199920663568708,36.33133835588799],[-6.42888120978034,36.73129423644183]],[[7.650068816559295,0.34358628488916],[8.550068178991262,0.34358628488916],[9.454267538448212,0.394465191855477]],[[2.25007264196731,2.929808880350098],[2.025072801359273,3.82823430332105],[2.025072801359273,5.061986954416114],[2.440112507341202,6.356673335458259]],[[-14.849915244240792,26.964304734562898],[-15.862464526941396,27.807911809695806],[-15.862464526941396,27.94275199342137]],[[12.600065309935387,-9.014619375760487],[11.700065947503239,-9.512401827330285],[9.675067382031267,-10.620064860363238]],[[9.706417359822684,4.047345511205705],[9.225067700815375,3.266814816815666],[8.10006849777537,2.817450442654169],[6.300069772911254,2.592701464601932]],[[-0.204315619320974,5.558285889905858],[-0.224925604720645,3.279837005484997],[-0.224925604720645,1.481465914707545]],[[-4.026242911831877,5.323508791824841],[-3.937422974752714,3.266814816815666],[-3.824923054448695,2.367912558705407],[-3.699923142999785,1.468426767331968],[-3.599923213840749,1.018534216615524]],[[-17.445713405352947,14.686594841994992],[-18.449912693968844,15.018578573757472],[-19.34991205640081,15.018578573757472]],[[2.92507216379124,4.164912849976942],[5.400070410479286,2.817450442654169],[6.300069772911254,2.592701464601932],[7.425068975951258,1.468426767331968],[7.650068816559295,0.34358628488916],[8.10006849777537,-4.825692499217419],[8.550068178991262,-6.616650693475355],[9.675067382031267,-10.620064860363238],[9.450067541423229,-18.026426383713453],[10.80006658507127,-23.49392244589784],[14.400064034799321,-30.30995334464681],[15.9750629190554,-32.61276000573574],[17.55006180331148,-33.64904537742418],[18.445861168718828,-33.72721819637743]],[[-9.102749315587026,38.4430794831419],[-9.22491922904077,38.122730108392204],[-9.562418989952736,37.23235432155614],[-9.899918750864702,35.78566189952622],[-10.349918432080775,33.93964008831966],[-12.82491667876882,30.126049846722832],[-13.472831869789587,28.161052262220792],[-14.849915244240792,26.964304734562898],[-17.99991301275286,22.884654113882444],[-19.34991205640081,19.95262290516439],[-19.34991205640081,15.018578573757472],[-19.34991205640081,11.735650161405832],[-17.99991301275286,8.635699417327467],[-13.499916200592752,3.715978119298069],[-10.799918113296759,1.018534216615524],[-3.599923213840749,1.018534216615524],[-0.224925604720645,1.481465914707545],[1.575073120143199,2.592701464601932],[2.25007264196731,2.929808880350098],[2.92507216379124,4.164912849976942],[3.423511810692114,6.439066911484626]]]}},{"type":"Feature","properties":{"id":"seabras-1","name":"Seabras-1","color":"#a9812d","feature_id":"seabras-1-0","coordinates":[-39.41007171898076,7.4926203176150326],"length_km":10410.974008860678},"geometry":{"type":"MultiLineString","coordinates":[[[-65.55639941646365,32.63736524178349],[-65.68543019646702,32.41623974317996],[-65.4999949252911,31.977134139206512],[-65.09715278445925,31.895737453957153],[-64.93362657765282,31.938695789532012],[-64.63356460651755,32.158659496218654]],[[-46.4124928850147,-24.00886839636483],[-44.54989420449707,-25.360305087215615],[-41.399896435985006,-25.540896076259312],[-36.67489978321695,-24.111502734257467],[-31.04972912191171,-18.025284192896695],[-29.249730397047685,-13.697820288632505],[-28.574905521329885,-9.29042430103552],[-30.824903908752983,-4.152767729405749],[-34.199901536529,0.568578852526193],[-39.59989771112098,7.744889052551447],[-48.75732182969582,17.469778422735878],[-61.80549550139919,29.53533695395528],[-65.04599822690268,31.435859958063894],[-65.52234028551544,32.609814615316346],[-68.05679741739422,34.636546883011455],[-73.13341895565995,39.63896955471727],[-74.06286329723282,40.15283384719588]]]}},{"type":"Feature","properties":{"id":"south-america-1-sam-1","name":"South America-1 (SAm-1)","color":"#35ad49","feature_id":"south-america-1-sam-1-0","coordinates":[-44.93080040956221,6.775101791950589],"length_km":22854.46276113669},"geometry":{"type":"MultiLineString","coordinates":[[[-46.328062944825646,-23.961842897597087],[-44.54989420449707,-24.81691653965546],[-42.74989547963305,-24.72611802920699],[-40.94989675476902,-23.287413403488653],[-39.03739810960098,-21.937383135692397],[-37.34989930504097,-18.026426383713453],[-36.44989994260901,-13.698987269610743],[-33.74990185531301,-8.401139048122838],[-33.74990185531301,-6.616650693475355],[-34.64990121774498,-4.825692499217419],[-35.99990026139302,-4.152767748013638],[-38.542968459859594,-3.718735129291092],[-38.474898508080976,-1.231315750217412],[-39.59989771112098,1.468426767331968],[-46.79989261057708,8.635699417327467],[-52.19988878516916,13.92930384327183],[-57.59988495976114,16.534196198259725],[-59.399883684625166,17.82393441253792],[-62.09988177192116,19.104405475930452],[-65.24987954043323,18.998067525948983],[-65.69987922164921,18.785187974742005],[-65.98107902244469,18.678647022154717],[-66.10666893347558,18.46610423294742],[-66.37487874347323,19.104405475930452],[-66.37487874347323,20.375041253465433],[-67.49987794651324,22.05298561667754],[-69.29987667137726,23.298598065875897],[-73.3498738023213,25.348717422116714],[-76.94987125204935,27.364667993860262],[-77.84987061448132,27.464533937820843],[-79.64986933934534,26.713351447732887],[-80.08893155227202,26.350584577319996]],[[-46.328062944825646,-23.961842897597087],[-45.44989356692904,-25.134186547061336],[-43.64989484206502,-27.95174728521976],[-45.89989324814503,-32.61276000573574],[-50.84988974152112,-35.22626671976625],[-53.99988751003309,-36.3215277599179],[-56.695445600474535,-36.47095527632105]],[[-44.54989420449707,-24.81691653965546],[-43.42489500145707,-23.905969261790265],[-43.20956515399876,-22.903486555497956]],[[-80.91442094663405,-2.272903429731061],[-81.44986806420927,-2.580536704984131],[-84.14986615150535,-3.029995968008661]],[[-81.05378084438966,-4.153834937264977],[-81.44986806420927,-3.479268678970064],[-81.44986806420927,-2.580536704984131]],[[-66.10666893347558,18.46610423294742],[-66.48737866377725,18.678647022154717],[-67.04987826529725,18.785187974742005],[-67.49987794651324,18.465364393137126],[-67.72487778712127,17.82393441253792],[-70.19987603380923,14.5835116451186],[-71.54987507745727,13.710817738179635],[-74.14709323756358,12.416907726166608],[-74.81237276627336,11.735650161405832]],[[-38.50448848711925,-12.96997203534297],[-37.34989930504097,-13.698987269610743],[-36.44989994260901,-13.698987269610743]],[[-80.08893155227202,26.350584577319996],[-79.87486917995338,25.348717422116714],[-80.32486886116926,24.73717827217609],[-80.99986838299347,24.12261698700344],[-83.2498667890733,24.12261698700344],[-84.82486567332928,23.09178547692239],[-85.72486503576134,21.635297384859552],[-86.17486471697732,20.375041253465433],[-86.96236415910536,18.251816319028222],[-87.86236352153733,16.534196198259725],[-88.31236320275332,16.10232559580297]],[[-71.62043502747198,-33.04554123247811],[-71.99987475867334,-31.85146566557725],[-73.79987348353728,-27.15383128539156],[-72.89987412110531,-19.517593878237122],[-70.30675595809456,-18.473543073651214],[-72.89987412110531,-19.092898581968107],[-75.59987220840131,-18.45381377577717],[-77.84987061448132,-15.441023659568087],[-78.07487045508935,-13.698987269610743],[-77.84987061449132,-12.82299562562977],[-76.87428130559793,-12.278420041799619],[-79.19986965812936,-12.383840433185572],[-81.89986774542525,-11.503333845984299],[-84.14986615150535,-6.616650693475355],[-84.14986615150535,-3.029995968008661],[-86.39986455758554,0.568578852526193],[-90.4498616885295,3.715978119298069],[-91.79986073217735,9.52441134501949],[-91.34986105096155,13.054150695298627],[-90.8222236775613,13.934797333208856]],[[-67.49987794651324,18.465364393137126],[-68.06237754803324,18.678647022154717]]]}},{"type":"Feature","properties":{"id":"south-american-crossing-sac","name":"South American Crossing (SAC)","color":"#276fac","feature_id":"south-american-crossing-sac-0","coordinates":[-36.76679294468002,-3.394605574712276],"length_km":18359.21833708904},"geometry":{"type":"MultiLineString","coordinates":[[[-78.29987029569729,4.164912849976942],[-79.64986933934534,5.061986954416114]],[[-56.695445600474535,-36.47095527632105],[-53.99988751003309,-36.140033391295425],[-50.84988974152112,-34.85783936223576],[-46.79989261057708,-32.61276000573574],[-44.54989420449707,-27.95174728521976],[-45.67489340753708,-25.134186547061336],[-46.328062944825646,-23.961842897597087],[-44.99989388571306,-24.316706749469176],[-43.64989484206502,-23.70010845220312],[-43.20956515399876,-22.903486555497956],[-42.29989579841706,-23.803113122349238],[-40.94989675476902,-24.111502734257467],[-37.68739906595303,-23.18403842445051],[-33.74990185531301,-18.026426383713453],[-31.949903130448895,-13.698987269610743],[-30.599904086800947,-9.29042430103552],[-31.837403200816922,-5.161910652822947],[-34.199901536529,-3.816084221750208],[-35.99990026139302,-3.254657364797681],[-38.542968459859594,-3.718735129291092],[-38.69989834868901,-1.231315750217412],[-40.049897392336966,1.468426767331968],[-46.79989261057708,6.852191098754328],[-51.2998894227371,8.635699417327467],[-54.89988687246515,10.85308969074528],[-57.149885278545156,13.054150695298627],[-60.29988304705723,16.534196198259725],[-61.64988209070518,17.395022634700517],[-63.4498808155692,17.82393441253792],[-64.12488033739322,18.038005439608753],[-64.5748800186092,18.038005439608753],[-64.81925984548825,17.773909269375704],[-64.99142972352155,17.83127418857191],[-66.14987890286528,16.965102599435927],[-68.39987730894529,15.886035719079029],[-70.19987603380923,15.23578178303578],[-71.54987507745727,14.5835116451186],[-74.24987316475335,13.273238157547594],[-78.29987029569729,11.515266158038768],[-79.87486917995338,9.967915186974132],[-79.90025916196684,9.353803196949398]],[[-67.0339082766105,10.608129806992471],[-67.38737802620922,11.294709319565477],[-67.72487778712127,12.615395567393394],[-68.84987699016128,13.92930384327183],[-70.19987603380923,15.23578178303578]],[[-71.62043502747198,-33.04554123247811],[-72.44987443988924,-31.85146566557725],[-74.24987316475335,-27.15383128539156],[-78.29987029569729,-15.441023659568087],[-78.29987029569729,-13.698987269610743],[-78.07487045508935,-12.82299562562977],[-76.87428130559793,-12.278420041799619],[-79.19986965812936,-12.163983680780412],[-80.99986838299328,-11.503333845984299],[-83.2498667890733,-6.616650693475355],[-83.2498667890733,-3.029995968008661],[-82.79986710785731,0.568578852526193],[-81.89986774542544,2.367912558705407],[-79.64986933934534,5.061986954416114],[-79.19986965812936,7.29876275445952],[-79.53736941904133,8.190543417795496],[-79.54654941253821,8.934106573765973]]]}},{"type":"Feature","properties":{"id":"south-atlantic-cable-system-sacs","name":"South Atlantic Cable System (SACS)","color":"#28ab4a","feature_id":"south-atlantic-cable-system-sacs-0","coordinates":[-13.018183576553122,-7.457062047705449],"length_km":5821.143578444543},"geometry":{"type":"LineString","coordinates":[[-38.542968459859594,-3.718735129291092],[-35.99990026139302,-2.805287932307917],[-34.199901536529,-2.580536704984131],[-29.812404616687857,-3.142332683643806],[-19.799911737616885,-6.616650693475355],[-5.399921938704683,-8.401139048122838],[7.200069135343221,-9.068306003874412],[11.700065947503239,-9.068306003874412],[12.600065309935387,-9.198513131358409],[13.201440382068753,-9.49008098880628]]}},{"type":"Feature","properties":{"id":"southern-cross-cable-network-sccn","name":"Southern Cross Cable Network (SCCN)","color":"#2f85a6","feature_id":"southern-cross-cable-network-sccn-0","coordinates":[-161.285002465349,9.832870419957944],"length_km":26568.71076142879},"geometry":{"type":"MultiLineString","coordinates":[[[-155.8220359890499,20.0232931385683],[-156.14981514606572,20.269544035929588],[-156.89688520046363,20.45695112375976],[-157.34732486998877,20.5096446349069],[-157.85851393560765,20.89386327374593],[-158.13057429534098,21.35406896575391]],[[-124.19983777972962,45.862402571602836],[-125.09983714216159,45.331071073324864],[-128.69983459188964,42.743713464436695],[-138.60310458621126,35.78747880468752],[-147.6030982105313,31.288652857283093],[-152.99981737755394,26.562513149236715],[-157.94981387092994,22.90768438285807],[-158.51231347244993,22.05298561667754],[-158.51231237260097,21.73983373091106],[-158.51231346392387,21.635297384859552],[-158.13057429534098,21.35406896575391],[-158.73731331305797,20.796306105108872],[-159.97481243640226,18.678647022154717],[-162.89981036430603,14.801154224791581],[-166.499807814034,10.41081650540272],[-171.89980398862608,2.367912558705407],[-176.39980080078607,-6.616650693475355],[-179.0997988904665,-15.006817032918805],[-179.54979856929816,-16.953454989809906],[-179.99979825051412,-17.383402005942457]],[[-179.99979825051412,-35.59302880961419],[-175.49980143835413,-34.11602012163193],[-172.79980335105813,-25.540896076259312],[-169.19980590133008,-15.441023659568087],[-168.29980653889803,-10.17745743036107],[-163.799809726738,2.367912558705407],[-160.19981227700995,13.054150695298627],[-157.49981418971396,18.678647022154717],[-156.48731490757396,19.740987365524937],[-155.8220359890499,20.0232931385683]],[[-155.92481530545814,20.269544035929588],[-155.69981546484993,20.58581909604039],[-152.99981737755394,23.298598065875897],[-147.6030982105313,26.564516489168216],[-138.60310458621126,28.1630268819071],[-127.79983522945767,31.670513047087127],[-122.39983905486586,34.49779087043369],[-120.8472016490899,35.367078251717096]],[[179.9999603175593,-17.383402005942457],[178.8749611145193,-18.028803650699082]],[[178.43744782917764,-18.123810943537187],[178.0874480765248,-18.880139975101173],[173.69996478053517,-23.905969261790265],[166.49996988107907,-27.15383128539156],[160.64997402527118,-31.083834718767243],[154.79996457419256,-33.00992295652956],[152.0920653907799,-34.11735567246567],[151.591830457126,-33.96305435950763],[151.273987072028,-33.76116106060912]],[[151.19625712709274,-33.913571605570375],[152.09996648689665,-34.39497520616068],[155.69996393662453,-35.22626671976625],[159.29996138635258,-35.77578304431546],[167.39995564824065,-36.3215277599179],[172.7999518228328,-36.68325067019043],[174.62339053109176,-36.78757761230096]],[[174.76792042870525,-36.78780986154051],[175.04990772894308,-36.59297842795038],[175.04990772894308,-36.140033391295425],[175.49990741015898,-35.77578304431546],[178.64990517867093,-36.140033391295425],[179.99990422231897,-35.59302880961419]]]}},{"type":"Feature","properties":{"id":"tata-tgn-atlantic-south","name":"Tata TGN-Atlantic South","color":"#b86d35","feature_id":"tata-tgn-atlantic-south-0","coordinates":[-38.53944305109249,45.90450350872602],"length_km":5687.662616316283},"geometry":{"type":"LineString","coordinates":[[-2.974873656631674,51.22244696599781],[-4.049922895056732,51.3766517753536],[-5.399921938704683,51.16550007909214],[-7.199920663568708,51.02419288763878],[-10.799918113296759,50.31116725161073],[-16.199914287888834,49.87814473780419],[-23.399909187344846,49.58728674004685],[-39.59989771112098,45.646541495187385],[-50.39989006030513,42.578254086072846],[-61.19988240948919,41.0693404382162],[-68.39987730894529,40.72920412488655],[-71.09987539624129,40.30157665795881],[-74.06286329723282,40.15283384719588]]}},{"type":"Feature","properties":{"id":"tata-tgn-pacific","name":"Tata TGN-Pacific","color":"#b86637","feature_id":"tata-tgn-pacific-0","coordinates":[-141.98468279027563,47.36074149138619],"length_km":20596.386493441893},"geometry":{"type":"MultiLineString","coordinates":[[[-118.79984160513764,33.93964008831966],[-119.2498412869497,33.87739543625145],[-120.59984033000157,33.93964008831966],[-122.3998390548656,34.31215165223547],[-123.29983841789374,35.05222991093673],[-125.54983682337766,38.651811712711336],[-126.44983618580963,42.743713464436695],[-125.09983714216159,45.0138336439531],[-124.19983777972962,45.74476367411526]],[[-124.19983777972962,45.92112887154667],[-125.09983714216159,46.0383951936514],[-129.5998339543217,46.272182853813646],[-138.59982757864174,47.19740739556967],[-151.19981865269,47.80541217589291],[-179.99979825051412,47.80541217589291]],[[-124.19983777972962,45.97979307748938],[-125.09983714216159,46.19436398821858],[-129.5998339543217,46.5823550820958],[-138.59982757864174,47.80541217589291],[-151.1998186526899,48.40638249553803],[-179.99979825051412,48.40638249553803]],[[179.99995828233068,48.40638249553803],[172.7999518228328,48.40638249553803],[160.19996074878455,45.331071073324864],[149.39996839960057,39.35121757117122],[141.2999741377125,35.05222991093673],[140.3437248151284,35.05222991093673]],[[179.99995828233068,47.80541217589291],[172.7999518228328,47.80541217589291],[160.19996074878455,44.694829089578164],[149.39996839960057,39.00237890905839],[143.09997286257644,35.05222991093673],[138.59997605041642,34.032921789964035],[137.69997668798445,34.31215165223547]],[[137.58747676768036,34.31215165223547],[138.59997605041642,24.12261698700344],[139.94997509406446,21.635297384859552],[142.19997350014447,16.965102599435927],[143.32497270318447,15.23578178303578],[144.22497206561644,13.92930384327183],[144.69470173285575,13.464772962370143]]]}},{"type":"Feature","properties":{"id":"telstra-endeavour","name":"Telstra Endeavour","color":"#393d98","feature_id":"telstra-endeavour-0","coordinates":[168.29156866934412,-24.230720295320193],"length_km":8265.109109585628},"geometry":{"type":"MultiLineString","coordinates":[[[151.22848710426095,-33.882038650285516],[152.0332142221154,-33.848958880365075],[154.80001816943468,-32.423034994424306],[160.64997402527118,-30.30995334464681],[166.49996988107907,-25.540896076259312],[170.9999666932391,-22.250099679090077],[173.69996478053517,-17.168553094226155],[179.9999603175593,-8.401139048122838]],[[-179.99979825051412,-8.401139048122838],[-176.39980080078607,-3.029995968008661],[-172.79980335105813,2.367912558705407],[-167.39980717646606,10.41081650540272],[-163.799809726738,14.801154224791581],[-160.1998122770105,18.678647022154717],[-158.849813233362,20.796306105108872],[-158.3998135521461,21.425997872385402],[-158.24204999203212,21.54876173571662]]]}},{"type":"Feature","properties":{"id":"the-east-african-marine-system-teams","name":"The East African Marine System (TEAMS)","color":"#73bf43","feature_id":"the-east-african-marine-system-teams-0","coordinates":[57.17435522397155,8.258620151746948],"length_km":5067.128258863461},"geometry":{"type":"LineString","coordinates":[[39.672896131288006,-4.052924364763054],[42.30004427019158,-4.60145376483711],[46.57504124174374,-2.35574573664619],[53.10003661937573,1.468426767331968],[56.25003438848378,5.510071711803246],[57.60003343153574,9.52441134501949],[62.10003024369576,15.669513225155248],[63.00002960612773,20.796306105108872],[63.00002960612773,22.469443964829516],[59.85003183761575,24.32780311165363],[58.50003279396771,24.660522249648846],[56.92503390971164,24.915858493558826],[56.33372432860119,25.121690004958644]]}},{"type":"Feature","properties":{"id":"trans-pacific-express-tpe-cable-system","name":"Trans-Pacific Express (TPE) Cable System","color":"#a2c539","feature_id":"trans-pacific-express-tpe-cable-system-0","coordinates":[-151.72507890815905,46.5823550820958],"length_km":15315.127099239959},"geometry":{"type":"MultiLineString","coordinates":[[[134.99997860068837,30.708139993541643],[136.7999773255523,31.478822672736147],[138.59997605041642,32.62301664000789],[140.39997477528055,33.37780603565933],[143.09997286257644,34.49779087043369],[149.39996839960057,37.94551049545967],[160.19996074878455,43.401144973153954],[172.7999518228328,46.5823550820958],[179.99995828233068,46.5823550820958]],[[132.74998019460836,30.320465424761444],[132.74998019460836,30.708139993541643],[130.49998178852834,30.708139993541643],[128.92498290427227,31.670513047087127],[127.85028816476637,33.19758857732108],[128.36248330275228,34.31215165223547]],[[127.91248362153638,34.31215165223547],[127.12498417940834,33.189714664600466],[125.99998497636834,32.8123187832876],[124.19998625150441,33.189714664600466],[122.84998720785636,33.93964008831966],[120.8249886423844,35.419780517080355],[120.34246898420574,36.08731090741939],[120.59998880177636,35.419780517080355],[122.39998752664029,33.93964008831966],[123.74998657028833,33.001218522654476],[124.19998625150441,31.86180860227073]],[[140.1046624838865,34.89859296336222],[140.06247501377248,34.69072647741027],[139.94997509406446,34.405022750715936],[139.04997573163232,33.93964008831966],[136.7999773255523,31.86180860227073],[134.99997860068837,31.09426282763951],[132.74998019460836,30.708139993541643]],[[122.17498768603225,31.19054975154414],[123.5249867296803,30.126049846722832],[123.74998657028833,28.161052262220792],[121.94998784542422,26.562513149236715],[121.49998816420832,25.75470426341523]],[[121.61248808451225,25.75470426341523],[121.94998784542422,26.36108632539156],[125.26189309097956,27.59034732929116],[130.49998178852834,30.708139993541643]],[[134.99997860068837,30.708139993541643],[132.74998019460836,30.320465424761444],[130.49998178852834,30.5144959597591],[124.19998625150441,31.86180860227073],[122.84998720785636,31.957307911004964],[121.94998784542422,31.766210259727007]],[[-123.94016937986751,45.64401775142813],[-125.09983714216159,44.85455225626708],[-129.5998339543217,44.05151922873524],[-138.59982757864174,45.646541495187385],[-151.1998186526899,46.5823550820958],[-179.99979825051412,46.5823550820958]]]}},{"type":"Feature","properties":{"id":"unity","name":"Unity","color":"#77459a","feature_id":"unity-0","coordinates":[-148.18220341164763,44.06630066756654],"length_km":8995.71470828903},"geometry":{"type":"MultiLineString","coordinates":[[[-118.38802345391501,33.84447040216643],[-120.59984033000157,33.799525734581415],[-122.3998390548656,34.12610104005753],[-129.5998339543217,37.94551049545967],[-138.5998275786419,42.07923561816413],[-151.19983391146832,44.69205655553234],[-179.99981350929238,44.69205655553234]],[[140.06247501377248,34.96776525378359],[140.3437248151284,34.960082324548345],[141.2999741377125,34.867831005273345],[143.09997286257644,34.867831005273345],[149.39996839960057,36.1498667868178],[160.19996074878455,41.40772623743595],[172.79992073307184,44.694811588752586],[179.99992719256971,44.694811588752586]]]}},{"type":"Feature","properties":{"id":"west-africa-cable-system-wacs","name":"West Africa Cable System (WACS)","color":"#394da1","feature_id":"west-africa-cable-system-wacs-0","coordinates":[-6.06677813221715,-0.7813866362255875],"length_km":16100.797738591382},"geometry":{"type":"MultiLineString","coordinates":[[[14.533463940297649,-22.68542973223072],[13.05006499115128,-22.873434953546333],[9.900067222639304,-23.49392244589784]],[[12.349965487108514,-5.933373731471328],[10.80006658507127,-5.945707155070644],[8.10006849777537,-6.616650693475355]],[[9.000067860207336,-10.620064860363238],[11.700065947503239,-9.29042430103552],[12.600065309935387,-9.29042430103552],[13.201440382068753,-9.49008098880628]],[[10.80006658507127,-5.945707155070644],[11.250066265691444,-5.236602021940458],[11.863635831629022,-4.77878776891936]],[[4.500071048047319,0.118588418888312],[6.300069772911254,1.693340822791726],[8.10006849777537,2.367912558705407],[9.000067860207336,3.266814816815666],[9.208657712440504,4.014706479784149]],[[3.423511810692114,6.439066911484626],[3.375071845007315,4.164912849976942],[2.25007264196731,2.03066189047467]],[[1.227803366152544,6.126307297218732],[1.46257319924337,5.061986954416114],[3.375071845007315,4.164912849976942]],[[-9.449919069648717,38.29952060596925],[-11.024917953904795,36.51238821239364],[-11.924917316336764,35.419780517080355],[-13.499916200592752,32.052708023486204],[-14.624915403632755,30.5144959597591],[-15.299914925456777,28.95155473219332],[-16.199914287888834,27.76358852605777],[-17.99991301275286,24.94136317175375],[-18.899912375184826,22.884654113882444],[-20.24991141883287,19.95262290516439],[-20.24991141883287,15.23578178303578],[-20.24991141883287,11.735650161405832],[-18.899912375184826,8.635699417327467],[-14.399915563024809,2.817450442654169],[-10.799918113296759,-0.781386636225587],[-3.599923213840749,-0.781386636225587],[0.225074076495339,0.13163185678509],[1.575073120143199,1.35596103499925],[2.25007264196731,2.03066189047467],[4.500071048047319,0.118588418888312],[6.300069772911254,-1.231315750217412],[7.650068816559295,-4.825692499217419],[8.10006849777537,-6.616650693475355],[9.000067860207336,-10.620064860363238],[8.550068178991262,-18.026426383713453],[9.900067222639304,-23.49392244589784],[13.950064353583429,-30.30995334464681],[15.750063078447363,-32.61276000573574],[16.65006244087933,-33.27363077142247],[18.15553137439108,-33.348058456467676]],[[-0.204315619320974,5.558285889905858],[0.225074076495339,3.279837005484997],[0.225074076495339,0.13163185678509]],[[-4.026242911831877,5.323508791824841],[-4.162422815360751,3.266814816815666],[-4.274922735664679,2.367912558705407],[-4.14992282421577,1.468426767331968],[-3.599923213840749,-0.781386636225587]],[[-23.521209101414858,14.923035560171673],[-22.499909824912876,15.018578573757472],[-20.24991141883287,15.018578573757472]],[[-16.199914287888834,27.76358852605777],[-15.74991460667276,27.564309487941923],[-15.46866480591276,27.564309487941923],[-15.299914925456777,27.663994423747],[-15.299914925456777,27.813351446514346]]]}},{"type":"Feature","properties":{"id":"yellow","name":"Yellow","color":"#b99633","feature_id":"yellow-0","coordinates":[-38.70797214670583,44.59606592327159],"length_km":5561.968299869321},"geometry":{"type":"LineString","coordinates":[[-72.93786409419282,40.75584308487114],[-71.09987539624129,40.12976255393115],[-68.39987730894529,40.38732029077508],[-61.19988240948919,40.38732029077508],[-50.39989006030513,41.576261830098154],[-39.59989771112098,44.37405751055857],[-23.399909187344846,48.40638249553803],[-16.199914287888834,49.29468421942562],[-10.799918113296759,50.022920456254944],[-8.099920026000767,50.31116725161073],[-5.399921938704683,50.811421859282945],[-4.544402544762735,50.82820142743812]]}},{"type":"Feature","properties":{"id":"america-movil-submarine-cable-system-1-amx-1","name":"America Movil Submarine Cable System-1 (AMX-1)","color":"#3e60ac","feature_id":"america-movil-submarine-cable-system-1-amx-1-0","coordinates":[-45.68283490232327,8.348070685535115],"length_km":16147.434134425253},"geometry":{"type":"MultiLineString","coordinates":[[[-83.03765938887449,9.988597517410145],[-83.02486694906135,10.41081650540272],[-82.12486758662938,11.662208223864337],[-82.2373675069334,12.981078187892704],[-82.12486758662938,13.419186961310027],[-81.89986774602134,14.074846740630262],[-80.5498687017773,16.10232559580297]],[[-81.73000034886279,12.55112730403864],[-81.89986774602134,12.871429169544808],[-82.2373675069334,12.981078187892704]],[[-70.68356569115905,19.803388017159396],[-70.19987603380923,20.05833455139623],[-69.29987667137726,19.95262290516439],[-67.04987826529725,19.210675111642853],[-66.48737866377725,18.891661584303154],[-66.10666893347558,18.46610423294742]],[[-70.6911856857609,19.799436355797177],[-70.6498757150253,20.375041253465433],[-70.6498757150253,21.216397899942]],[[-69.29987667137726,22.261369678340607],[-70.6498757150253,21.216397899942],[-73.3498738023213,20.58581909604039],[-73.9123734038413,19.95262290516439],[-74.4748730053613,19.316876111628712],[-74.92487268657729,18.251816319028222],[-75.26237244748934,17.395022634700517],[-75.26237244748934,11.735650161405832]],[[-75.50573227509088,10.38680745163333],[-76.0498718896173,11.515266158038768],[-79.19986965872535,14.946128218512863],[-80.5498687017773,16.10232559580297],[-81.89986774542525,17.395022634700517],[-84.59986583272125,19.104405475930452],[-85.27486535454535,20.375041253465433],[-85.04986551393732,21.635297384859552],[-84.82486567332928,22.469443964829516],[-83.2498667890733,23.50508968095737],[-80.99986838299347,23.50508968095737],[-79.64986933934534,24.73717827217609],[-79.53736941904133,25.348717422116714],[-80.16016897784432,26.010548668010795]],[[-88.19986328244948,16.10232559580297],[-87.6373636809293,16.534196198259725],[-86.73736431849733,18.251816319028222],[-85.27486535454535,20.375041253465433]],[[-38.50448848711925,-12.96997203534297],[-37.34989930504097,-13.455974309054534],[-35.99990026139302,-13.504596782344024],[-35.549900580176946,-13.698987269610743]],[[-43.20956515399876,-22.903486555497956],[-42.29989579841706,-23.494056688715368],[-40.94989675476902,-23.49392244589784],[-38.69989834868901,-22.250099679090077],[-36.44989994260901,-18.026426383713453],[-35.549900580176946,-13.698987269610743],[-33.299902174096935,-8.401139048122838],[-33.299902174096935,-6.616650693475355],[-34.199901536529,-4.825692499217419],[-35.99990026139302,-3.92832730414264],[-38.542968459859594,-3.718735129291092],[-38.24989866747303,-1.231315750217412],[-39.149898029904996,1.468426767331968],[-46.79989261057708,9.52441134501949],[-52.19988878516916,14.801154224791581],[-57.59988495976114,17.395022634700517],[-59.399883684625166,18.251816319028222],[-62.09988177192116,19.316876111628712],[-63.89988049678528,19.316876111628712],[-65.69987922164921,19.529070924350908],[-67.49987794651324,21.216397899942],[-69.29987667137726,22.261369678340607],[-73.3498738023213,24.32780311165181],[-76.94987125204935,26.964304734562898],[-79.19986965812936,29.1482487910328],[-80.5498687017773,30.126049846722832]],[[-65.69987922164921,19.529070924350908],[-66.03737898256126,19.104405475930452],[-66.10666893347558,18.46610423294742]]]}},{"type":"Feature","properties":{"id":"eastern-africa-submarine-system-eassy","name":"Eastern Africa Submarine System (EASSy)","color":"#28a27f","feature_id":"eastern-africa-submarine-system-eassy-0","coordinates":[55.41741010630586,10.449991164659412],"length_km":10415.36193292816},"geometry":{"type":"MultiLineString","coordinates":[[[51.30003789451161,1.468426767331968],[46.80004108235159,2.255504211923801],[45.344182113695986,2.041205223228781]],[[43.24330360197787,-11.700589282272533],[42.75004395140765,-11.943944931746815],[42.07504442958354,-11.943944931746815]],[[43.66314330455965,-23.354724804059778],[42.75004395140765,-23.59705596407509],[41.40004490775961,-23.49392244589784]],[[32.58062115552212,-25.968268155407962],[33.750050327087614,-26.450941899317534],[35.55004905195155,-26.752713396100123]],[[39.672896131288006,-4.052924364763054],[42.30004427019158,-4.152767748013638]],[[40.500045545327644,-6.169450529574503],[41.62504474836765,-9.29042430103552],[42.07504442958354,-11.943944931746815],[42.525044110203716,-14.861883917661954],[42.52504411079961,-15.224032284647373],[42.52504411079961,-20.574419057276128],[41.40004490775961,-23.49392244589784],[35.55004905195155,-26.752713396100123],[33.750050327087614,-28.54634921047574],[32.850050964655466,-28.940898806450146],[31.757961738301827,-28.950559666538012]],[[37.21967786917097,19.61556659454616],[37.80004745803156,19.529070924350908],[39.150046502275686,18.251816319028222],[40.16254578501358,16.534196198259725],[41.62504474896373,14.801154224791581],[42.13129439033177,13.92930384327183],[42.806293912155695,13.054150695298627],[43.1297311836258,12.834868817846521],[43.21410612385384,12.615395567393394],[43.453168453307654,12.395734000022975],[44.55004267627159,11.460143029456674],[45.450042038703735,11.405009147532946],[48.60003980781179,12.175887185507976],[52.65003693815965,13.163718917913586],[54.00003598419185,13.492128176464083],[55.575034866063774,12.615395567393394],[55.35003502545574,9.52441134501949],[54.45003566361985,5.510071711803246],[51.30003789451161,1.468426767331968],[46.12504156052766,-1.906058394384765],[42.30004427019158,-4.152767748013638],[40.95004522654354,-5.497950688314882],[40.500045545327644,-6.169450529574503],[39.82504602290763,-6.523516459651954],[39.269676416932725,-6.823132108349236]],[[43.16164365982675,11.573660387694234],[43.65004331383962,11.56544739308108],[44.55004267627159,11.460143029456674]]]}},{"type":"Feature","properties":{"id":"pacific-light-cable-network-plcn","name":"Pacific Light Cable Network (PLCN)","color":"#bc3e96","feature_id":"pacific-light-cable-network-plcn-0","coordinates":[-148.39382924040027,42.803904611812975],"length_km":12429.138388415415},"geometry":{"type":"MultiLineString","coordinates":[[[-118.41596126184768,33.873229987687004],[-120.59984033000157,33.612349285068355],[-122.3998390548656,33.75276987113061],[-129.5998339543217,37.23235432155614],[-138.5998275786419,40.72920412488655],[-151.19983391146832,43.39831121479671],[-179.99981350929238,43.39831121479671]],[[125.99998497636834,21.006499845176737],[130.94998146974444,22.469443964829516],[132.74998019460836,23.298598065875897],[138.59997605041642,25.75470426341523],[160.19996074878455,40.04369219283004],[172.79992073307184,43.4011270858577],[179.99992719256971,43.4011270858577]],[[125.99998497636834,21.006499845176737],[124.14876558466459,23.55170502223485],[122.84998720785636,24.53265756616073],[122.17498768603225,24.788256058863375],[121.80145279439779,24.863504254255204]],[[125.99998497636834,21.006499845176737],[124.6499859321244,19.24608308700458],[123.29998688907226,17.82393441253792],[122.39998752664029,16.10232559580297],[122.17498768603225,15.886035719079029],[121.94998784542422,15.777803371817374],[121.56018812156209,15.761539465842137]]]}},{"type":"Feature","properties":{"id":"imewe","name":"IMEWE","color":"#2966b1","feature_id":"imewe-0","coordinates":[48.42281650540944,12.901288725928946],"length_km":12016.96592726408},"geometry":{"type":"MultiLineString","coordinates":[[[56.33372432860119,25.121690004958644],[56.92503390971164,24.864833316508726],[58.50003279396771,24.507068968791266],[59.85003183761575,23.91710129093604],[62.10003024369576,22.469443964829516],[62.10003024369576,19.104405475930452]],[[35.55004905195155,34.405022750715936],[32.40005128343957,33.28381101905092],[30.57465671568515,32.55671109167817],[28.800053833711523,32.41438660250263]],[[15.06744356202158,37.51344748573393],[15.750063078447363,37.411283634923244],[16.2563127198154,36.87321951208928],[16.425062600271474,35.78566189952622],[16.65006244028343,34.35479262780249]],[[5.372530429989069,43.29362778902908],[5.962570011999289,41.74435878948223],[7.200069135343221,38.651811712711336],[7.875068657167333,37.94551049545967],[9.000067859611436,37.76786242517874],[10.348617229769278,37.76786242517874],[10.91256650537538,37.411283634923244],[11.58756602600751,37.23235432155614],[12.703817920304136,35.893519314921924],[14.437792044692554,34.87439333360583],[16.65006244028343,34.35479262780249],[19.265309401660918,34.74032582202073],[22.050058614875596,34.516395933671596],[25.28984215557834,33.80029687462985],[28.800053833711523,32.41438660250263],[29.8935130590948,31.191465077638455]],[[32.52993119143143,29.972545436050364],[32.56877616391329,29.63833609362628],[32.73755104435154,29.344566989489813],[33.01880084511154,28.95155473219332],[33.511262996247716,28.161052262220792],[34.312549928607616,27.364667993860262],[35.184424310963514,26.562513149236715],[35.887548812863514,25.75470426341523],[36.900048095598684,24.12261698700344],[38.137547218943524,22.05298561667754],[38.92504666107156,20.375041253465433],[40.2750457047196,18.251816319028222],[41.28754498745659,16.534196198259725],[42.24379431003961,14.801154224791581],[42.75004395140765,13.92930384327183],[43.11566869239569,13.054150695298627],[43.270356082813635,12.834868817846521],[43.35473102304167,12.615395567393394],[43.734418254067656,12.395734000022975],[44.55004267627159,12.010882360458767],[45.450042038703735,12.175887185507976],[48.60003980721571,12.944533868662969],[55.35003502545574,15.23578178303578],[58.95003247518379,16.965102599435927],[62.10003024369576,19.104405475930452],[66.60002705585578,19.529070924350908],[70.20002450558383,19.316876111628712],[72.87590260996693,19.07607425728523]],[[67.02854675228855,24.889731701235817],[66.48752713555186,23.298598065875897],[66.15002737463989,20.375041253465433],[66.60002705585578,19.529070924350908]],[[38.137547218943524,22.05298561667754],[39.18275647850768,21.481533475502996]]]}},{"type":"Feature","properties":{"id":"seacomtata-tgn-eurasia","name":"SEACOM/Tata TGN-Eurasia","color":"#2f95b3","feature_id":"seacomtata-tgn-eurasia-0","coordinates":[48.786506248119146,12.89857027648199],"length_km":12939.357480359513},"geometry":{"type":"MultiLineString","coordinates":[[[72.90833590939474,19.08952099458581],[70.232457805012,19.224110884087814],[66.63246035528395,19.330303180397426],[61.90746370251589,18.692125701453875],[58.98246577461195,16.763395784717346],[55.3824683248839,15.032320749690347],[48.632473106643694,12.848741565417857],[45.48247533813172,12.07980921311095],[44.58247597569975,11.9697778362716],[43.73872657341975,12.409630577439529],[43.37310183243171,12.629280326722103],[43.28872689220368,12.848741565417857],[43.119977011747785,13.068011238315085],[42.726227290683674,13.94311365796037],[42.219977649315815,14.81491029925734],[41.20747836658065,16.54783600485381],[40.19497908384366,18.26532858133193],[38.8449800401958,20.38837908615011],[38.057480598067585,22.066172634374052],[36.81998147472256,24.13560242446459],[35.807482191987575,25.767518777408142],[35.13248267016364,26.575239208943042],[34.288733267883636,27.3773037156237],[33.47345884543558,28.1735958463088],[32.64989110585475,29.116661999999696]],[[32.6130544549501,-25.955475388503118],[33.7824836265156,-26.53889211948715],[35.807482191987575,-26.940767064044092]],[[42.33247756961974,-4.362957099929003],[39.70532943071599,-4.038731181161849]],[[55.3824683248839,15.032320749690347],[55.832468006099795,12.629280326722103],[55.832468006099795,9.538443559786772],[55.382468325479806,5.524234439321599],[52.232470556371744,1.482650691528387],[46.382474700563684,-2.116699612616721],[42.33247756961974,-4.362957099929003],[40.9824785259717,-5.707714823439762],[40.64497876505974,-6.155304105140356],[39.857479322335614,-6.565263959445051],[39.30210971636071,-6.809004030362613]],[[31.790395037729812,-28.938108319513333],[32.88248426408364,-29.026862190178257],[33.7824836265156,-28.731334165606242],[35.807482191987575,-26.940767064044092],[41.65747804779581,-23.68707917734726],[42.78247725083582,-20.561097385973355],[42.78247725083582,-15.210302528564313],[42.78247725023955,-14.84813082933909],[42.33247756961974,-11.930023974236281],[41.88247788840367,-9.276382018590148],[40.64497876505974,-6.155304105140356]],[[43.180426968924365,11.608807305452311],[43.68247661326779,11.804597919620866],[44.58247597569975,11.9697778362716]],[[38.057480598067585,22.066172634374052],[39.21518977793548,21.494773132194442]]]}},{"type":"Feature","properties":{"id":"middle-east-north-africa-mena-cable-systemgulf-bridge-international","name":"Middle East North Africa (MENA) Cable System/Gulf Bridge International","color":"#51b847","feature_id":"middle-east-north-africa-mena-cable-systemgulf-bridge-international-0","coordinates":[45.405295722056806,12.593544017219301],"length_km":7748.982917174131},"geometry":{"type":"MultiLineString","coordinates":[[[58.50003279396771,24.12261698700344],[58.331282913511636,23.91710129093513],[58.1762030233719,23.68487753168473]],[[32.65318110412008,29.113614162980063],[33.42698805595291,28.161052262220792],[34.031300127847615,27.364667993860262],[34.84692455005155,26.562513149236715],[35.43754913164762,25.75470426341523],[36.45004841438352,24.12261698700344],[37.68754753772763,22.05298561667754],[38.47504697985549,20.375041253465433],[39.82504602350353,18.251816319028222],[40.83754530624179,16.534196198259725],[41.96254450927961,14.801154224791581],[42.46879415064765,13.92930384327183],[42.975043792015505,13.054150695298627],[43.20004363262354,12.834868817846521],[43.28441857285158,12.615395567393394],[43.59379335368765,12.395734000022975],[44.55004267627159,12.175887185507976],[45.450042038703735,12.615395567393394],[48.60003980721571,13.3827080361257],[55.35003502545574,16.10232559580297],[58.95003247518379,17.82393441253792],[60.075031678223795,19.104405475930452],[60.187531597931816,22.469443964829516],[59.85003183761575,22.988259503893058],[58.50003279396771,24.12261698700344]],[[12.591375316091606,37.65058617278613],[12.173015612461494,37.27875778048947],[12.037565708415386,36.87321951208928],[12.150065628719313,35.78566189952622],[12.487565389636371,35.419780517080355],[14.400064034799321,34.12610104005753],[16.65006244087933,33.37780603565933],[19.350060528175415,33.75276987113061],[22.050058614875596,33.73717891339029],[25.200056383983473,33.001218522654476],[27.900054471279375,32.148008778540614],[29.70093319552029,31.072270031660306]],[[37.68754753772763,22.05298561667754],[39.18275647850768,21.481533475502996]]]}},{"type":"Feature","properties":{"id":"eac-c2c","name":"EAC-C2C","color":"#3a56a6","feature_id":"eac-c2c-0","coordinates":[124.22374543987213,20.728817443467044],"length_km":32811.74557936672},"geometry":{"type":"MultiLineString","coordinates":[[[120.12788799225589,21.457551614722192],[118.79999007691224,21.53068533396254],[117.4499910332642,21.84429407917369],[115.64999230840026,22.05298561667754],[114.97499278657615,22.365445686418724],[114.2586832940168,22.31829267897149]],[[114.20292333351767,22.22205041973683],[114.63749302566418,20.796306105108872],[115.42499246779222,18.238460810952724],[116.99999135204831,16.10232559580297],[119.24998975812831,14.365653759228442],[120.14998912056028,13.820086409698062]],[[121.3874882439044,25.742038029757644],[121.94998784542422,26.751029869608292],[123.5249867296803,28.148653708881287],[123.29998688907226,30.113886122109243],[122.17498768603225,30.74440510438189],[121.92508786246776,30.852678537714777],[122.17498768603225,31.034033881789767],[122.84998720785636,31.46682894547825],[128.0249835418403,32.80049917601868],[128.69998306366443,34.30053553069045],[128.99949285148878,35.158882668071676]],[[129.5999824260964,34.30053553069045],[129.5999824260964,32.80049917601868],[129.5999824260964,31.6585439519066],[130.49998178852834,30.889328974889438],[132.74998019460836,30.889328974889438],[134.99997860068837,31.274720539181796],[136.7999773255523,32.04078843974209],[137.02497716616034,32.80049917601868],[136.87399727311598,34.32521554537484],[137.69997668798445,33.927972678693564],[138.59997605041642,33.927972678693564],[139.27497557224055,34.114459241194844],[139.8937251339125,34.393419492403375],[140.0343500336966,34.679162981906806],[140.02028754365847,34.84090484813936]],[[139.95485509060742,34.965046603583694],[140.09059999384857,34.84090484813936],[140.3155998344566,34.679162981906806],[140.4562247354325,34.393419492403375],[140.7374745361925,33.927972678693564],[140.6249746158884,32.421443555350706],[140.17497493467252,30.889328974889438],[138.82497589102445,28.544693071144245],[137.24997700676838,26.95177029188127],[132.74998019460836,23.698382121989734],[130.94998146974444,22.87169786996185],[125.99998497636834,21.203287979625483],[122.84998720785636,20.361858037652684],[121.11255349138571,19.18943026293286],[120.24308317969574,18.601843893825777],[119.11906339140688,17.549363428561175],[119.02498991752027,15.222213115980855],[119.58748951904028,14.569901804622967],[120.14998912056028,13.915654478895796]],[[120.14998912056028,13.2595509442811],[118.79999007691224,12.601672204746537],[116.09999198961616,9.06644423990714],[114.07499342414418,7.730954611330002],[110.69999581502417,5.943831970446426],[110.02499629320025,5.496074035021858],[107.99999772772827,4.599574515521482],[107.09999836529612,4.038674649466248],[105.74999932164808,3.252775080426739],[105.24374968028005,2.803404866588448],[104.62500011860807,1.454368851373345],[104.28790035741287,1.215596520932091],[104.16377044534778,1.210005277728221],[103.98701057056589,1.375392999849927],[104.17793043531677,1.249346186877429],[104.28790035741287,1.299801162778933],[104.42847525782817,1.454368851373345],[104.79374999906415,2.803404866588448],[105.74999932164808,4.150887372137655],[107.09999836529612,4.935905975277167],[107.99999772772827,5.6080461663058],[110.47499597441613,7.730954611330002],[111.71249509776025,9.954064678408844],[112.72499438049614,12.601672204746537],[114.29999326415633,17.095079260964887],[114.52499310536027,18.238460810952724],[114.1874933444483,20.783159233732995],[114.2586832940168,22.305283024668398]],[[124.6499859327203,35.419780517080355],[121.49998816420832,35.78566189952622],[120.34246898420574,36.08731090741939]],[[103.98701057056589,1.389451396800233],[104.18740042860793,1.289568782938401],[104.28790035741287,1.341927435270103],[104.4004002777168,1.468426767331968],[104.73750003891219,2.817450442654169],[105.74999932164808,4.277107602190303],[107.09999836529612,5.061986954416114],[107.99999772772827,5.733989114150127],[110.24999613380828,7.744889052551447],[111.4874952571522,9.967915186974132],[112.49999453988829,12.615395567393394],[114.07499342354828,17.10851996079568],[114.29999326475222,18.251816319028222],[114.07499342414418,20.796306105108872],[114.20292333351767,22.22205041973683]],[[121.3832882468796,25.149980712893985],[121.72498800481618,25.75470426341523],[121.94998784542422,26.05828756029904],[122.84998720785636,26.260240971577822],[128.69998306366443,25.75470426341523],[131.39998115096031,26.1593079707739],[132.74998019460836,26.1593079707739]],[[114.2586832940168,22.31829267897149],[114.97499278657615,22.261369678340607],[115.64999230840026,21.948678137927157],[117.4499910332642,21.635297384859552],[118.79999007691224,21.006499845176737],[121.02775554672928,20.827994119059056],[122.84998720785636,20.796306105108872],[125.99998497636834,22.05298561667754],[128.69998306366443,23.298598065875897],[132.74998019460836,26.1593079707739]],[[114.2586832940168,22.31829267897149],[114.97499278657615,22.313417380769337],[115.64999230840026,22.000841467910675],[117.4499910332642,21.73983373091106],[118.79999007691224,21.216397899942],[121.02775554672928,21.038143463338198],[122.84998720785636,21.216397899942],[124.19998625150441,22.05298561667754],[125.09998561393637,24.12261698700344],[125.38123541469638,25.55188275942587],[125.66248521545637,26.1593079707739],[126.67498449819227,28.55704546571133],[126.89998433880031,29.540507745394493],[127.34998402001638,30.708139993541643],[125.09998561393637,33.189714664600466],[124.6499859327203,34.31215165223547],[124.6499859327203,35.419780517080355],[125.54998529515227,36.1498667868178],[126.39158469895561,36.57633045558579],[125.54998529515227,35.96797434759339],[125.09998561393637,35.419780517080355],[125.09998561393637,34.31215165223547],[125.32498545454442,33.189714664600466],[128.24998338244836,30.708139993541643],[130.49998178852834,29.540507745394493],[132.74998019460836,29.344566989489813],[134.99997860068837,29.344566989489813],[137.69997668798445,30.126049846722832],[139.27497557224055,30.901396088515508],[141.2999741377125,32.052708023486204],[142.19997350014447,34.405022750715936],[142.19997350014447,35.05222991093673],[141.97497365953643,35.78566189952622],[140.6124746247436,36.383483735312474],[141.7499738189284,35.78566189952622],[141.7499738189284,35.05222991093673],[140.84997445649643,34.12610104005753],[138.59997605041642,33.37780603565933],[137.69997668798445,33.659181629050494],[136.87399727311598,34.33682825203173],[137.4749768473764,32.8123187832876],[137.69997668798445,31.286738814391754],[137.24997700676838,29.344566989489813],[132.74998019460836,26.1593079707739]],[[125.09998561393637,24.109781889526705],[123.74998657028833,25.2342865621001],[122.84998720785636,25.539194978687103],[121.94998784542422,25.43764443864878]],[[121.94998784542422,25.33600821718872],[122.39998752664029,24.928611492263457],[122.84998720785636,24.007054825363046],[122.39998752664029,22.45644844059945],[121.04998848299225,19.72775079133148],[119.76865992030457,18.516117231747454],[118.93597086667044,17.741878907400267],[118.79999007691224,15.222213115980855],[119.36248967843224,14.569901804622967],[120.14998912056028,14.352030559547492]],[[120.14998912056028,14.024826773554539],[118.79999007691224,13.040451242220165],[115.64999230840026,9.06644423990714],[113.84999358353615,7.730954611330002],[110.47499597441613,5.943831970446426],[109.79999645259221,5.496074035021858],[107.99999772772827,4.711703227447191],[107.09999836529612,4.150887372137655],[105.74999932164808,3.365087426296303],[105.18749972012807,2.803404866588448],[104.5969251384969,1.454368851373345],[104.28790035741287,1.229630815177437],[104.1731404387099,1.23626927314036],[103.98701057056589,1.375392999849927]],[[120.12788799225589,21.457551614722192],[120.37498896116831,21.635297384859552],[120.54373884162439,22.05298561667754]]]}},{"type":"Feature","properties":{"id":"asia-submarine-cable-express-asecahaya-malaysia","name":"Asia Submarine-cable Express (ASE)/Cahaya Malaysia","color":"#1ab4dd","feature_id":"asia-submarine-cable-express-asecahaya-malaysia-0","coordinates":[115.69140756853142,11.342080245282922],"length_km":7806.630084071394},"geometry":{"type":"MultiLineString","coordinates":[[[130.90187508980648,23.865759963390698],[128.69998306366443,25.55188275942587],[128.0249835418403,25.957179978764344],[127.70078377150666,26.087749635462092]],[[105.07499979982416,2.803404866588448],[104.79751925424193,2.51340691774082],[104.40000027800004,2.42411333960119]],[[126.85721436909918,19.466476359726585],[125.54998529515227,19.303604751105766],[120.96988207969743,19.75148716830613],[120.32093903559283,19.446973787349965],[119.44922323177441,18.712816516277243],[118.5501687609761,17.89128877075432],[117.56329095300164,15.439678695520064],[116.21249190992026,12.601672204746537],[114.7499929459683,9.06644423990714],[112.94999422110418,7.730954611330002],[110.02499629320025,5.943831970446426],[109.34999677137613,5.496074035021858],[107.99999772772827,4.935905975277167],[107.09999836529612,4.375264548610226],[105.74999932164808,3.589672857320649],[105.07499979982416,2.803404866588448],[104.54077517827399,1.454368851373345],[104.28790035741287,1.271733251829275],[104.19932042016363,1.308321449396243],[103.98594057132397,1.371284183356795]],[[140.1327874639626,34.89859296336222],[140.11872497392463,34.69072647741027],[140.0624750143684,34.405022750715936],[139.72497525345642,33.93964008831966],[139.94997509406446,32.43331330641721],[139.49997541284839,30.901396088515508],[138.82497589102445,29.73606949729215],[137.24997700676838,28.55704546571133],[132.74998019460836,25.348717422116714],[130.94998146974444,23.91710129093513],[126.85721436909918,19.479734448240144],[124.19998625150441,15.23578178303578],[123.5249867296803,14.365653759228442],[122.95008713694455,14.11652289884896]],[[119.44922323177441,18.712816516277243],[118.34999039569617,20.163975031975873],[116.99999135204831,21.111485983488812],[115.64999230840026,21.79207342302722],[114.97499278657615,22.157216226160177],[114.2586832940168,22.31829267897149]]]}},{"type":"Feature","properties":{"id":"tata-tgn-intra-asia-tgn-ia","name":"Tata TGN-Intra Asia (TGN-IA)","color":"#3eb65c","feature_id":"tata-tgn-intra-asia-tgn-ia-0","coordinates":[118.52909995594474,16.566185059272218],"length_km":6623.732706397634},"geometry":{"type":"MultiLineString","coordinates":[[[120.22031319972999,18.785187974742005],[120.59998880058437,18.785187974742005],[121.04998848299225,18.785187974742005],[121.5130781549352,18.418302135034143]],[[114.18397334694201,22.24961578335278],[114.7499929459683,21.896495662923588],[115.64999230840026,21.635250907408707],[116.99999135204831,21.32123529551186],[118.34999039569617,20.58581909604039],[120.14998912056028,19.740987365524937],[121.04998848299225,19.529070924350908]],[[103.98701057056589,1.375392999849927],[104.18262043199422,1.262333057432152],[104.28790035741287,1.243665035636787],[104.56885015838535,1.454368851373345],[105.13124975997611,2.803404866588448],[105.74999932164808,3.477386828549033],[107.09999836529612,4.263084147817874],[107.99999772772827,4.82381385611519],[109.57499661198416,5.496074035021858],[110.24999613380828,5.943831970446426],[113.17499406171221,7.730954611330002],[114.97499278657615,9.06644423990714],[116.54999167083223,12.601672204746537],[118.01327063423176,15.439678695520064],[119.02751712903803,17.654665480331733],[120.01268452973224,18.601843893825777],[121.04998848299225,19.515816877621507],[122.84998720785636,19.72775079133148],[125.99998497636834,20.361858037652684],[130.94998146974444,21.203287979625483],[138.59997605041642,24.109781889526705]],[[107.07919838003114,10.342138429683002],[107.77499788712005,9.635342384764561],[108.6749972495522,9.302441529883154],[111.59999517745614,9.302441529883154],[113.39999390232026,9.08033076823294],[114.97499278657615,9.08033076823294]]]}},{"type":"Feature","properties":{"id":"asia-pacific-gateway-apg","name":"Asia Pacific Gateway (APG)","color":"#b63894","feature_id":"asia-pacific-gateway-apg-0","coordinates":[121.3880914113467,20.6648377037875],"length_km":10645.261237718194},"geometry":{"type":"MultiLineString","coordinates":[[[125.54998529515227,24.12261698700344],[122.84998720785636,24.83931282559271],[122.17498768603225,24.89034854048814],[121.80144795065142,24.863504112487785]],[[121.92508786246776,30.86475026744717],[122.17498768603225,30.80481662242606],[124.19998625150441,30.126049846722832],[125.54998529515227,29.540507745394493],[127.57498386062441,28.55704546571133]],[[127.79998370123228,29.540507745394493],[125.99998497636834,30.708139993541643],[124.19998625150441,31.478822672736147],[122.84998720785636,31.766210259727007],[121.94998784542422,31.670513047087127]],[[136.7999773255523,31.286738814391754],[137.24997700676838,32.8123187832876],[136.87399727311598,34.33682825203173]],[[112.0499948586722,12.615395567393394],[111.59999517745614,13.054150695298627],[110.69999581502417,14.801154224791581],[109.79999645259221,15.56116563526334],[108.89999709016024,15.886035719079029],[108.19247759137373,16.043393005208348]],[[107.99999772772827,6.852191098754328],[105.29999964043219,7.075530930004602],[103.04883123518101,7.228431783286316],[101.70000219070414,7.187160551695455],[100.5951029728293,7.198818071264419]],[[113.84999358353615,18.251816319028222],[113.84999358353615,20.796306105108872],[114.2586832940168,22.31829267897149]],[[128.69998306366443,30.5144959597591],[128.69998306366443,31.670513047087127],[128.92498290427227,32.8123187832876],[129.1499827448803,34.31215165223547],[128.99949285148878,35.17037876180022]],[[103.38789161939158,4.128192842398844],[103.95000059678415,3.940475772228814],[106.19999900286416,5.510071711803246],[107.99999772772827,6.852191098754328],[111.03749557593613,9.967915186974132],[112.0499948586722,12.615395567393394],[113.84999358353615,18.251816319028222],[116.99999135204831,19.740987365524937],[120.14998912056028,20.375041253465433],[121.04998848299225,20.58581909604039],[122.84998720785636,21.006499845176737],[124.6499859327203,22.05298561667754],[125.54998529515227,24.12261698700344],[125.9437350162162,25.55188275942587],[126.3374847372803,26.1593079707739],[127.57498386062441,28.55704546571133],[127.79998370123228,29.540507745394493],[128.69998306366443,30.5144959597591],[130.49998178852834,30.320465424761444],[132.74998019460836,30.126049846722832],[134.99997860068837,30.5144959597591],[136.7999773255523,31.286738814391754],[138.59997605041642,32.43331330641721],[139.49997541284839,33.93964008831966],[140.00622505421643,34.405022750715936],[140.09059999384857,34.69072647741027],[140.11872497392463,34.89859296336222]],[[103.98701057056589,1.389451396800233],[104.18740042860793,1.302655424517022],[104.48462521805108,1.468426767331968],[105.29999964043219,2.367912558705407],[106.19999900286416,4.613591578862773],[106.19999900286416,5.510071711803246]]]}},{"type":"Feature","properties":{"id":"seamewe-4","name":"SeaMeWe-4","color":"#187bb6","feature_id":"seamewe-4-0","coordinates":[80.36459874739873,12.86336844112738],"length_km":19175.206028594188},"geometry":{"type":"MultiLineString","coordinates":[[[12.150065628719313,38.38775473578444],[12.262565549023423,38.21117903702318],[12.262565549023423,36.87321951208928],[12.273600696860054,36.389404427280255],[12.642252779453505,35.84291709297635],[14.415591366954907,34.776285033076995],[16.624110734755995,34.25341409729844],[19.279602673945632,34.61921094546477],[22.049976930010512,34.39634721320805],[25.238146034548574,33.683386224796195],[28.649657890488538,32.36444613424394],[29.8935130590948,31.191465077638455]],[[32.54282118230001,29.95909144105574],[32.59687614400701,29.63833609362628],[32.76567602442743,29.344566989489813],[33.04692582518743,28.95155473219332]],[[33.52527548632113,28.161052262220792],[34.48129980906351,27.364667993860262],[35.43754913164762,26.562513149236715],[36.22504857377548,25.75470426341523],[37.237547856509735,24.12261698700344],[38.47504697985549,22.05298561667754],[39.18275647850768,21.481533475502996]],[[100.0579133539753,6.354447103901104],[99.99248340032653,6.205536229871337],[99.67350362629472,5.956366566270154],[99.00000410340806,5.845915088460266],[97.42500521915215,6.405200795356032]],[[62.55002992491184,19.95262290516439],[62.55002992491184,22.469443964829516],[59.85003183761575,24.12261698700435],[58.50003279396771,24.583819112323365],[56.92503390971164,24.89034854048814],[56.33372432860119,25.121690004958644]],[[39.26254642198353,20.375041253465433],[40.61254546563157,18.251816319028222],[41.62504474836765,16.534196198259725],[42.4125441904955,14.801154224791581],[42.918793831863546,13.92930384327183],[43.20004363262354,13.054150695298627],[43.31254355292765,12.834868817846521],[43.39691849315569,12.615395567393394],[43.81879319429551,12.395734000022975],[44.55004267627159,12.120896898039394],[45.450042038703735,12.395734000022975],[48.60003980721571,13.163718917913586],[55.35003502545574,15.669513225155248],[58.95003247518379,17.395022634700517],[62.55002992491184,19.95262290516439],[66.60002705585578,19.95262290516439],[70.20002450558383,19.529070924350908],[72.87590260996693,19.07607425728523]],[[13.05006499115128,38.38775473578444],[12.600065309935387,38.51986519151931],[12.150065628719313,38.38775473578444],[10.348617229769278,38.38775473578444],[9.000067859611436,38.38775473578444],[8.10006849777537,38.38775473578444],[7.650068816559295,38.651811712711336],[6.187569852607326,41.74435878948223],[5.372530429989069,43.29362778902908]],[[9.867357245811593,37.276816253475154],[9.675067382031267,37.589786573603064],[9.000067859611436,38.38775473578444]],[[7.755438741914387,36.90282046530194],[7.875068657167333,37.23235432155614],[7.987568577471261,37.94551049545967],[8.10006849777537,38.38775473578444]],[[103.64609081207688,1.338585852071497],[103.50000091556807,1.229280895999666],[103.34065102845322,1.299701188578927],[102.68279723997186,1.698782263242901],[102.15000187192003,2.03066189047467],[101.25000250948806,2.480311786858737],[100.46250306736002,3.266814816815666],[99.7875035455361,4.613591578862773],[99.00000410340806,5.286069860821008],[97.42500521915215,6.405200795356032],[95.40000665368001,8.190543417795496],[93.14492825119876,9.52441134501949],[91.80000920395213,10.41081650540272],[90.00001047908802,12.395734000022975],[88.20001175422391,13.492079936238724],[83.70001494206389,13.929255692764443],[81.45001653598388,13.273238157547594],[80.24298739105474,13.06385310188338],[81.45001653598388,11.073982781226615],[82.1250160560201,9.52441134501949],[82.575015737236,7.744852765542954],[82.35001589841602,5.51002310933014],[81.00001685476798,4.613591578862773],[78.69513048465001,4.613591578862773],[76.39024411453221,6.852191098754328],[74.19513367248999,9.52441134501949],[72.84513462884213,13.492128176464083],[72.2250230710558,16.965102599435927],[72.87590260996693,19.07607425728523]],[[79.87208765380376,6.927036656836354],[79.4250179705119,5.957818681088533],[78.46194979910258,4.807252410277582]],[[91.99482906593992,21.42927456664916],[91.80000920395213,19.95262290516439],[88.20001175422391,13.492079936238724]],[[66.60002705585578,19.95262290516439],[66.37502721524774,20.375041253465433],[66.60002705585578,23.298598065875897],[67.02854675228855,24.889731701235817]],[[101.25000250948806,2.480311786858737],[101.70000219070414,2.367912558705407]]]}},{"type":"Feature","properties":{"id":"southeast-asia-japan-cable-sjc","name":"Southeast Asia-Japan Cable (SJC)","color":"#6bbd44","feature_id":"southeast-asia-japan-cable-sjc-0","coordinates":[120.61553906063712,20.15077387441527],"length_km":7935.407364474857},"geometry":{"type":"MultiLineString","coordinates":[[[116.67753158048176,23.355006811273547],[117.1687412325042,22.884654113882444],[118.34999039569617,22.105110548108275],[120.14998912056028,20.163975031975873]],[[115.19999262718419,14.801154224791581],[116.99999135204831,14.147583506948735],[118.79999007691224,13.92930384327183],[120.14998912056028,14.147583506948735],[120.62298878548306,14.088232047424347]],[[116.09999198961616,18.251816319028222],[115.19999262718419,19.529070924350908],[114.7499929459683,20.796306105108872],[114.29999326475222,21.635297384859552],[114.20292333351767,22.22205041973683]],[[104.16846044202524,1.223182297279849],[104.28790035741287,1.25769918146722],[104.51270019816243,1.454368851373345],[105.01874983967218,2.803404866588448],[105.74999932164808,3.701945083003531],[107.09999836529612,4.487428146931985],[107.99999772772827,5.047979159038785],[108.6749972495522,5.496074035021858],[109.34999677137613,5.943831970446426],[111.82499501806417,7.730954611330002],[113.73750165524976,9.954064678408844],[114.7499929459683,13.040451242220165],[115.19999262718419,14.787557926772921],[116.09999198961616,18.238460810952724],[120.14998912056028,20.15077387441527],[121.04998848299225,20.15077387441527],[122.84998720785636,20.572653976019456],[125.99998497636834,21.41290665207303],[130.94998146974444,23.28568165987353],[132.74998019460836,24.109781889526705],[137.24997700676838,27.352178406079517],[138.82497589102445,28.939248910255287],[139.94997509406446,30.889328974889438],[140.39997477528055,32.421443555350706],[139.94997509406446,33.927972678693564],[140.3437248151284,34.393419492403375],[140.25934987430446,34.679162981906806],[140.06247501377248,34.84090484813936],[139.95485509060742,34.965046603583694]],[[114.57069307298597,4.703623084487131],[114.29999326475222,5.061986954416114],[111.82499501806417,7.744889052551447]]]}},{"type":"Feature","properties":{"id":"europe-india-gateway-eig","name":"Europe India Gateway (EIG)","color":"#a35e29","feature_id":"europe-india-gateway-eig-0","coordinates":[2.5644083518901257,37.80327438205269],"length_km":14679.44840081802},"geometry":{"type":"MultiLineString","coordinates":[[[-5.624921779312721,35.92243557734424],[-5.34758197578279,36.15601951413446]],[[32.65318110412008,29.113614162980063],[33.455063036063365,28.161052262220792],[34.36879988875958,27.364667993860262],[35.26879925119155,26.562513149236715],[36.000048733167624,25.75470426341523],[37.01254801590261,24.12261698700344],[38.250047139247634,22.05298561667754],[39.037546581375494,20.375041253465433],[40.387545625023535,18.251816319028222],[41.4000449077607,16.534196198259725],[42.30004427019158,14.801154224791581],[42.806293911559614,13.92930384327183],[43.14379367247158,13.054150695298627],[43.28441857285158,12.834868817846521],[43.368793513079616,12.615395567393394],[43.76254323414373,12.395734000022975],[44.55004267627159,12.065895273570327],[45.450042038703735,12.285833556268383],[48.60003980721571,13.054150695298627],[55.35003502545574,15.452760959322058],[58.95003247518379,17.180187287481317],[62.3250300843038,19.529070924350908],[66.60002705585578,19.740987365524937],[70.20002450558383,19.42300815558179],[72.87590260996693,19.07607425728523]],[[29.70093319552029,31.072270031660306],[27.900054471279375,32.052708023486204],[25.200056383983473,32.85958149046064],[22.050058614875596,33.59673284538102],[19.350060528175415,33.565491482352044],[16.65006244087933,33.189714664600466],[14.400064034799321,33.98629373718467],[12.31881550918157,35.419780517080355],[12.037565708415386,35.78566189952622],[11.925065788111276,36.24065523321488],[11.925065786919474,37.23235432155614],[10.91256650537538,37.67887792909206],[10.348617229769278,38.03417390064187],[9.000067859611436,38.03417390064187],[5.400070410479286,38.122730108392204],[2.25007264196731,37.76786242517874],[-2.249924170192707,36.05897312258681],[-4.499922576272716,35.96797434759339],[-5.624921779312721,35.92243557734424],[-6.299921301136742,35.876870570092834],[-8.999919388432733,36.05897312258681],[-9.899918750864702,36.42191605012598],[-11.249917794512742,37.94551049545967],[-13.499916200592752,39.6983233549332],[-13.499916200592752,44.05151922873524],[-9.449919069648717,48.70423463096067],[-7.649920344784692,49.58728674004685],[-6.074881460557064,50.31116725161073],[-4.544402544762735,50.82820142743812]],[[-11.249917794512742,37.94551049545967],[-10.349918432080775,37.94551049545967],[-9.337419149344699,38.122730108392204],[-9.102749315587026,38.4430794831419]],[[14.400064034799321,33.98629373718467],[13.500064672367355,33.09551711711581]],[[7.42672897477544,43.7382556879356],[7.31256905564733,43.401144973153954],[7.425068975951258,41.74435878948223],[7.969108590548607,39.42351982330401],[8.325068338383225,38.651811712711336],[8.437568258687335,38.38775473578444],[9.000067859611436,38.03417390064187]],[[44.55004267627159,12.065895273570327],[43.65004331383962,11.818224495851856],[43.16164365982675,11.573660387694234]],[[38.250047139247634,22.05298561667754],[39.18275647850768,21.481533475502996]],[[56.33372432860119,25.121690004958644],[56.92503390971164,24.660522249648846],[57.88604869707389,23.67872371194431]],[[58.162533033055745,23.968510996734643],[58.72503263457575,24.32780311165181],[59.85003183761575,24.019900203433572],[62.3250300843038,22.469443964829516],[62.3250300843038,19.529070924350908]]]}},{"type":"Feature","properties":{"id":"asia-africa-europe-1-aae-1","name":"Asia Africa Europe-1 (AAE-1)","color":"#a1489b","feature_id":"asia-africa-europe-1-aae-1-0","coordinates":[50.2473894305509,13.095718171129844],"length_km":23146.91440727875},"geometry":{"type":"MultiLineString","coordinates":[[[107.07919838003114,10.342138429683002],[107.77499788712005,9.85709470870232],[108.6749972495522,9.746236973759974],[110.24999613380828,9.967915186974132]],[[94.39121736831608,16.85803225570866],[93.60000792881607,16.749771315644697],[92.25000888516803,14.801154224791581],[90.90000984151999,9.52441134501949],[90.4500101603039,6.181561339870244],[90.00001047908802,4.613591578862773]],[[19.350060528175415,34.12610104005753],[19.350060528175415,37.67887792909206],[18.957770806077196,39.48474996079946],[18.787560926655413,40.04369219283004],[18.675061006351484,40.38732029077508],[18.00006148452737,40.89949091487166],[16.868812285914967,41.12570905852263]],[[45.033542333756095,12.800877546853616],[45.450042038703735,11.84577637362577]],[[22.57920153405357,33.894592669629816],[22.950057977903466,35.419780517080355],[23.42456935990921,35.78566189952622],[23.737557420031504,35.78566189952622],[24.012167225495322,35.512042558637575]],[[103.50674091079348,10.63040170321047],[103.72500075617612,9.52441134501949],[103.72500075617612,8.190543417795496],[103.04883123518101,7.340023741610628]],[[97.42500521915215,5.957818681088533],[99.00000410340806,6.069699469736006],[100.06611334816643,6.613518860854109]],[[100.5951029728293,7.198818071264419],[101.70000219070414,7.24296510649207],[103.04883123518101,7.340023741610628],[105.29999964043219,7.29876275445952],[107.99999772772827,8.07917551824101],[110.24999613380828,9.967915186974132],[111.59999517745614,12.615395567393394],[113.6249937429283,18.251816319028222],[113.73749366323221,20.796306105108872],[114.26024329291157,22.20803425139447]],[[58.95003247518379,23.60821444135838],[59.85003183761575,23.50508968095737]],[[61.20003088126379,17.395022634700517],[61.20003088126379,22.469443964829516],[59.85003183761575,23.50508968095737],[59.04197976437238,23.912037834470418],[58.50003279396771,24.199600518565422],[56.92503390971164,24.788256058863375],[56.33372432860119,25.121690004958644],[56.92503390971164,25.348717422116714],[56.98128386986378,26.1593079707739],[56.812533989407704,26.562513149236715],[56.2500343878877,26.663094151095223],[55.80003470667163,26.31067461914043],[55.35003502545574,26.1593079707739],[53.55003630059162,26.1593079707739],[52.20003725694376,25.957179978764344],[51.519277739200085,25.294608758024626]],[[72.87590260996693,19.07607425728523],[71.77502338983992,16.965102599435927]],[[5.372530429989069,43.29362778902908],[6.300069772911254,41.74435878948223],[7.875068657167333,38.651811712711336],[8.212568418079297,38.38775473578444],[9.000067859611436,38.122730108392204],[10.348617229769278,38.122730108392204],[10.91256650537538,37.76786242517874],[12.037565707223584,37.23235432155614],[12.037565708415386,36.24065523321488],[12.37506546932735,35.78566189952622],[12.825065150547426,35.419780517080355],[14.400064034799321,34.405022750715936],[16.65006244087933,33.75276987113061],[19.350060528175415,34.12610104005753],[22.050058614875596,34.017381950753595],[25.200056383983473,33.28381101905092],[27.900054471279375,32.33831157801293],[29.70093319552029,31.072270031660306]],[[32.65318110412008,29.113614162980063],[33.39891307584246,28.161052262220792],[33.97505016769547,27.364667993860262],[34.76254960982351,26.562513149236715],[35.32504921134351,25.75470426341523],[36.33754849407959,24.12261698700344],[37.57504761742352,22.05298561667754],[38.36254705955156,20.375041253465433],[39.7125461031996,18.251816319028222],[40.72504538593768,16.534196198259725],[41.90629454912765,14.801154224791581],[42.4125441904955,13.92930384327183],[42.94691881193962,13.054150695298627],[43.1859811425856,12.834868817846521],[43.270356082813635,12.615395567393394],[43.56566837361158,12.395734000022975],[44.55004267627159,11.790718790556442],[45.450042038703735,11.84577637362577],[48.60003980781179,12.615395567393394],[55.35003502545574,14.5835116451186],[58.95003247518379,16.10232559580297],[61.20003088126379,17.395022634700517],[66.60002705585578,18.465364393137126],[71.77502338983992,16.965102599435927],[71.94513526640998,13.492128176464083],[73.29513431005802,9.52441134501949],[75.49024475210024,6.852191098754328],[78.69513048465001,4.164912849976942],[79.65001781052403,3.641132700076536],[81.00001685476798,3.266814816815666],[85.500013666928,3.715978119298069],[90.00001047908802,4.613591578862773],[92.70000856638391,5.061986954416114],[94.27500745064,6.05726941024067],[95.38036761595671,6.386923570693734],[95.38114147367385,6.387150045560412],[95.3819171164044,6.387368581851069],[95.38269448296793,6.387579177173141],[95.38347351218404,6.387781829134064],[95.38425414287228,6.387976535341275],[95.38503631385225,6.388163293402209],[95.3858199639435,6.388342100924303],[95.38660503196563,6.388512955514994],[95.38739145673821,6.388675854781716],[95.3881791770808,6.388830796331908],[95.388968131813,6.388977777773004],[95.38975825975437,6.38911679671244],[95.3905494997245,6.389247850757656],[95.39134179054295,6.389370937516083],[95.3921350710293,6.389486054595161],[95.39292928000313,6.389593199602325],[95.39372435628403,6.389692370145011],[95.39452023869154,6.389783563830655],[95.39531686604526,6.389866778266694],[95.39611417716478,6.389942011060564],[95.39691211086965,6.390009259819701],[95.39771060597947,6.390068522151541],[95.39850960131379,6.390119795663521],[95.3993090356922,6.390163077963077],[95.40010884793428,6.390198366657645],[95.4009089768596,6.390225659354662],[95.40170936128773,6.390244953661562],[95.40250994003826,6.390256247185783],[95.40331065193077,6.390259537534762],[95.40411143578481,6.390254822315933],[95.40491223041998,6.390242099136734],[95.40571297465584,6.3902213656046],[95.40651360731198,6.390192619326969],[95.40731406720798,6.390155857911275],[95.4081142931634,6.390111078964956],[95.40891422399781,6.390058280095448],[95.40971379853082,6.389997458910186],[95.41051295558196,6.389928613016608],[95.41131163397085,6.389851740022148],[95.41210977251704,6.389766837534244],[95.41290731004013,6.389673903160332],[95.41370418535966,6.389572934507847],[95.41450033729524,6.389463929184227],[95.41529570466642,6.389346884796907],[95.4160902262928,6.389221798953324],[95.41688384099393,6.389088669260913],[95.41767648758942,6.388947493327112],[95.4184681048988,6.388798268759357],[95.42004800693766,6.388475664151725],[95.42004800693766,6.388475664151725],[97.42500521915215,5.957818681088533],[99.67500362523216,5.733989114150127]],[[43.14799366949638,11.594869371447825],[43.65004331383962,11.735552251813294],[44.55004267627159,11.790718790556442]],[[39.18275647850768,21.481533475502996],[37.57504761742352,22.05298561667754]],[[67.02854675228855,24.889731701235817],[65.25002801161183,24.1568376182792],[61.20003088126379,22.469443964829516]]]}},{"type":"Feature","properties":{"id":"bay-of-bengal-gateway-bbg","name":"Bay of Bengal Gateway (BBG)","color":"#a4332b","feature_id":"bay-of-bengal-gateway-bbg-0","coordinates":[73.99739965125022,9.217315450838143],"length_km":8808.057652704862},"geometry":{"type":"MultiLineString","coordinates":[[[79.88937764155526,6.82077608459704],[79.53751789081602,5.957818681088533],[78.45083428096896,4.580645089775046]],[[80.24298739105474,13.06385310188338],[81.45001653598388,12.615395567393394],[83.70001494206389,12.175887185507976],[89.10001111665605,10.85308969074528],[93.1500082476,9.296323017976968],[95.40000665368001,7.967776882259704],[97.42500521915215,7.131299528983607],[99.67500362523216,5.622041180883233]],[[72.00002323044777,16.965102599435927],[72.87590260996693,19.07607425728523]],[[97.42500521915215,7.131299528983607],[95.40000665368001,6.504566780877437],[94.27500745064,6.380356264139964],[92.70000856638391,5.510071711803246],[90.00001047908802,5.510071711803246],[85.500013666928,4.613591578862773],[81.00001685476798,3.491423322320592],[79.65001781052403,3.865649782482034],[78.69513048465001,4.389285926050993],[75.94024443331631,6.852191098754328],[73.74513399127409,9.52441134501949],[72.39513494762605,13.492128176464083],[72.00002323044777,16.965102599435927],[66.82502689646383,20.58581909604039],[62.662529845215765,23.91710129093513],[59.85003265596724,24.839308763626327],[58.72503263457575,24.94136317175375],[56.92503390971164,25.017845517489846],[56.33372432860119,25.121690004958644]],[[58.72503263457575,24.94136317175375],[58.162533033055745,24.01990020343248]]]}},{"type":"Feature","properties":{"id":"indonesia-global-gateway-igg-system","name":"Indonesia Global Gateway (IGG) System","color":"#5a9f43","feature_id":"indonesia-global-gateway-igg-system-0","coordinates":[113.43510331475238,-5.562666118998097],"length_km":5150.006088200784},"geometry":{"type":"MultiLineString","coordinates":[[[116.83129147155694,-1.265389667588013],[117.08124129449006,-1.402870227222247],[117.85190631946367,-1.51533365197483]],[[120.59998880177636,1.918228780215599],[118.79999007691224,2.817450442654169],[117.89999071448027,3.154491498099848],[117.67499087387223,3.154491498099848]],[[117.4499910332642,-4.37714437553184],[118.79999007691224,-4.937784304559489],[119.41238964308275,-5.152180217334703]],[[116.58310164737706,-5.749115659923423],[116.54999167083223,-6.616650693475355],[115.8749921490083,-8.178490278944933],[115.84442576381582,-8.371311450744278],[115.69240000000046,-8.405888110965844]],[[114.46970572643436,-5.945707155070644],[113.39999390232026,-6.616650693475355],[113.28349007860268,-6.902248768835711]],[[107.21249828560005,-4.60145376483711],[106.76249860438416,-5.273944363641298],[106.83339855415794,-6.1289648492105]],[[101.72812717078021,1.749465440761394],[102.15000187192003,1.74956539407541],[102.68279723997186,1.530149411893926],[103.34065102845322,1.215421560433802],[103.50000091556807,1.187252773694101],[103.89883584657089,1.191468968816909],[104.1378104637381,1.238391276726424],[104.28790035741287,1.173518198634015]],[[104.62500011860807,1.131014326431719],[104.8499999592161,1.018534216615524],[105.29999964043219,1.018534216615524],[106.64999868408005,-0.331409329660265],[107.0366484101739,-2.130918480960333],[107.15309832767961,-3.029995968008661],[107.21249828560005,-4.60145376483711],[112.0499948586722,-5.049857167366764],[114.46970572643436,-5.945707155070644],[116.58310164737706,-5.749115659923423],[117.4499910332642,-4.37714437553184],[117.59999092700289,-2.730375485267853],[117.85190631946367,-1.51533365197483],[118.54128832686715,-0.810555324740758],[119.24998975693651,0.568578852526193],[119.47498959814027,1.018534216615524],[120.59998880177636,1.918228780215599],[124.19998625150441,1.918228780215599],[124.8396357983706,1.490779296094715]]]}},{"type":"Feature","properties":{"id":"seamewe-5","name":"SeaMeWe-5","color":"#c71f8e","feature_id":"seamewe-5-0","coordinates":[41.37831144972093,16.761911867249623],"length_km":19035.960127238235},"geometry":{"type":"MultiLineString","coordinates":[[[38.10697724059967,24.070648010417838],[37.80004745803156,24.01990020343248],[37.34051417738748,24.03538146018735],[37.12504793620581,24.12261698700344]],[[42.95452380595619,14.797809010241023],[42.75004395140765,14.746763925028056],[42.35629423034354,14.801154224791581]],[[101.25000250948806,2.367912558705407],[101.36255242975687,2.143087178471855]],[[99.56250370492806,4.613591578862773],[98.67598433294692,3.752031394331533]],[[27.900054471279375,32.43331330641721],[28.800053833711523,34.31215165223547],[28.575053993103488,36.1498667868178],[28.462554072799378,36.51238821239364]],[[16.65006244087933,33.93964008831966],[16.425050392653056,34.649035185051616],[16.260905867940522,35.801066734347735],[16.08756283935933,36.87321951208928],[15.750063078447363,37.32187222983504],[15.06744356202158,37.51344748573393]],[[94.39121736831608,16.85803225570866],[93.60000792881607,16.965102599435927],[91.80000920395213,17.82393441253792]],[[90.11661039648769,21.820818029820398],[90.90000984151999,20.375041253465433],[91.80000920395213,17.82393441253792],[91.80000920395213,14.801154224791581],[90.4500101603039,9.52441134501949],[90.22501031969605,7.967776882259704],[90.00001047908802,7.29876275445952]],[[67.02854675228855,24.889731701235817],[65.25002801161183,24.054148269801107],[63.22502944673577,22.469443964829516]],[[65.75491565746178,16.88633703423319],[63.45836261477361,20.88077116880577],[63.22502944673577,22.469443964829516],[59.85003183761575,24.430271928050523],[58.50003279396771,24.73717827217609],[56.92503390971164,24.94136317175375],[56.33372432860119,25.121690004958644]],[[5.930340034831254,43.125291587014985],[6.300069772911254,42.743713464436695],[6.525069613519291,41.74435878948223],[8.10006849777537,38.651811712711336],[8.325068338383225,38.38775473578444],[9.000067859611436,38.21117903702318],[10.348617229769278,38.21117903702318],[10.91256650537538,37.85673997565852],[12.150065627527512,37.23235432155614],[12.150065628719313,36.24065523321488],[12.487565389631278,35.78566189952622],[12.99381503100241,35.419780517080355],[14.400064034799321,34.54413627297858],[16.65006244087933,33.93964008831966],[19.350060528175415,34.31215165223547],[22.050058614875596,34.157137999942634],[25.200056383983473,33.42476549736121],[27.900054471279375,32.43331330641721],[29.70093319552029,31.072270031660306]],[[32.65318110412008,29.113614162980063],[33.46910052611805,28.161052262220792],[34.42504984891155,27.364667993860262],[35.35317419141958,26.562513149236715],[36.11254865347155,25.75470426341523],[37.12504793620581,24.12261698700344],[38.36254705955156,22.05298561667754],[39.1500465016796,20.375041253465433],[40.500045545327644,18.251816319028222],[41.51254482806354,16.534196198259725],[42.35629423034354,14.801154224791581],[42.86254387171158,13.92930384327183],[43.17191865254765,13.054150695298627],[43.29848106288953,12.834868817846521],[43.38285600311756,12.615395567393394],[43.53754339353551,12.395734000022975],[44.55004267627159,11.735650161405832],[45.450042038703735,11.735650161405832],[48.60003980781179,12.505588131780646],[55.35003502545574,14.365653759228442],[58.95003247518379,15.886035719079029],[60.975031038271794,15.994209911785974],[65.75491565746178,16.88633703423319],[70.65002418679991,13.41205503289061],[72.10979915852461,9.280734132427842],[74.30490960056683,6.606897166243519],[76.95001972382386,5.510071711803246],[79.65001781111994,5.510071711803246],[80.53985718074962,5.940820740520149]],[[80.53985718074962,5.940820740520149],[81.00001685476798,5.510071711803246],[82.80001557963192,5.510071711803246],[85.500013666928,6.405200795356032],[90.00001047908802,7.29876275445952],[92.70000856638391,6.852191098754328],[94.27500745064,6.852191098754328],[95.40000665368001,6.740481724921185],[97.42500521915215,6.237476972533139],[98.77500426280001,5.286069860821008],[99.56250370492806,4.613591578862773],[100.3500031470561,3.266814816815666],[101.25000250948806,2.367912558705407],[102.15000187192003,1.974446286104158],[102.68279723997186,1.670619462234952],[103.34065102845322,1.271608282704793],[103.50000091556807,1.215271594284278],[103.64609081207688,1.338585852071497]],[[43.16164365982675,11.573660387694234],[43.65004331383962,11.70798932055409],[44.55004267627159,11.735650161405832]],[[59.366662179443516,22.700003992423866],[59.51253207610789,22.884654113882444],[59.85003183761575,24.430271928050523]]]}},{"type":"Feature","properties":{"id":"mednautilus-submarine-system","name":"MedNautilus Submarine System","color":"#53b847","feature_id":"mednautilus-submarine-system-0","coordinates":[25.334243624117097,34.44940588724045],"length_km":6416.471695596696},"geometry":{"type":"MultiLineString","coordinates":[[[28.283554199604758,40.7445175271339],[27.67066463378159,40.571463184888],[27.204354964119698,40.51728976261815],[26.755425282145552,40.45564648003217],[26.64400536107653,40.37549034333631],[26.59292539726206,40.31736166816431],[26.51655545136326,40.27677072796649],[26.447805500066316,40.223703402432285]],[[26.376225550774205,40.14678775490784],[26.344195573464642,40.1045953658932],[26.246055642987802,40.05564908987763],[25.3125563042874,39.524987333511675],[25.3125563042874,38.651811712711336],[25.650056065199365,37.411283634923244],[25.200056383983473,37.23235432155614],[24.63755678246347,37.277126582876754],[24.166306973235315,37.239619217488034],[23.962557260639358,37.589786573603064]],[[25.875055905807404,36.602754740329765],[25.42505622459151,36.602754740329765],[24.975056543375437,36.33133835588799],[24.52505686215936,36.33133835588799],[24.187557101843478,36.51238821239364],[23.850057340335432,37.589786573603064]],[[28.305814183835448,40.72743084616859],[27.678864627972537,40.55231865816978],[27.18003498134806,40.495681130064526],[26.754935282492717,40.43735937424689],[26.677105337628205,40.3798096956335],[26.60562538826515,40.30783009842792],[26.531695440637915,40.26268557434711],[26.481695476058352,40.22138987189183]],[[26.39077554046697,40.13245384394122],[26.34819557063097,40.08674097664053],[26.260015633098497,40.04234480534568],[26.09819574773315,39.97772318814789],[25.42505622459151,39.524987333511675],[25.42505622459151,38.651811712711336],[25.875055905807404,37.411283634923244],[26.212555666719368,37.23235432155614],[26.325055587023478,36.87321951208928],[25.875055905807404,36.602754740329765],[25.933624491168022,35.836586354437685]],[[34.200050008303506,33.09551711711581],[33.750050327087614,33.565491482352044],[33.750050327087614,34.49779087043369],[33.61060042587536,34.82728147271538]],[[24.012167225495322,35.512042558637575],[23.962557260639358,35.78566189952622],[23.850057339143632,36.51238821239364],[23.737557420031504,37.589786573603064]],[[15.06744356202158,37.51344748573393],[15.750063078447363,37.50058844605323],[18.00006148452737,36.51238821239364],[19.342345691678812,36.33070897538892],[22.050058614875596,35.54192681258013],[23.303036242879482,34.96583678559484],[24.435213175209686,34.662356899182655],[31.050052239791533,33.09551711711581],[33.750050327087614,32.33831157801293],[34.76967960477271,32.04501185826483],[34.53754976861957,32.243210016262736],[34.65004968892368,32.62301664000789],[34.97190100000024,32.76170000000019]],[[34.97190100000024,32.76170000000019],[34.200050008303506,33.09551711711581],[31.050052239791533,33.565491482352044],[28.800053833711523,34.31215165223547],[26.574796150245934,35.455341306260905],[25.933624491168022,35.836586354437685],[25.386282830980196,35.785858781085906]],[[24.012167225495322,35.512042558637575],[23.737557420031504,35.83127933955618],[22.950057977903466,35.96797434759339],[18.00006148452737,36.87321951208928],[15.750063078447363,37.589786573603064],[15.06744356202158,37.51344748573393]]]}},{"type":"Feature","properties":{"id":"falcon","name":"FALCON","color":"#c62026","feature_id":"falcon-0","coordinates":[57.68145496109924,24.435579750299368],"length_km":11654.447305007747},"geometry":{"type":"MultiLineString","coordinates":[[[42.95452380595619,14.797809010241023],[42.75004395140765,14.692360031374392],[42.1312943897355,14.801154224791581]],[[48.5317798555716,29.92363278689715],[48.71253972751964,29.540507745394493],[48.60003980721571,29.1482487910328]],[[48.5317798555716,29.92363278689715],[48.60003980721571,29.540507745394493],[48.4875398869116,29.246454972180413]],[[60.627371286941326,25.258664579046147],[59.85003265596724,24.634955698183607]],[[52.182457269397545,16.213003862431094],[52.65003693815965,15.669513225155248],[52.65003693815965,15.23578178303578]],[[75.54513271613803,6.628746603597807],[79.20001812990387,6.628746603597807],[79.87208765380376,6.927036656836354]],[[76.50002004260797,8.190543417795496],[75.54513271613803,6.628746603597807],[74.25002163652778,5.398081130463647],[73.5000221678345,4.16666819886197]],[[72.87590260996693,19.07607425728523],[70.20002450558383,19.740987365524937],[66.60002705585578,20.375041253465433],[63.675029127951845,22.469443964829516],[59.85003265596724,24.634955698183607],[58.50003279396771,23.968510996734643],[58.1762030233719,23.68487753168473],[57.88128323229574,24.12261698700344],[57.37503359092771,25.348717422116714],[57.15003375031967,26.1593079707739],[56.98128386986378,26.562513149236715],[56.74300403866356,26.97318161868908],[56.27415437080105,27.18725294593831],[56.2500343878877,26.813799487940788],[55.80003470667163,26.411476060868516],[55.35003502545574,26.36108632539156],[53.55003630059162,26.36108632539156],[52.875036778767694,26.562513149236715],[52.20003725694376,26.964304734562898],[51.525037735119646,27.364667993860262],[50.175038691471606,27.962503359972466],[49.16253940873571,28.65581241773305],[48.60003980721571,29.1482487910328],[48.4875398869116,29.246454972180413]],[[48.60003980721571,28.853067255226264],[49.16253940873571,28.458185766004554],[50.175038691471606,26.964304734562898],[50.175038691471606,26.461843796188983]],[[50.214198663730556,26.28537535931817],[50.34378857192768,26.461843796188983],[50.51253845238375,26.461843796188983],[50.57601840741415,26.229494838391265],[51.187537974207686,26.36108632539156],[51.637537655423756,26.260240971577822],[51.75003757572769,26.05828756029904],[51.519277739200085,25.294608758024626],[52.20003725694376,25.75470426341523],[53.55003630059162,25.855985466072205],[55.1250351848477,25.855985466072205],[55.1250351848477,25.55188275942587],[55.30853505485483,25.269353998130182],[55.237585105116516,25.55188275942587],[55.35003502545574,25.75470426341523],[55.80003470667163,26.10880867686235]],[[56.362534308191634,26.461843796188983],[56.58753414879967,26.461843796188983],[56.756284029255745,26.1593079707739],[57.26253367062378,25.348717422116714],[57.825033272143784,24.12261698700344],[58.1762030233719,23.68487753168473],[58.50003279396771,23.81422051502533],[58.95003247518379,23.65974644119216],[59.85003183761575,22.884654113882444],[60.07503167703199,22.469443964829516],[59.85003183761575,19.104405475930452],[58.95003247518379,18.038005439608753],[55.35003502545574,16.318380026359527],[52.65003693815965,15.23578178303578],[48.60003980721571,13.492128176464083],[45.450042038703735,12.72515592356304],[44.55004267627159,12.230866087669199],[43.67816829391569,12.395734000022975],[43.3266060429656,12.615395567393394],[43.24223110273756,12.834868817846521],[43.05941873224354,13.054150695298627],[42.637544031103545,13.92930384327183],[42.1312943897355,14.801154224791581],[41.062545146848734,16.534196198259725],[40.05004586411157,18.251816319028222],[38.70004682046353,20.375041253465433],[37.91254737833549,22.05298561667754],[36.67504825499064,24.12261698700344],[35.66254897225548,25.75470426341523],[35.01567443050744,26.562513149236715],[34.200050008303506,27.364667993860262],[33.497250506175384,28.161052262220792],[32.99067586503547,28.95155473219332],[32.70942606427547,29.344566989489813],[32.54067268382206,29.63833609362628],[32.54068118381597,29.974234637029465]],[[38.70004682046353,20.375041253465433],[37.58923757400337,19.81323778521068],[37.21967786917097,19.61556659454616]]]}}]} \ No newline at end of file diff --git a/frontend/src/__tests__/hooks/useDataPollingViewport.test.ts b/frontend/src/__tests__/hooks/useDataPollingViewport.test.ts new file mode 100644 index 0000000..39121c1 --- /dev/null +++ b/frontend/src/__tests__/hooks/useDataPollingViewport.test.ts @@ -0,0 +1,26 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { VIEWPORT_COMMITTED_EVENT } from '@/components/map/hooks/useViewportBounds'; +import { setLiveDataBounds } from '@/lib/liveDataViewport'; + +describe('viewport fast refetch wiring', () => { + beforeEach(() => { + vi.useFakeTimers(); + setLiveDataBounds({ south: 10, west: 20, north: 12, east: 22 }); + }); + + afterEach(() => { + setLiveDataBounds(null); + vi.useRealTimers(); + vi.restoreAllMocks(); + }); + + it('VIEWPORT_COMMITTED_EVENT is a stable custom event name', () => { + expect(VIEWPORT_COMMITTED_EVENT).toBe('shadowbroker:viewport-committed'); + const handler = vi.fn(); + window.addEventListener(VIEWPORT_COMMITTED_EVENT, handler); + window.dispatchEvent(new CustomEvent(VIEWPORT_COMMITTED_EVENT)); + expect(handler).toHaveBeenCalledTimes(1); + window.removeEventListener(VIEWPORT_COMMITTED_EVENT, handler); + }); +}); diff --git a/frontend/src/__tests__/lib/submarineCables.test.ts b/frontend/src/__tests__/lib/submarineCables.test.ts new file mode 100644 index 0000000..3c3401d --- /dev/null +++ b/frontend/src/__tests__/lib/submarineCables.test.ts @@ -0,0 +1,61 @@ +import { sanitizeSubmarineCables } from '@/lib/submarineCables'; + +describe('sanitizeSubmarineCables', () => { + it('removes synthetic corridor overlays', () => { + const out = sanitizeSubmarineCables({ + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + properties: { name: 'SEA-ME-WE Corridor' }, + geometry: { + type: 'LineString', + coordinates: [ + [-5, 51], + [73, 17], + ], + }, + }, + { + type: 'Feature', + properties: { name: 'FEA' }, + geometry: { + type: 'LineString', + coordinates: [ + [32, 30], + [33, 29], + ], + }, + }, + ], + }); + expect(out.features).toHaveLength(1); + expect(out.features[0].properties?.name).toBe('FEA'); + }); + + it('splits trans-ocean jumps into separate segments', () => { + const out = sanitizeSubmarineCables({ + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + properties: { name: 'Test Pacific' }, + geometry: { + type: 'LineString', + coordinates: [ + [-120, 35], + [-125, 36], + [100, 13], + [101, 12], + ], + }, + }, + ], + }); + const geom = out.features[0].geometry; + expect(geom?.type).toBe('MultiLineString'); + if (geom?.type === 'MultiLineString') { + expect(geom.coordinates).toHaveLength(2); + } + }); +}); diff --git a/frontend/src/__tests__/map/telegramGeoJSON.test.ts b/frontend/src/__tests__/map/telegramGeoJSON.test.ts new file mode 100644 index 0000000..22afc4f --- /dev/null +++ b/frontend/src/__tests__/map/telegramGeoJSON.test.ts @@ -0,0 +1,94 @@ +import { describe, expect, it } from 'vitest'; +import { + applyTelegramAlertAvoidance, + buildTelegramOsintGeoJSON, + telegramClusterKey, + telegramClusterNearNewsAlert, + telegramMapPinCoords, + TELEGRAM_ALERT_AVOID_METERS, +} from '@/components/map/geoJSONBuilders'; + +describe('telegramMapPinCoords', () => { + it('stays on the geocoded city when no threat alert overlaps', () => { + const [lat, lng] = telegramMapPinCoords(31.046, 34.851, false); + expect(lat).toBe(31.046); + expect(lng).toBe(34.851); + }); + + it('nudges ~5 mi northeast only when avoiding an alert', () => { + const [lat, lng] = telegramMapPinCoords(31.046, 34.851, true); + expect(lat).toBeGreaterThan(31.046); + expect(lng).toBeGreaterThan(34.851); + const toRad = (deg: number) => (deg * Math.PI) / 180; + const dLat = toRad(lat - 31.046); + const meters = 6371000 * dLat; + expect(meters).toBeGreaterThan(4_000); + expect(meters).toBeLessThan(TELEGRAM_ALERT_AVOID_METERS + 2_000); + }); +}); + +describe('telegramClusterNearNewsAlert', () => { + it('detects news on the same city grid', () => { + const news = [{ coords: [31.046, 34.851] as [number, number] }]; + expect(telegramClusterNearNewsAlert(31.049, 34.849, news)).toBe(true); + expect(telegramClusterNearNewsAlert(50.45, 30.52, news)).toBe(false); + }); +}); + +describe('telegramClusterKey', () => { + it('groups nearby coordinates to the same city bucket', () => { + expect(telegramClusterKey(50.451, 30.521)).toBe(telegramClusterKey(50.449, 30.519)); + }); +}); + +describe('buildTelegramOsintGeoJSON', () => { + it('places the dot on the geocoded city by default', () => { + const geo = buildTelegramOsintGeoJSON({ + posts: [ + { + id: 'tg-1', + title: 'Strike near Kyiv', + coords: [50.45, 30.52], + }, + ], + }); + const feature = geo?.features[0]; + expect(feature).toBeTruthy(); + const [lng, lat] = feature!.geometry!.coordinates as [number, number]; + expect(lat).toBeCloseTo(50.45, 2); + expect(lng).toBeCloseTo(30.52, 2); + }); + + it('merges posts in the same city into one pin', () => { + const geo = buildTelegramOsintGeoJSON({ + posts: [ + { id: 'a', title: 'Post A', coords: [50.45, 30.52] }, + { id: 'b', title: 'Post B', coords: [50.451, 30.521] }, + { id: 'c', title: 'Post C', coords: [48.0, 37.8] }, + ], + }); + expect(geo?.features).toHaveLength(2); + const kyiv = geo?.features.find((f) => f.properties?.post_count === 2); + expect(kyiv).toBeTruthy(); + expect(kyiv?.properties?.id).toBe(telegramClusterKey(50.45, 30.52)); + }); +}); + +describe('applyTelegramAlertAvoidance', () => { + it('offsets only clusters that share a grid cell with a news alert', () => { + const geo = buildTelegramOsintGeoJSON({ + posts: [ + { id: 'il', title: 'Israel post', coords: [31.046, 34.851] }, + { id: 'ua', title: 'Kyiv post', coords: [50.45, 30.52] }, + ], + }); + const placed = applyTelegramAlertAvoidance(geo, [{ coords: [31.046, 34.851] }]); + const israel = placed?.features.find((f) => f.properties?.id === telegramClusterKey(31.046, 34.851)); + const kyiv = placed?.features.find((f) => f.properties?.id === telegramClusterKey(50.45, 30.52)); + const [ilLng, ilLat] = israel!.geometry!.coordinates as [number, number]; + const [uaLng, uaLat] = kyiv!.geometry!.coordinates as [number, number]; + expect(ilLat).toBeGreaterThan(31.046); + expect(uaLat).toBeCloseTo(50.45, 2); + expect(uaLng).toBeCloseTo(30.52, 2); + }); +}); diff --git a/frontend/src/__tests__/utils/viewportPrivacy.test.ts b/frontend/src/__tests__/utils/viewportPrivacy.test.ts index c5be8a2..38c4df1 100644 --- a/frontend/src/__tests__/utils/viewportPrivacy.test.ts +++ b/frontend/src/__tests__/utils/viewportPrivacy.test.ts @@ -5,6 +5,10 @@ import { coarsenViewBounds, expandBoundsToRadius, } from '@/lib/viewportPrivacy'; +import { + liveDataBoundsKey, + setLiveDataBounds, +} from '@/lib/liveDataViewport'; describe('viewport privacy helper', () => { it('coarsens narrow bounds outward without clipping the original view', () => { @@ -45,6 +49,14 @@ describe('viewport privacy helper', () => { expect(b).toBe(a); }); + it('liveDataBoundsKey matches quantized fetch params and clears for world view', () => { + setLiveDataBounds({ south: 33.6, west: -84.5, north: 33.8, east: -84.2 }); + expect(liveDataBoundsKey()).toBe('33,-85,34,-84'); + + setLiveDataBounds(null); + expect(liveDataBoundsKey()).toBeNull(); + }); + it('expands bounds to a fixed preload radius around the current view center', () => { const original = { south: 39.55, diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index e0606d6..0a67e19 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -21,6 +21,10 @@ import InfonetTerminal from '@/components/InfonetTerminal'; import { leaveWormhole, fetchWormholeState } from '@/mesh/wormholeClient'; import { teardownWormholeOnClose } from '@/lib/wormholeTeardown'; import ShodanPanel from '@/components/ShodanPanel'; +import ReconPanel from '@/components/ReconPanel'; +import ScmPanel from '@/components/ScmPanel'; +import EntityGraphPanel from '@/components/EntityGraphPanel'; +import { isEntityGraphEligible } from '@/lib/entityGraph'; import AIIntelPanel from '@/components/AIIntelPanel'; import GlobalTicker from '@/components/GlobalTicker'; import ErrorBoundary from '@/components/ErrorBoundary'; @@ -71,6 +75,10 @@ export default function Dashboard() { useDataPolling(); const { mouseCoords, locationLabel, handleMouseCoords } = useReverseGeocode(); const [selectedEntity, setSelectedEntity] = useState(null); + const [showEntityGraph, setShowEntityGraph] = useState(false); + useEffect(() => { + setShowEntityGraph(false); + }, [selectedEntity]); const [trackedSdr, setTrackedSdr] = useState(null); const [trackedScanner, setTrackedScanner] = useState(null); const { regionDossier, regionDossierLoading, handleMapRightClick } = useRegionDossier( @@ -186,6 +194,11 @@ export default function Dashboard() { sentinel_hub: false, viirs_nightlights: false, road_corridor_trends: false, + malware_c2: false, + submarine_cables: false, + scm_suppliers: false, + cyber_threats: false, + telegram_osint: true, // Hazards — no fire, rest ON earthquakes: true, firms: false, @@ -636,7 +649,15 @@ export default function Dashboard() { )} - {/* 4. AI INTEL (Below Shodan) */} + {/* 4. RECON + SCM */} + {secondaryBootReady && ( +
+ + +
+ )} + + {/* 5. AI INTEL */} {secondaryBootReady && (
{ + if (isEntityGraphEligible(selectedEntity)) setShowEntityGraph(true); + }} onArticleClick={(idx, lat, lng, title) => { if (lat !== undefined && lng !== undefined) { setFlyToLocation({ lat, lng, ts: Date.now() }); @@ -989,6 +1013,10 @@ export default function Dashboard() { onSettingsClick={() => setSettingsOpen(true)} /> + {showEntityGraph && selectedEntity && isEntityGraphEligible(selectedEntity) && ( + setShowEntityGraph(false)} /> + )} + {/* INFONET TERMINAL */} ; +} + +interface GraphLink { + source: string; + target: string; + label: string; +} + +interface Props { + entity: SelectedEntity | null; + onClose: () => void; +} + +const TYPE_COLORS: Record = { + aircraft: 'text-cyan-300', + vessel: 'text-cyan-400', + company: 'text-amber-300', + person: 'text-violet-300', + country: 'text-emerald-300', + sanction: 'text-red-300', + ip: 'text-orange-300', + event: 'text-yellow-300', +}; + +export default function EntityGraphPanel({ entity, onClose }: Props) { + const [isMinimized, setIsMinimized] = useState(false); + const [nodes, setNodes] = useState([]); + const [links, setLinks] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const loadGraph = useCallback(async () => { + if (!entity || !isEntityGraphEligible(entity)) return; + const type = mapEntityToGraphType(entity.type); + if (!type) return; + const id = String(entity.name || entity.extra?.callsign || entity.extra?.registration || entity.id); + const params = new URLSearchParams({ type, id }); + if (entity.extra?.registration) params.set('registration', String(entity.extra.registration)); + if (entity.extra?.icao24) params.set('icao24', String(entity.extra.icao24)); + if (entity.extra?.model) params.set('model', String(entity.extra.model)); + + setLoading(true); + setError(null); + try { + const res = await fetch(`${API_BASE}/api/entity/expand?${params}`); + const data = await res.json(); + if (!res.ok) throw new Error(data.detail || data.error || 'Expand failed'); + setNodes(data.nodes || []); + setLinks(data.links || []); + } catch (err) { + setError(err instanceof Error ? err.message : 'Graph unavailable'); + setNodes([]); + setLinks([]); + } finally { + setLoading(false); + } + }, [entity]); + + useEffect(() => { + if (entity) loadGraph(); + else { + setNodes([]); + setLinks([]); + } + }, [entity, loadGraph]); + + if (!entity || !isEntityGraphEligible(entity)) return null; + + return ( +
+
setIsMinimized((prev) => !prev)} + > +
+ + + ENTITY GRAPH + +
+
+ + {isMinimized ? ( + + ) : ( + + )} +
+
+ + {!isMinimized && ( +
+
+ {entity.type.toUpperCase()} · {entity.name || entity.id} +
+ + {loading && ( +
+ + RESOLVING… +
+ )} + + {error && ( +
+ {error} +
+ )} + + {!loading && !error && ( + <> +
+ {nodes.map((n) => ( +
+
+ {n.type} +
+
{n.label}
+
+ ))} +
+ + {links.length > 0 && ( +
+
RELATIONSHIPS
+ {links.slice(0, 24).map((l, i) => ( +
+ {l.label}: {l.source.split(':').pop()} → {l.target.split(':').pop()} +
+ ))} +
+ )} + + )} +
+ )} +
+ ); +} diff --git a/frontend/src/components/MapLegend.tsx b/frontend/src/components/MapLegend.tsx index febb7a3..0ed0065 100644 --- a/frontend/src/components/MapLegend.tsx +++ b/frontend/src/components/MapLegend.tsx @@ -183,6 +183,7 @@ const LEGEND: LegendCategory[] = [ color: 'text-red-400 border-red-500/30', items: [ { svg: triangle('#ffaa00'), label: 'GDELT / LiveUA event (yellow)' }, + { svg: dot('#ef4444'), label: 'Telegram OSINT post (red, geolocated)' }, { svg: triangle('#ff0000'), label: 'Violent / Kinetic event (red)' }, { svg: ``, diff --git a/frontend/src/components/MaplibreViewer.tsx b/frontend/src/components/MaplibreViewer.tsx index 804c00c..b36dc1f 100644 --- a/frontend/src/components/MaplibreViewer.tsx +++ b/frontend/src/components/MaplibreViewer.tsx @@ -158,16 +158,25 @@ import { UavLabels, EarthquakeLabels, ThreatMarkers, + TelegramOsintMarkers, } from '@/components/map/MapMarkers'; import type { DashboardData, Flight, KiwiSDR, MaplibreViewerProps, Scanner, Ship, SigintSignal } from '@/types/dashboard'; import { useDataKeys } from '@/hooks/useDataStore'; import { useInterpolation } from '@/components/map/hooks/useInterpolation'; import { useClusterLabels } from '@/components/map/hooks/useClusterLabels'; import { spreadAlertItems } from '@/utils/alertSpread'; +import { + applyTelegramAlertAvoidance, + telegramClusterKey, + telegramClusterNearNewsAlert, + telegramMapPinCoords, +} from '@/components/map/geoJSONBuilders'; import { useViewportBounds } from '@/components/map/hooks/useViewportBounds'; +import { getLiveDataBounds } from '@/lib/liveDataViewport'; import { MeasurementLayers } from '@/components/map/layers/MeasurementLayers'; import { buildCctvProxyUrl } from '@/lib/cctvProxy'; +import { sanitizeSubmarineCables } from '@/lib/submarineCables'; import { CctvFullscreenModal } from '@/components/MaplibreViewer/CctvFullscreenModal'; import { SatellitePopup } from '@/components/MaplibreViewer/popups/SatellitePopup'; import { ShipPopup } from '@/components/MaplibreViewer/popups/ShipPopup'; @@ -176,6 +185,7 @@ import { CorrelationPopup } from '@/components/MaplibreViewer/popups/Correlation import { WastewaterPopup } from '@/components/MaplibreViewer/popups/WastewaterPopup'; import { MilitaryBasePopup } from '@/components/MaplibreViewer/popups/MilitaryBasePopup'; import { RegionDossierPanel } from '@/components/MaplibreViewer/popups/RegionDossierPanel'; +import { TelegramOsintPopup } from '@/components/MaplibreViewer/popups/TelegramOsintPopup'; import { buildSentinelTileUrl, hasSentinelCredentials, @@ -294,6 +304,8 @@ const MAP_EXTRA_DATA_KEYS = [ 'commercial_flights', 'correlations', 'crowdthreat', + 'malware_threats', + 'telegram_osint', 'datacenters', 'firms_fires', 'fishing_activity', @@ -1156,6 +1168,30 @@ const MaplibreViewer = ({ const staticUapSightings = activeLayers.uap_sightings ? data?.uap_sightings : undefined; const staticWastewater = activeLayers.wastewater ? data?.wastewater : undefined; const staticCrowdthreat = activeLayers.crowdthreat ? data?.crowdthreat : undefined; + const staticMalwareThreats = activeLayers.malware_c2 ? data?.malware_threats?.threats : undefined; + const staticTelegramOsintPosts = activeLayers.telegram_osint + ? data?.telegram_osint?.posts + : undefined; + + const [submarineCablesGeoJSON, setSubmarineCablesGeoJSON] = useState(null); + useEffect(() => { + if (!activeLayers.submarine_cables) { + setSubmarineCablesGeoJSON(null); + return; + } + let cancelled = false; + fetch('/data/submarine-cables.json') + .then((r) => r.json()) + .then((geo) => { + if (!cancelled) setSubmarineCablesGeoJSON(sanitizeSubmarineCables(geo)); + }) + .catch(() => { + if (!cancelled) setSubmarineCablesGeoJSON(null); + }); + return () => { + cancelled = true; + }; + }, [activeLayers.submarine_cables]); const dynamicMapLayers = useDynamicMapLayersWorker( { @@ -1186,6 +1222,7 @@ const MaplibreViewer = ({ ], { bounds: mapBounds, + serverBboxScoped: getLiveDataBounds() !== null, dtSeconds: dtSeconds.current, trackedIcaos: Array.from(trackedIcaoSet), activeLayers: { @@ -1247,6 +1284,8 @@ const MaplibreViewer = ({ uapSightings: staticUapSightings, wastewater: staticWastewater, crowdthreat: staticCrowdthreat, + malwareThreats: staticMalwareThreats, + telegramOsintPosts: staticTelegramOsintPosts, }, [ staticCctv, @@ -1270,6 +1309,9 @@ const MaplibreViewer = ({ staticUapSightings, staticWastewater, staticCrowdthreat, + staticMalwareThreats, + staticTelegramOsintPosts, + mapZoom, ], { bounds: mapBounds, @@ -1293,6 +1335,8 @@ const MaplibreViewer = ({ uap_sightings: activeLayers.uap_sightings, wastewater: activeLayers.wastewater, crowdthreat: activeLayers.crowdthreat, + malware_c2: activeLayers.malware_c2, + telegram_osint: activeLayers.telegram_osint, }, }, [ @@ -1316,6 +1360,8 @@ const MaplibreViewer = ({ activeLayers.uap_sightings, activeLayers.wastewater, activeLayers.crowdthreat, + activeLayers.malware_c2, + activeLayers.telegram_osint, ], ); @@ -1351,8 +1397,15 @@ const MaplibreViewer = ({ uapSightingsGeoJSON, wastewaterGeoJSON, crowdthreatGeoJSON, + malwareGeoJSON, + telegramOsintGeoJSON, } = staticMapLayers; + const telegramOsintGeoJSONPlaced = useMemo( + () => applyTelegramAlertAvoidance(telegramOsintGeoJSON, data?.news), + [telegramOsintGeoJSON, data?.news], + ); + // Extract cluster label positions via shared hook const shipClusters = useClusterLabels(mapRef, 'ships-clusters-layer', shipsGeoJSON); const eqClusters = useClusterLabels(mapRef, 'eq-clusters-layer', earthquakesGeoJSON); @@ -1659,6 +1712,9 @@ const MaplibreViewer = ({ wastewaterGeoJSON && 'wastewater-dot', wastewaterGeoJSON && 'wastewater-layer', crowdthreatGeoJSON && 'crowdthreat-layer', + malwareGeoJSON && 'malware-clusters', + malwareGeoJSON && 'malware-layer', + submarineCablesGeoJSON && 'submarine-cables-layer', sarAnomaliesGeoJSON && 'sar-anomalies-layer', sarAoisGeoJSON && 'sar-aois-fill', aiIntelGeoJSON && 'ai-intel-clusters', @@ -1731,6 +1787,9 @@ const MaplibreViewer = ({ useImperativeSource(mapForHook, 'uap-sightings-source', uapSightingsGeoJSON, 100); useImperativeSource(mapForHook, 'wastewater-source', wastewaterGeoJSON, 100); useImperativeSource(mapForHook, 'crowdthreat-source', crowdthreatGeoJSON, 100); + useImperativeSource(mapForHook, 'malware-source', malwareGeoJSON, 100); + useImperativeSource(mapForHook, 'telegram-osint-source', telegramOsintGeoJSONPlaced, 100); + useImperativeSource(mapForHook, 'submarine-cables-source', submarineCablesGeoJSON, 600); useImperativeSource(mapForHook, 'ships', shipsGeoJSON, 75); useImperativeSource(mapForHook, 'meshtastic-source', meshtasticGeoJSON, 60); useImperativeSource(mapForHook, 'aprs-source', aprsGeoJSON, 60); @@ -1761,7 +1820,7 @@ const MaplibreViewer = ({ return (
+ {/* Telegram OSINT — one pin per geocoded city; scroll posts in popup */} + + ', ['get', 'post_count'], 1], 14, 11], + 8, + ['case', ['>', ['get', 'post_count'], 1], 20, 16], + 12, + ['case', ['>', ['get', 'post_count'], 1], 26, 22], + ], + 'circle-color': '#ef4444', + 'circle-stroke-width': 0, + 'circle-stroke-color': '#fca5a5', + 'circle-opacity': 0, + }} + /> + + + {/* Malware C2 — abuse.ch Feodo + URLhaus */} + + + + + + {/* Submarine cables — static TeleGeography GeoJSON */} + + + + {/* Ships — rendered below flights (water surface level) */} )} + {activeLayers.telegram_osint && !isMapInteracting && telegramOsintGeoJSONPlaced?.features?.length ? ( + + ) : null} + {/* Satellite positions — mission-type icons */} {/* satellites: data pushed imperatively */} @@ -5428,6 +5559,66 @@ const MaplibreViewer = ({ ); })()} + {/* Earthquake popup */} + {selectedEntity?.type === 'earthquake' && + (() => { + const extra = (selectedEntity.extra || {}) as Record; + const idx = Number(selectedEntity.id); + const eq = Number.isFinite(idx) + ? data?.earthquakes?.[idx] + : data?.earthquakes?.find((e) => e.id === String(selectedEntity.id)); + const lat = typeof eq?.lat === 'number' ? eq.lat : Number(extra.lat); + const lng = typeof eq?.lng === 'number' ? eq.lng : Number(extra.lng); + if (!Number.isFinite(lat) || !Number.isFinite(lng)) return null; + const mag = eq?.mag ?? Number(extra.mag); + const place = eq?.place || String(extra.place || selectedEntity.name || 'Unknown location'); + const accent = mag >= 6 ? '#ef4444' : mag >= 4.5 ? '#f97316' : '#eab308'; + return ( + onEntityClick?.(null)} + className="threat-popup" + maxWidth="280px" + > +
+
+ M{Number.isFinite(mag) ? mag.toFixed(1) : '?'} — EARTHQUAKE +
+
+ Location: {place} +
+
+ Coords:{' '} + + {lat.toFixed(3)}, {lng.toFixed(3)} + +
+ {oracleIntel?.found && ( +
+
REGION INTEL
+
+ ORACLE: {oracleIntel.tier} + {oracleIntel.avg_sentiment != null && ( + + {' '} + · SENT {oracleIntel.avg_sentiment > 0 ? '+' : ''} + {oracleIntel.avg_sentiment.toFixed(2)} + + )} +
+
+ )} +
+ SEISMIC — USGS +
+
+
+ ); + })()} + {/* Volcano popup */} {selectedEntity?.type === 'volcano' && (() => { @@ -5521,6 +5712,28 @@ const MaplibreViewer = ({ return ; })()} + {(() => { + if (selectedEntity?.type !== 'telegram_osint' || !data?.telegram_osint?.posts) return null; + const allPosts = data.telegram_osint.posts; + const clusterPosts = allPosts.filter((p) => { + if (!p.coords || p.coords.length < 2) return false; + const key = telegramClusterKey(p.coords[0], p.coords[1]); + return key === selectedEntity.id || p.id === selectedEntity.id; + }); + const anchor = clusterPosts[0]?.coords; + if (!anchor || anchor.length < 2) return null; + const avoidAlert = telegramClusterNearNewsAlert(anchor[0], anchor[1], data?.news); + const [pinLat, pinLng] = telegramMapPinCoords(anchor[0], anchor[1], avoidAlert); + return ( + onEntityClick?.(null)} + /> + ); + })()} + {(() => { if (selectedEntity?.type !== 'gdelt' || !data?.gdelt) return null; const item = data.gdelt.find( diff --git a/frontend/src/components/MaplibreViewer/popups/TelegramOsintPopup.tsx b/frontend/src/components/MaplibreViewer/popups/TelegramOsintPopup.tsx new file mode 100644 index 0000000..a52dfd5 --- /dev/null +++ b/frontend/src/components/MaplibreViewer/popups/TelegramOsintPopup.tsx @@ -0,0 +1,255 @@ +'use client'; + +import React, { useMemo } from 'react'; +import { Popup } from 'react-map-gl/maplibre'; +import { Radio } from 'lucide-react'; +import { useTranslation } from '@/i18n'; +import { TELEGRAM_MARKER_OFFSET } from '@/components/map/geoJSONBuilders'; +import { buildTelegramMediaProxyUrl } from '@/lib/telegramProxy'; +import type { TelegramOsintPost } from '@/types/dashboard'; + +export interface TelegramOsintPopupProps { + posts: TelegramOsintPost[]; + lat: number; + lng: number; + onClose: () => void; +} + +function formatTime(pubDate?: string) { + if (!pubDate) return ''; + try { + return new Date(pubDate).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + } catch { + return ''; + } +} + +function riskTheme(rs: number) { + if (rs >= 9) { + return { + hex: '#ef4444', + threatColor: 'text-red-400', + borderColor: 'border-red-700', + bgHeaderColor: 'bg-red-950/50', + bgClass: 'bg-red-950/20 border-red-500/30', + titleClass: 'text-cyan-300 font-bold', + badgeClass: 'bg-red-500/10 text-red-400 border-red-500/30', + }; + } + if (rs >= 7) { + return { + hex: '#f97316', + threatColor: 'text-orange-400', + borderColor: 'border-orange-700', + bgHeaderColor: 'bg-orange-950/50', + bgClass: 'bg-orange-950/20 border-orange-500/30', + titleClass: 'text-cyan-300 font-bold', + badgeClass: 'bg-orange-500/10 text-orange-400 border-orange-500/30', + }; + } + if (rs >= 4) { + return { + hex: '#eab308', + threatColor: 'text-yellow-400', + borderColor: 'border-yellow-800', + bgHeaderColor: 'bg-yellow-950/50', + bgClass: 'bg-yellow-950/20 border-yellow-500/30', + titleClass: 'text-cyan-300 font-bold', + badgeClass: 'bg-yellow-500/10 text-yellow-500 border-yellow-500/30', + }; + } + return { + hex: '#22c55e', + threatColor: 'text-green-400', + borderColor: 'border-green-800', + bgHeaderColor: 'bg-green-950/50', + bgClass: 'bg-green-950/20 border-green-500/30', + titleClass: 'text-cyan-300 font-medium', + badgeClass: 'bg-green-500/10 text-green-400 border-green-500/30', + }; +} + +function postHeadline(post: TelegramOsintPost): string { + return String(post.title || post.description || 'Telegram intercept').trim(); +} + +function postDetail(post: TelegramOsintPost): string | null { + const title = String(post.title || '').trim(); + const description = String(post.description || '').trim(); + if (!description || description === title || description.startsWith(title)) return null; + const extra = description.startsWith(title) ? description.slice(title.length).trim() : description; + return extra || null; +} + +function TelegramPostMedia({ post }: { post: TelegramOsintPost }) { + const { t } = useTranslation(); + const proxyUrl = post.media_url ? buildTelegramMediaProxyUrl(post.media_url) : null; + + let media: React.ReactNode = null; + if (post.media_type === 'video' && proxyUrl) { + media = ( +