diff --git a/custom/plugins/page/petiton-map.docs.md b/custom/plugins/page/petiton-map.docs.md deleted file mode 100644 index 21d2f67..0000000 --- a/custom/plugins/page/petiton-map.docs.md +++ /dev/null @@ -1,109 +0,0 @@ -# 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. diff --git a/docs/petition-map.md b/docs/petition-map.md new file mode 100644 index 0000000..3bdc048 --- /dev/null +++ b/docs/petition-map.md @@ -0,0 +1,170 @@ +# 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