Fix incorrect URL pointing to the generated waypoint files by calling will_render...
[ikiwiki.git] / IkiWiki / Plugin / osm.pm
1 #!/usr/bin/perl
2 # Copyright 2011 Blars Blarson
3 # Released under GPL version 2
4
5 package IkiWiki::Plugin::osm;
6 use utf8;
7 use strict;
8 use warnings;
9 use IkiWiki 3.0;
10
11 sub import {
12         add_underlay("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);
19 }
20
21 sub getsetup () {
22         return
23                 plugin => {
24                         safe => 1,
25                         rebuild => 1,
26                         section => "special-purpose",
27                 },
28                 osm_default_zoom => {
29                         type => "integer",
30                         example => "15",
31                         description => "the default zoom when you click on the map link",
32                         safe => 1,
33                         rebuild => 1,
34                 },
35                 osm_default_icon => {
36                         type => "string",
37                         example => "ikiwiki/images/osm.png",
38                         description => "the icon shown on links and on the main map",
39                         safe => 0,
40                         rebuild => 1,
41                 },
42                 osm_alt => {
43                         type => "string",
44                         example => "",
45                         description => "the alt tag of links, defaults to empty",
46                         safe => 0,
47                         rebuild => 1,
48                 },
49                 osm_format => {
50                         type => "string",
51                         example => "KML",
52                         description => "the output format for waypoints, can be KML, GeoJSON or CSV (one or many, comma-separated)",
53                         safe => 1,
54                         rebuild => 1,
55                 },
56                 osm_tag_default_icon => {
57                         type => "string",
58                         example => "icon.png",
59                         description => "the icon attached to a tag, displayed on the map for tagged pages",
60                         safe => 0,
61                         rebuild => 1,
62                 },
63 }
64
65 sub register_rendered_files {
66         my $map = shift;
67         my $page = shift;
68         my $dest = shift;
69
70         if ($page eq $dest) {
71                 my %formats = get_formats();
72                 if ($formats{'GeoJSON'}) {
73                         will_render($page, "$map/pois.json");
74                 }
75                 if ($formats{'CSV'}) {
76                         will_render($page, "$map/pois.txt");
77                 }
78                 if ($formats{'KML'}) {
79                         will_render($page, "$map/pois.kml");
80                 }
81         }
82 }
83
84 sub preprocess {
85         my %params=@_;
86         my $page = $params{page};
87         my $dest = $params{destpage};
88         my $loc = $params{loc}; # sanitized below
89         my $lat = $params{lat}; # sanitized below
90         my $lon = $params{lon}; # sanitized below
91         my $href = $params{href};
92
93         my ($width, $height, $float);
94         $height = scrub($params{'height'} || "300px", $page, $dest); # sanitized here
95         $width = scrub($params{'width'} || "500px", $page, $dest); # sanitized here
96         $float = (defined($params{'right'}) && 'right') || (defined($params{'left'}) && 'left'); # sanitized here
97         
98         my $zoom = scrub($params{'zoom'} // $config{'osm_default_zoom'} // 15, $page, $dest); # sanitized below
99         my $map;
100         $map = $params{'map'} || 'map';
101         
102         $map = scrub($map, $page, $dest); # sanitized here
103         my $name = scrub($params{'name'} || $map, $page, $dest);
104
105         if (defined($lon) || defined($lat) || defined($loc)) {
106                 ($lon, $lat) = scrub_lonlat($loc, $lon, $lat);
107         }
108
109         if ($zoom !~ /^\d\d?$/ || $zoom < 2 || $zoom > 18) {
110                 error("Bad zoom");
111         }
112
113         if (! defined $href || ! length $href) {
114                 $href=IkiWiki::cgiurl(
115                         do => "osm",
116                         map => $map,
117                 );
118         }
119
120         register_rendered_files($map, $page, $dest);
121
122         $pagestate{$page}{'osm'}{$map}{'displays'}{$name} = {
123                 height => $height,
124                 width => $width,
125                 float => $float,
126                 zoom => $zoom,
127                 fullscreen => 0,
128                 editable => defined($params{'editable'}),
129                 lat => $lat,
130                 lon => $lon,
131                 href => $href,
132         };
133         return "<div id=\"mapdiv-$name\"></div>";
134 }
135
136 sub process_waypoint {
137         my %params=@_;
138         my $loc = $params{'loc'}; # sanitized below
139         my $lat = $params{'lat'}; # sanitized below
140         my $lon = $params{'lon'}; # sanitized below
141         my $page = $params{'page'}; # not sanitized?
142         my $dest = $params{'destpage'}; # not sanitized?
143         my $hidden = defined($params{'hidden'}); # sanitized here
144         my ($p) = $page =~ /(?:^|\/)([^\/]+)\/?$/; # shorter page name
145         my $name = scrub($params{'name'} || $p, $page, $dest); # sanitized here
146         my $desc = scrub($params{'desc'} || '', $page, $dest); # sanitized here
147         my $zoom = scrub($params{'zoom'} // $config{'osm_default_zoom'} // 15, $page, $dest); # sanitized below
148         my $icon = $config{'osm_default_icon'} || "ikiwiki/images/osm.png"; # sanitized: we trust $config
149         my $map = scrub($params{'map'} || 'map', $page, $dest); # sanitized here
150         my $alt = $config{'osm_alt'} ? "alt=\"$config{'osm_alt'}\"" : ''; # sanitized: we trust $config
151         if ($zoom !~ /^\d\d?$/ || $zoom < 2 || $zoom > 18) {
152                 error("Bad zoom");
153         }
154
155         ($lon, $lat) = scrub_lonlat($loc, $lon, $lat);
156         if (!defined($lat) || !defined($lon)) {
157                 error("Must specify lat and lon");
158         }
159
160         my $tag = $params{'tag'};
161         foreach my $t (keys %{$typedlinks{$page}{'tag'}}) {
162                 if ($icon = get_tag_icon($t)) {
163                         $tag = $t;
164                         last;
165                 }
166                 $t =~ s!/$config{'tagbase'}/!!;
167                 if ($icon = get_tag_icon($t)) {
168                         $tag = $t;
169                         last;
170                 }
171         }
172         $icon = urlto($icon, $dest, 1);
173         $tag = '' unless $tag;
174         register_rendered_files($map, $page, $dest);
175         $pagestate{$page}{'osm'}{$map}{'waypoints'}{$name} = {
176                 page => $page,
177                 desc => $desc,
178                 icon => $icon,
179                 tag => $tag,
180                 lat => $lat,
181                 lon => $lon,
182                 # How to link back to the page from the map, not to be
183                 # confused with the URL of the map itself sent to the
184                 # embeded map below. Note: used in generated KML etc file,
185                 # so must be absolute.
186                 href => urlto($page),
187         };
188
189         my $mapurl = IkiWiki::cgiurl(
190                 do => "osm",
191                 map => $map,
192                 lat => $lat,
193                 lon => $lon,
194                 zoom => $zoom,
195         );
196         my $output = '';
197         if (defined($params{'embed'})) {
198                 $output .= preprocess(%params,
199                         href => $mapurl,
200                 );
201         }
202         if (!$hidden) {
203                 $output .= "<a href=\"$mapurl\"><img class=\"img\" src=\"$icon\" $alt /></a>";
204         }
205         return $output;
206 }
207
208 # get the icon from the given tag
209 sub get_tag_icon($) {
210         my $tag = shift;
211         # look for an icon attached to the tag
212         my $attached = $tag . '/' . $config{'osm_tag_default_icon'};
213         if (srcfile($attached)) {
214                 return $attached;
215         }
216         else {
217                 return undef;
218         }
219 }
220
221 sub scrub_lonlat($$$) {
222         my ($loc, $lon, $lat) = @_;
223         if ($loc) {
224                 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*$/) {
225                         $lat = $1;
226                         $lon = $2;
227                 }
228                 else {
229                         error("Bad loc");
230                 }
231         }
232         if (defined($lat)) {
233                 if ($lat =~ /^(\-?)(\d+)(?:(\.\d*)°?|(?:°|\s)\s*(\d+)(?:(\.\d*)\'?|(?:\'|\s)\s*(\d+(?:\.\d*)?\"?)|\'?)|°?)\s*([NS])?\s*$/) {
234                         $lat = $2 + ($3//0) + ((($4//0) + (($5//0) + (($6//0)/60.)))/60.);
235                         if (($1 eq '-') || (($7//'') eq 'S')) {
236                                 $lat = - $lat;
237                         }
238                 }
239                 else {
240                         error("Bad lat");
241                 }
242         }
243         if (defined($lon)) {
244                 if ($lon =~ /^(\-?)(\d+)(?:(\.\d*)°?|(?:°|\s)\s*(\d+)(?:(\.\d*)\'?|(?:\'|\s)\s*(\d+(?:\.\d*)?\"?)|\'?)|°?)\s*([EW])?$/) {
245                         $lon = $2 + ($3//0) + ((($4//0) + (($5//0) + (($6//0)/60.)))/60.);
246                         if (($1 eq '-') || (($7//'') eq 'W')) {
247                                 $lon = - $lon;
248                         }
249                 }
250                 else {
251                         error("Bad lon");
252                 }
253         }
254         if ($lat < -90 || $lat > 90 || $lon < -180 || $lon > 180) {
255                 error("Location out of range");
256         }
257         return ($lon, $lat);
258 }
259
260 sub savestate {
261         my %waypoints = ();
262         my %linestrings = ();
263
264         foreach my $page (keys %pagestate) {
265                 if (exists $pagestate{$page}{'osm'}) {
266                         foreach my $map (keys %{$pagestate{$page}{'osm'}}) {
267                                 foreach my $name (keys %{$pagestate{$page}{'osm'}{$map}{'waypoints'}}) {
268                                         debug("found waypoint $name");
269                                         $waypoints{$map}{$name} = $pagestate{$page}{'osm'}{$map}{'waypoints'}{$name};
270                                 }
271                         }
272                 }
273         }
274
275         foreach my $page (keys %pagestate) {
276                 if (exists $pagestate{$page}{'osm'}) {
277                         foreach my $map (keys %{$pagestate{$page}{'osm'}}) {
278                                 # examine the links on this page
279                                 foreach my $name (keys %{$pagestate{$page}{'osm'}{$map}{'waypoints'}}) {
280                                         if (exists $links{$page}) {
281                                                 foreach my $otherpage (@{$links{$page}}) {
282                                                         if (exists $waypoints{$map}{$otherpage}) {
283                                                                 push(@{$linestrings{$map}}, [
284                                                                         [ $waypoints{$map}{$name}{'lon'}, $waypoints{$map}{$name}{'lat'} ],
285                                                                         [ $waypoints{$map}{$otherpage}{'lon'}, $waypoints{$map}{$otherpage}{'lat'} ]
286                                                                 ]);
287                                                         }
288                                                 }
289                                         }
290                                 }
291                         }
292                         # clear the state, it will be regenerated on the next parse
293                         # the idea here is to clear up removed waypoints...
294                         $pagestate{$page}{'osm'} = ();
295                 }
296         }
297
298         my %formats = get_formats();
299         if ($formats{'GeoJSON'}) {
300                 writejson(\%waypoints, \%linestrings);
301         }
302         if ($formats{'CSV'}) {
303                 writecsvs(\%waypoints, \%linestrings);
304         }
305         if ($formats{'KML'}) {
306                 writekml(\%waypoints, \%linestrings);
307         }
308 }
309
310 sub writejson($;$) {
311         my %waypoints = %{$_[0]};
312         my %linestrings = %{$_[1]};
313         eval q{use JSON};
314         error $@ if $@;
315         foreach my $map (keys %waypoints) {
316                 my %geojson = ( "type" => "FeatureCollection", "features" => []);
317                 foreach my $name (keys %{$waypoints{$map}}) {
318                         my %marker = ( "type" => "Feature",
319                                 "geometry" => { "type" => "Point", "coordinates" => [ $waypoints{$map}{$name}{'lon'}, $waypoints{$map}{$name}{'lat'} ] },
320                                 "properties" => $waypoints{$map}{$name} );
321                         push @{$geojson{'features'}}, \%marker;
322                 }
323                 foreach my $linestring (@{$linestrings{$map}}) {
324                         my %json  = ( "type" => "Feature",
325                                 "geometry" => { "type" => "LineString", "coordinates" => $linestring });
326                         push @{$geojson{'features'}}, \%json;
327                 }
328                 writefile("pois.json", $config{destdir} . "/$map", to_json(\%geojson));
329         }
330 }
331
332 sub writekml($;$) {
333         my %waypoints = %{$_[0]};
334         my %linestrings = %{$_[1]};
335         eval q{use XML::Writer};
336         error $@ if $@;
337         foreach my $map (keys %waypoints) {
338                 my $output;
339                 my $writer = XML::Writer->new( OUTPUT => \$output,
340                         DATA_MODE => 1, ENCODING => 'UTF-8');
341                 $writer->xmlDecl();
342                 $writer->startTag("kml", "xmlns" => "http://www.opengis.net/kml/2.2");
343
344                 # first pass: get the icons
345                 foreach my $name (keys %{$waypoints{$map}}) {
346                         my %options = %{$waypoints{$map}{$name}};
347                         $writer->startTag("Style", id => $options{tag});
348                         $writer->startTag("IconStyle");
349                         $writer->startTag("Icon");
350                         $writer->startTag("href");
351                         $writer->characters($options{icon});
352                         $writer->endTag();
353                         $writer->endTag();
354                         $writer->endTag();
355                         $writer->endTag();
356                 }
357         
358                 foreach my $name (keys %{$waypoints{$map}}) {
359                         my %options = %{$waypoints{$map}{$name}};
360                         $writer->startTag("Placemark");
361                         $writer->startTag("name");
362                         $writer->characters($name);
363                         $writer->endTag();
364                         $writer->startTag("styleUrl");
365                         $writer->characters('#' . $options{tag});
366                         $writer->endTag();
367                         #$writer->emptyTag('atom:link', href => $options{href});
368                         # to make it easier for us as the atom:link parameter is
369                         # hard to access from javascript
370                         $writer->startTag('href');
371                         $writer->characters($options{href});
372                         $writer->endTag();
373                         $writer->startTag("description");
374                         $writer->characters($options{desc});
375                         $writer->endTag();
376                         $writer->startTag("Point");
377                         $writer->startTag("coordinates");
378                         $writer->characters($options{lon} . "," . $options{lat});
379                         $writer->endTag();
380                         $writer->endTag();
381                         $writer->endTag();
382                 }
383                 
384                 my $i = 0;
385                 foreach my $linestring (@{$linestrings{$map}}) {
386                         $writer->startTag("Placemark");
387                         $writer->startTag("name");
388                         $writer->characters("linestring " . $i++);
389                         $writer->endTag();
390                         $writer->startTag("LineString");
391                         $writer->startTag("coordinates");
392                         my $str = '';
393                         foreach my $coord (@{$linestring}) {
394                                 $str .= join(',', @{$coord}) . " \n";
395                         }
396                         $writer->characters($str);
397                         $writer->endTag();
398                         $writer->endTag();
399                         $writer->endTag();
400                 }
401                 $writer->endTag();
402                 $writer->end();
403
404                 writefile("pois.kml", $config{destdir} . "/$map", $output);
405         }
406 }
407
408 sub writecsvs($;$) {
409         my %waypoints = %{$_[0]};
410         foreach my $map (keys %waypoints) {
411                 my $poisf = "lat\tlon\ttitle\tdescription\ticon\ticonSize\ticonOffset\n";
412                 foreach my $name (keys %{$waypoints{$map}}) {
413                         my %options = %{$waypoints{$map}{$name}};
414                         my $line = 
415                                 $options{'lat'} . "\t" .
416                                 $options{'lon'} . "\t" .
417                                 $name . "\t" .
418                                 $options{'desc'} . '<br /><a href="' . $options{'page'} . '">' . $name . "</a>\t" .
419                                 $options{'icon'} . "\n";
420                         $poisf .= $line;
421                 }
422                 writefile("pois.txt", $config{destdir} . "/$map", $poisf);
423         }
424 }
425
426 # pipe some data through the HTML scrubber
427 #
428 # code taken from the meta.pm plugin
429 sub scrub($$$) {
430         if (IkiWiki::Plugin::htmlscrubber->can("sanitize")) {
431                 return IkiWiki::Plugin::htmlscrubber::sanitize(
432                         content => shift, page => shift, destpage => shift);
433         }
434         else {
435                 return shift;
436         }
437 }
438
439 # taken from toggle.pm
440 sub format (@) {
441         my %params=@_;
442
443         if ($params{content}=~m!<div[^>]*id="mapdiv-[^"]*"[^>]*>!g) {
444                 if (! ($params{content}=~s!</body>!include_javascript($params{page})."</body>"!em)) {
445                         # no <body> tag, probably in preview mode
446                         $params{content}=$params{content} . include_javascript($params{page});
447                 }
448         }
449         return $params{content};
450 }
451
452 sub preferred_format() {
453         if (!defined($config{'osm_format'}) || !$config{'osm_format'}) {
454                 $config{'osm_format'} = 'KML';
455         }
456         my @spl = split(/, */, $config{'osm_format'});
457         return shift @spl;
458 }
459
460 sub get_formats() {
461         if (!defined($config{'osm_format'}) || !$config{'osm_format'}) {
462                 $config{'osm_format'} = 'KML';
463         }
464         map { $_ => 1 } split(/, */, $config{'osm_format'});
465 }
466
467 sub include_javascript ($) {
468         my $page=shift;
469         my $loader;
470
471         if (exists $pagestate{$page}{'osm'}) {
472                 foreach my $map (keys %{$pagestate{$page}{'osm'}}) {
473                         foreach my $name (keys %{$pagestate{$page}{'osm'}{$map}{'displays'}}) {
474                                 $loader .= map_setup_code($map, $name, %{$pagestate{$page}{'osm'}{$map}{'displays'}{$name}});
475                         }
476                 }
477         }
478         if ($loader) {
479                 return embed_map_code($page) . "<script type=\"text/javascript\" charset=\"utf-8\">$loader</script>";
480         }
481         else {
482                 return '';
483         }
484 }
485
486 sub cgi($) {
487         my $cgi=shift;
488
489         return unless defined $cgi->param('do') &&
490                 $cgi->param("do") eq "osm";
491         
492         IkiWiki::loadindex();
493
494         IkiWiki::decode_cgi_utf8($cgi);
495
496         my $map = $cgi->param('map');
497         if (!defined $map || $map !~ /^[a-z]*$/) {
498                 error("invalid map parameter");
499         }
500
501         print "Content-Type: text/html\r\n";
502         print ("\r\n");
503         print "<html><body>";
504         print "<div id=\"mapdiv-$map\"></div>";
505         print embed_map_code();
506         print "<script type=\"text/javascript\" charset=\"utf-8\">";
507         print map_setup_code($map, $map,
508                 lat => "urlParams['lat']",
509                 lon => "urlParams['lon']",
510                 zoom => "urlParams['zoom']",
511                 fullscreen => 1,
512                 editable => 1,
513         );
514         print "</script>";
515         print "</body></html>";
516
517         exit 0;
518 }
519
520 sub embed_map_code(;$) {
521         my $page=shift;
522         return '<script src="http://www.openlayers.org/api/OpenLayers.js" type="text/javascript" charset="utf-8"></script>'.
523                 '<script src="'.urlto("ikiwiki/osm.js", $page).
524                 '" type="text/javascript" charset="utf-8"></script>'."\n";
525 }
526
527 sub map_setup_code($;@) {
528         my $map=shift;
529         my $name=shift;
530         my %options=@_;
531
532         eval q{use JSON};
533         error $@ if $@;
534                                 
535         $options{'format'} = preferred_format();
536
537         my %formats = get_formats();
538         if ($formats{'GeoJSON'}) {
539                 $options{'jsonurl'} = urlto($map."/pois.json");
540         }
541         if ($formats{'CSV'}) {
542                 $options{'csvurl'} = urlto($map."/pois.txt");
543         }
544         if ($formats{'KML'}) {
545                 $options{'kmlurl'} = urlto($map."/pois.kml");
546         }
547
548         return "mapsetup('mapdiv-$name', " . to_json(\%options) . ");";
549 }
550
551 1;