2 # Copyright 2011 Blars Blarson
3 # Released under GPL version 2
5 package IkiWiki::Plugin::osm;
13 hook(type => "getsetup", id => "osm", call => \&getsetup);
14 hook(type => "format", id => "osm", call => \&format);
15 hook(type => "preprocess", id => "osm", call => \&preprocess);
16 hook(type => "preprocess", id => "waypoint", call => \&process_waypoint);
17 hook(type => "changes", id => "waypoint", call => \&changes);
18 hook(type => "cgi", id => "osm", call => \&cgi);
26 section => "special-purpose",
31 description => "the default zoom when you click on the map link",
37 example => "ikiwiki/images/osm.png",
38 description => "the icon shown on links and on the main map",
45 description => "the alt tag of links, defaults to empty",
52 description => "the output format for waypoints, can be KML, GeoJSON or CSV (one or many, comma-separated)",
56 osm_tag_default_icon => {
58 example => "icon.png",
59 description => "the icon attached to a tag, displayed on the map for tagged pages",
63 osm_openlayers_url => {
65 example => "http://www.openlayers.org/api/OpenLayers.js",
66 description => "Url for the OpenLayers.js file",
72 example => { 'OSM', 'GoogleSatellite' },
73 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.",
77 osm_google_apikey => {
80 description => "Google maps API key, Google layer not used if missing, see https://code.google.com/apis/console/ to get an API key",
86 sub register_rendered_files {
92 my %formats = get_formats();
93 if ($formats{'GeoJSON'}) {
94 will_render($page, "$map/pois.json");
96 if ($formats{'CSV'}) {
97 will_render($page, "$map/pois.txt");
99 if ($formats{'KML'}) {
100 will_render($page, "$map/pois.kml");
107 my $page = $params{page};
108 my $dest = $params{destpage};
109 my $loc = $params{loc}; # sanitized below
110 my $lat = $params{lat}; # sanitized below
111 my $lon = $params{lon}; # sanitized below
112 my $href = $params{href};
114 my ($width, $height, $float);
115 $height = scrub($params{'height'} || "300px", $page, $dest); # sanitized here
116 $width = scrub($params{'width'} || "500px", $page, $dest); # sanitized here
117 $float = (defined($params{'right'}) && 'right') || (defined($params{'left'}) && 'left'); # sanitized here
119 my $zoom = scrub($params{'zoom'} // $config{'osm_default_zoom'} // 15, $page, $dest); # sanitized below
121 $map = $params{'map'} || 'map';
123 $map = scrub($map, $page, $dest); # sanitized here
124 my $name = scrub($params{'name'} || $map, $page, $dest);
126 if (defined($lon) || defined($lat) || defined($loc)) {
127 ($lon, $lat) = scrub_lonlat($loc, $lon, $lat);
130 if ($zoom !~ /^\d\d?$/ || $zoom < 2 || $zoom > 18) {
134 if (! defined $href || ! length $href) {
135 $href=IkiWiki::cgiurl(
141 register_rendered_files($map, $page, $dest);
143 $pagestate{$page}{'osm'}{$map}{'displays'}{$name} = {
149 editable => defined($params{'editable'}),
153 google_apikey => $config{'osm_google_apikey'},
155 return "<div id=\"mapdiv-$name\"></div>";
158 sub process_waypoint {
160 my $loc = $params{'loc'}; # sanitized below
161 my $lat = $params{'lat'}; # sanitized below
162 my $lon = $params{'lon'}; # sanitized below
163 my $page = $params{'page'}; # not sanitized?
164 my $dest = $params{'destpage'}; # not sanitized?
165 my $hidden = defined($params{'hidden'}); # sanitized here
166 my ($p) = $page =~ /(?:^|\/)([^\/]+)\/?$/; # shorter page name
167 my $name = scrub($params{'name'} || $p, $page, $dest); # sanitized here
168 my $desc = scrub($params{'desc'} || '', $page, $dest); # sanitized here
169 my $zoom = scrub($params{'zoom'} // $config{'osm_default_zoom'} // 15, $page, $dest); # sanitized below
170 my $icon = $config{'osm_default_icon'} || "ikiwiki/images/osm.png"; # sanitized: we trust $config
171 my $map = scrub($params{'map'} || 'map', $page, $dest); # sanitized here
172 my $alt = $config{'osm_alt'} ? "alt=\"$config{'osm_alt'}\"" : ''; # sanitized: we trust $config
173 if ($zoom !~ /^\d\d?$/ || $zoom < 2 || $zoom > 18) {
177 ($lon, $lat) = scrub_lonlat($loc, $lon, $lat);
178 if (!defined($lat) || !defined($lon)) {
179 error("Must specify lat and lon");
182 my $tag = $params{'tag'};
183 foreach my $t (keys %{$typedlinks{$page}{'tag'}}) {
184 if ($icon = get_tag_icon($t)) {
188 $t =~ s!/$config{'tagbase'}/!!;
189 if ($icon = get_tag_icon($t)) {
194 $icon = urlto($icon, $dest, 1);
195 $icon =~ s!/*$!!; # hack - urlto shouldn't be appending a slash in the first place
196 $tag = '' unless $tag;
197 register_rendered_files($map, $page, $dest);
198 $pagestate{$page}{'osm'}{$map}{'waypoints'}{$name} = {
205 # How to link back to the page from the map, not to be
206 # confused with the URL of the map itself sent to the
207 # embeded map below. Note: used in generated KML etc file,
208 # so must be absolute.
209 href => urlto($page),
212 my $mapurl = IkiWiki::cgiurl(
220 if (defined($params{'embed'})) {
221 $output .= preprocess(%params,
226 $output .= "<a href=\"$mapurl\"><img class=\"img\" src=\"$icon\" $alt /></a>";
231 # get the icon from the given tag
232 sub get_tag_icon($) {
234 # look for an icon attached to the tag
235 my $attached = $tag . '/' . $config{'osm_tag_default_icon'};
236 if (srcfile($attached, 1)) {
244 sub scrub_lonlat($$$) {
245 my ($loc, $lon, $lat) = @_;
247 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*$/) {
256 if ($lat =~ /^(\-?)(\d+)(?:(\.\d*)°?|(?:°|\s)\s*(\d+)(?:(\.\d*)\'?|(?:\'|\s)\s*(\d+(?:\.\d*)?\"?)|\'?)|°?)\s*([NS])?\s*$/) {
257 $lat = $2 + ($3//0) + ((($4//0) + (($5//0) + (($6//0)/60.)))/60.);
258 if (($1 eq '-') || (($7//'') eq 'S')) {
267 if ($lon =~ /^(\-?)(\d+)(?:(\.\d*)°?|(?:°|\s)\s*(\d+)(?:(\.\d*)\'?|(?:\'|\s)\s*(\d+(?:\.\d*)?\"?)|\'?)|°?)\s*([EW])?$/) {
268 $lon = $2 + ($3//0) + ((($4//0) + (($5//0) + (($6//0)/60.)))/60.);
269 if (($1 eq '-') || (($7//'') eq 'W')) {
277 if ($lat < -90 || $lat > 90 || $lon < -180 || $lon > 180) {
278 error("Location out of range");
285 my %linestrings = ();
287 foreach my $page (keys %pagestate) {
288 if (exists $pagestate{$page}{'osm'}) {
289 foreach my $map (keys %{$pagestate{$page}{'osm'}}) {
290 foreach my $name (keys %{$pagestate{$page}{'osm'}{$map}{'waypoints'}}) {
291 debug("found waypoint $name");
292 $waypoints{$map}{$name} = $pagestate{$page}{'osm'}{$map}{'waypoints'}{$name};
298 foreach my $page (keys %pagestate) {
299 if (exists $pagestate{$page}{'osm'}) {
300 foreach my $map (keys %{$pagestate{$page}{'osm'}}) {
301 # examine the links on this page
302 foreach my $name (keys %{$pagestate{$page}{'osm'}{$map}{'waypoints'}}) {
303 if (exists $links{$page}) {
304 foreach my $otherpage (@{$links{$page}}) {
305 if (exists $waypoints{$map}{$otherpage}) {
306 push(@{$linestrings{$map}}, [
307 [ $waypoints{$map}{$name}{'lon'}, $waypoints{$map}{$name}{'lat'} ],
308 [ $waypoints{$map}{$otherpage}{'lon'}, $waypoints{$map}{$otherpage}{'lat'} ]
315 # clear the state, it will be regenerated on the next parse
316 # the idea here is to clear up removed waypoints...
317 $pagestate{$page}{'osm'} = ();
321 my %formats = get_formats();
322 if ($formats{'GeoJSON'}) {
323 writejson(\%waypoints, \%linestrings);
325 if ($formats{'CSV'}) {
326 writecsvs(\%waypoints, \%linestrings);
328 if ($formats{'KML'}) {
329 writekml(\%waypoints, \%linestrings);
334 my %waypoints = %{$_[0]};
335 my %linestrings = %{$_[1]};
338 foreach my $map (keys %waypoints) {
339 my %geojson = ( "type" => "FeatureCollection", "features" => []);
340 foreach my $name (keys %{$waypoints{$map}}) {
341 my %marker = ( "type" => "Feature",
342 "geometry" => { "type" => "Point", "coordinates" => [ $waypoints{$map}{$name}{'lon'}, $waypoints{$map}{$name}{'lat'} ] },
343 "properties" => $waypoints{$map}{$name} );
344 push @{$geojson{'features'}}, \%marker;
346 foreach my $linestring (@{$linestrings{$map}}) {
347 my %json = ( "type" => "Feature",
348 "geometry" => { "type" => "LineString", "coordinates" => $linestring });
349 push @{$geojson{'features'}}, \%json;
351 writefile("pois.json", $config{destdir} . "/$map", to_json(\%geojson));
356 my %waypoints = %{$_[0]};
357 my %linestrings = %{$_[1]};
358 eval q{use XML::Writer};
360 foreach my $map (keys %waypoints) {
362 my $writer = XML::Writer->new( OUTPUT => \$output,
363 DATA_MODE => 1, DATA_INDENT => ' ', ENCODING => 'UTF-8');
365 $writer->startTag("kml", "xmlns" => "http://www.opengis.net/kml/2.2");
366 $writer->startTag("Document");
368 # first pass: get the icons
369 my %tags_map = (); # keep track of tags seen
370 foreach my $name (keys %{$waypoints{$map}}) {
371 my %options = %{$waypoints{$map}{$name}};
372 if (!$tags_map{$options{tag}}) {
373 debug("found new style " . $options{tag});
374 $tags_map{$options{tag}} = ();
375 $writer->startTag("Style", id => $options{tag});
376 $writer->startTag("IconStyle");
377 $writer->startTag("Icon");
378 $writer->startTag("href");
379 $writer->characters($options{icon});
385 $tags_map{$options{tag}}{$name} = \%options;
388 foreach my $name (keys %{$waypoints{$map}}) {
389 my %options = %{$waypoints{$map}{$name}};
390 $writer->startTag("Placemark");
391 $writer->startTag("name");
392 $writer->characters($name);
394 $writer->startTag("styleUrl");
395 $writer->characters('#' . $options{tag});
397 #$writer->emptyTag('atom:link', href => $options{href});
398 # to make it easier for us as the atom:link parameter is
399 # hard to access from javascript
400 $writer->startTag('href');
401 $writer->characters($options{href});
403 $writer->startTag("description");
404 $writer->characters($options{desc});
406 $writer->startTag("Point");
407 $writer->startTag("coordinates");
408 $writer->characters($options{lon} . "," . $options{lat});
415 foreach my $linestring (@{$linestrings{$map}}) {
416 $writer->startTag("Placemark");
417 $writer->startTag("name");
418 $writer->characters("linestring " . $i++);
420 $writer->startTag("LineString");
421 $writer->startTag("coordinates");
423 foreach my $coord (@{$linestring}) {
424 $str .= join(',', @{$coord}) . " \n";
426 $writer->characters($str);
435 writefile("pois.kml", $config{destdir} . "/$map", $output);
440 my %waypoints = %{$_[0]};
441 foreach my $map (keys %waypoints) {
442 my $poisf = "lat\tlon\ttitle\tdescription\ticon\ticonSize\ticonOffset\n";
443 foreach my $name (keys %{$waypoints{$map}}) {
444 my %options = %{$waypoints{$map}{$name}};
446 $options{'lat'} . "\t" .
447 $options{'lon'} . "\t" .
449 $options{'desc'} . '<br /><a href="' . $options{'page'} . '">' . $name . "</a>\t" .
450 $options{'icon'} . "\n";
453 writefile("pois.txt", $config{destdir} . "/$map", $poisf);
457 # pipe some data through the HTML scrubber
459 # code taken from the meta.pm plugin
461 if (IkiWiki::Plugin::htmlscrubber->can("sanitize")) {
462 return IkiWiki::Plugin::htmlscrubber::sanitize(
463 content => shift, page => shift, destpage => shift);
470 # taken from toggle.pm
474 if ($params{content}=~m!<div[^>]*id="mapdiv-[^"]*"[^>]*>!g) {
475 if (! ($params{content}=~s!</body>!include_javascript($params{page})."</body>"!em)) {
476 # no <body> tag, probably in preview mode
477 $params{content}=$params{content} . include_javascript($params{page});
480 return $params{content};
483 sub preferred_format() {
484 if (!defined($config{'osm_format'}) || !$config{'osm_format'}) {
485 $config{'osm_format'} = 'KML';
487 my @spl = split(/, */, $config{'osm_format'});
492 if (!defined($config{'osm_format'}) || !$config{'osm_format'}) {
493 $config{'osm_format'} = 'KML';
495 map { $_ => 1 } split(/, */, $config{'osm_format'});
498 sub include_javascript ($) {
502 if (exists $pagestate{$page}{'osm'}) {
503 foreach my $map (keys %{$pagestate{$page}{'osm'}}) {
504 foreach my $name (keys %{$pagestate{$page}{'osm'}{$map}{'displays'}}) {
505 $loader .= map_setup_code($map, $name, %{$pagestate{$page}{'osm'}{$map}{'displays'}{$name}});
510 return embed_map_code($page) . "<script type=\"text/javascript\">$loader</script>";
520 return unless defined $cgi->param('do') &&
521 $cgi->param("do") eq "osm";
523 IkiWiki::loadindex();
525 IkiWiki::decode_cgi_utf8($cgi);
527 my $map = $cgi->param('map');
528 if (!defined $map || $map !~ /^[a-z]*$/) {
529 error("invalid map parameter");
532 print "Content-Type: text/html\r\n";
534 print "<html><body>";
535 print "<div id=\"mapdiv-$map\"></div>";
536 print embed_map_code();
537 print "<script type=\"text/javascript\">";
538 print map_setup_code($map, $map,
539 lat => "urlParams['lat']",
540 lon => "urlParams['lon']",
541 zoom => "urlParams['zoom']",
544 google_apikey => $config{'osm_google_apikey'},
547 print "</body></html>";
552 sub embed_map_code(;$) {
554 my $olurl = $config{osm_openlayers_url} || "http://www.openlayers.org/api/OpenLayers.js";
555 my $code = '<script src="'.$olurl.'" type="text/javascript" charset="utf-8"></script>'."\n".
556 '<script src="'.urlto("ikiwiki/osm.js", $page).
557 '" type="text/javascript" charset="utf-8"></script>'."\n";
558 if ($config{'osm_google_apikey'}) {
559 $code .= '<script src="http://maps.google.com/maps?file=api&v=2&key='.$config{'osm_google_apikey'}.'&sensor=false" type="text/javascript" charset="utf-8"></script>';
564 sub map_setup_code($;@) {
569 my $mapurl = $config{osm_map_url};
574 $options{'format'} = preferred_format();
576 my %formats = get_formats();
577 if ($formats{'GeoJSON'}) {
578 $options{'jsonurl'} = urlto($map."/pois.json");
580 if ($formats{'CSV'}) {
581 $options{'csvurl'} = urlto($map."/pois.txt");
583 if ($formats{'KML'}) {
584 $options{'kmlurl'} = urlto($map."/pois.kml");
588 $options{'mapurl'} = $mapurl;
590 $options{'layers'} = $config{osm_layers};
592 $name=~s/'//g; # $name comes from user input
593 return "mapsetup('mapdiv-$name', " . to_json(\%options) . ");";