<html><head><meta name="color-scheme" content="light dark"></head><body><pre style="word-wrap: break-word; white-space: pre-wrap;">#!/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 =&gt; "getsetup", id =&gt; "osm", call =&gt; \&amp;getsetup);
	hook(type =&gt; "format", id =&gt; "osm", call =&gt; \&amp;format);
	hook(type =&gt; "preprocess", id =&gt; "osm", call =&gt; \&amp;preprocess);
	hook(type =&gt; "preprocess", id =&gt; "waypoint", call =&gt; \&amp;process_waypoint);
	hook(type =&gt; "savestate", id =&gt; "waypoint", call =&gt; \&amp;savestate);
	hook(type =&gt; "cgi", id =&gt; "osm", call =&gt; \&amp;cgi);
}

sub getsetup () {
	return
		plugin =&gt; {
			safe =&gt; 1,
			rebuild =&gt; 1,
			section =&gt; "special-purpose",
		},
		osm_default_zoom =&gt; {
			type =&gt; "integer",
			example =&gt; "15",
			description =&gt; "the default zoom when you click on the map link",
			safe =&gt; 1,
			rebuild =&gt; 1,
		},
		osm_default_icon =&gt; {
			type =&gt; "string",
			example =&gt; "ikiwiki/images/osm.png",
			description =&gt; "the icon shown on links and on the main map",
			safe =&gt; 0,
			rebuild =&gt; 1,
		},
		osm_alt =&gt; {
			type =&gt; "string",
			example =&gt; "",
			description =&gt; "the alt tag of links, defaults to empty",
			safe =&gt; 0,
			rebuild =&gt; 1,
		},
		osm_format =&gt; {
			type =&gt; "string",
			example =&gt; "KML",
			description =&gt; "the output format for waypoints, can be KML, GeoJSON or CSV (one or many, comma-separated)",
			safe =&gt; 1,
			rebuild =&gt; 1,
		},
		osm_tag_default_icon =&gt; {
			type =&gt; "string",
			example =&gt; "icon.png",
			description =&gt; "the icon attached to a tag, displayed on the map for tagged pages",
			safe =&gt; 0,
			rebuild =&gt; 1,
		},
		osm_openlayers_url =&gt; {
			type =&gt; "string",
			example =&gt; "http://www.openlayers.org/api/OpenLayers.js",
			description =&gt; "Url for the OpenLayers.js file",
			safe =&gt; 0,
			rebuild =&gt; 1,
		},
		osm_layers =&gt; {
			type =&gt; "string",
			example =&gt; { 'OSM', 'GoogleSatellite' },
			description =&gt; "Layers to use in the map. Can be either the 'OSM' string or a type option for Google maps (GoogleNormal, GoogleSatellite, GoogleHybrid or GooglePhysical). It can also be an arbitrary URL in a syntax acceptable for OpenLayers.Layer.OSM.url parameter.",
			safe =&gt; 0,
			rebuild =&gt; 1,
		},
	        osm_google_apikey =&gt; {
			type =&gt; "string",
			example =&gt; "",
			description =&gt; "Google maps API key, Google layer not used if missing, see https://code.google.com/apis/console/ to get an API key",
			safe =&gt; 1,
			rebuild =&gt; 1,
		},
}

sub register_rendered_files {
	my $map = shift;
	my $page = shift;
	my $dest = shift;

	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");
		}
	}
}

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'}) &amp;&amp; 'right') || (defined($params{'left'}) &amp;&amp; '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 &lt; 2 || $zoom &gt; 18) {
		error("Bad zoom");
	}

	if (! defined $href || ! length $href) {
		$href=IkiWiki::cgiurl(
			do =&gt; "osm",
			map =&gt; $map,
		);
	}

	register_rendered_files($map, $page, $dest);

	$pagestate{$page}{'osm'}{$map}{'displays'}{$name} = {
		height =&gt; $height,
		width =&gt; $width,
		float =&gt; $float,
		zoom =&gt; $zoom,
		fullscreen =&gt; 0,
		editable =&gt; defined($params{'editable'}),
		lat =&gt; $lat,
		lon =&gt; $lon,
		href =&gt; $href,
		google_apikey =&gt; $config{'osm_google_apikey'},
	};
	return "&lt;div id=\"mapdiv-$name\"&gt;&lt;/div&gt;";
}

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 &lt; 2 || $zoom &gt; 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);
	$icon =~ s!/*$!!; # hack - urlto shouldn't be appending a slash in the first place
	$tag = '' unless $tag;
	register_rendered_files($map, $page, $dest);
	$pagestate{$page}{'osm'}{$map}{'waypoints'}{$name} = {
		page =&gt; $page,
		desc =&gt; $desc,
		icon =&gt; $icon,
		tag =&gt; $tag,
		lat =&gt; $lat,
		lon =&gt; $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 =&gt; urlto($page),
	};

	my $mapurl = IkiWiki::cgiurl(
		do =&gt; "osm",
		map =&gt; $map,
		lat =&gt; $lat,
		lon =&gt; $lon,
		zoom =&gt; $zoom,
	);
	my $output = '';
	if (defined($params{'embed'})) {
		$output .= preprocess(%params,
			href =&gt; $mapurl,
		);
	}
	if (!$hidden) {
		$output .= "&lt;a href=\"$mapurl\"&gt;&lt;img class=\"img\" src=\"$icon\" $alt /&gt;&lt;/a&gt;";
	}
	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 &lt; -90 || $lat &gt; 90 || $lon &lt; -180 || $lon &gt; 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" =&gt; "FeatureCollection", "features" =&gt; []);
		foreach my $name (keys %{$waypoints{$map}}) {
			my %marker = ( "type" =&gt; "Feature",
				"geometry" =&gt; { "type" =&gt; "Point", "coordinates" =&gt; [ $waypoints{$map}{$name}{'lon'}, $waypoints{$map}{$name}{'lat'} ] },
				"properties" =&gt; $waypoints{$map}{$name} );
			push @{$geojson{'features'}}, \%marker;
		}
		foreach my $linestring (@{$linestrings{$map}}) {
			my %json  = ( "type" =&gt; "Feature",
				"geometry" =&gt; { "type" =&gt; "LineString", "coordinates" =&gt; $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-&gt;new( OUTPUT =&gt; \$output,
			DATA_MODE =&gt; 1, DATA_INDENT =&gt; ' ', ENCODING =&gt; 'UTF-8');
		$writer-&gt;xmlDecl();
		$writer-&gt;startTag("kml", "xmlns" =&gt; "http://www.opengis.net/kml/2.2");
		$writer-&gt;startTag("Document");

		# first pass: get the icons
		my %tags_map = (); # keep track of tags seen
		foreach my $name (keys %{$waypoints{$map}}) {
			my %options = %{$waypoints{$map}{$name}};
			if (!$tags_map{$options{tag}}) {
			    debug("found new style " . $options{tag});
			    $tags_map{$options{tag}} = ();
			    $writer-&gt;startTag("Style", id =&gt; $options{tag});
			    $writer-&gt;startTag("IconStyle");
			    $writer-&gt;startTag("Icon");
			    $writer-&gt;startTag("href");
			    $writer-&gt;characters($options{icon});
			    $writer-&gt;endTag();
			    $writer-&gt;endTag();
			    $writer-&gt;endTag();
			    $writer-&gt;endTag();
			}
			$tags_map{$options{tag}}{$name} = \%options;
		}
	
		foreach my $name (keys %{$waypoints{$map}}) {
			my %options = %{$waypoints{$map}{$name}};
			$writer-&gt;startTag("Placemark");
			$writer-&gt;startTag("name");
			$writer-&gt;characters($name);
			$writer-&gt;endTag();
			$writer-&gt;startTag("styleUrl");
			$writer-&gt;characters('#' . $options{tag});
			$writer-&gt;endTag();
			#$writer-&gt;emptyTag('atom:link', href =&gt; $options{href});
			# to make it easier for us as the atom:link parameter is
			# hard to access from javascript
			$writer-&gt;startTag('href');
			$writer-&gt;characters($options{href});
			$writer-&gt;endTag();
			$writer-&gt;startTag("description");
			$writer-&gt;characters($options{desc});
			$writer-&gt;endTag();
			$writer-&gt;startTag("Point");
			$writer-&gt;startTag("coordinates");
			$writer-&gt;characters($options{lon} . "," . $options{lat});
			$writer-&gt;endTag();
			$writer-&gt;endTag();
			$writer-&gt;endTag();
		}
		
		my $i = 0;
		foreach my $linestring (@{$linestrings{$map}}) {
			$writer-&gt;startTag("Placemark");
			$writer-&gt;startTag("name");
			$writer-&gt;characters("linestring " . $i++);
			$writer-&gt;endTag();
			$writer-&gt;startTag("LineString");
			$writer-&gt;startTag("coordinates");
			my $str = '';
			foreach my $coord (@{$linestring}) {
				$str .= join(',', @{$coord}) . " \n";
			}
			$writer-&gt;characters($str);
			$writer-&gt;endTag();
			$writer-&gt;endTag();
			$writer-&gt;endTag();
		}
		$writer-&gt;endTag();
		$writer-&gt;endTag();
		$writer-&gt;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'} . '&lt;br /&gt;&lt;a href="' . $options{'page'} . '"&gt;' . $name . "&lt;/a&gt;\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-&gt;can("sanitize")) {
		return IkiWiki::Plugin::htmlscrubber::sanitize(
			content =&gt; shift, page =&gt; shift, destpage =&gt; shift);
	}
	else {
		return shift;
	}
}

# taken from toggle.pm
sub format (@) {
	my %params=@_;

	if ($params{content}=~m!&lt;div[^&gt;]*id="mapdiv-[^"]*"[^&gt;]*&gt;!g) {
		if (! ($params{content}=~s!&lt;/body&gt;!include_javascript($params{page})."&lt;/body&gt;"!em)) {
			# no &lt;body&gt; 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 { $_ =&gt; 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) . "&lt;script type=\"text/javascript\"&gt;$loader&lt;/script&gt;";
	}
	else {
        	return '';
	}
}

sub cgi($) {
	my $cgi=shift;

	return unless defined $cgi-&gt;param('do') &amp;&amp;
		$cgi-&gt;param("do") eq "osm";
	
	IkiWiki::loadindex();

	IkiWiki::decode_cgi_utf8($cgi);

	my $map = $cgi-&gt;param('map');
	if (!defined $map || $map !~ /^[a-z]*$/) {
		error("invalid map parameter");
	}

	print "Content-Type: text/html\r\n";
	print ("\r\n");
	print "&lt;html&gt;&lt;body&gt;";
	print "&lt;div id=\"mapdiv-$map\"&gt;&lt;/div&gt;";
	print embed_map_code();
	print "&lt;script type=\"text/javascript\"&gt;";
	print map_setup_code($map, $map,
		lat =&gt; "urlParams['lat']",
		lon =&gt; "urlParams['lon']",
		zoom =&gt; "urlParams['zoom']",
		fullscreen =&gt; 1,
		editable =&gt; 1,
		google_apikey =&gt; $config{'osm_google_apikey'},
	);
	print "&lt;/script&gt;";
	print "&lt;/body&gt;&lt;/html&gt;";

	exit 0;
}

sub embed_map_code(;$) {
	my $page=shift;
	my $olurl = $config{osm_openlayers_url} || "http://www.openlayers.org/api/OpenLayers.js";
	my $code = '&lt;script src="'.$olurl.'" type="text/javascript" charset="utf-8"&gt;&lt;/script&gt;'."\n".
		'&lt;script src="'.urlto("ikiwiki/osm.js", $page).
		'" type="text/javascript" charset="utf-8"&gt;&lt;/script&gt;'."\n";
	if ($config{'osm_google_apikey'}) {
	    $code .= '&lt;script src="http://maps.google.com/maps?file=api&amp;amp;v=2&amp;amp;key='.$config{'osm_google_apikey'}.'&amp;sensor=false" type="text/javascript" charset="utf-8"&gt;&lt;/script&gt;';
	}
	return $code;
}

sub map_setup_code($;@) {
	my $map=shift;
	my $name=shift;
	my %options=@_;

	my $mapurl = $config{osm_map_url};

	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");
	}

	if ($mapurl) {
		$options{'mapurl'} = $mapurl;
	}
        $options{'layers'} = $config{osm_layers};

	$name=~s/'//g; # $name comes from user input
	return "mapsetup('mapdiv-$name', " . to_json(\%options) . ");";
}

1;
</pre></body></html>