innhold/custom/plugins/page/petition-map.php

206 lines
8.2 KiB
PHP
Raw Permalink Normal View History

<?php
/**
* Petition Map Plugin (page plugin)
*
* Enable via metadata.ini: plugins = "petition-form, petition-map"
* Requires petition_id to be set (inherited from petition-form setup).
*
* Injects template variable $petition_map containing the full widget HTML.
* Embed in a content file: <?= $petition_map ?>
*
* Data flow:
* CSV -> petitionMapBuildData() -> cache (custom/data/) + public JSON (custom/assets/)
* Public JSON is served at /petition-map-data.json by the router's asset mechanism.
* JS fetches it and renders dots on a Leaflet map.
*
* Public JSON format:
* { "generated": int, "total": int, "regions": { "oslo": { "count": 42, "signers": [{"n":"Kari","a":false}] } } }
* n = first name (null for anonymous), a = anonymous (bool)
* No emails, surnames, tokens, or IP hashes ever leave the server.
*/
define('PETITION_MAP_CACHE_TTL', 60);
define('PETITION_MAP_CACHE_PATH', dirname(__DIR__, 2) . '/data/petition-map-cache.json');
define('PETITION_MAP_PUBLIC_PATH', dirname(__DIR__, 2) . '/assets/petition-map-data.json');
/**
* Build public-safe map data from the petition CSV.
*/
function petitionMapBuildData(string $csvPath): array {
$regions = [];
$total = 0;
if (!file_exists($csvPath)) {
return ['generated' => time(), 'total' => 0, 'regions' => (object)[]];
}
$fp = fopen($csvPath, 'r');
if (!$fp) {
return ['generated' => time(), 'total' => 0, 'regions' => (object)[]];
}
if (flock($fp, LOCK_SH)) {
fgetcsv($fp, null, ',', '"', '');
while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) {
if (!isset($row[6]) || $row[6] !== 'confirmed') continue;
$region = $row[4] ?? '';
$display = $row[5] ?? 'semi';
if (empty($region)) continue;
$anonymous = ($display === 'anonymous');
$name = $anonymous ? null : trim($row[2] ?? '');
if ($name !== null) {
// First name only — take the first word to keep map labels compact
$name = mb_substr(explode(' ', $name)[0], 0, 50);
if ($name === '') $name = null;
}
if (!isset($regions[$region])) {
$regions[$region] = ['count' => 0, 'signers' => []];
}
$regions[$region]['count']++;
$regions[$region]['signers'][] = ['n' => $name, 'a' => $anonymous];
$total++;
}
flock($fp, LOCK_UN);
}
fclose($fp);
return [
'generated' => time(),
'total' => $total,
'regions' => empty($regions) ? (object)[] : $regions,
];
}
function petitionMapReadCache(): ?array {
$path = PETITION_MAP_CACHE_PATH;
if (!file_exists($path)) return null;
if ((time() - filemtime($path)) > PETITION_MAP_CACHE_TTL) return null;
$json = file_get_contents($path);
if (!$json) return null;
$data = json_decode($json, true);
return is_array($data) ? $data : null;
}
function petitionMapWriteCache(array $data): void {
$json = json_encode($data, JSON_UNESCAPED_UNICODE);
if ($json === false) return;
foreach ([PETITION_MAP_CACHE_PATH, PETITION_MAP_PUBLIC_PATH] as $path) {
$dir = dirname($path);
if (!is_dir($dir)) {
mkdir($dir, 0750, true);
}
$tmp = $path . '.tmp.' . getmypid();
if (file_put_contents($tmp, $json, LOCK_EX) !== false) {
rename($tmp, $path);
}
}
}
function petitionMapGetData(string $csvPath): array {
$cached = petitionMapReadCache();
if ($cached !== null) return $cached;
$data = petitionMapBuildData($csvPath);
petitionMapWriteCache($data);
return $data;
}
/**
* Render the map widget HTML block.
*/
function petitionMapRenderWidget(Context $ctx, bool $fullpage = false): string {
$langPrefix = $ctx->get('langPrefix', '');
// CSS hash for cache busting
$cssPath = dirname(__DIR__, 2) . '/assets/petition-map.css';
$cssHash = file_exists($cssPath) ? substr(hash_file('md5', $cssPath), 0, 8) : '';
$jsPath = dirname(__DIR__, 2) . '/assets/petition-map.js';
$jsHash = file_exists($jsPath) ? substr(hash_file('md5', $jsPath), 0, 8) : '';
$dataJsonPath = PETITION_MAP_PUBLIC_PATH;
$dataHash = file_exists($dataJsonPath) ? substr(hash_file('md5', $dataJsonPath), 0, 8) : '';
$html = '';
// Leaflet CSS
$html .= '<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="anonymous">';
// Map CSS
$html .= '<link rel="stylesheet" href="/petition-map.css?v=' . $cssHash . '">';
// Map container
$sectionClass = 'petition-map-section escape' . ($fullpage ? ' petition-map-section--fullpage' : '');
$html .= '<section class="' . $sectionClass . '">';
$html .= '<h1 id="petition-map-count" class="petition-map-count" aria-live="polite"></h1>';
$html .= '<div id="petition-map" aria-label="Kart over underskrifter i Norge">';
$html .= '<div class="map-loading" id="map-loading" aria-live="polite">Laster kart…</div>';
$iconExpand = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="10 2 14 2 14 6"/><polyline points="6 14 2 14 2 10"/><line x1="14" y1="2" x2="9" y2="7"/><line x1="2" y1="14" x2="7" y2="9"/></svg>';
if (!$fullpage) {
$html .= '<a href="' . $langPrefix . '/underskriftskampanje/medisinsk-cannabis-pa-resept/kart/" class="petition-map-fullscreen-btn" title="Fullskjerm" aria-label="Åpne kart i fullskjerm">' . $iconExpand . '</a>';
}
if ($fullpage) {
$iconCollapse = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="4 14 10 14 10 20"/><polyline points="20 10 14 10 14 4"/><line x1="10" y1="14" x2="3" y2="21"/><line x1="21" y1="3" x2="14" y2="10"/></svg>';
$html .= '<a href="' . $langPrefix . '/underskriftskampanje/medisinsk-cannabis-pa-resept/#petition-map" class="petition-map-fullscreen-btn petition-map-fullscreen-btn--minimize" title="Minimer" aria-label="Gå tilbake til underskriftssiden">' . $iconCollapse . '</a>';
}
$html .= '</div>';
$html .= '<a href="' . $langPrefix . '/underskriftskampanje/medisinsk-cannabis-pa-resept/#sign-now" class="petition-map-cta-btn">Signer nå!</a>';
$html .= '</section>';
// Config for JS
$html .= '<script>';
$html .= 'window.PETITION_MAP_CONFIG={';
$html .= 'dataUrl:"/petition-map-data.json?v=' . $dataHash . '",';
$html .= 'geojsonUrl:"/norway-fylker.geojson",';
$html .= 'anonIconUrl:"/anon.svg",';
$html .= 'pollInterval:60000';
$html .= '};';
$html .= '</script>';
// Leaflet JS + map JS
$html .= '<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin="anonymous"></script>';
$html .= '<script src="/petition-map.js?v=' . $jsHash . '"></script>';
return $html;
}
// --- Hook ---
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx) {
// Resolve page directory and metadata (reuse petition-form helpers if available)
if (!function_exists('petitionResolvePageDir')) return $vars;
$pageDir = petitionResolvePageDir($ctx);
if (!$pageDir) return $vars;
$metadata = loadMetadata($pageDir);
$plugins = $metadata['plugins'] ?? '';
if (strpos($plugins, 'petition-map') === false) return $vars;
$petitionId = $metadata['petition_id']
?? (function_exists('petitionGetIdFromPath') ? petitionGetIdFromPath($pageDir) : null);
if (!$petitionId) return $vars;
$petitionId = preg_replace('/[^a-z0-9\-_]/i', '', $petitionId);
if (empty($petitionId)) return $vars;
$csvPath = dirname(__DIR__, 2) . "/data/petitions/{$petitionId}.csv";
// Refresh cache + public JSON if stale
petitionMapGetData($csvPath);
// Inject widget HTML as template variable
$fullpage = !empty($metadata['map_fullpage']);
$vars['petition_map'] = petitionMapRenderWidget($ctx, $fullpage);
return $vars;
});