#!/usr/bin/perl # Copyright 2011 Blars Blarson # Released under GPL version 2 package IkiWiki::Plugin::osm; use utf8; use strict; use warnings; use IkiWiki 3.0; sub import { add_underlay("osm"); hook(type => "getsetup", id => "osm", call => \&getsetup); hook(type => "format", id => "osm", call => \&format); hook(type => "preprocess", id => "osm", call => \&preprocess); hook(type => "preprocess", id => "waypoint", call => \&process_waypoint); hook(type => "savestate", id => "waypoint", call => \&savestate); hook(type => "cgi", id => "osm", call => \&cgi); } sub getsetup () { return plugin => { safe => 1, rebuild => 1, section => "special-purpose", }, osm_default_zoom => { type => "integer", example => "15", description => "the default zoom when you click on the map link", safe => 1, rebuild => 1, }, osm_default_icon => { type => "string", example => "ikiwiki/images/osm.png", description => "the icon shown on links and on the main map", safe => 0, rebuild => 1, }, osm_alt => { type => "string", example => "", description => "the alt tag of links, defaults to empty", safe => 0, rebuild => 1, }, osm_format => { type => "string", example => "KML", description => "the output format for waypoints, can be KML, GeoJSON or CSV (one or many, comma-separated)", safe => 1, rebuild => 1, }, osm_tag_default_icon => { type => "string", example => "icon.png", description => "the icon attached to a tag, displayed on the map for tagged pages", safe => 0, rebuild => 1, }, osm_openlayers_url => { type => "string", example => "http://www.openlayers.org/api/OpenLayers.js", description => "Url for the OpenLayers.js file", safe => 0, rebuild => 1, }, osm_layers => { type => "string", example => { 'OSM', 'GoogleSatellite' }, description => "Layers to use in the map. Can be either the 'OSM' string or a type option for Google maps (GoogleNormal, GoogleSatellite, GoogleHybrid or GooglePhysical). It can also be an arbitrary URL in a syntax acceptable for OpenLayers.Layer.OSM.url parameter.", safe => 0, rebuild => 1, }, osm_google_apikey => { type => "string", example => "", description => "Google maps API key, Google layer not used if missing, see https://code.google.com/apis/console/ to get an API key", safe => 1, rebuild => 1, }, } sub register_rendered_files { my $map = shift; my $page = shift; my $dest = shift; if ($page eq $dest) { my %formats = get_formats(); if ($formats{'GeoJSON'}) { will_render($page, "$map/pois.json"); } if ($formats{'CSV'}) { will_render($page, "$map/pois.txt"); } if ($formats{'KML'}) { will_render($page, "$map/pois.kml"); } } } sub preprocess { my %params=@_; my $page = $params{page}; my $dest = $params{destpage}; my $loc = $params{loc}; # sanitized below my $lat = $params{lat}; # sanitized below my $lon = $params{lon}; # sanitized below my $href = $params{href}; my ($width, $height, $float); $height = scrub($params{'height'} || "300px", $page, $dest); # sanitized here $width = scrub($params{'width'} || "500px", $page, $dest); # sanitized here $float = (defined($params{'right'}) && 'right') || (defined($params{'left'}) && 'left'); # sanitized here my $zoom = scrub($params{'zoom'} // $config{'osm_default_zoom'} // 15, $page, $dest); # sanitized below my $map; $map = $params{'map'} || 'map'; $map = scrub($map, $page, $dest); # sanitized here my $name = scrub($params{'name'} || $map, $page, $dest); if (defined($lon) || defined($lat) || defined($loc)) { ($lon, $lat) = scrub_lonlat($loc, $lon, $lat); } if ($zoom !~ /^\d\d?$/ || $zoom < 2 || $zoom > 18) { error("Bad zoom"); } if (! defined $href || ! length $href) { $href=IkiWiki::cgiurl( do => "osm", map => $map, ); } register_rendered_files($map, $page, $dest); $pagestate{$page}{'osm'}{$map}{'displays'}{$name} = { height => $height, width => $width, float => $float, zoom => $zoom, fullscreen => 0, editable => defined($params{'editable'}), lat => $lat, lon => $lon, href => $href, google_apikey => $config{'osm_google_apikey'}, }; return "
"; } sub process_waypoint { my %params=@_; my $loc = $params{'loc'}; # sanitized below my $lat = $params{'lat'}; # sanitized below my $lon = $params{'lon'}; # sanitized below my $page = $params{'page'}; # not sanitized? my $dest = $params{'destpage'}; # not sanitized? my $hidden = defined($params{'hidden'}); # sanitized here my ($p) = $page =~ /(?:^|\/)([^\/]+)\/?$/; # shorter page name my $name = scrub($params{'name'} || $p, $page, $dest); # sanitized here my $desc = scrub($params{'desc'} || '', $page, $dest); # sanitized here my $zoom = scrub($params{'zoom'} // $config{'osm_default_zoom'} // 15, $page, $dest); # sanitized below my $icon = $config{'osm_default_icon'} || "ikiwiki/images/osm.png"; # sanitized: we trust $config my $map = scrub($params{'map'} || 'map', $page, $dest); # sanitized here my $alt = $config{'osm_alt'} ? "alt=\"$config{'osm_alt'}\"" : ''; # sanitized: we trust $config if ($zoom !~ /^\d\d?$/ || $zoom < 2 || $zoom > 18) { error("Bad zoom"); } ($lon, $lat) = scrub_lonlat($loc, $lon, $lat); if (!defined($lat) || !defined($lon)) { error("Must specify lat and lon"); } my $tag = $params{'tag'}; foreach my $t (keys %{$typedlinks{$page}{'tag'}}) { if ($icon = get_tag_icon($t)) { $tag = $t; last; } $t =~ s!/$config{'tagbase'}/!!; if ($icon = get_tag_icon($t)) { $tag = $t; last; } } $icon = urlto($icon, $dest, 1); $tag = '' unless $tag; register_rendered_files($map, $page, $dest); $pagestate{$page}{'osm'}{$map}{'waypoints'}{$name} = { page => $page, desc => $desc, icon => $icon, tag => $tag, lat => $lat, lon => $lon, # How to link back to the page from the map, not to be # confused with the URL of the map itself sent to the # embeded map below. Note: used in generated KML etc file, # so must be absolute. href => urlto($page), }; my $mapurl = IkiWiki::cgiurl( do => "osm", map => $map, lat => $lat, lon => $lon, zoom => $zoom, ); my $output = ''; if (defined($params{'embed'})) { $output .= preprocess(%params, href => $mapurl, ); } if (!$hidden) { $output .= ""; } return $output; } # get the icon from the given tag sub get_tag_icon($) { my $tag = shift; # look for an icon attached to the tag my $attached = $tag . '/' . $config{'osm_tag_default_icon'}; if (srcfile($attached)) { return $attached; } else { return undef; } } sub scrub_lonlat($$$) { my ($loc, $lon, $lat) = @_; if ($loc) { if ($loc =~ /^\s*(\-?\d+(?:\.\d*°?|(?:°?|\s)\s*\d+(?:\.\d*\'?|(?:\'|\s)\s*\d+(?:\.\d*)?\"?|\'?)°?)[NS]?)\s*\,?\;?\s*(\-?\d+(?:\.\d*°?|(?:°?|\s)\s*\d+(?:\.\d*\'?|(?:\'|\s)\s*\d+(?:\.\d*)?\"?|\'?)°?)[EW]?)\s*$/) { $lat = $1; $lon = $2; } else { error("Bad loc"); } } if (defined($lat)) { if ($lat =~ /^(\-?)(\d+)(?:(\.\d*)°?|(?:°|\s)\s*(\d+)(?:(\.\d*)\'?|(?:\'|\s)\s*(\d+(?:\.\d*)?\"?)|\'?)|°?)\s*([NS])?\s*$/) { $lat = $2 + ($3//0) + ((($4//0) + (($5//0) + (($6//0)/60.)))/60.); if (($1 eq '-') || (($7//'') eq 'S')) { $lat = - $lat; } } else { error("Bad lat"); } } if (defined($lon)) { if ($lon =~ /^(\-?)(\d+)(?:(\.\d*)°?|(?:°|\s)\s*(\d+)(?:(\.\d*)\'?|(?:\'|\s)\s*(\d+(?:\.\d*)?\"?)|\'?)|°?)\s*([EW])?$/) { $lon = $2 + ($3//0) + ((($4//0) + (($5//0) + (($6//0)/60.)))/60.); if (($1 eq '-') || (($7//'') eq 'W')) { $lon = - $lon; } } else { error("Bad lon"); } } if ($lat < -90 || $lat > 90 || $lon < -180 || $lon > 180) { error("Location out of range"); } return ($lon, $lat); } sub savestate { my %waypoints = (); my %linestrings = (); foreach my $page (keys %pagestate) { if (exists $pagestate{$page}{'osm'}) { foreach my $map (keys %{$pagestate{$page}{'osm'}}) { foreach my $name (keys %{$pagestate{$page}{'osm'}{$map}{'waypoints'}}) { debug("found waypoint $name"); $waypoints{$map}{$name} = $pagestate{$page}{'osm'}{$map}{'waypoints'}{$name}; } } } } foreach my $page (keys %pagestate) { if (exists $pagestate{$page}{'osm'}) { foreach my $map (keys %{$pagestate{$page}{'osm'}}) { # examine the links on this page foreach my $name (keys %{$pagestate{$page}{'osm'}{$map}{'waypoints'}}) { if (exists $links{$page}) { foreach my $otherpage (@{$links{$page}}) { if (exists $waypoints{$map}{$otherpage}) { push(@{$linestrings{$map}}, [ [ $waypoints{$map}{$name}{'lon'}, $waypoints{$map}{$name}{'lat'} ], [ $waypoints{$map}{$otherpage}{'lon'}, $waypoints{$map}{$otherpage}{'lat'} ] ]); } } } } } # clear the state, it will be regenerated on the next parse # the idea here is to clear up removed waypoints... $pagestate{$page}{'osm'} = (); } } my %formats = get_formats(); if ($formats{'GeoJSON'}) { writejson(\%waypoints, \%linestrings); } if ($formats{'CSV'}) { writecsvs(\%waypoints, \%linestrings); } if ($formats{'KML'}) { writekml(\%waypoints, \%linestrings); } } sub writejson($;$) { my %waypoints = %{$_[0]}; my %linestrings = %{$_[1]}; eval q{use JSON}; error $@ if $@; foreach my $map (keys %waypoints) { my %geojson = ( "type" => "FeatureCollection", "features" => []); foreach my $name (keys %{$waypoints{$map}}) { my %marker = ( "type" => "Feature", "geometry" => { "type" => "Point", "coordinates" => [ $waypoints{$map}{$name}{'lon'}, $waypoints{$map}{$name}{'lat'} ] }, "properties" => $waypoints{$map}{$name} ); push @{$geojson{'features'}}, \%marker; } foreach my $linestring (@{$linestrings{$map}}) { my %json = ( "type" => "Feature", "geometry" => { "type" => "LineString", "coordinates" => $linestring }); push @{$geojson{'features'}}, \%json; } writefile("pois.json", $config{destdir} . "/$map", to_json(\%geojson)); } } sub writekml($;$) { my %waypoints = %{$_[0]}; my %linestrings = %{$_[1]}; eval q{use XML::Writer}; error $@ if $@; foreach my $map (keys %waypoints) { my $output; my $writer = XML::Writer->new( OUTPUT => \$output, DATA_MODE => 1, DATA_INDENT => ' ', ENCODING => 'UTF-8'); $writer->xmlDecl(); $writer->startTag("kml", "xmlns" => "http://www.opengis.net/kml/2.2"); $writer->startTag("Document"); # first pass: get the icons my %tags_map = (); # keep track of tags seen foreach my $name (keys %{$waypoints{$map}}) { my %options = %{$waypoints{$map}{$name}}; if (!$tags_map{$options{tag}}) { debug("found new style " . $options{tag}); $tags_map{$options{tag}} = (); $writer->startTag("Style", id => $options{tag}); $writer->startTag("IconStyle"); $writer->startTag("Icon"); $writer->startTag("href"); $writer->characters($options{icon}); $writer->endTag(); $writer->endTag(); $writer->endTag(); $writer->endTag(); } $tags_map{$options{tag}}{$name} = \%options; } foreach my $name (keys %{$waypoints{$map}}) { my %options = %{$waypoints{$map}{$name}}; $writer->startTag("Placemark"); $writer->startTag("name"); $writer->characters($name); $writer->endTag(); $writer->startTag("styleUrl"); $writer->characters('#' . $options{tag}); $writer->endTag(); #$writer->emptyTag('atom:link', href => $options{href}); # to make it easier for us as the atom:link parameter is # hard to access from javascript $writer->startTag('href'); $writer->characters($options{href}); $writer->endTag(); $writer->startTag("description"); $writer->characters($options{desc}); $writer->endTag(); $writer->startTag("Point"); $writer->startTag("coordinates"); $writer->characters($options{lon} . "," . $options{lat}); $writer->endTag(); $writer->endTag(); $writer->endTag(); } my $i = 0; foreach my $linestring (@{$linestrings{$map}}) { $writer->startTag("Placemark"); $writer->startTag("name"); $writer->characters("linestring " . $i++); $writer->endTag(); $writer->startTag("LineString"); $writer->startTag("coordinates"); my $str = ''; foreach my $coord (@{$linestring}) { $str .= join(',', @{$coord}) . " \n"; } $writer->characters($str); $writer->endTag(); $writer->endTag(); $writer->endTag(); } $writer->endTag(); $writer->endTag(); $writer->end(); writefile("pois.kml", $config{destdir} . "/$map", $output); } } sub writecsvs($;$) { my %waypoints = %{$_[0]}; foreach my $map (keys %waypoints) { my $poisf = "lat\tlon\ttitle\tdescription\ticon\ticonSize\ticonOffset\n"; foreach my $name (keys %{$waypoints{$map}}) { my %options = %{$waypoints{$map}{$name}}; my $line = $options{'lat'} . "\t" . $options{'lon'} . "\t" . $name . "\t" . $options{'desc'} . '
' . $name . "\t" . $options{'icon'} . "\n"; $poisf .= $line; } writefile("pois.txt", $config{destdir} . "/$map", $poisf); } } # pipe some data through the HTML scrubber # # code taken from the meta.pm plugin sub scrub($$$) { if (IkiWiki::Plugin::htmlscrubber->can("sanitize")) { return IkiWiki::Plugin::htmlscrubber::sanitize( content => shift, page => shift, destpage => shift); } else { return shift; } } # taken from toggle.pm sub format (@) { my %params=@_; if ($params{content}=~m!]*id="mapdiv-[^"]*"[^>]*>!g) { if (! ($params{content}=~s!!include_javascript($params{page}).""!em)) { # no tag, probably in preview mode $params{content}=$params{content} . include_javascript($params{page}); } } return $params{content}; } sub preferred_format() { if (!defined($config{'osm_format'}) || !$config{'osm_format'}) { $config{'osm_format'} = 'KML'; } my @spl = split(/, */, $config{'osm_format'}); return shift @spl; } sub get_formats() { if (!defined($config{'osm_format'}) || !$config{'osm_format'}) { $config{'osm_format'} = 'KML'; } map { $_ => 1 } split(/, */, $config{'osm_format'}); } sub include_javascript ($) { my $page=shift; my $loader; if (exists $pagestate{$page}{'osm'}) { foreach my $map (keys %{$pagestate{$page}{'osm'}}) { foreach my $name (keys %{$pagestate{$page}{'osm'}{$map}{'displays'}}) { $loader .= map_setup_code($map, $name, %{$pagestate{$page}{'osm'}{$map}{'displays'}{$name}}); } } } if ($loader) { return embed_map_code($page) . ""; } else { return ''; } } sub cgi($) { my $cgi=shift; return unless defined $cgi->param('do') && $cgi->param("do") eq "osm"; IkiWiki::loadindex(); IkiWiki::decode_cgi_utf8($cgi); my $map = $cgi->param('map'); if (!defined $map || $map !~ /^[a-z]*$/) { error("invalid map parameter"); } print "Content-Type: text/html\r\n"; print ("\r\n"); print ""; print "
"; print embed_map_code(); print ""; print ""; exit 0; } sub embed_map_code(;$) { my $page=shift; my $olurl = $config{osm_openlayers_url} || "http://www.openlayers.org/api/OpenLayers.js"; my $code = ''."\n". ''."\n"; if ($config{'osm_google_apikey'}) { $code .= ''; } return $code; } sub map_setup_code($;@) { my $map=shift; my $name=shift; my %options=@_; my $mapurl = $config{osm_map_url}; eval q{use JSON}; error $@ if $@; $options{'format'} = preferred_format(); my %formats = get_formats(); if ($formats{'GeoJSON'}) { $options{'jsonurl'} = urlto($map."/pois.json"); } if ($formats{'CSV'}) { $options{'csvurl'} = urlto($map."/pois.txt"); } if ($formats{'KML'}) { $options{'kmlurl'} = urlto($map."/pois.kml"); } if ($mapurl) { $options{'mapurl'} = $mapurl; } $options{'layers'} = $config{osm_layers}; return "mapsetup('mapdiv-$name', " . to_json(\%options) . ");"; } 1;