# petition-map plugin Page plugin that renders an interactive Leaflet map of petition signers across Norway's 15 fylker. ## Enable In a page's `metadata.ini` (must also have petition-form loaded first): ```ini plugins = "petition-form, petition-map" ``` Embed in a content file (e.g. `90-form.php`, between the form and the signatures list): ```php ``` The plugin renders a `
` which breaks out of the content grid to fill the full page width. ## Architecture ``` petition-form CSV │ ▼ petition-map.php (PHP, page plugin) ├── Reads CSV, extracts: first name, anonymous flag, region, confirmed status ├── Writes private cache: custom/data/petition-map-cache.json (60s TTL) └── Writes public JSON: custom/assets/petition-map-data.json (served at /petition-map-data.json) │ ▼ petition-map.js (client-side) ├── Fetches /petition-map-data.json + /norway-fylker.geojson ├── Renders Leaflet map with CartoDB Positron tiles (muted, no labels) ├── Draws 15 fylke outlines from GeoJSON ├── Places name labels (L.tooltip permanent) randomly within each fylke polygon └── Polls every 60s for new signers ``` ## Files | File | Location | Purpose | |---|---|---| | `petition-map.php` | `custom/plugins/page/` | PHP plugin: cache logic, data builder, HTML widget renderer, TEMPLATE_VARS hook | | `petition-map.js` | `custom/assets/` | Client JS: Leaflet map, dot placement, point-in-polygon, polling | | `petition-map.css` | `custom/assets/` | Map and label styles | | `norway-fylker.geojson` | `custom/assets/` | Simplified fylke boundaries (15 current fylker, from OpenStreetMap Overpass, RDP simplified to ~69KB) | | `petition-map-data.json` | `custom/assets/` | Auto-generated public signer data (written by PHP, served as static asset) | | `petition-map-cache.json` | `custom/data/` | Private server-side cache (same data, controls rebuild frequency) | | `anon.svg` | `custom/assets/` | Mask icon for anonymous signers | | `generate-mock-petitions.php` | `custom/tools/` | Dev-only CLI to generate mock CSV data | ## Public JSON format ```json { "generated": 1772052913, "total": 500, "regions": { "oslo": { "count": 57, "signers": [ {"n": "Kari", "a": false}, {"n": null, "a": true} ] } } } ``` - `n` = first name (null if anonymous or empty) - `a` = anonymous boolean - No emails, surnames, tokens, or IP hashes ever leave the server ## Privacy rules (from CSV `display` column) - `full` or `semi`: first name shown on map - `anonymous`: shown as mask icon, no name - Only `confirmed` status rows are included ## Region keys 15 keys matching the 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` ## JS internals - **Point placement**: rejection sampling within fylke polygon bounding box, ray-casting point-in-polygon test, 4% edge inset - **Label rendering**: `L.tooltip({ permanent: true })` bound to invisible zero-size `L.marker` in a custom `dots` pane (z-index 450) - **Lazy init**: map initialises only when `#petition-map` enters the viewport (IntersectionObserver, 10% threshold); falls back to immediate init on older browsers - **Initial animation**: all labels shuffled (Fisher-Yates) and staggered over 6s - **Polling**: fetches data JSON every 60s with cache-bust timestamp; new signers added immediately - **Fylke interaction**: hover highlights polygon, click shows popup with fylke name + count ## Cache invalidation Delete `custom/data/petition-map-cache.json` to force a rebuild on next page load. The mock generator does this automatically. The public JSON is rewritten whenever 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.