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