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