Offline Flood Connectivity Mapper for Disaster Response React + TypeScript · MapLibre GL JS · PMTiles · Pyodide (NetworkX in-browser) · Tailwind CSS + shadcn/ui
Copyright © 2026 Darshan K. · MIT License
NeerNet is a progressive web app that runs entirely in the browser — no backend required. It renders a vector basemap using MapLibre GL JS + PMTiles, lets you draw an AOI polygon, and executes Python graph algorithms client-side inside a Web Worker via Pyodide/WebAssembly.
Current capabilities:
- Fetches 1,68,060+ Kerala waterways from OpenStreetMap via Overpass API
- Runs NetworkX connected_components, BFS flood simulation, watershed stats, critical path detection, and risk scoring entirely in the browser
- Animated flood BFS with step-by-step visualization
- Click-to-set flood source on the map
- Export results as GeoJSON
Two offline modes are included:
| Mode | How it works |
|---|---|
| Cache-as-you-pan | The Service Worker intercepts every tile/style/glyph fetch and stores it in Cache Storage. When you go offline, previously viewed areas load without errors. |
| Download Region Pack | Explicitly download a self-contained pack (one .pmtiles file + style + sprites) via the sidebar. Works without any prior panning. |
This project was built for FOSS Hack 2026. It uses two Partner Projects:
- Pyodide — Python/NetworkX runs entirely in the browser via WebAssembly
- MapLibre GL JS — Vector tile rendering with OpenStreetMap data
Related contribution: PR #6133 — fixing console-v2 multiline paste and autocomplete bugs in Pyodide (approved by core maintainer).
- Node.js ≥ 18
- npm ≥ 9 (or pnpm / bun)
# 1. Clone / unzip the project
cd FloodGraph
# 2. Install dependencies
npm install
# 3. Start the dev server
npm run devNote: The dev server sets
Cross-Origin-Opener-Policy: same-originandCross-Origin-Embedder-Policy: require-corpheaders, which are required for Pyodide'sSharedArrayBuffersupport.
npm run build # outputs to dist/
npm run preview # serve the prod build locallyBy default NeerNet uses OpenFreeMap — a fully free, open-source tile service with no API key required.
Style URL: https://tiles.openfreemap.org/styles/liberty
You can swap this in src/map.ts → BASEMAP_STYLE_URL.
- Download (or generate) a
.pmtilesfile for your region. Good sources: Protomaps planet extract, Geofabrik. - Place the file in
public/(e.g.public/myregion.pmtiles). - Edit
BASEMAP_STYLE_URLinsrc/map.tsto point to a style that referencespmtiles:///myregion.pmtiles. - The
pmtiles://protocol prefix is handled automatically by thepmtilesnpm package'sProtocolregistered inmap.ts.
Browser Service Worker Network
│ fetch tile/style/glyph │ │
│────────────────────────▶│ │
│ │── cache hit? ─────────── │
│ │ yes → return cached │
│ │ no → fetch network ──▶│
│ │ store in cache ◀│
│◀────────────────────────│ │
PMTiles files are fetched as HTTP Range requests (status 206).
The Service Worker (public/sw.js) stores each range response under a URL key
that includes the range fragment (url#bytes_start-end), so different byte ranges
of the same file never overwrite each other.
Cached assets:
| Cache name | Content |
|---|---|
neernet-shell-v1 |
HTML, JS/CSS chunks, manifest |
neernet-tiles-v1 |
PMTiles range responses (cache-as-you-pan) |
neernet-styles-v1 |
Style JSON, sprites, glyph PBFs |
neernet-offline-packs-v1 |
Explicitly downloaded region packs |
Region packs are defined in public/offline-packs.json:
{
"version": "1",
"packs": [
{
"id": "kerala-india",
"name": "Kerala, India",
"pmtiles_url": "https://yourhost.com/kerala.pmtiles",
"style_url": "https://yourhost.com/kerala-style.json",
"sprite_urls": ["https://yourhost.com/sprites/v4@2x.png", "…"],
"glyph_url_prefix": "https://yourhost.com/fonts",
"size_mb_approx": 85,
"bbox": [74.85, 8.18, 77.64, 12.78]
}
]
}| Field | Description |
|---|---|
id |
Unique identifier |
name |
Display name shown in UI |
pmtiles_url |
Absolute URL to the .pmtiles file |
style_url |
MapLibre style JSON (must reference the pmtiles_url) |
sprite_urls |
List of sprite image + JSON URLs |
glyph_url_prefix |
Base URL for font glyphs (leave empty if style uses embedded fonts) |
size_mb_approx |
Approximate download size in MB (shown in UI) |
bbox |
[minLng, minLat, maxLng, maxLat] of the region |
The download client (src/main.ts → downloadPack) fetches each URL with streaming
progress and stores the full response in the neernet-offline-packs-v1 cache.
When MapLibre requests tiles from the same URL offline, the Service Worker serves
the cached full file and synthesises a 206 Partial Content response from the
requested byte range.
NeerNet currently demos offline behavior using cache-as-you-pan only.
- Ensure download-pack mode is disabled:
# .env.local
VITE_ENABLE_OFFLINE_PACKS=false- Start the app and keep DevTools open.
- While online, pan and zoom the map in your target demo area for 30-60 seconds.
- In DevTools -> Network, enable Offline.
- Reload the page.
- Verify previously visited areas still render from Service Worker cache.
When VITE_ENABLE_OFFLINE_PACKS=false, the UI shows an Offline Demo Mode panel instead of any download/apply pack controls.
Download-pack mode should only be re-enabled after a real PMTiles/style/glyph dataset is hosted and validated.
Python runs in a Web Worker (src/py/worker.ts) so it never blocks the UI thread.
connectivity(edges)— NetworkXconnected_componentstoy_flood(edges, source_nodes, steps)— BFS flood simulationanimated_flood(edges, source, steps)— returns per-step frames for animationrisk_score(edges, source_nodes)— betweenness centrality + flood distancewatershed_stats(edges)— outlets, headwaters, density, confluencecritical_path(edges)— bridges and articulation points
All messages follow:
// main → worker
{ id: string; type: 'connectivity' | 'toy_flood' | 'ping'; payload: unknown }
// worker → main
{ id: string; ok: true; result: unknown }
{ id: string; ok: false; error: string }
// special: status broadcasts
{ id: '__status__'; ok: true; result: { status: 'loading'|'ready'; message: string } }Use the existing OSM ingestion pipeline as the base for deeper flood physics:
- Fetch OSM road/river network using Overpass API or a pre-built GeoJSON.
- Convert features to
{ source: string; target: string }[]edge list. - Pass edge list + real source nodes to
worker.toyFlood(…). - For physics-based simulation, add SciPy (lazy-loaded):
// In worker.ts, inside initPyodide(): await pyodide.loadPackage(['scipy']);
- Implement real flood algorithms (e.g. D8 flow direction, LISFLOOD-FP simplified) in Python inside
worker.tsusingrunPythonAsync.
FloodGraph/
├── src/
│ ├── App.tsx # Central state management
│ ├── main.tsx # React entry point
│ ├── map.ts # MapLibre + PMTiles
│ ├── aoi.ts # AOI polygon draw
│ ├── waterways.ts # Overpass API + graph building
│ ├── components/
│ │ ├── Header.tsx
│ │ ├── MapView.tsx
│ │ ├── Sidebar.tsx
│ │ ├── MobileDrawer.tsx
│ │ └── sidebar/
│ │ ├── PyodideStatus.tsx
│ │ ├── AOISection.tsx
│ │ ├── WaterwaysSection.tsx
│ │ ├── ComputeSection.tsx
│ │ ├── ResultsSection.tsx
│ │ └── OfflinePackSection.tsx
│ ├── styles/
│ └── py/
│ ├── worker.ts # Pyodide Web Worker
│ └── client.ts # Worker client
https://rushdarshan.github.io/FloodGraph/
The manifest references icon-192.png and icon-512.png in public/.
Generate them from any 512×512 source image:
# macOS / Linux with ImageMagick:
convert icon-source.png -resize 192x192 public/icon-192.png
convert icon-source.png -resize 512x512 public/icon-512.pngA quick SVG placeholder:
# Create a minimal blue square icon
echo '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 192 192"><rect width="192" height="192" fill="#1a2744"/><text x="96" y="120" font-size="80" text-anchor="middle" fill="#3b82f6">🌊</text></svg>' > public/icon.svgPyodide needs SharedArrayBuffer which requires:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Already configured in vite.config.ts.
add_header Cross-Origin-Opener-Policy "same-origin";
add_header Cross-Origin-Embedder-Policy "require-corp";Header set Cross-Origin-Opener-Policy "same-origin"
Header set Cross-Origin-Embedder-Policy "require-corp"Use the coi-serviceworker trick — add public/coi-serviceworker.js from
https://github.com/gzuidhof/coi-serviceworker and load it as the first script
in index.html.
| Problem | Fix |
|---|---|
| Map shows blank / style 404 | Check browser console; OpenFreeMap may have CORS issues in some setups. Try a local style file. |
| Pyodide times out | First load downloads ~30 MB from CDN. Ensure network access; subsequent loads use the browser cache. |
| 206 responses not cached | Some browsers limit Cache Storage for range responses. Use the "Download Pack" flow for reliable offline. |
| SW not updating | Open DevTools → Application → Service Workers → "Update on reload" during development. |
SharedArrayBuffer not available |
Add COOP/COEP headers (see above). Without them Pyodide still works but is slower. |
- Real OSM graph ingestion via Overpass API
- Animated flood BFS visualization
- Click-to-set flood source
- Export results as GeoJSON
- NetworkX graph algorithms in browser via Pyodide
- D8 / flow-accumulation physics-based flood simulation
- Multi-region pack management
- Offline Pyodide (bundle wheel files in the pack)
Data: © OpenStreetMap contributors, via Overpass API. Tiles: OpenFreeMap (no API key required). Python runtime: Pyodide / NetworkX.