7.1 KiB
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:
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:
<?= $petition_map ?? '' ?>
Fullpage Mode
A standalone map page fills the viewport between the site header and footer. Enable with:
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
{
"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-sizeL.markerin a customdotspane (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_CONFIGinjected by PHP withdataUrl,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:
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):
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
- Privacy — Only
confirmedrows. Only first word of firstname. Never surnames, emails, tokens, or IP hashes in the public JSON. - Atomic writes —
petitionMapWriteCache()usestmp + renameto avoid serving partial JSON. Do not replace withfile_put_contentsdirectly. - petition-form must load first — The map hook guards with
if (!function_exists('petitionResolvePageDir')) return $vars. Always listpetition-formbeforepetition-mapinplugins. - 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 setpetition_idexplicitly inmetadata.inifor subpages. - 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+). - 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