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