2026-02-25 23:11:35 +01:00
< ? 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 ) {
2026-02-26 22:44:05 +01:00
// First name only — take the first word to keep map labels compact
$name = mb_substr ( explode ( ' ' , $name )[ 0 ], 0 , 50 );
2026-02-25 23:11:35 +01:00
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 .
*/
2026-02-26 22:44:05 +01:00
function petitionMapRenderWidget ( Context $ctx , bool $fullpage = false ) : string {
2026-02-25 23:11:35 +01:00
$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
2026-02-26 22:44:05 +01:00
$sectionClass = 'petition-map-section escape' . ( $fullpage ? ' petition-map-section--fullpage' : '' );
$html .= '<section class="' . $sectionClass . '">' ;
2026-02-26 22:21:41 +01:00
$html .= '<h1 id="petition-map-count" class="petition-map-count" aria-live="polite"></h1>' ;
2026-02-25 23:11:35 +01:00
$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>' ;
2026-02-26 22:44:05 +01:00
$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>' ;
}
2026-02-25 23:11:35 +01:00
$html .= '</div>' ;
2026-02-26 22:44:05 +01:00
$html .= '<a href="' . $langPrefix . '/underskriftskampanje/medisinsk-cannabis-pa-resept/#sign-now" class="petition-map-cta-btn">Signer nå!</a>' ;
2026-02-25 23:11:35 +01:00
$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
2026-02-26 22:44:05 +01:00
$fullpage = ! empty ( $metadata [ 'map_fullpage' ]);
$vars [ 'petition_map' ] = petitionMapRenderWidget ( $ctx , $fullpage );
2026-02-25 23:11:35 +01:00
return $vars ;
});