innhold/docs/petition-map.md
Ruben 3b85806b08 Move petition map documentation to docs directory
Remove outdated plugin documentation from custom directory
2026-02-26 23:05:05 +01:00

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-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:

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

  1. Privacy — Only confirmed rows. Only first word of firstname. Never surnames, emails, tokens, or IP hashes in the public JSON.
  2. Atomic writespetitionMapWriteCache() 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