542 lines
14 KiB
Perl
542 lines
14 KiB
Perl
#!/usr/bin/perl
|
|
# Copyright 2011 Blars Blarson
|
|
# Released under GPL version 2
|
|
|
|
package IkiWiki::Plugin::osm;
|
|
use utf8;
|
|
use strict;
|
|
use warnings;
|
|
use IkiWiki 3.0;
|
|
|
|
sub import {
|
|
add_underlay("osm");
|
|
hook(type => "getsetup", id => "osm", call => \&getsetup);
|
|
hook(type => "format", id => "osm", call => \&format);
|
|
hook(type => "preprocess", id => "osm", call => \&preprocess);
|
|
hook(type => "preprocess", id => "waypoint", call => \&process_waypoint);
|
|
hook(type => "savestate", id => "waypoint", call => \&savestate);
|
|
hook(type => "cgi", id => "osm", call => \&cgi);
|
|
}
|
|
|
|
sub getsetup () {
|
|
return
|
|
plugin => {
|
|
safe => 1,
|
|
rebuild => 1,
|
|
section => "special-purpose",
|
|
},
|
|
osm_default_zoom => {
|
|
type => "integer",
|
|
example => "15",
|
|
description => "the default zoom when you click on the map link",
|
|
safe => 1,
|
|
rebuild => 1,
|
|
},
|
|
osm_default_icon => {
|
|
type => "string",
|
|
example => "ikiwiki/images/osm.png",
|
|
description => "the icon shown on links and on the main map",
|
|
safe => 0,
|
|
rebuild => 1,
|
|
},
|
|
osm_alt => {
|
|
type => "string",
|
|
example => "",
|
|
description => "the alt tag of links, defaults to empty",
|
|
safe => 0,
|
|
rebuild => 1,
|
|
},
|
|
osm_format => {
|
|
type => "string",
|
|
example => "KML",
|
|
description => "the output format for waypoints, can be KML, GeoJSON or CSV (one or many, comma-separated)",
|
|
safe => 1,
|
|
rebuild => 1,
|
|
},
|
|
osm_tag_default_icon => {
|
|
type => "string",
|
|
example => "icon.png",
|
|
description => "the icon attached to a tag, displayed on the map for tagged pages",
|
|
safe => 0,
|
|
rebuild => 1,
|
|
},
|
|
}
|
|
|
|
sub preprocess {
|
|
my %params=@_;
|
|
my $page = $params{page};
|
|
my $dest = $params{destpage};
|
|
my $loc = $params{loc}; # sanitized below
|
|
my $lat = $params{lat}; # sanitized below
|
|
my $lon = $params{lon}; # sanitized below
|
|
my $href = $params{href};
|
|
|
|
my ($width, $height, $float);
|
|
$height = scrub($params{'height'} || "300px", $page, $dest); # sanitized here
|
|
$width = scrub($params{'width'} || "500px", $page, $dest); # sanitized here
|
|
$float = (defined($params{'right'}) && 'right') || (defined($params{'left'}) && 'left'); # sanitized here
|
|
|
|
my $zoom = scrub($params{'zoom'} // $config{'osm_default_zoom'} // 15, $page, $dest); # sanitized below
|
|
my $map;
|
|
$map = $params{'map'} || 'map';
|
|
|
|
$map = scrub($map, $page, $dest); # sanitized here
|
|
my $name = scrub($params{'name'} || $map, $page, $dest);
|
|
|
|
if (defined($lon) || defined($lat) || defined($loc)) {
|
|
($lon, $lat) = scrub_lonlat($loc, $lon, $lat);
|
|
}
|
|
|
|
if ($zoom !~ /^\d\d?$/ || $zoom < 2 || $zoom > 18) {
|
|
error("Bad zoom");
|
|
}
|
|
|
|
if (! defined $href || ! length $href) {
|
|
$href=IkiWiki::cgiurl(
|
|
do => "osm",
|
|
map => $map,
|
|
);
|
|
}
|
|
|
|
$pagestate{$page}{'osm'}{$map}{'displays'}{$name} = {
|
|
height => $height,
|
|
width => $width,
|
|
float => $float,
|
|
zoom => $zoom,
|
|
fullscreen => 0,
|
|
editable => defined($params{'editable'}),
|
|
lat => $lat,
|
|
lon => $lon,
|
|
href => $href,
|
|
};
|
|
return "<div id=\"mapdiv-$name\"></div>";
|
|
}
|
|
|
|
sub process_waypoint {
|
|
my %params=@_;
|
|
my $loc = $params{'loc'}; # sanitized below
|
|
my $lat = $params{'lat'}; # sanitized below
|
|
my $lon = $params{'lon'}; # sanitized below
|
|
my $page = $params{'page'}; # not sanitized?
|
|
my $dest = $params{'destpage'}; # not sanitized?
|
|
my $hidden = defined($params{'hidden'}); # sanitized here
|
|
my ($p) = $page =~ /(?:^|\/)([^\/]+)\/?$/; # shorter page name
|
|
my $name = scrub($params{'name'} || $p, $page, $dest); # sanitized here
|
|
my $desc = scrub($params{'desc'} || '', $page, $dest); # sanitized here
|
|
my $zoom = scrub($params{'zoom'} // $config{'osm_default_zoom'} // 15, $page, $dest); # sanitized below
|
|
my $icon = $config{'osm_default_icon'} || "ikiwiki/images/osm.png"; # sanitized: we trust $config
|
|
my $map = scrub($params{'map'} || 'map', $page, $dest); # sanitized here
|
|
my $alt = $config{'osm_alt'} ? "alt=\"$config{'osm_alt'}\"" : ''; # sanitized: we trust $config
|
|
if ($zoom !~ /^\d\d?$/ || $zoom < 2 || $zoom > 18) {
|
|
error("Bad zoom");
|
|
}
|
|
|
|
($lon, $lat) = scrub_lonlat($loc, $lon, $lat);
|
|
if (!defined($lat) || !defined($lon)) {
|
|
error("Must specify lat and lon");
|
|
}
|
|
|
|
my $tag = $params{'tag'};
|
|
foreach my $t (keys %{$typedlinks{$page}{'tag'}}) {
|
|
if ($icon = get_tag_icon($t)) {
|
|
$tag = $t;
|
|
last;
|
|
}
|
|
$t =~ s!/$config{'tagbase'}/!!;
|
|
if ($icon = get_tag_icon($t)) {
|
|
$tag = $t;
|
|
last;
|
|
}
|
|
}
|
|
$icon = urlto($icon, $dest, 1);
|
|
$tag = '' unless $tag;
|
|
if ($page eq $dest) {
|
|
my %formats = get_formats();
|
|
if ($formats{'GeoJSON'}) {
|
|
will_render($page, "$map/pois.json");
|
|
}
|
|
if ($formats{'CSV'}) {
|
|
will_render($page, "$map/pois.txt");
|
|
}
|
|
if ($formats{'KML'}) {
|
|
will_render($page, "$map/pois.kml");
|
|
}
|
|
}
|
|
$pagestate{$page}{'osm'}{$map}{'waypoints'}{$name} = {
|
|
page => $page,
|
|
desc => $desc,
|
|
icon => $icon,
|
|
tag => $tag,
|
|
lat => $lat,
|
|
lon => $lon,
|
|
# 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. Note: used in generated KML etc file,
|
|
# so must be absolute.
|
|
href => urlto($page),
|
|
};
|
|
|
|
my $mapurl = IkiWiki::cgiurl(
|
|
do => "osm",
|
|
map => $map,
|
|
lat => $lat,
|
|
lon => $lon,
|
|
zoom => $zoom,
|
|
);
|
|
my $output = '';
|
|
if (defined($params{'embed'})) {
|
|
$output .= preprocess(%params,
|
|
href => $mapurl,
|
|
);
|
|
}
|
|
if (!$hidden) {
|
|
$output .= "<a href=\"$mapurl\"><img class=\"img\" src=\"$icon\" $alt /></a>";
|
|
}
|
|
return $output;
|
|
}
|
|
|
|
# get the icon from the given tag
|
|
sub get_tag_icon($) {
|
|
my $tag = shift;
|
|
# look for an icon attached to the tag
|
|
my $attached = $tag . '/' . $config{'osm_tag_default_icon'};
|
|
if (srcfile($attached)) {
|
|
return $attached;
|
|
}
|
|
else {
|
|
return undef;
|
|
}
|
|
}
|
|
|
|
sub scrub_lonlat($$$) {
|
|
my ($loc, $lon, $lat) = @_;
|
|
if ($loc) {
|
|
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*$/) {
|
|
$lat = $1;
|
|
$lon = $2;
|
|
}
|
|
else {
|
|
error("Bad loc");
|
|
}
|
|
}
|
|
if (defined($lat)) {
|
|
if ($lat =~ /^(\-?)(\d+)(?:(\.\d*)°?|(?:°|\s)\s*(\d+)(?:(\.\d*)\'?|(?:\'|\s)\s*(\d+(?:\.\d*)?\"?)|\'?)|°?)\s*([NS])?\s*$/) {
|
|
$lat = $2 + ($3//0) + ((($4//0) + (($5//0) + (($6//0)/60.)))/60.);
|
|
if (($1 eq '-') || (($7//'') eq 'S')) {
|
|
$lat = - $lat;
|
|
}
|
|
}
|
|
else {
|
|
error("Bad lat");
|
|
}
|
|
}
|
|
if (defined($lon)) {
|
|
if ($lon =~ /^(\-?)(\d+)(?:(\.\d*)°?|(?:°|\s)\s*(\d+)(?:(\.\d*)\'?|(?:\'|\s)\s*(\d+(?:\.\d*)?\"?)|\'?)|°?)\s*([EW])?$/) {
|
|
$lon = $2 + ($3//0) + ((($4//0) + (($5//0) + (($6//0)/60.)))/60.);
|
|
if (($1 eq '-') || (($7//'') eq 'W')) {
|
|
$lon = - $lon;
|
|
}
|
|
}
|
|
else {
|
|
error("Bad lon");
|
|
}
|
|
}
|
|
if ($lat < -90 || $lat > 90 || $lon < -180 || $lon > 180) {
|
|
error("Location out of range");
|
|
}
|
|
return ($lon, $lat);
|
|
}
|
|
|
|
sub savestate {
|
|
my %waypoints = ();
|
|
my %linestrings = ();
|
|
|
|
foreach my $page (keys %pagestate) {
|
|
if (exists $pagestate{$page}{'osm'}) {
|
|
foreach my $map (keys %{$pagestate{$page}{'osm'}}) {
|
|
foreach my $name (keys %{$pagestate{$page}{'osm'}{$map}{'waypoints'}}) {
|
|
debug("found waypoint $name");
|
|
$waypoints{$map}{$name} = $pagestate{$page}{'osm'}{$map}{'waypoints'}{$name};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach my $page (keys %pagestate) {
|
|
if (exists $pagestate{$page}{'osm'}) {
|
|
foreach my $map (keys %{$pagestate{$page}{'osm'}}) {
|
|
# examine the links on this page
|
|
foreach my $name (keys %{$pagestate{$page}{'osm'}{$map}{'waypoints'}}) {
|
|
if (exists $links{$page}) {
|
|
foreach my $otherpage (@{$links{$page}}) {
|
|
if (exists $waypoints{$map}{$otherpage}) {
|
|
push(@{$linestrings{$map}}, [
|
|
[ $waypoints{$map}{$name}{'lon'}, $waypoints{$map}{$name}{'lat'} ],
|
|
[ $waypoints{$map}{$otherpage}{'lon'}, $waypoints{$map}{$otherpage}{'lat'} ]
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
# clear the state, it will be regenerated on the next parse
|
|
# the idea here is to clear up removed waypoints...
|
|
$pagestate{$page}{'osm'} = ();
|
|
}
|
|
}
|
|
|
|
my %formats = get_formats();
|
|
if ($formats{'GeoJSON'}) {
|
|
writejson(\%waypoints, \%linestrings);
|
|
}
|
|
if ($formats{'CSV'}) {
|
|
writecsvs(\%waypoints, \%linestrings);
|
|
}
|
|
if ($formats{'KML'}) {
|
|
writekml(\%waypoints, \%linestrings);
|
|
}
|
|
}
|
|
|
|
sub writejson($;$) {
|
|
my %waypoints = %{$_[0]};
|
|
my %linestrings = %{$_[1]};
|
|
eval q{use JSON};
|
|
error $@ if $@;
|
|
foreach my $map (keys %waypoints) {
|
|
my %geojson = ( "type" => "FeatureCollection", "features" => []);
|
|
foreach my $name (keys %{$waypoints{$map}}) {
|
|
my %marker = ( "type" => "Feature",
|
|
"geometry" => { "type" => "Point", "coordinates" => [ $waypoints{$map}{$name}{'lon'}, $waypoints{$map}{$name}{'lat'} ] },
|
|
"properties" => $waypoints{$map}{$name} );
|
|
push @{$geojson{'features'}}, \%marker;
|
|
}
|
|
foreach my $linestring (@{$linestrings{$map}}) {
|
|
my %json = ( "type" => "Feature",
|
|
"geometry" => { "type" => "LineString", "coordinates" => $linestring });
|
|
push @{$geojson{'features'}}, \%json;
|
|
}
|
|
writefile("pois.json", $config{destdir} . "/$map", to_json(\%geojson));
|
|
}
|
|
}
|
|
|
|
sub writekml($;$) {
|
|
my %waypoints = %{$_[0]};
|
|
my %linestrings = %{$_[1]};
|
|
eval q{use XML::Writer};
|
|
error $@ if $@;
|
|
foreach my $map (keys %waypoints) {
|
|
my $output;
|
|
my $writer = XML::Writer->new( OUTPUT => \$output,
|
|
DATA_MODE => 1, ENCODING => 'UTF-8');
|
|
$writer->xmlDecl();
|
|
$writer->startTag("kml", "xmlns" => "http://www.opengis.net/kml/2.2");
|
|
|
|
# first pass: get the icons
|
|
foreach my $name (keys %{$waypoints{$map}}) {
|
|
my %options = %{$waypoints{$map}{$name}};
|
|
$writer->startTag("Style", id => $options{tag});
|
|
$writer->startTag("IconStyle");
|
|
$writer->startTag("Icon");
|
|
$writer->startTag("href");
|
|
$writer->characters($options{icon});
|
|
$writer->endTag();
|
|
$writer->endTag();
|
|
$writer->endTag();
|
|
$writer->endTag();
|
|
}
|
|
|
|
foreach my $name (keys %{$waypoints{$map}}) {
|
|
my %options = %{$waypoints{$map}{$name}};
|
|
$writer->startTag("Placemark");
|
|
$writer->startTag("name");
|
|
$writer->characters($name);
|
|
$writer->endTag();
|
|
$writer->startTag("styleUrl");
|
|
$writer->characters('#' . $options{tag});
|
|
$writer->endTag();
|
|
#$writer->emptyTag('atom:link', href => $options{href});
|
|
# to make it easier for us as the atom:link parameter is
|
|
# hard to access from javascript
|
|
$writer->startTag('href');
|
|
$writer->characters($options{href});
|
|
$writer->endTag();
|
|
$writer->startTag("description");
|
|
$writer->characters($options{desc});
|
|
$writer->endTag();
|
|
$writer->startTag("Point");
|
|
$writer->startTag("coordinates");
|
|
$writer->characters($options{lon} . "," . $options{lat});
|
|
$writer->endTag();
|
|
$writer->endTag();
|
|
$writer->endTag();
|
|
}
|
|
|
|
my $i = 0;
|
|
foreach my $linestring (@{$linestrings{$map}}) {
|
|
$writer->startTag("Placemark");
|
|
$writer->startTag("name");
|
|
$writer->characters("linestring " . $i++);
|
|
$writer->endTag();
|
|
$writer->startTag("LineString");
|
|
$writer->startTag("coordinates");
|
|
my $str = '';
|
|
foreach my $coord (@{$linestring}) {
|
|
$str .= join(',', @{$coord}) . " \n";
|
|
}
|
|
$writer->characters($str);
|
|
$writer->endTag();
|
|
$writer->endTag();
|
|
$writer->endTag();
|
|
}
|
|
$writer->endTag();
|
|
$writer->end();
|
|
|
|
writefile("pois.kml", $config{destdir} . "/$map", $output);
|
|
}
|
|
}
|
|
|
|
sub writecsvs($;$) {
|
|
my %waypoints = %{$_[0]};
|
|
foreach my $map (keys %waypoints) {
|
|
my $poisf = "lat\tlon\ttitle\tdescription\ticon\ticonSize\ticonOffset\n";
|
|
foreach my $name (keys %{$waypoints{$map}}) {
|
|
my %options = %{$waypoints{$map}{$name}};
|
|
my $line =
|
|
$options{'lat'} . "\t" .
|
|
$options{'lon'} . "\t" .
|
|
$name . "\t" .
|
|
$options{'desc'} . '<br /><a href="' . $options{'page'} . '">' . $name . "</a>\t" .
|
|
$options{'icon'} . "\n";
|
|
$poisf .= $line;
|
|
}
|
|
writefile("pois.txt", $config{destdir} . "/$map", $poisf);
|
|
}
|
|
}
|
|
|
|
# pipe some data through the HTML scrubber
|
|
#
|
|
# code taken from the meta.pm plugin
|
|
sub scrub($$$) {
|
|
if (IkiWiki::Plugin::htmlscrubber->can("sanitize")) {
|
|
return IkiWiki::Plugin::htmlscrubber::sanitize(
|
|
content => shift, page => shift, destpage => shift);
|
|
}
|
|
else {
|
|
return shift;
|
|
}
|
|
}
|
|
|
|
# taken from toggle.pm
|
|
sub format (@) {
|
|
my %params=@_;
|
|
|
|
if ($params{content}=~m!<div[^>]*id="mapdiv-[^"]*"[^>]*>!g) {
|
|
if (! ($params{content}=~s!</body>!include_javascript($params{page})."</body>"!em)) {
|
|
# no <body> tag, probably in preview mode
|
|
$params{content}=$params{content} . include_javascript($params{page});
|
|
}
|
|
}
|
|
return $params{content};
|
|
}
|
|
|
|
sub preferred_format() {
|
|
if (!defined($config{'osm_format'}) || !$config{'osm_format'}) {
|
|
$config{'osm_format'} = 'KML';
|
|
}
|
|
my @spl = split(/, */, $config{'osm_format'});
|
|
return shift @spl;
|
|
}
|
|
|
|
sub get_formats() {
|
|
if (!defined($config{'osm_format'}) || !$config{'osm_format'}) {
|
|
$config{'osm_format'} = 'KML';
|
|
}
|
|
map { $_ => 1 } split(/, */, $config{'osm_format'});
|
|
}
|
|
|
|
sub include_javascript ($) {
|
|
my $page=shift;
|
|
my $loader;
|
|
|
|
if (exists $pagestate{$page}{'osm'}) {
|
|
foreach my $map (keys %{$pagestate{$page}{'osm'}}) {
|
|
foreach my $name (keys %{$pagestate{$page}{'osm'}{$map}{'displays'}}) {
|
|
$loader .= map_setup_code($map, $name, %{$pagestate{$page}{'osm'}{$map}{'displays'}{$name}});
|
|
}
|
|
}
|
|
}
|
|
if ($loader) {
|
|
return embed_map_code($page) . "<script type=\"text/javascript\" charset=\"utf-8\">$loader</script>";
|
|
}
|
|
else {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
sub cgi($) {
|
|
my $cgi=shift;
|
|
|
|
return unless defined $cgi->param('do') &&
|
|
$cgi->param("do") eq "osm";
|
|
|
|
IkiWiki::loadindex();
|
|
|
|
IkiWiki::decode_cgi_utf8($cgi);
|
|
|
|
my $map = $cgi->param('map');
|
|
if (!defined $map || $map !~ /^[a-z]*$/) {
|
|
error("invalid map parameter");
|
|
}
|
|
|
|
print "Content-Type: text/html\r\n";
|
|
print ("\r\n");
|
|
print "<html><body>";
|
|
print "<div id=\"mapdiv-$map\"></div>";
|
|
print embed_map_code();
|
|
print "<script type=\"text/javascript\" charset=\"utf-8\">";
|
|
print map_setup_code($map, $map,
|
|
lat => "urlParams['lat']",
|
|
lon => "urlParams['lon']",
|
|
zoom => "urlParams['zoom']",
|
|
fullscreen => 1,
|
|
editable => 1,
|
|
);
|
|
print "</script>";
|
|
print "</body></html>";
|
|
|
|
exit 0;
|
|
}
|
|
|
|
sub embed_map_code(;$) {
|
|
my $page=shift;
|
|
return '<script src="http://www.openlayers.org/api/OpenLayers.js" type="text/javascript" charset="utf-8"></script>'.
|
|
'<script src="'.urlto("ikiwiki/osm.js", $page).
|
|
'" type="text/javascript" charset="utf-8"></script>'."\n";
|
|
}
|
|
|
|
sub map_setup_code($;@) {
|
|
my $map=shift;
|
|
my $name=shift;
|
|
my %options=@_;
|
|
|
|
eval q{use JSON};
|
|
error $@ if $@;
|
|
|
|
$options{'format'} = preferred_format();
|
|
|
|
my %formats = get_formats();
|
|
if ($formats{'GeoJSON'}) {
|
|
$options{'jsonurl'} = urlto($map."/pois.json");
|
|
}
|
|
if ($formats{'CSV'}) {
|
|
$options{'csvurl'} = urlto($map."/pois.txt");
|
|
}
|
|
if ($formats{'KML'}) {
|
|
$options{'kmlurl'} = urlto($map."/pois.kml");
|
|
}
|
|
|
|
return "mapsetup('mapdiv-$name', " . to_json(\%options) . ");";
|
|
}
|
|
|
|
1;
|