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 => "savestate", id => "waypoint", call => \&savestate);
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",
67 my $page = $params{'page'};
68 my $dest = $params{'destpage'};
69 my $loc = $params{'loc'}; # sanitized below
70 my $lat = $params{'lat'}; # sanitized below
71 my $lon = $params{'lon'}; # sanitized below
72 my $href = $params{'href'};
74 my $fullscreen = defined($params{'fullscreen'}); # sanitized here
75 my ($width, $height, $float);
82 $height = scrub($params{'height'} || "300px", $page, $dest); # sanitized here
83 $width = scrub($params{'width'} || "500px", $page, $dest); # sanitized here
84 $float = (defined($params{'right'}) && 'right') || (defined($params{'left'}) && 'left'); # sanitized here
86 my $zoom = scrub($params{'zoom'} // $config{'osm_default_zoom'} // 15, $page, $dest); # sanitized below
89 $map = $params{'map'} || $page;
92 $map = $params{'map'} || 'map';
94 $map = scrub($map, $page, $dest); # sanitized here
95 my $name = scrub($params{'name'} || $map, $page, $dest);
97 if (defined($lon) || defined($lat) || defined($loc)) {
98 ($lon, $lat) = scrub_lonlat($loc, $lon, $lat);
101 if ($zoom !~ /^\d\d?$/ || $zoom < 2 || $zoom > 18) {
105 if (! defined $href || ! length $href) {
106 $href=IkiWiki::cgiurl(
112 $pagestate{$page}{'osm'}{$map}{'displays'}{$name} = {
117 fullscreen => $fullscreen,
118 editable => defined($params{'editable'}),
123 return "<div id=\"mapdiv-$name\"></div>";
126 sub process_waypoint {
128 my $loc = $params{'loc'}; # sanitized below
129 my $lat = $params{'lat'}; # sanitized below
130 my $lon = $params{'lon'}; # sanitized below
131 my $page = $params{'page'}; # not sanitized?
132 my $dest = $params{'destpage'}; # not sanitized?
133 my $hidden = defined($params{'hidden'}); # sanitized here
134 my ($p) = $page =~ /(?:^|\/)([^\/]+)\/?$/; # shorter page name
135 my $name = scrub($params{'name'} || $p, $page, $dest); # sanitized here
136 my $desc = scrub($params{'desc'} || '', $page, $dest); # sanitized here
137 my $zoom = scrub($params{'zoom'} // $config{'osm_default_zoom'} // 15, $page, $dest); # sanitized below
138 my $icon = $config{'osm_default_icon'} || "ikiwiki/images/osm.png"; # sanitized: we trust $config
139 my $map = scrub($params{'map'} || 'map', $page, $dest); # sanitized here
140 my $alt = $config{'osm_alt'} ? "alt=\"$config{'osm_alt'}\"" : ''; # sanitized: we trust $config
141 if ($zoom !~ /^\d\d?$/ || $zoom < 2 || $zoom > 18) {
145 ($lon, $lat) = scrub_lonlat($loc, $lon, $lat);
146 if (!defined($lat) || !defined($lon)) {
147 error("Must specify lat and lon");
150 my $tag = $params{'tag'};
151 foreach my $t (keys %{$typedlinks{$page}{'tag'}}) {
152 if ($icon = get_tag_icon($t)) {
156 $t =~ s!/$config{'tagbase'}/!!;
157 if ($icon = get_tag_icon($t)) {
162 $icon = urlto($icon, $dest, 1);
163 $tag = '' unless $tag;
164 if ($page eq $dest) {
165 my %formats = get_formats();
166 if ($formats{'GeoJSON'}) {
167 will_render($page, "$map/pois.json");
169 if ($formats{'CSV'}) {
170 will_render($page, "$map/pois.txt");
172 if ($formats{'KML'}) {
173 will_render($page, "$map/pois.kml");
176 my $href = IkiWiki::cgiurl(
183 if (defined($destsources{htmlpage($map)})) {
184 $href = urlto($map,$page) . "?lat=$lat&lon=$lon&zoom=$zoom";
185 $href =~ s!&!&!g;
187 $pagestate{$page}{'osm'}{$map}{'waypoints'}{$name} = {
194 # how to link back to the page from the map, not to be
195 # confused with the URL of the map itself sent to the
197 href => urlto($page,$map),
200 if (defined($params{'embed'})) {
201 $params{'href'} = $href; # propagate down to embeded
202 $output .= preprocess(%params);
205 $output .= "<a href=\"$href\"><img class=\"img\" src=\"$icon\" $alt /></a>";
210 # get the icon from the given tag
211 sub get_tag_icon($) {
213 # look for an icon attached to the tag
214 my $attached = $tag . '/' . $config{'osm_tag_default_icon'};
215 if (srcfile($attached)) {
223 sub scrub_lonlat($$$) {
224 my ($loc, $lon, $lat) = @_;
226 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*$/) {
235 if ($lat =~ /^(\-?)(\d+)(?:(\.\d*)°?|(?:°|\s)\s*(\d+)(?:(\.\d*)\'?|(?:\'|\s)\s*(\d+(?:\.\d*)?\"?)|\'?)|°?)\s*([NS])?\s*$/) {
236 $lat = $2 + ($3//0) + ((($4//0) + (($5//0) + (($6//0)/60.)))/60.);
237 if (($1 eq '-') || (($7//'') eq 'S')) {
246 if ($lon =~ /^(\-?)(\d+)(?:(\.\d*)°?|(?:°|\s)\s*(\d+)(?:(\.\d*)\'?|(?:\'|\s)\s*(\d+(?:\.\d*)?\"?)|\'?)|°?)\s*([EW])?$/) {
247 $lon = $2 + ($3//0) + ((($4//0) + (($5//0) + (($6//0)/60.)))/60.);
248 if (($1 eq '-') || (($7//'') eq 'W')) {
256 if ($lat < -90 || $lat > 90 || $lon < -180 || $lon > 180) {
257 error("Location out of range");
264 my %linestrings = ();
266 foreach my $page (keys %pagestate) {
267 if (exists $pagestate{$page}{'osm'}) {
268 foreach my $map (keys %{$pagestate{$page}{'osm'}}) {
269 foreach my $name (keys %{$pagestate{$page}{'osm'}{$map}{'waypoints'}}) {
270 debug("found waypoint $name");
271 $waypoints{$map}{$name} = $pagestate{$page}{'osm'}{$map}{'waypoints'}{$name};
277 foreach my $page (keys %pagestate) {
278 if (exists $pagestate{$page}{'osm'}) {
279 foreach my $map (keys %{$pagestate{$page}{'osm'}}) {
280 # examine the links on this page
281 foreach my $name (keys %{$pagestate{$page}{'osm'}{$map}{'waypoints'}}) {
282 if (exists $links{$page}) {
283 foreach my $otherpage (@{$links{$page}}) {
284 if (exists $waypoints{$map}{$otherpage}) {
285 push(@{$linestrings{$map}}, [
286 [ $waypoints{$map}{$name}{'lon'}, $waypoints{$map}{$name}{'lat'} ],
287 [ $waypoints{$map}{$otherpage}{'lon'}, $waypoints{$map}{$otherpage}{'lat'} ]
294 # clear the state, it will be regenerated on the next parse
295 # the idea here is to clear up removed waypoints...
296 $pagestate{$page}{'osm'} = ();
300 my %formats = get_formats();
301 if ($formats{'GeoJSON'}) {
302 writejson(\%waypoints, \%linestrings);
304 if ($formats{'CSV'}) {
305 writecsvs(\%waypoints, \%linestrings);
307 if ($formats{'KML'}) {
308 writekml(\%waypoints, \%linestrings);
313 my %waypoints = %{$_[0]};
314 my %linestrings = %{$_[1]};
317 foreach my $map (keys %waypoints) {
318 my %geojson = ( "type" => "FeatureCollection", "features" => []);
319 foreach my $name (keys %{$waypoints{$map}}) {
320 my %marker = ( "type" => "Feature",
321 "geometry" => { "type" => "Point", "coordinates" => [ $waypoints{$map}{$name}{'lon'}, $waypoints{$map}{$name}{'lat'} ] },
322 "properties" => $waypoints{$map}{$name} );
323 push @{$geojson{'features'}}, \%marker;
325 foreach my $linestring (@{$linestrings{$map}}) {
326 my %json = ( "type" => "Feature",
327 "geometry" => { "type" => "LineString", "coordinates" => $linestring });
328 push @{$geojson{'features'}}, \%json;
330 writefile("pois.json", $config{destdir} . "/$map", to_json(\%geojson));
335 my %waypoints = %{$_[0]};
336 my %linestrings = %{$_[1]};
337 eval q{use XML::Writer};
339 foreach my $map (keys %waypoints) {
344 <?xml version="1.0" encoding="UTF-8"?>
345 <kml xmlns="http://www.opengis.net/kml/2.2">
347 <name>Simple placemark</name>
348 <description>Attached to the ground. Intelligently places itself
349 at the height of the underlying terrain.</description>
351 <coordinates>-122.0822035425683,37.42228990140251,0</coordinates>
359 <Style id="sh_sunny_copy69">
363 <href>http://waypoints.google.com/mapfiles/kml/shapes/sunny.png</href>
365 <hotSpot x="0.5" y="0.5" xunits="fraction" yunits="fraction"/>
368 <color>ff00aaff</color>
376 my $writer = XML::Writer->new( OUTPUT => \$output,
377 DATA_MODE => 1, ENCODING => 'UTF-8');
379 $writer->startTag("kml", "xmlns" => "http://www.opengis.net/kml/2.2");
381 # first pass: get the icons
382 foreach my $name (keys %{$waypoints{$map}}) {
383 my %options = %{$waypoints{$map}{$name}};
384 $writer->startTag("Style", id => $options{tag});
385 $writer->startTag("IconStyle");
386 $writer->startTag("Icon");
387 $writer->startTag("href");
388 $writer->characters($options{icon});
395 foreach my $name (keys %{$waypoints{$map}}) {
396 my %options = %{$waypoints{$map}{$name}};
397 $writer->startTag("Placemark");
398 $writer->startTag("name");
399 $writer->characters($name);
401 $writer->startTag("styleUrl");
402 $writer->characters('#' . $options{tag});
404 #$writer->emptyTag('atom:link', href => $options{href});
405 # to make it easier for us as the atom:link parameter is
406 # hard to access from javascript
407 $writer->startTag('href');
408 $writer->characters($options{href});
410 $writer->startTag("description");
411 $writer->characters($options{desc});
413 $writer->startTag("Point");
414 $writer->startTag("coordinates");
415 $writer->characters($options{lon} . "," . $options{lat});
422 foreach my $linestring (@{$linestrings{$map}}) {
423 $writer->startTag("Placemark");
424 $writer->startTag("name");
425 $writer->characters("linestring " . $i++);
427 $writer->startTag("LineString");
428 $writer->startTag("coordinates");
430 foreach my $coord (@{$linestring}) {
431 $str .= join(',', @{$coord}) . " \n";
433 $writer->characters($str);
441 writefile("pois.kml", $config{destdir} . "/$map", $output);
446 my %waypoints = %{$_[0]};
447 foreach my $map (keys %waypoints) {
448 my $poisf = "lat\tlon\ttitle\tdescription\ticon\ticonSize\ticonOffset\n";
449 foreach my $name (keys %{$waypoints{$map}}) {
450 my %options = %{$waypoints{$map}{$name}};
452 $options{'lat'} . "\t" .
453 $options{'lon'} . "\t" .
455 $options{'desc'} . '<br /><a href="' . $options{'page'} . '">' . $name . "</a>\t" .
456 $options{'icon'} . "\n";
459 writefile("pois.txt", $config{destdir} . "/$map", $poisf);
463 # pipe some data through the HTML scrubber
465 # code taken from the meta.pm plugin
467 if (IkiWiki::Plugin::htmlscrubber->can("sanitize")) {
468 return IkiWiki::Plugin::htmlscrubber::sanitize(
469 content => shift, page => shift, destpage => shift);
476 # taken from toggle.pm
480 if ($params{content}=~m!<div[^>]*id="mapdiv-[^"]*"[^>]*>!g) {
481 if (! ($params{content}=~s!</body>!include_javascript($params{page})."</body>"!em)) {
482 # no <body> tag, probably in preview mode
483 $params{content}=$params{content} . include_javascript($params{page});
486 return $params{content};
489 sub preferred_format() {
490 if (!defined($config{'osm_format'}) || !$config{'osm_format'}) {
491 $config{'osm_format'} = 'KML';
493 my @spl = split(/, */, $config{'osm_format'});
498 if (!defined($config{'osm_format'}) || !$config{'osm_format'}) {
499 $config{'osm_format'} = 'KML';
501 map { $_ => 1 } split(/, */, $config{'osm_format'});
504 sub include_javascript ($) {
508 if (exists $pagestate{$page}{'osm'}) {
509 foreach my $map (keys %{$pagestate{$page}{'osm'}}) {
510 foreach my $name (keys %{$pagestate{$page}{'osm'}{$map}{'displays'}}) {
511 $loader .= map_setup_code($map, $name, %{$pagestate{$page}{'osm'}{$map}{'displays'}{$name}});
516 return embed_map_code($page) . "<script type=\"text/javascript\" charset=\"utf-8\">$loader</script>";
526 return unless defined $cgi->param('do') &&
527 $cgi->param("do") eq "osm";
529 IkiWiki::loadindex();
531 IkiWiki::decode_cgi_utf8($cgi);
533 my $map = $cgi->param('map');
534 if (!defined $map || $map !~ /^[a-z]*$/) {
535 error("invalid map parameter");
538 print "Content-Type: text/html\r\n";
540 print "<html><body>";
541 print "<div id=\"mapdiv-$map\"></div>";
542 print embed_map_code();
543 print "<script type=\"text/javascript\" charset=\"utf-8\">";
544 print map_setup_code($map, $map,
545 lat => "urlParams['lat']",
546 lon => "urlParams['lon']",
547 zoom => "urlParams['zoom']",
552 print "</body></html>";
557 sub embed_map_code(;$) {
559 return '<script src="http://www.openlayers.org/api/OpenLayers.js" type="text/javascript" charset="utf-8"></script>'.
560 '<script src="'.urlto("ikiwiki/osm.js", $page).
561 '" type="text/javascript" charset="utf-8"></script>'."\n";
564 sub map_setup_code($;@) {
572 $options{'format'} = preferred_format();
574 my %formats = get_formats();
575 if ($formats{'GeoJSON'}) {
576 $options{'jsonurl'} = urlto($map."/pois.json");
578 if ($formats{'CSV'}) {
579 $options{'csvurl'} = urlto($map."/pois.txt");
581 if ($formats{'KML'}) {
582 $options{'kmlurl'} = urlto($map."/pois.kml");
585 return "mapsetup('mapdiv-$name', " . to_json(\%options) . ");";