summaryrefslogtreecommitdiff
path: root/IkiWiki/Plugin/osm.pm
blob: c9650d014aa1348f9c83efb77e87c76f20620452 (plain)
  1. #!/usr/bin/perl
  2. # Copyright 2011 Blars Blarson
  3. # Released under GPL version 2
  4. package IkiWiki::Plugin::osm;
  5. use utf8;
  6. use strict;
  7. use warnings;
  8. use IkiWiki 3.0;
  9. sub import {
  10. add_underlay("osm");
  11. hook(type => "getsetup", id => "osm", call => \&getsetup);
  12. hook(type => "format", id => "osm", call => \&format);
  13. hook(type => "preprocess", id => "osm", call => \&preprocess);
  14. hook(type => "preprocess", id => "waypoint", call => \&process_waypoint);
  15. hook(type => "savestate", id => "waypoint", call => \&savestate);
  16. hook(type => "cgi", id => "osm", call => \&cgi);
  17. }
  18. sub getsetup () {
  19. return
  20. plugin => {
  21. safe => 1,
  22. rebuild => 1,
  23. section => "special-purpose",
  24. },
  25. osm_default_zoom => {
  26. type => "integer",
  27. example => "15",
  28. description => "the default zoom when you click on the map link",
  29. safe => 1,
  30. rebuild => 1,
  31. },
  32. osm_default_icon => {
  33. type => "string",
  34. example => "ikiwiki/images/osm.png",
  35. description => "the icon shown on links and on the main map",
  36. safe => 0,
  37. rebuild => 1,
  38. },
  39. osm_alt => {
  40. type => "string",
  41. example => "",
  42. description => "the alt tag of links, defaults to empty",
  43. safe => 0,
  44. rebuild => 1,
  45. },
  46. osm_format => {
  47. type => "string",
  48. example => "KML",
  49. description => "the output format for waypoints, can be KML, GeoJSON or CSV (one or many, comma-separated)",
  50. safe => 1,
  51. rebuild => 1,
  52. },
  53. osm_tag_default_icon => {
  54. type => "string",
  55. example => "icon.png",
  56. description => "the icon attached to a tag, displayed on the map for tagged pages",
  57. safe => 0,
  58. rebuild => 1,
  59. },
  60. osm_openlayers_url => {
  61. type => "string",
  62. example => "http://www.openlayers.org/api/OpenLayers.js",
  63. description => "Url for the OpenLayers.js file",
  64. safe => 0,
  65. rebuild => 1,
  66. },
  67. osm_layers => {
  68. type => "string",
  69. example => { 'OSM', 'GoogleSatellite' },
  70. description => "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.",
  71. safe => 0,
  72. rebuild => 1,
  73. },
  74. osm_google_apikey => {
  75. type => "string",
  76. example => "",
  77. description => "Google maps API key, Google layer not used if missing, see https://code.google.com/apis/console/ to get an API key",
  78. safe => 1,
  79. rebuild => 1,
  80. },
  81. }
  82. sub register_rendered_files {
  83. my $map = shift;
  84. my $page = shift;
  85. my $dest = shift;
  86. if ($page eq $dest) {
  87. my %formats = get_formats();
  88. if ($formats{'GeoJSON'}) {
  89. will_render($page, "$map/pois.json");
  90. }
  91. if ($formats{'CSV'}) {
  92. will_render($page, "$map/pois.txt");
  93. }
  94. if ($formats{'KML'}) {
  95. will_render($page, "$map/pois.kml");
  96. }
  97. }
  98. }
  99. sub preprocess {
  100. my %params=@_;
  101. my $page = $params{page};
  102. my $dest = $params{destpage};
  103. my $loc = $params{loc}; # sanitized below
  104. my $lat = $params{lat}; # sanitized below
  105. my $lon = $params{lon}; # sanitized below
  106. my $href = $params{href};
  107. my ($width, $height, $float);
  108. $height = scrub($params{'height'} || "300px", $page, $dest); # sanitized here
  109. $width = scrub($params{'width'} || "500px", $page, $dest); # sanitized here
  110. $float = (defined($params{'right'}) && 'right') || (defined($params{'left'}) && 'left'); # sanitized here
  111. my $zoom = scrub($params{'zoom'} // $config{'osm_default_zoom'} // 15, $page, $dest); # sanitized below
  112. my $map;
  113. $map = $params{'map'} || 'map';
  114. $map = scrub($map, $page, $dest); # sanitized here
  115. my $name = scrub($params{'name'} || $map, $page, $dest);
  116. if (defined($lon) || defined($lat) || defined($loc)) {
  117. ($lon, $lat) = scrub_lonlat($loc, $lon, $lat);
  118. }
  119. if ($zoom !~ /^\d\d?$/ || $zoom < 2 || $zoom > 18) {
  120. error("Bad zoom");
  121. }
  122. if (! defined $href || ! length $href) {
  123. $href=IkiWiki::cgiurl(
  124. do => "osm",
  125. map => $map,
  126. );
  127. }
  128. register_rendered_files($map, $page, $dest);
  129. $pagestate{$page}{'osm'}{$map}{'displays'}{$name} = {
  130. height => $height,
  131. width => $width,
  132. float => $float,
  133. zoom => $zoom,
  134. fullscreen => 0,
  135. editable => defined($params{'editable'}),
  136. lat => $lat,
  137. lon => $lon,
  138. href => $href,
  139. google_apikey => $config{'osm_google_apikey'},
  140. };
  141. return "<div id=\"mapdiv-$name\"></div>";
  142. }
  143. sub process_waypoint {
  144. my %params=@_;
  145. my $loc = $params{'loc'}; # sanitized below
  146. my $lat = $params{'lat'}; # sanitized below
  147. my $lon = $params{'lon'}; # sanitized below
  148. my $page = $params{'page'}; # not sanitized?
  149. my $dest = $params{'destpage'}; # not sanitized?
  150. my $hidden = defined($params{'hidden'}); # sanitized here
  151. my ($p) = $page =~ /(?:^|\/)([^\/]+)\/?$/; # shorter page name
  152. my $name = scrub($params{'name'} || $p, $page, $dest); # sanitized here
  153. my $desc = scrub($params{'desc'} || '', $page, $dest); # sanitized here
  154. my $zoom = scrub($params{'zoom'} // $config{'osm_default_zoom'} // 15, $page, $dest); # sanitized below
  155. my $icon = $config{'osm_default_icon'} || "ikiwiki/images/osm.png"; # sanitized: we trust $config
  156. my $map = scrub($params{'map'} || 'map', $page, $dest); # sanitized here
  157. my $alt = $config{'osm_alt'} ? "alt=\"$config{'osm_alt'}\"" : ''; # sanitized: we trust $config
  158. if ($zoom !~ /^\d\d?$/ || $zoom < 2 || $zoom > 18) {
  159. error("Bad zoom");
  160. }
  161. ($lon, $lat) = scrub_lonlat($loc, $lon, $lat);
  162. if (!defined($lat) || !defined($lon)) {
  163. error("Must specify lat and lon");
  164. }
  165. my $tag = $params{'tag'};
  166. foreach my $t (keys %{$typedlinks{$page}{'tag'}}) {
  167. if ($icon = get_tag_icon($t)) {
  168. $tag = $t;
  169. last;
  170. }
  171. $t =~ s!/$config{'tagbase'}/!!;
  172. if ($icon = get_tag_icon($t)) {
  173. $tag = $t;
  174. last;
  175. }
  176. }
  177. $icon = urlto($icon, $dest, 1);
  178. $icon =~ s!/*$!!; # hack - urlto shouldn't be appending a slash in the first place
  179. $tag = '' unless $tag;
  180. register_rendered_files($map, $page, $dest);
  181. $pagestate{$page}{'osm'}{$map}{'waypoints'}{$name} = {
  182. page => $page,
  183. desc => $desc,
  184. icon => $icon,
  185. tag => $tag,
  186. lat => $lat,
  187. lon => $lon,
  188. # How to link back to the page from the map, not to be
  189. # confused with the URL of the map itself sent to the
  190. # embeded map below. Note: used in generated KML etc file,
  191. # so must be absolute.
  192. href => urlto($page),
  193. };
  194. my $mapurl = IkiWiki::cgiurl(
  195. do => "osm",
  196. map => $map,
  197. lat => $lat,
  198. lon => $lon,
  199. zoom => $zoom,
  200. );
  201. my $output = '';
  202. if (defined($params{'embed'})) {
  203. $output .= preprocess(%params,
  204. href => $mapurl,
  205. );
  206. }
  207. if (!$hidden) {
  208. $output .= "<a href=\"$mapurl\"><img class=\"img\" src=\"$icon\" $alt /></a>";
  209. }
  210. return $output;
  211. }
  212. # get the icon from the given tag
  213. sub get_tag_icon($) {
  214. my $tag = shift;
  215. # look for an icon attached to the tag
  216. my $attached = $tag . '/' . $config{'osm_tag_default_icon'};
  217. if (srcfile($attached)) {
  218. return $attached;
  219. }
  220. else {
  221. return undef;
  222. }
  223. }
  224. sub scrub_lonlat($$$) {
  225. my ($loc, $lon, $lat) = @_;
  226. if ($loc) {
  227. 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*$/) {
  228. $lat = $1;
  229. $lon = $2;
  230. }
  231. else {
  232. error("Bad loc");
  233. }
  234. }
  235. if (defined($lat)) {
  236. if ($lat =~ /^(\-?)(\d+)(?:(\.\d*)°?|(?:°|\s)\s*(\d+)(?:(\.\d*)\'?|(?:\'|\s)\s*(\d+(?:\.\d*)?\"?)|\'?)|°?)\s*([NS])?\s*$/) {
  237. $lat = $2 + ($3//0) + ((($4//0) + (($5//0) + (($6//0)/60.)))/60.);
  238. if (($1 eq '-') || (($7//'') eq 'S')) {
  239. $lat = - $lat;
  240. }
  241. }
  242. else {
  243. error("Bad lat");
  244. }
  245. }
  246. if (defined($lon)) {
  247. if ($lon =~ /^(\-?)(\d+)(?:(\.\d*)°?|(?:°|\s)\s*(\d+)(?:(\.\d*)\'?|(?:\'|\s)\s*(\d+(?:\.\d*)?\"?)|\'?)|°?)\s*([EW])?$/) {
  248. $lon = $2 + ($3//0) + ((($4//0) + (($5//0) + (($6//0)/60.)))/60.);
  249. if (($1 eq '-') || (($7//'') eq 'W')) {
  250. $lon = - $lon;
  251. }
  252. }
  253. else {
  254. error("Bad lon");
  255. }
  256. }
  257. if ($lat < -90 || $lat > 90 || $lon < -180 || $lon > 180) {
  258. error("Location out of range");
  259. }
  260. return ($lon, $lat);
  261. }
  262. sub savestate {
  263. my %waypoints = ();
  264. my %linestrings = ();
  265. foreach my $page (keys %pagestate) {
  266. if (exists $pagestate{$page}{'osm'}) {
  267. foreach my $map (keys %{$pagestate{$page}{'osm'}}) {
  268. foreach my $name (keys %{$pagestate{$page}{'osm'}{$map}{'waypoints'}}) {
  269. debug("found waypoint $name");
  270. $waypoints{$map}{$name} = $pagestate{$page}{'osm'}{$map}{'waypoints'}{$name};
  271. }
  272. }
  273. }
  274. }
  275. foreach my $page (keys %pagestate) {
  276. if (exists $pagestate{$page}{'osm'}) {
  277. foreach my $map (keys %{$pagestate{$page}{'osm'}}) {
  278. # examine the links on this page
  279. foreach my $name (keys %{$pagestate{$page}{'osm'}{$map}{'waypoints'}}) {
  280. if (exists $links{$page}) {
  281. foreach my $otherpage (@{$links{$page}}) {
  282. if (exists $waypoints{$map}{$otherpage}) {
  283. push(@{$linestrings{$map}}, [
  284. [ $waypoints{$map}{$name}{'lon'}, $waypoints{$map}{$name}{'lat'} ],
  285. [ $waypoints{$map}{$otherpage}{'lon'}, $waypoints{$map}{$otherpage}{'lat'} ]
  286. ]);
  287. }
  288. }
  289. }
  290. }
  291. }
  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'} = ();
  295. }
  296. }
  297. my %formats = get_formats();
  298. if ($formats{'GeoJSON'}) {
  299. writejson(\%waypoints, \%linestrings);
  300. }
  301. if ($formats{'CSV'}) {
  302. writecsvs(\%waypoints, \%linestrings);
  303. }
  304. if ($formats{'KML'}) {
  305. writekml(\%waypoints, \%linestrings);
  306. }
  307. }
  308. sub writejson($;$) {
  309. my %waypoints = %{$_[0]};
  310. my %linestrings = %{$_[1]};
  311. eval q{use JSON};
  312. error $@ if $@;
  313. foreach my $map (keys %waypoints) {
  314. my %geojson = ( "type" => "FeatureCollection", "features" => []);
  315. foreach my $name (keys %{$waypoints{$map}}) {
  316. my %marker = ( "type" => "Feature",
  317. "geometry" => { "type" => "Point", "coordinates" => [ $waypoints{$map}{$name}{'lon'}, $waypoints{$map}{$name}{'lat'} ] },
  318. "properties" => $waypoints{$map}{$name} );
  319. push @{$geojson{'features'}}, \%marker;
  320. }
  321. foreach my $linestring (@{$linestrings{$map}}) {
  322. my %json = ( "type" => "Feature",
  323. "geometry" => { "type" => "LineString", "coordinates" => $linestring });
  324. push @{$geojson{'features'}}, \%json;
  325. }
  326. writefile("pois.json", $config{destdir} . "/$map", to_json(\%geojson));
  327. }
  328. }
  329. sub writekml($;$) {
  330. my %waypoints = %{$_[0]};
  331. my %linestrings = %{$_[1]};
  332. eval q{use XML::Writer};
  333. error $@ if $@;
  334. foreach my $map (keys %waypoints) {
  335. my $output;
  336. my $writer = XML::Writer->new( OUTPUT => \$output,
  337. DATA_MODE => 1, DATA_INDENT => ' ', ENCODING => 'UTF-8');
  338. $writer->xmlDecl();
  339. $writer->startTag("kml", "xmlns" => "http://www.opengis.net/kml/2.2");
  340. $writer->startTag("Document");
  341. # first pass: get the icons
  342. my %tags_map = (); # keep track of tags seen
  343. foreach my $name (keys %{$waypoints{$map}}) {
  344. my %options = %{$waypoints{$map}{$name}};
  345. if (!$tags_map{$options{tag}}) {
  346. debug("found new style " . $options{tag});
  347. $tags_map{$options{tag}} = ();
  348. $writer->startTag("Style", id => $options{tag});
  349. $writer->startTag("IconStyle");
  350. $writer->startTag("Icon");
  351. $writer->startTag("href");
  352. $writer->characters($options{icon});
  353. $writer->endTag();
  354. $writer->endTag();
  355. $writer->endTag();
  356. $writer->endTag();
  357. }
  358. $tags_map{$options{tag}}{$name} = \%options;
  359. }
  360. foreach my $name (keys %{$waypoints{$map}}) {
  361. my %options = %{$waypoints{$map}{$name}};
  362. $writer->startTag("Placemark");
  363. $writer->startTag("name");
  364. $writer->characters($name);
  365. $writer->endTag();
  366. $writer->startTag("styleUrl");
  367. $writer->characters('#' . $options{tag});
  368. $writer->endTag();
  369. #$writer->emptyTag('atom:link', href => $options{href});
  370. # to make it easier for us as the atom:link parameter is
  371. # hard to access from javascript
  372. $writer->startTag('href');
  373. $writer->characters($options{href});
  374. $writer->endTag();
  375. $writer->startTag("description");
  376. $writer->characters($options{desc});
  377. $writer->endTag();
  378. $writer->startTag("Point");
  379. $writer->startTag("coordinates");
  380. $writer->characters($options{lon} . "," . $options{lat});
  381. $writer->endTag();
  382. $writer->endTag();
  383. $writer->endTag();
  384. }
  385. my $i = 0;
  386. foreach my $linestring (@{$linestrings{$map}}) {
  387. $writer->startTag("Placemark");
  388. $writer->startTag("name");
  389. $writer->characters("linestring " . $i++);
  390. $writer->endTag();
  391. $writer->startTag("LineString");
  392. $writer->startTag("coordinates");
  393. my $str = '';
  394. foreach my $coord (@{$linestring}) {
  395. $str .= join(',', @{$coord}) . " \n";
  396. }
  397. $writer->characters($str);
  398. $writer->endTag();
  399. $writer->endTag();
  400. $writer->endTag();
  401. }
  402. $writer->endTag();
  403. $writer->endTag();
  404. $writer->end();
  405. writefile("pois.kml", $config{destdir} . "/$map", $output);
  406. }
  407. }
  408. sub writecsvs($;$) {
  409. my %waypoints = %{$_[0]};
  410. foreach my $map (keys %waypoints) {
  411. my $poisf = "lat\tlon\ttitle\tdescription\ticon\ticonSize\ticonOffset\n";
  412. foreach my $name (keys %{$waypoints{$map}}) {
  413. my %options = %{$waypoints{$map}{$name}};
  414. my $line =
  415. $options{'lat'} . "\t" .
  416. $options{'lon'} . "\t" .
  417. $name . "\t" .
  418. $options{'desc'} . '<br /><a href="' . $options{'page'} . '">' . $name . "</a>\t" .
  419. $options{'icon'} . "\n";
  420. $poisf .= $line;
  421. }
  422. writefile("pois.txt", $config{destdir} . "/$map", $poisf);
  423. }
  424. }
  425. # pipe some data through the HTML scrubber
  426. #
  427. # code taken from the meta.pm plugin
  428. sub scrub($$$) {
  429. if (IkiWiki::Plugin::htmlscrubber->can("sanitize")) {
  430. return IkiWiki::Plugin::htmlscrubber::sanitize(
  431. content => shift, page => shift, destpage => shift);
  432. }
  433. else {
  434. return shift;
  435. }
  436. }
  437. # taken from toggle.pm
  438. sub format (@) {
  439. my %params=@_;
  440. if ($params{content}=~m!<div[^>]*id="mapdiv-[^"]*"[^>]*>!g) {
  441. if (! ($params{content}=~s!</body>!include_javascript($params{page})."</body>"!em)) {
  442. # no <body> tag, probably in preview mode
  443. $params{content}=$params{content} . include_javascript($params{page});
  444. }
  445. }
  446. return $params{content};
  447. }
  448. sub preferred_format() {
  449. if (!defined($config{'osm_format'}) || !$config{'osm_format'}) {
  450. $config{'osm_format'} = 'KML';
  451. }
  452. my @spl = split(/, */, $config{'osm_format'});
  453. return shift @spl;
  454. }
  455. sub get_formats() {
  456. if (!defined($config{'osm_format'}) || !$config{'osm_format'}) {
  457. $config{'osm_format'} = 'KML';
  458. }
  459. map { $_ => 1 } split(/, */, $config{'osm_format'});
  460. }
  461. sub include_javascript ($) {
  462. my $page=shift;
  463. my $loader;
  464. if (exists $pagestate{$page}{'osm'}) {
  465. foreach my $map (keys %{$pagestate{$page}{'osm'}}) {
  466. foreach my $name (keys %{$pagestate{$page}{'osm'}{$map}{'displays'}}) {
  467. $loader .= map_setup_code($map, $name, %{$pagestate{$page}{'osm'}{$map}{'displays'}{$name}});
  468. }
  469. }
  470. }
  471. if ($loader) {
  472. return embed_map_code($page) . "<script type=\"text/javascript\" charset=\"utf-8\">$loader</script>";
  473. }
  474. else {
  475. return '';
  476. }
  477. }
  478. sub cgi($) {
  479. my $cgi=shift;
  480. return unless defined $cgi->param('do') &&
  481. $cgi->param("do") eq "osm";
  482. IkiWiki::loadindex();
  483. IkiWiki::decode_cgi_utf8($cgi);
  484. my $map = $cgi->param('map');
  485. if (!defined $map || $map !~ /^[a-z]*$/) {
  486. error("invalid map parameter");
  487. }
  488. print "Content-Type: text/html\r\n";
  489. print ("\r\n");
  490. print "<html><body>";
  491. print "<div id=\"mapdiv-$map\"></div>";
  492. print embed_map_code();
  493. print "<script type=\"text/javascript\" charset=\"utf-8\">";
  494. print map_setup_code($map, $map,
  495. lat => "urlParams['lat']",
  496. lon => "urlParams['lon']",
  497. zoom => "urlParams['zoom']",
  498. fullscreen => 1,
  499. editable => 1,
  500. google_apikey => $config{'osm_google_apikey'},
  501. );
  502. print "</script>";
  503. print "</body></html>";
  504. exit 0;
  505. }
  506. sub embed_map_code(;$) {
  507. my $page=shift;
  508. my $olurl = $config{osm_openlayers_url} || "http://www.openlayers.org/api/OpenLayers.js";
  509. my $code = '<script src="'.$olurl.'" type="text/javascript" charset="utf-8"></script>'."\n".
  510. '<script src="'.urlto("ikiwiki/osm.js", $page).
  511. '" type="text/javascript" charset="utf-8"></script>'."\n";
  512. if ($config{'osm_google_apikey'}) {
  513. $code .= '<script src="http://maps.google.com/maps?file=api&amp;v=2&amp;key='.$config{'osm_google_apikey'}.'&sensor=false" type="text/javascript" charset="utf-8"></script>';
  514. }
  515. return $code;
  516. }
  517. sub map_setup_code($;@) {
  518. my $map=shift;
  519. my $name=shift;
  520. my %options=@_;
  521. my $mapurl = $config{osm_map_url};
  522. eval q{use JSON};
  523. error $@ if $@;
  524. $options{'format'} = preferred_format();
  525. my %formats = get_formats();
  526. if ($formats{'GeoJSON'}) {
  527. $options{'jsonurl'} = urlto($map."/pois.json");
  528. }
  529. if ($formats{'CSV'}) {
  530. $options{'csvurl'} = urlto($map."/pois.txt");
  531. }
  532. if ($formats{'KML'}) {
  533. $options{'kmlurl'} = urlto($map."/pois.kml");
  534. }
  535. if ($mapurl) {
  536. $options{'mapurl'} = $mapurl;
  537. }
  538. $options{'layers'} = $config{osm_layers};
  539. return "mapsetup('mapdiv-$name', " . to_json(\%options) . ");";
  540. }
  541. 1;