# Petition Map Reference For LLM agents working on the petition map. Read when modifying map functionality. ## Overview Interactive Leaflet map showing confirmed petition signers distributed across Norway's 15 fylker. Reads data from the petition CSV, builds a privacy-safe public JSON, and renders animated name labels on the map. Located in `custom/plugins/page/petition-map.php`. Requires `petition-form` to be loaded first (uses its helper functions). ## How It Activates Add to a page's `metadata.ini`: ```ini plugins = "petition-form, petition-map" petition_id = "my-petition" # Required if folder slug differs from petition CSV name ``` Then embed in a PHP content file: ```php ``` ## Fullpage Mode A standalone map page fills the viewport between the site header and footer. Enable with: ```ini map_fullpage = true ``` The fullpage map page for the medisinsk-cannabis petition lives at: ``` content/underskriftskampanje/medisinsk-cannabis-pa-resept/kart/ metadata.ini # plugins, petition_id, map_fullpage = true 10-map.php # ``` URL: `/underskriftskampanje/medisinsk-cannabis-pa-resept/kart/` In fullpage mode: - The map grows to fill all remaining viewport height (flex chain via `:has()` CSS) - A "Signer nå!" CTA button is shown bottom-center, linking back to the petition form - The expand button is hidden; a minimize button (inward arrows) links back to the petition page In embedded mode (default): - Map height is `clamp(420px, 70vh, 800px)` - An expand button (outward arrows SVG) appears top-right inside the map, linking to the fullpage ## Files | File | Location | Purpose | |---|---|---| | `petition-map.php` | `custom/plugins/page/` | PHP plugin: data builder, cache, HTML renderer, hook | | `petition-map.js` | `custom/assets/` | Client JS: Leaflet map, dot placement, polling | | `petition-map.css` | `custom/assets/` | Map and label styles | | `norway-fylker.geojson` | `custom/assets/` | Simplified fylke boundaries (~69KB, RDP simplified) | | `petition-map-data.json` | `custom/assets/` | Auto-generated public signer data (served at `/petition-map-data.json`) | | `petition-map-cache.json` | `custom/data/` | Private server-side cache (controls 60s rebuild frequency) | | `anon.svg` | `custom/assets/` | Mask icon for anonymous signers | | `generate-mock-petitions.php` | `custom/tools/` | Dev CLI to generate mock CSV data | ## Data Flow ``` petition CSV │ ▼ petitionMapBuildData() ├── Only confirmed rows (row[6] === 'confirmed') ├── Reads: firstname (row[2]), region (row[4]), display (row[5]) ├── Privacy: anonymous display -> null name; others -> first word of firstname only ├── Writes: custom/data/petition-map-cache.json (private, 60s TTL) └── Writes: custom/assets/petition-map-data.json (public, served as static asset) │ ▼ petition-map.js ├── Fetches /petition-map-data.json + /norway-fylker.geojson ├── Places animated name labels randomly within each fylke polygon └── Polls every 60s for new signers ``` ## Public JSON Format ```json { "generated": 1772052913, "total": 500, "regions": { "oslo": { "count": 57, "signers": [ {"n": "Kari", "a": false}, {"n": null, "a": true} ] } } } ``` - `n` = first name, first word only (null if anonymous or empty) - `a` = anonymous boolean - No emails, surnames, tokens, or IP hashes ever leave the server ## Privacy Rules | `display` value | Map label | |---|---| | `full` or `semi` | First word of firstname field | | `anonymous` | Mask icon, no name | Only `confirmed` status rows are included. ## Key Functions | Function | Purpose | |---|---| | `petitionMapBuildData(csvPath)` | Reads CSV, builds privacy-safe data array | | `petitionMapReadCache()` | Returns cached data if within 60s TTL, else null | | `petitionMapWriteCache(data)` | Atomically writes both cache and public JSON using tmp+rename | | `petitionMapGetData(csvPath)` | Returns cached data or rebuilds; entry point for the hook | | `petitionMapRenderWidget(ctx, fullpage)` | Returns full HTML string for the widget | ## JS Internals - **Lazy init**: `IntersectionObserver` (10% threshold) delays init until the map enters the viewport; falls back to immediate init - **Initial animation**: all labels shuffled (Fisher-Yates) and staggered over 30 seconds - **Point placement**: rejection sampling within fylke bounding box, ray-casting point-in-polygon, 4% edge inset (max 200 attempts, fallback to centroid) - **Label rendering**: `L.tooltip({ permanent: true })` on invisible zero-size `L.marker` in a custom `dots` pane (z-index 450) - **Polling**: fetches data JSON every 60s with cache-bust timestamp; new signers added with sprinkle effect - **Fylke interaction**: hover highlights polygon, click shows popup with fylke name and count - **Config**: `window.PETITION_MAP_CONFIG` injected by PHP with `dataUrl`, `geojsonUrl`, `anonIconUrl`, `pollInterval` ## Region Keys 15 keys matching petition-form select options and GeoJSON `name` property: `agder`, `akershus`, `buskerud`, `finnmark`, `innlandet`, `more_og_romsdal`, `nordland`, `oslo`, `rogaland`, `telemark`, `troms`, `trondelag`, `vestfold`, `vestland`, `ostfold` ## Cache Invalidation Delete `custom/data/petition-map-cache.json` to force a rebuild on next page load: ```sh podman exec stopplidelsen.no rm /var/www/custom/data/petition-map-cache.json ``` The mock generator does this automatically. The public JSON is always rewritten when the cache rebuilds. ## Dev Tools Generate mock data (run inside the container): ```sh podman exec stopplidelsen.no php /var/www/custom/tools/generate-mock-petitions.php ``` Interactive: asks for count and regional variance. Writes CSV and invalidates cache files. ## Critical: Do Not Break 1. **Privacy** — Only `confirmed` rows. Only first word of firstname. Never surnames, emails, tokens, or IP hashes in the public JSON. 2. **Atomic writes** — `petitionMapWriteCache()` uses `tmp + rename` to avoid serving partial JSON. Do not replace with `file_put_contents` directly. 3. **petition-form must load first** — The map hook guards with `if (!function_exists('petitionResolvePageDir')) return $vars`. Always list `petition-form` before `petition-map` in `plugins`. 4. **petition_id on subpages** — If the map is on a subdirectory page (e.g. `kart/`), `petitionGetIdFromPath()` will derive the ID from the folder name (`kart`), which is wrong. Always set `petition_id` explicitly in `metadata.ini` for subpages. 5. **Fullpage height** — The flex chain (`:has(.petition-map-section--fullpage) main`) requires `:has()` support. Supported in all modern browsers (Firefox 121+, Chrome 105+, Safari 15.4+). 6. **Cache busting** — The PHP widget injects MD5 hashes of the CSS, JS, and data JSON files as query params. These update automatically when files change. ## Currently Used On - `/underskriftskampanje/medisinsk-cannabis-pa-resept/` — embedded between the form and signature list - `/underskriftskampanje/medisinsk-cannabis-pa-resept/kart/` — standalone fullpage map