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