Move petition map documentation to docs directory
Remove outdated plugin documentation from custom directory
This commit is contained in:
parent
9d02424aac
commit
3b85806b08
2 changed files with 170 additions and 109 deletions
|
|
@ -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
|
||||
<?= $petition_map ?? '' ?>
|
||||
```
|
||||
|
||||
The plugin renders a `<section class="petition-map-section escape">` 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.
|
||||
170
docs/petition-map.md
Normal file
170
docs/petition-map.md
Normal file
|
|
@ -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
|
||||
<?= $petition_map ?? '' ?>
|
||||
```
|
||||
|
||||
## 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 # <?= $petition_map ?? '' ?>
|
||||
```
|
||||
|
||||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue