2 # Copyright 2011 Blars Blarson
3 # Released under GPL version 2
5 package IkiWiki::Plugin::osm;
12 add_underlay("javascript");
13 hook(type => "getsetup", id => "osm", call => \&getsetup);
14 hook(type => "format", id => "osm", call => \&format);
15 hook(type => "preprocess", id => "osm", call => \&preprocess); # backward compatibility
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);
26 section => "special-purpose",
31 description => "the default zoon when you click on the map link",
37 example => "img/osm.png",
38 description => "the icon showed on links and on the main map",
45 description => "the alt tag of links, defaults to empty",
52 description => "the output format for waypoints, can be KML, GeoJSON or CSV (one or many, comma-separated)",
56 osm_tag_default_icon => {
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",
65 example => { 'test' => '/img/test.png',
66 'trailer' => '/img/trailer.png' ,},
67 description => "tag to icon mapping, leading slash is important!",
75 my $page = $params{'page'};
76 my $dest = $params{'destpage'};
77 my $loc = $params{'loc'}; # sanitized below
78 my $lat = $params{'lat'}; # sanitized below
79 my $lon = $params{'lon'}; # sanitized below
80 my $href = $params{'href'};
82 my $fullscreen = defined($params{'fullscreen'}); # sanitized here
83 my ($width, $height, $float);
89 $height = scrub($params{'height'} || "300px", $page, $dest); # sanitized here
90 $width = scrub($params{'width'} || "500px", $page, $dest); # sanitized here
91 $float = (defined($params{'right'}) && 'right') || (defined($params{'left'}) && 'left'); # sanitized here
93 my $zoom = scrub($params{'zoom'} // $config{'osm_default_zoom'} // 15, $page, $dest); # sanitized below
96 $map = $params{'map'} || $page;
98 $map = $params{'map'} || 'map';
100 $map = scrub($map, $page, $dest); # sanitized here
101 my $name = scrub($params{'name'} || $map, $page, $dest);
103 if (defined($lon) || defined($lat) || defined($loc)) {
104 ($lon, $lat) = scrub_lonlat($loc, $lon, $lat);
107 if ($zoom !~ /^\d\d?$/ || $zoom < 2 || $zoom > 18) {
110 $pagestate{$page}{'osm'}{$map}{'displays'}{$name} =
116 'fullscreen' => $fullscreen,
117 'editable' => defined($params{'editable'}),
122 return "<div id=\"mapdiv-$name\"></div>";
125 sub process_waypoint {
127 my $loc = $params{'loc'}; # sanitized below
128 my $lat = $params{'lat'}; # sanitized below
129 my $lon = $params{'lon'}; # sanitized below
130 my $page = $params{'page'}; # not sanitized?
131 my $dest = $params{'destpage'}; # not sanitized?
132 my $hidden = defined($params{'hidden'}); # sanitized here
133 my ($p) = $page =~ /(?:^|\/)([^\/]+)\/?$/; # shorter page name
134 my $name = scrub($params{'name'} || $p, $page, $dest); # sanitized here
135 my $desc = scrub($params{'desc'} || '', $page, $dest); # sanitized here
136 my $zoom = scrub($params{'zoom'} // $config{'osm_default_zoom'} // 15, $page, $dest); # sanitized below
137 my $icon = $config{'osm__default_icon'} || "img/osm.png"; # sanitized: we trust $config
138 my $map = scrub($params{'map'} || 'map', $page, $dest); # sanitized here
139 my $alt = $config{'osm_alt'} ? "alt=\"$config{'osm_alt'}\"" : ''; # sanitized: we trust $config
140 if ($zoom !~ /^\d\d?$/ || $zoom < 2 || $zoom > 18) {
144 ($lon, $lat) = scrub_lonlat($loc, $lon, $lat);
145 if (!defined($lat) || !defined($lon)) {
146 error("Must specify lat and lon");
149 my $tag = $params{'tag'};
151 if (!defined($config{'osm_tag_icons'}->{$tag})) {
152 error("invalid tag specified, see osm_tag_icons configuration or don't specify any");
154 $icon = $config{'osm_tag_icons'}->{$tag};
156 foreach my $t (keys %{$typedlinks{$page}{'tag'}}) {
157 if ($icon = get_tag_icon($t)) {
161 $t =~ s!/$config{'tagbase'}/!!;
162 if ($icon = get_tag_icon($t)) {
168 $icon = "/img/unknown.png" unless $icon;
169 $tag = '' unless $tag;
170 if ($page eq $dest) {
171 if (!defined($config{'osm_format'}) || !$config{'osm_format'}) {
172 $config{'osm_format'} = 'KML';
174 my %formats = map { $_ => 1 } split(/, */, $config{'osm_format'});
175 if ($formats{'GeoJSON'}) {
176 will_render($page,$config{destdir} . "/$map/pois.json");
178 if ($formats{'CSV'}) {
179 will_render($page,$config{destdir} . "/$map/pois.txt");
181 if ($formats{'KML'}) {
182 will_render($page,$config{destdir} . "/$map/pois.kml");
186 my $href = "/ikiwiki.cgi?do=osm&map=$map&lat=$lat&lon=$lon&zoom=$zoom";
187 if (defined($destsources{htmlpage($map)})) {
188 $href = urlto($map,$page) . "?lat=$lat&lon=$lon&zoom=$zoom";
190 $pagestate{$page}{'osm'}{$map}{'waypoints'}{$name} =
198 'href' => urlto($page,$map), # how to link back to the page from the map, not to be confused with the URL of the map itself sent to the embeded map below
201 if (defined($params{'embed'})) {
202 $params{'href'} = $href; # propagate down to embeded
203 $output .= preprocess(%params);
206 $href =~ s!&!&!g;
207 $output .= "<a href=\"$href\"><img class=\"img\" src=\"$icon\" $alt /></a>";
212 # get the icon from the given tag
213 sub get_tag_icon($) {
215 # look for an icon attached to the tag
216 my $attached = $tag . '/' . $config{'osm_tag_default_icon'};
217 if (srcfile($attached)) {
220 # look for the old way: mappings
221 if ($config{'osm_tag_icons'}->{$tag}) {
222 return $config{'osm_tag_icons'}->{$tag};
228 sub scrub_lonlat($$$) {
229 my ($loc, $lon, $lat) = @_;
231 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*$/) {
239 if($lat =~ /^(\-?)(\d+)(?:(\.\d*)°?|(?:°|\s)\s*(\d+)(?:(\.\d*)\'?|(?:\'|\s)\s*(\d+(?:\.\d*)?\"?)|\'?)|°?)\s*([NS])?\s*$/) {
240 $lat = $2 + ($3//0) + ((($4//0) + (($5//0) + (($6//0)/60.)))/60.);
241 if (($1 eq '-') || (($7//'') eq 'S')) {
249 if($lon =~ /^(\-?)(\d+)(?:(\.\d*)°?|(?:°|\s)\s*(\d+)(?:(\.\d*)\'?|(?:\'|\s)\s*(\d+(?:\.\d*)?\"?)|\'?)|°?)\s*([EW])?$/) {
250 $lon = $2 + ($3//0) + ((($4//0) + (($5//0) + (($6//0)/60.)))/60.);
251 if (($1 eq '-') || (($7//'') eq 'W')) {
258 if ($lat < -90 || $lat > 90 || $lon < -180 || $lon > 180) {
259 error("Location out of range");
266 my %linestrings = ();
267 foreach my $page (keys %pagestate) {
268 if (exists $pagestate{$page}{'osm'}) {
269 foreach my $map (keys %{$pagestate{$page}{'osm'}}) {
270 foreach my $name (keys %{$pagestate{$page}{'osm'}{$map}{'waypoints'}}) {
271 debug("found waypoint $name");
272 $waypoints{$map}{$name} = $pagestate{$page}{'osm'}{$map}{'waypoints'}{$name};
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}}, [ [ $waypoints{$map}{$name}{'lon'}, $waypoints{$map}{$name}{'lat'} ],
286 [ $waypoints{$map}{$otherpage}{'lon'}, $waypoints{$map}{$otherpage}{'lat'} ] ]);
292 # clear the state, it will be regenerated on the next parse
293 # the idea here is to clear up removed waypoints...
294 $pagestate{$page}{'osm'} = ();
297 if (!defined($config{'osm_format'}) || !$config{'osm_format'}) {
298 $config{'osm_format'} = 'KML';
300 my %formats = map { $_ => 1 } split(/, */, $config{'osm_format'});
301 if ($formats{'GeoJSON'}) {
302 writejson(\%waypoints, \%linestrings);
304 if ($formats{'CSV'}) {
305 writecsvs(\%waypoints, \%linestrings);
307 if ($formats{'KML'}) {
308 writekml(\%waypoints, \%linestrings);
313 my %waypoints = %{$_[0]};
314 my %linestrings = %{$_[1]};
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);
325 foreach my $linestring (@{$linestrings{$map}}) {
326 my %json = ( "type" => "Feature",
327 "geometry" => { "type" => "LineString", "coordinates" => $linestring });
328 push(@{$geojson{'features'}}, \%json);
330 debug('writing pois file pois.json in ' . $config{destdir} . "/$map");
331 writefile("pois.json",$config{destdir} . "/$map",to_json(\%geojson));
336 my %waypoints = %{$_[0]};
337 my %linestrings = %{$_[1]};
338 eval q{use XML::Writer};
340 foreach my $map (keys %waypoints) {
341 debug("writing pois file pois.kml in " . $config{destdir} . "/$map");
346 <?xml version="1.0" encoding="UTF-8"?>
347 <kml xmlns="http://www.opengis.net/kml/2.2">
349 <name>Simple placemark</name>
350 <description>Attached to the ground. Intelligently places itself
351 at the height of the underlying terrain.</description>
353 <coordinates>-122.0822035425683,37.42228990140251,0</coordinates>
361 <Style id="sh_sunny_copy69">
365 <href>http://waypoints.google.com/mapfiles/kml/shapes/sunny.png</href>
367 <hotSpot x="0.5" y="0.5" xunits="fraction" yunits="fraction"/>
370 <color>ff00aaff</color>
378 my $output = IO::File->new(">".$config{destdir} . "/$map/pois.kml");
380 my $writer = XML::Writer->new( OUTPUT => $output, DATA_MODE => 1, ENCODING => 'UTF-8');
382 $writer->startTag("kml", "xmlns" => "http://www.opengis.net/kml/2.2");
385 # first pass: get the icons
386 foreach my $name (keys %{$waypoints{$map}}) {
387 my %options = %{$waypoints{$map}{$name}};
388 $writer->startTag("Style", id => $options{tag});
389 $writer->startTag("IconStyle");
390 $writer->startTag("Icon");
391 $writer->startTag("href");
392 $writer->characters($options{icon});
399 foreach my $name (keys %{$waypoints{$map}}) {
400 my %options = %{$waypoints{$map}{$name}};
401 $writer->startTag("Placemark");
402 $writer->startTag("name");
403 $writer->characters($name);
405 $writer->startTag("styleUrl");
406 $writer->characters('#' . $options{tag});
408 #$writer->emptyTag('atom:link', href => $options{href});
409 $writer->startTag('href'); # to make it easier for us as the atom:link parameter is hard to access from javascript
410 $writer->characters($options{href});
412 $writer->startTag("description");
413 $writer->characters($options{desc});
415 $writer->startTag("Point");
416 $writer->startTag("coordinates");
417 $writer->characters($options{lon} . "," . $options{lat});
424 foreach my $linestring (@{$linestrings{$map}}) {
425 $writer->startTag("Placemark");
426 $writer->startTag("name");
427 $writer->characters("linestring " . $i++);
429 $writer->startTag("LineString");
430 $writer->startTag("coordinates");
432 foreach my $coord (@{$linestring}) {
433 $str .= join(',', @{$coord}) . " \n";
435 $writer->characters($str);
447 my %waypoints = %{$_[0]};
448 foreach my $map (keys %waypoints) {
449 my $poisf = "lat\tlon\ttitle\tdescription\ticon\ticonSize\ticonOffset\n";
450 foreach my $name (keys %{$waypoints{$map}}) {
451 my %options = %{$waypoints{$map}{$name}};
453 $options{'lat'} . "\t" .
454 $options{'lon'} . "\t" .
456 $options{'desc'} . '<br /><a href="' . $options{'page'} . '">' . $name . "</a>\t" .
457 $options{'icon'} . "\n";
460 debug("writing pois file pois.txt in " . $config{destdir} . "/$map");
461 writefile("pois.txt",$config{destdir} . "/$map",$poisf);
465 # pipe some data through the HTML scrubber
467 # code taken from the meta.pm plugin
469 if (IkiWiki::Plugin::htmlscrubber->can("sanitize")) {
470 return IkiWiki::Plugin::htmlscrubber::sanitize(
471 content => shift, page => shift, destpage => shift);
478 # taken from toggle.pm
482 if ($params{content}=~m!<div[^>]*id="mapdiv-[^"]*"[^>]*>!g) {
483 if (! ($params{content}=~s!</body>!include_javascript($params{page})."</body>"!em)) {
484 # no <body> tag, probably in preview mode
485 $params{content}=$params{content} . include_javascript($params{page});
488 return $params{content};
491 sub prefered_format() {
492 if (!defined($config{'osm_format'}) || !$config{'osm_format'}) {
493 $config{'osm_format'} = 'KML';
495 my @spl = split(/, */, $config{'osm_format'});
499 sub include_javascript ($) {
505 if (exists $pagestate{$page}{'osm'}) {
506 foreach my $map (keys %{$pagestate{$page}{'osm'}}) {
507 foreach my $name (keys %{$pagestate{$page}{'osm'}{$map}{'displays'}}) {
508 my %options = %{$pagestate{$page}{'osm'}{$map}{'displays'}{$name}};
509 $options{'map'} = $map;
510 $options{'format'} = prefered_format();
511 $loader .= "mapsetup(\"mapdiv-$name\", " . to_json(\%options) . ");\n";
516 return embed_map_code() . "<script type=\"text/javascript\" charset=\"utf-8\">$loader</script>";
525 return unless defined $cgi->param('do') &&
526 $cgi->param("do") eq "osm";
528 IkiWiki::decode_cgi_utf8($cgi);
530 my $map = $cgi->param('map');
531 if (!defined $map || $map !~ /^[a-z]*$/) {
532 error("invalid map parameter");
535 print "Content-Type: text/html\r\n";
537 print "<html><body>";
538 print "<div id=\"mapdiv-$map\"></div>";
539 print embed_map_code($map);
540 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>";
541 print "</body></html>";
546 sub embed_map_code() {
548 <script src="http://www.openlayers.org/api/OpenLayers.js"></script>
550 // taken from http://stackoverflow.com/questions/901115/get-query-string-values-in-javascript
554 a = /\\+/g, // Regex for replacing addition symbol with a space
555 r = /([^&=]+)=?([^&]*)/g,
556 d = function (s) { return decodeURIComponent(s.replace(a, " ")); },
557 q = window.location.search.substring(1);
559 while (e = r.exec(q))
560 urlParams[d(e[1])] = d(e[2]);
563 function mapsetup(divname, options) {
564 div = document.getElementById(divname);
565 if (options.fullscreen) {
566 permalink = 'permalink';
569 div.style.position = 'absolute';
570 div.style.width = '100%';
571 div.style.height = '100%';
573 div.style.height = options.height;
574 div.style.width = options.width;
575 div.style.float = options.float;
576 permalink = {base: options.href, title: "View larger map"};
578 map = new OpenLayers.Map(divname, {
580 new OpenLayers.Control.Navigation(),
581 new OpenLayers.Control.ScaleLine(),
582 new OpenLayers.Control.Permalink(permalink)
584 displayProjection: new OpenLayers.Projection("EPSG:4326"),
589 map.addLayer(new OpenLayers.Layer.OSM());
590 if (options.format == 'CSV') {
591 pois = new OpenLayers.Layer.Text( "CSV",
592 { location:"/" + options.map + "/pois.txt",
593 projection: map.displayProjection
595 } else if (options.format == 'GeoJSON') {
596 pois = new OpenLayers.Layer.Vector("GeoJSON", {
597 protocol: new OpenLayers.Protocol.HTTP({
598 url: "/" + options.map + "/pois.json",
599 format: new OpenLayers.Format.GeoJSON()
601 strategies: [new OpenLayers.Strategy.Fixed()]
604 pois = new OpenLayers.Layer.Vector("KML", {
605 protocol: new OpenLayers.Protocol.HTTP({
606 url: "/" + options.map + "/pois.kml",
607 format: new OpenLayers.Format.KML({
609 extractAttributes: true
612 strategies: [new OpenLayers.Strategy.Fixed()]});
615 select = new OpenLayers.Control.SelectFeature(pois);
616 map.addControl(select);
620 "featureselected": function (event) {
621 var feature = event.feature;
622 var content = '<h2><a href="' + feature.attributes.href + '">' +feature.attributes.name + "</a></h2>" + feature.attributes.description;
623 popup = new OpenLayers.Popup.FramedCloud("chicken",
624 feature.geometry.getBounds().getCenterLonLat(),
625 new OpenLayers.Size(100,100),
627 null, true, function () {select.unselectAll()});
628 feature.popup = popup;
631 "featureunselected": function (event) {
632 var feature = event.feature;
634 map.removePopup(feature.popup);
635 feature.popup.destroy();
636 delete feature.popup;
641 if (options.editable) {
642 vlayer = new OpenLayers.Layer.Vector( "Editable" );
643 map.addControl(new OpenLayers.Control.EditingToolbar(vlayer));
644 map.addLayer(vlayer);
647 if (options.fullscreen) {
648 map.addControl(new OpenLayers.Control.PanZoomBar());
649 map.addControl(new OpenLayers.Control.LayerSwitcher({'ascending':false}));
650 map.addControl(new OpenLayers.Control.MousePosition());
651 map.addControl(new OpenLayers.Control.KeyboardDefaults());
653 map.addControl(new OpenLayers.Control.ZoomPanel());
656 //Set start centrepoint and zoom
657 if (!options.lat || !options.lon) {
658 options.lat = urlParams['lat'];
659 options.lon = urlParams['lon'];
662 options.zoom = urlParams['zoom'];
664 if (options.lat && options.lon) {
665 var lat = options.lat;
666 var lon = options.lon;
667 var zoom= options.zoom || 10;
668 center = new OpenLayers.LonLat( lon, lat )
670 new OpenLayers.Projection("EPSG:4326"), // transform from WGS 1984
671 map.getProjectionObject() // to Spherical Mercator Projection
673 map.setCenter (center, zoom);
675 pois.events.register("loadend", this, function () { map.zoomToExtent(pois.getDataExtent()); });