2 # Copyright 2011 Blars Blarson
3 # Released under GPL version 2
5 package IkiWiki::Plugin::osm;
12 add_underlay("javascript");
14 hook(type => "getsetup", id => "osm", call => \&getsetup);
15 hook(type => "format", id => "osm", call => \&format);
16 hook(type => "preprocess", id => "osm", call => \&preprocess);
17 hook(type => "preprocess", id => "waypoint", call => \&process_waypoint);
18 hook(type => "savestate", id => "waypoint", call => \&savestate);
19 hook(type => "cgi", id => "osm", call => \&cgi);
27 section => "special-purpose",
32 description => "the default zoom when you click on the map link",
38 example => "/ikiwiki/images/osm.png",
39 description => "the icon shon on links and on the main map",
46 description => "the alt tag of links, defaults to empty",
53 description => "the output format for waypoints, can be KML, GeoJSON or CSV (one or many, comma-separated)",
57 osm_tag_default_icon => {
59 example => "icon.png",
60 description => "the icon attached to a tag so that pages tagged with that tag will have that icon on the map",
67 'test' => '/img/test.png',
68 'trailer' => '/img/trailer.png'
70 description => "tag to icon mapping, leading slash is important!",
78 my $page = $params{'page'};
79 my $dest = $params{'destpage'};
80 my $loc = $params{'loc'}; # sanitized below
81 my $lat = $params{'lat'}; # sanitized below
82 my $lon = $params{'lon'}; # sanitized below
83 my $href = $params{'href'};
85 my $fullscreen = defined($params{'fullscreen'}); # sanitized here
86 my ($width, $height, $float);
93 $height = scrub($params{'height'} || "300px", $page, $dest); # sanitized here
94 $width = scrub($params{'width'} || "500px", $page, $dest); # sanitized here
95 $float = (defined($params{'right'}) && 'right') || (defined($params{'left'}) && 'left'); # sanitized here
97 my $zoom = scrub($params{'zoom'} // $config{'osm_default_zoom'} // 15, $page, $dest); # sanitized below
100 $map = $params{'map'} || $page;
103 $map = $params{'map'} || 'map';
105 $map = scrub($map, $page, $dest); # sanitized here
106 my $name = scrub($params{'name'} || $map, $page, $dest);
108 if (defined($lon) || defined($lat) || defined($loc)) {
109 ($lon, $lat) = scrub_lonlat($loc, $lon, $lat);
112 if ($zoom !~ /^\d\d?$/ || $zoom < 2 || $zoom > 18) {
115 $pagestate{$page}{'osm'}{$map}{'displays'}{$name} = {
120 fullscreen => $fullscreen,
121 editable => defined($params{'editable'}),
126 return "<div id=\"mapdiv-$name\"></div>";
129 sub process_waypoint {
131 my $loc = $params{'loc'}; # sanitized below
132 my $lat = $params{'lat'}; # sanitized below
133 my $lon = $params{'lon'}; # sanitized below
134 my $page = $params{'page'}; # not sanitized?
135 my $dest = $params{'destpage'}; # not sanitized?
136 my $hidden = defined($params{'hidden'}); # sanitized here
137 my ($p) = $page =~ /(?:^|\/)([^\/]+)\/?$/; # shorter page name
138 my $name = scrub($params{'name'} || $p, $page, $dest); # sanitized here
139 my $desc = scrub($params{'desc'} || '', $page, $dest); # sanitized here
140 my $zoom = scrub($params{'zoom'} // $config{'osm_default_zoom'} // 15, $page, $dest); # sanitized below
141 my $icon = $config{'osm__default_icon'} || "/ikiwiki/images/osm.png"; # sanitized: we trust $config
142 my $map = scrub($params{'map'} || 'map', $page, $dest); # sanitized here
143 my $alt = $config{'osm_alt'} ? "alt=\"$config{'osm_alt'}\"" : ''; # sanitized: we trust $config
144 if ($zoom !~ /^\d\d?$/ || $zoom < 2 || $zoom > 18) {
148 ($lon, $lat) = scrub_lonlat($loc, $lon, $lat);
149 if (!defined($lat) || !defined($lon)) {
150 error("Must specify lat and lon");
153 my $tag = $params{'tag'};
155 if (!defined($config{'osm_tag_icons'}->{$tag})) {
156 error("invalid tag specified, see osm_tag_icons configuration or don't specify any");
158 $icon = $config{'osm_tag_icons'}->{$tag};
161 foreach my $t (keys %{$typedlinks{$page}{'tag'}}) {
162 if ($icon = get_tag_icon($t)) {
166 $t =~ s!/$config{'tagbase'}/!!;
167 if ($icon = get_tag_icon($t)) {
173 $icon = "/ikiwiki/images/osm.png" unless $icon;
174 $tag = '' unless $tag;
175 if ($page eq $dest) {
176 if (!defined($config{'osm_format'}) || !$config{'osm_format'}) {
177 $config{'osm_format'} = 'KML';
179 my %formats = map { $_ => 1 } split(/, */, $config{'osm_format'});
180 if ($formats{'GeoJSON'}) {
181 will_render($page,$config{destdir} . "/$map/pois.json");
183 if ($formats{'CSV'}) {
184 will_render($page,$config{destdir} . "/$map/pois.txt");
186 if ($formats{'KML'}) {
187 will_render($page,$config{destdir} . "/$map/pois.kml");
190 my $href = "/ikiwiki.cgi?do=osm&map=$map&lat=$lat&lon=$lon&zoom=$zoom";
191 if (defined($destsources{htmlpage($map)})) {
192 $href = urlto($map,$page) . "?lat=$lat&lon=$lon&zoom=$zoom";
194 $pagestate{$page}{'osm'}{$map}{'waypoints'}{$name} = {
201 # how to link back to the page from the map, not to be
202 # confused with the URL of the map itself sent to the
204 href => urlto($page,$map),
207 if (defined($params{'embed'})) {
208 $params{'href'} = $href; # propagate down to embeded
209 $output .= preprocess(%params);
212 $href =~ s!&!&!g;
213 $output .= "<a href=\"$href\"><img class=\"img\" src=\"$icon\" $alt /></a>";
218 # get the icon from the given tag
219 sub get_tag_icon($) {
221 # look for an icon attached to the tag
222 my $attached = $tag . '/' . $config{'osm_tag_default_icon'};
223 if (srcfile($attached)) {
226 # look for the old way: mappings
227 if ($config{'osm_tag_icons'}->{$tag}) {
228 return $config{'osm_tag_icons'}->{$tag};
235 sub scrub_lonlat($$$) {
236 my ($loc, $lon, $lat) = @_;
238 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*$/) {
247 if ($lat =~ /^(\-?)(\d+)(?:(\.\d*)°?|(?:°|\s)\s*(\d+)(?:(\.\d*)\'?|(?:\'|\s)\s*(\d+(?:\.\d*)?\"?)|\'?)|°?)\s*([NS])?\s*$/) {
248 $lat = $2 + ($3//0) + ((($4//0) + (($5//0) + (($6//0)/60.)))/60.);
249 if (($1 eq '-') || (($7//'') eq 'S')) {
258 if ($lon =~ /^(\-?)(\d+)(?:(\.\d*)°?|(?:°|\s)\s*(\d+)(?:(\.\d*)\'?|(?:\'|\s)\s*(\d+(?:\.\d*)?\"?)|\'?)|°?)\s*([EW])?$/) {
259 $lon = $2 + ($3//0) + ((($4//0) + (($5//0) + (($6//0)/60.)))/60.);
260 if (($1 eq '-') || (($7//'') eq 'W')) {
268 if ($lat < -90 || $lat > 90 || $lon < -180 || $lon > 180) {
269 error("Location out of range");
276 my %linestrings = ();
278 foreach my $page (keys %pagestate) {
279 if (exists $pagestate{$page}{'osm'}) {
280 foreach my $map (keys %{$pagestate{$page}{'osm'}}) {
281 foreach my $name (keys %{$pagestate{$page}{'osm'}{$map}{'waypoints'}}) {
282 debug("found waypoint $name");
283 $waypoints{$map}{$name} = $pagestate{$page}{'osm'}{$map}{'waypoints'}{$name};
289 foreach my $page (keys %pagestate) {
290 if (exists $pagestate{$page}{'osm'}) {
291 foreach my $map (keys %{$pagestate{$page}{'osm'}}) {
292 # examine the links on this page
293 foreach my $name (keys %{$pagestate{$page}{'osm'}{$map}{'waypoints'}}) {
294 if (exists $links{$page}) {
295 foreach my $otherpage (@{$links{$page}}) {
296 if (exists $waypoints{$map}{$otherpage}) {
297 push(@{$linestrings{$map}}, [
298 [ $waypoints{$map}{$name}{'lon'}, $waypoints{$map}{$name}{'lat'} ],
299 [ $waypoints{$map}{$otherpage}{'lon'}, $waypoints{$map}{$otherpage}{'lat'} ]
306 # clear the state, it will be regenerated on the next parse
307 # the idea here is to clear up removed waypoints...
308 $pagestate{$page}{'osm'} = ();
312 if (!defined($config{'osm_format'}) || !$config{'osm_format'}) {
313 $config{'osm_format'} = 'KML';
315 my %formats = map { $_ => 1 } split(/, */, $config{'osm_format'});
316 if ($formats{'GeoJSON'}) {
317 writejson(\%waypoints, \%linestrings);
319 if ($formats{'CSV'}) {
320 writecsvs(\%waypoints, \%linestrings);
322 if ($formats{'KML'}) {
323 writekml(\%waypoints, \%linestrings);
328 my %waypoints = %{$_[0]};
329 my %linestrings = %{$_[1]};
332 foreach my $map (keys %waypoints) {
333 my %geojson = ( "type" => "FeatureCollection", "features" => []);
334 foreach my $name (keys %{$waypoints{$map}}) {
335 my %marker = ( "type" => "Feature",
336 "geometry" => { "type" => "Point", "coordinates" => [ $waypoints{$map}{$name}{'lon'}, $waypoints{$map}{$name}{'lat'} ] },
337 "properties" => $waypoints{$map}{$name} );
338 push @{$geojson{'features'}}, \%marker;
340 foreach my $linestring (@{$linestrings{$map}}) {
341 my %json = ( "type" => "Feature",
342 "geometry" => { "type" => "LineString", "coordinates" => $linestring });
343 push @{$geojson{'features'}}, \%json;
345 debug('writing pois file pois.json in ' . $config{destdir} . "/$map");
346 writefile("pois.json",$config{destdir} . "/$map",to_json(\%geojson));
351 my %waypoints = %{$_[0]};
352 my %linestrings = %{$_[1]};
353 eval q{use XML::Writer};
355 foreach my $map (keys %waypoints) {
356 debug("writing pois file pois.kml in " . $config{destdir} . "/$map");
361 <?xml version="1.0" encoding="UTF-8"?>
362 <kml xmlns="http://www.opengis.net/kml/2.2">
364 <name>Simple placemark</name>
365 <description>Attached to the ground. Intelligently places itself
366 at the height of the underlying terrain.</description>
368 <coordinates>-122.0822035425683,37.42228990140251,0</coordinates>
376 <Style id="sh_sunny_copy69">
380 <href>http://waypoints.google.com/mapfiles/kml/shapes/sunny.png</href>
382 <hotSpot x="0.5" y="0.5" xunits="fraction" yunits="fraction"/>
385 <color>ff00aaff</color>
393 my $output = IO::File->new(">".$config{destdir} . "/$map/pois.kml");
395 my $writer = XML::Writer->new( OUTPUT => $output, DATA_MODE => 1, ENCODING => 'UTF-8');
397 $writer->startTag("kml", "xmlns" => "http://www.opengis.net/kml/2.2");
399 # first pass: get the icons
400 foreach my $name (keys %{$waypoints{$map}}) {
401 my %options = %{$waypoints{$map}{$name}};
402 $writer->startTag("Style", id => $options{tag});
403 $writer->startTag("IconStyle");
404 $writer->startTag("Icon");
405 $writer->startTag("href");
406 $writer->characters($options{icon});
413 foreach my $name (keys %{$waypoints{$map}}) {
414 my %options = %{$waypoints{$map}{$name}};
415 $writer->startTag("Placemark");
416 $writer->startTag("name");
417 $writer->characters($name);
419 $writer->startTag("styleUrl");
420 $writer->characters('#' . $options{tag});
422 #$writer->emptyTag('atom:link', href => $options{href});
423 # to make it easier for us as the atom:link parameter is
424 # hard to access from javascript
425 $writer->startTag('href');
426 $writer->characters($options{href});
428 $writer->startTag("description");
429 $writer->characters($options{desc});
431 $writer->startTag("Point");
432 $writer->startTag("coordinates");
433 $writer->characters($options{lon} . "," . $options{lat});
440 foreach my $linestring (@{$linestrings{$map}}) {
441 $writer->startTag("Placemark");
442 $writer->startTag("name");
443 $writer->characters("linestring " . $i++);
445 $writer->startTag("LineString");
446 $writer->startTag("coordinates");
448 foreach my $coord (@{$linestring}) {
449 $str .= join(',', @{$coord}) . " \n";
451 $writer->characters($str);
463 my %waypoints = %{$_[0]};
464 foreach my $map (keys %waypoints) {
465 my $poisf = "lat\tlon\ttitle\tdescription\ticon\ticonSize\ticonOffset\n";
466 foreach my $name (keys %{$waypoints{$map}}) {
467 my %options = %{$waypoints{$map}{$name}};
469 $options{'lat'} . "\t" .
470 $options{'lon'} . "\t" .
472 $options{'desc'} . '<br /><a href="' . $options{'page'} . '">' . $name . "</a>\t" .
473 $options{'icon'} . "\n";
476 debug("writing pois file pois.txt in " . $config{destdir} . "/$map");
477 writefile("pois.txt",$config{destdir} . "/$map",$poisf);
481 # pipe some data through the HTML scrubber
483 # code taken from the meta.pm plugin
485 if (IkiWiki::Plugin::htmlscrubber->can("sanitize")) {
486 return IkiWiki::Plugin::htmlscrubber::sanitize(
487 content => shift, page => shift, destpage => shift);
494 # taken from toggle.pm
498 if ($params{content}=~m!<div[^>]*id="mapdiv-[^"]*"[^>]*>!g) {
499 if (! ($params{content}=~s!</body>!include_javascript($params{page})."</body>"!em)) {
500 # no <body> tag, probably in preview mode
501 $params{content}=$params{content} . include_javascript($params{page});
504 return $params{content};
507 sub prefered_format() {
508 if (!defined($config{'osm_format'}) || !$config{'osm_format'}) {
509 $config{'osm_format'} = 'KML';
511 my @spl = split(/, */, $config{'osm_format'});
515 sub include_javascript ($) {
521 if (exists $pagestate{$page}{'osm'}) {
522 foreach my $map (keys %{$pagestate{$page}{'osm'}}) {
523 foreach my $name (keys %{$pagestate{$page}{'osm'}{$map}{'displays'}}) {
524 my %options = %{$pagestate{$page}{'osm'}{$map}{'displays'}{$name}};
525 $options{'map'} = $map;
526 $options{'format'} = prefered_format();
527 $loader .= "mapsetup(\"mapdiv-$name\", " . to_json(\%options) . ");\n";
532 return embed_map_code() . "<script type=\"text/javascript\" charset=\"utf-8\">$loader</script>";
542 return unless defined $cgi->param('do') &&
543 $cgi->param("do") eq "osm";
545 IkiWiki::decode_cgi_utf8($cgi);
547 my $map = $cgi->param('map');
548 if (!defined $map || $map !~ /^[a-z]*$/) {
549 error("invalid map parameter");
552 print "Content-Type: text/html\r\n";
554 print "<html><body>";
555 print "<div id=\"mapdiv-$map\"></div>";
556 print embed_map_code($map);
557 print "<script type=\"text/javascript\" charset=\"utf-8\">mapsetup( 'mapdiv-$map', { 'map': '$map', 'lat': urlParams['lat'], 'lon': urlParams['lon'], 'zoom': urlParams['zoom'], 'fullscreen': 1, 'editable': 1, 'format': '" . prefered_format() . "'});</script>";
558 print "</body></html>";
563 sub embed_map_code() {
564 return '<script src="http://www.openlayers.org/api/OpenLayers.js" type="text/javascript" charset="utf-8"></script>'.
565 '<script src="'.urlto("ikiwiki/osm.js", $from).
566 '" type="text/javascript" charset="utf-8"></script>'."\n";