Skip to content
Draft
40 changes: 39 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This file provides guidance to Claude Code when working with code in this reposi

## Project

`oref-map` is a live alert map of Israel ("מפת העורף") showing colored Voronoi area polygons for alert statuses per location. It uses Leaflet + OpenStreetMap + d3-delaunay + polygon-clipping. Static assets on Cloudflare Pages; API proxy uses a two-tier architecture: Pages Functions serve TLV users directly, non-TLV users are redirected to a placement-pinned Worker.
`oref-map` is a live alert map of Israel ("מפת העורף") showing colored area polygons for alert statuses per location. It uses MapLibre GL JS + self-hosted PMTiles (Protomaps, Cloudflare R2). Polygons are pre-computed GeoJSON (`locations_polygons.json`). Static assets on Cloudflare Pages; API proxy uses a two-tier architecture: Pages Functions serve TLV users directly, non-TLV users are redirected to a placement-pinned Worker.

**Public URL**: https://oref-map.org

Expand Down Expand Up @@ -32,6 +32,44 @@ cd worker && npx wrangler deploy # deploy API proxy Worker
- `tools/poll-coderabbit.sh` — Polls CodeRabbit review status on a PR via GitHub commit status API
- `docs/map-requirements.md` — Feature requirements doc

## Docs — when to read which file

Read the relevant doc before making changes in that area:

| Task | Read |
|------|------|
| Map rendering, basemap tiles, polygon source, API proxy architecture, Cloudflare setup | `docs/architecture.md` |
| Changing map bounds, replacing or extending PMTiles on R2, understanding tile coverage | `docs/architecture.md` § "Basemap Tiles (PMTiles on R2)" |
| Ellipse mode behavior, geometry math, cluster algorithm | `docs/ellipse-feature.md` |
| Alg-C ellipse service (Python backend), request/response format | `docs/ellipse-alg-C.md` |
| Ellipse probability window metric | `docs/ellipse-probability-window.md` |
| Feature requirements, UX decisions | `docs/map-requirements.md` |
| Oref API endpoints, response shapes, geo-blocking | `docs/oref-sources.md` (and this file) |

## Replacing the basemap tiles (one-time)

When the user asks to change or extend the PMTiles coverage:

1. **Download a new PMTiles file** using the `pmtiles` CLI (install: `brew install protomaps/homebrew-go-pmtiles/go-pmtiles`). It extracts only the needed bbox via HTTP range requests — no full 120 GB download:
```bash
pmtiles extract https://build.protomaps.com/20260404.pmtiles middle-east.pmtiles \
--bbox=32,10,65,42 --maxzoom=15 --download-threads=4
```
Replace `20260404` with a recent date from https://maps.protomaps.com/builds/.
The bbox format is `MIN_LON,MIN_LAT,MAX_LON,MAX_LAT`.

2. **Upload to R2** with:
```bash
wrangler r2 object put <bucket-name>/middle-east.pmtiles \
--file=<path-to-downloaded-file>.pmtiles \
--content-type=application/vnd.mapbox-vector-tile
```
The bucket name is visible in Cloudflare dashboard → R2. The public URL does not change.

3. **Update `maxBounds` in `web/index.html`** (search for `maxBounds`) to match the new bounding box.

4. **Verify** by running `npx pmtiles show <url>` to confirm the new bounds.

## Feature flags

Beta/debug features are gated behind URL parameters with an `f-` prefix (e.g. `?f-log`). On page load, a single block of JS parses all `f-*` params and:
Expand Down
83 changes: 76 additions & 7 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ hour and last 24 hours. The client polls this endpoint once per minute.

## Stack

- **Map**: Leaflet.js (v1.9.4) + OpenStreetMap tiles
- **Voronoi**: d3-delaunay (v6) for polygon computation, polygon-clipping (v0.15) for clipping to Israel border
- **Map**: MapLibre GL JS + PMTiles (self-hosted Middle East extract on Cloudflare R2, Protomaps basemap with Hebrew labels)
- **Polygons**: Pre-computed GeoJSON in `web/locations_polygons.json` loaded at startup into a MapLibre `alerts-source`
- **API proxy (tier 1)**: Cloudflare Pages Functions (`functions/api/`) — serves TLV users directly, redirects others
- **API proxy (tier 2)**: Cloudflare Worker (`worker/`) with placement `region = "azure:israelcentral"` — fallback for non-TLV users
- **History storage**: Cloudflare R2 bucket (`oref-history`) with per-day JSONL files
Expand Down Expand Up @@ -115,13 +115,82 @@ Unknown titles default to red and log a console warning.

## Map Rendering

### Voronoi Polygons
### Polygons

All ~1,430 location coordinates from `cities_geo.json` are tessellated at startup using d3-delaunay into Voronoi cells. Cells are clipped to Israel's border polygon using polygon-clipping. Each location owns one polygon cell.
Location polygons are pre-computed offline and shipped as `web/locations_polygons.json`. On startup the page fetches this file and loads all ~1,450 features into the MapLibre `alerts-source` GeoJSON source. Each feature's `fillColor`, `fillOpacity`, `lineColor`, and `lineOpacity` properties are updated in place via `setData()` whenever alert state changes — no layer recreation needed.

- Computed once at startup, not on every alert update.
- Only fill color and opacity change per alert event.
- Adjacent polygons of the same color visually merge into contiguous threat zones (shared borders become invisible due to matching stroke color).
- Per-feature state is driven by data properties, not Leaflet `setStyle`.
- The `featureMap` lookup (`name → GeoJSON Feature`) is exposed on `AppState` for use by extensions (e.g. ellipse mode).

### Basemap Tiles (PMTiles on R2)

The basemap is a self-hosted Protomaps vector tile file stored on Cloudflare R2 and served via the R2 public bucket URL:

```
https://pub-0cb002f302e94002b76aa0bc30eb8763.r2.dev/middle-east.pmtiles
```

**Current file coverage:**
| Property | Value |
|----------|-------|
| File | `middle-east.pmtiles` |
| Bounds (lng) | 32.0 – 65.0 |
| Bounds (lat) | 11.0 – 39.0 |
| Zoom | 0 – 10 |
| Built | 2026-04-07 (Planetiler 0.10.1, OSM data 2026-04-06) |

This covers Israel, Lebanon, Syria, Jordan, Iraq, Iran, Saudi Arabia, Egypt (Sinai), the Gulf states, and Yemen.

#### Inspecting the current file

```bash
npx pmtiles show https://pub-0cb002f302e94002b76aa0bc30eb8763.r2.dev/middle-east.pmtiles
```

#### Regenerating with a larger bounding box

The current file was generated with **Planetiler** (v0.10.1, run locally). For future updates the recommended approach is the `pmtiles` CLI, which uses HTTP range requests to extract only the needed tiles from Protomaps' hosted planet — no 120 GB download required.

**Option A — `pmtiles extract` CLI (recommended for one-off changes):**

Install: `brew install protomaps/homebrew-go-pmtiles/go-pmtiles`

Find a recent build date at https://maps.protomaps.com/builds/, then run:
```bash
pmtiles extract https://build.protomaps.com/20260404.pmtiles middle-east.pmtiles \
--bbox=32,10,65,42 --maxzoom=15 --download-threads=4
```
bbox format: `MIN_LON,MIN_LAT,MAX_LON,MAX_LAT`. Replace the date with the one from the builds index. The command fetches only the tiles in the bbox via HTTP range requests (a few GB, not 120 GB).

> **Do not use `slice.openstreetmap.us`** — that site downloads raw `.osm.pbf` data (OSM XML/binary), not `.pmtiles`.

**Option B — Planetiler (best for full control or custom schemas):**
```bash
java -jar planetiler.jar \
--download \
--area=middle-east \
--bounds=32,10,65,42 \
--output=middle-east-extended.pmtiles
```

Both produce the same `protomaps` basemap schema — the map code works identically with either output.

2. **Upload to R2** using Wrangler (bucket name visible in Cloudflare dashboard → R2):
```bash
wrangler r2 object put <bucket-name>/middle-east.pmtiles \
--file=middle-east.pmtiles \
--content-type=application/vnd.mapbox-vector-tile
```
The public URL (`pub-0cb002...r2.dev`) does not change after upload.

3. **Update `maxBounds` in `web/index.html`** to match the new lat extent (e.g. `[[32.0, 10.0], [65.0, 42.0]]` for Yemen coverage).

#### R2 bucket info

- **Public URL**: `https://pub-0cb002f302e94002b76aa0bc30eb8763.r2.dev/`
- **Public access**: enabled — the client fetches tiles directly from R2 at runtime via the `pmtiles://` protocol, no Worker involved.
- Bucket name is visible in Cloudflare dashboard → R2.

### Geocoding

Expand All @@ -137,7 +206,7 @@ All ~1,430 location coordinates from `cities_geo.json` are tessellated at startu
- **Legend**: Bottom-right — color key
- **Timeline panel**: Bottom-center — date navigation + slider to scrub through any day's history
- **About modal**: Triggered by ⓘ button or title click. Closes on backdrop click or Escape.
- **Popups**: Click a polygon to see alert history for that location (newest first).
- **Location panel**: Click a polygon to open a slide-in panel with alert history for that location (bottom-sheet on mobile, sidebar on desktop).

All overlays use `position: fixed`, `z-index: 1000`, semi-transparent white backgrounds with `border-radius` and `box-shadow`. RTL layout throughout.

Expand Down
2 changes: 1 addition & 1 deletion docs/ellipse-alg-C.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ On success the server returns:
}
```

The browser converts that response into a renderable Leaflet geometry in [`buildAlgCServiceRenderable(...)`](/home/tomer/projects/oref-map/web/ellipse-mode.js#L1710).
The browser converts that response into a renderable geometry in `buildAlgCServiceRenderable()` in `web/ellipse-mode.js`, using inline Web Mercator projection math (no Leaflet dependency). The result is pushed to the `algc-overlay` MapLibre GeoJSON source.

### Error responses

Expand Down
4 changes: 2 additions & 2 deletions docs/ellipse-feature.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Ellipse mode uses the following app state inputs:

- `locationStates`: current displayed state per location
- `locationHistory`: recent alert history per location
- `locationPolygons`: Leaflet polygons for each location
- `featureMap`: GeoJSON Feature objects for each location (keyed by location name)
- `userPosition`: current user geolocation if available

It also lazily loads `oref_points.json`, which maps alert location names to `[lat, lng]` points used for ellipse fitting and marker placement.
Expand Down Expand Up @@ -136,7 +136,7 @@ If the cluster has exactly one usable point, the geometry becomes:

For clusters with two or more usable points:

1. points are projected into the map CRS
1. points are projected into Web Mercator (EPSG:3857) using inline math (no map CRS dependency)
2. a major axis is estimated
3. the point spread is measured in the major-axis and minor-axis basis
4. padded semi-axes are produced
Expand Down
Loading