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