StreamMaps
StreamMaps is a map based web app that estimates how much upstream habitat each river/stream barrier blocks.
About the project
Stream restoration decisions, made by government officials and promoted by environmental activists, are constrained by time and limited field data. StreamMaps takes raw open map data and makes looking and analyzing stream barriers interactive. You can pan the map, analyze, see what’s blocking connectivity and share results.
What inspired us
We were inspired by the clarity and usability of the Apple Maps website: the map stays central, UI panels feel lightweight, and everything is discoverable without overwhelming the screen. We wanted that same “clean map product” feeling, but focused on stream barriers and restoration impact.
What we learned
- How to integrate MapLibre GL into a React/Next.js app and keep the map stable while UI state changes.
- How to implement custom HTML markers that stay perfectly aligned with the map (including selection states and zoom-based visibility).
- How to build and style multiple GeoJSON layers (streams, accessible streams, barriers, reports, bounding box overlays).
- How to query and cache OpenStreetMap data via Overpass and place search via Nominatim.
- How to infer approximate flow direction using a DEM elevation API and apply it to stream graph orientation.
- How to design a lightweight backend “job + polling” workflow for analysis.
How we built our project (pipeline)
- The frontend starts analysis with
POST /analyzesending the current map viewport bbox. - The server creates an in-memory job and immediately begins work:
- Overpass fetch of waterways and barrier-tagged features
- optional bbox tiling fallback if Overpass times out
- DEM sampling of stream way endpoints using Open-Meteo elevation to infer downhill direction
- The server builds directed stream graph
- finds outlet-like nodes
- BFS upstream using reverse adjacency
- snaps barriers to stream nodes (spatial hash within ~30m)
- assigns each stream segment to the “first downstream barrier”
- sums segment lengths,
gain_kmper barrier
- The server returns payload
streams_geojsonaccessible_streams_geojsonbarriers_geojsonrankings[](sort by gain)
- Results are cached on server disk (
./cache/...)
Community reporting
- Users can submit:
- a location-based report (pick a point on the map), or
- a barrier-based report (“incorrect barrier”, etc.)
- Reports are stored append-only in
data/reports.jsonl. - The server aggregates reports + feedback (confirm/renounce) into a confidence score.
- The frontend renders report pins and can incorporate report signals into barrier confidence.
Sharing deep links
- Barrier details can be shared via a link containing a serialized
barrier_payload. - Report details can be shared via a link containing
report=<id>+ metadata. - On load, the app reads the URL params, flies to the location, and opens the correct details view.
The challenges we faced
- API rate limiting and timeouts
We added:
- on-disk caching for Overpass, search, DEM, and final results
- fallback to multiple Overpass endpoints
- bbox tiling fallback when large queries fail
- limiting marker counts
how to run locally:
npm install
npm run dev
Built With
- esri-world-imagery
- express.js
- maplibregl
- node.js
- openstreetmap
- react
- sonner
- tailwind
- typescript
- zod
Log in or sign up for Devpost to join the conversation.