1<?php 2 3/* 4 * Copyright (c) 2008-2023 Mark C. Prins <mprins@users.sf.net> 5 * 6 * Permission to use, copy, modify, and distribute this software for any 7 * purpose with or without fee is hereby granted, provided that the above 8 * copyright notice and this permission notice appear in all copies. 9 * 10 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 * 18 * @phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps 19 */ 20use dokuwiki\Extension\SyntaxPlugin; 21use geoPHP\Geometry\Point; 22use dokuwiki\Logger; 23 24/** 25 * DokuWiki Plugin openlayersmap (Syntax Component). 26 * Provides for display of an OpenLayers based map in a wiki page. 27 * 28 * @author Mark Prins 29 */ 30class syntax_plugin_openlayersmap_olmap extends SyntaxPlugin 31{ 32 /** 33 * defaults of the known attributes of the olmap tag. 34 */ 35 private $dflt = ['id' => 'olmap', 'width' => '550px', 'height' => '450px', 'lat' => 50.0, 'lon' => 5.1, 'zoom' => 12, 'autozoom' => 1, 'controls' => true, 'baselyr' => 'OpenStreetMap', 'gpxfile' => '', 'kmlfile' => '', 'geojsonfile' => '', 'summary' => '']; 36 37 /** 38 * 39 * @see DokuWiki_Syntax_Plugin::getType() 40 */ 41 public function getType(): string 42 { 43 return 'substition'; 44 } 45 46 /** 47 * 48 * @see DokuWiki_Syntax_Plugin::getPType() 49 */ 50 public function getPType(): string 51 { 52 return 'block'; 53 } 54 55 /** 56 * 57 * @see Doku_Parser_Mode::getSort() 58 */ 59 public function getSort(): int 60 { 61 return 901; 62 } 63 64 /** 65 * 66 * @see Doku_Parser_Mode::connectTo() 67 */ 68 public function connectTo($mode) 69 { 70 $this->Lexer->addSpecialPattern( 71 '<olmap ?[^>\n]*>.*?</olmap>', 72 $mode, 73 'plugin_openlayersmap_olmap' 74 ); 75 } 76 77 /** 78 * 79 * @see DokuWiki_Syntax_Plugin::handle() 80 */ 81 public function handle($match, $state, $pos, Doku_Handler $handler): array 82 { 83 // break matched data into its components 84 $_tag = explode('>', substr($match, 7, -9), 2); 85 $str_params = $_tag[0]; 86 if (array_key_exists(1, $_tag)) { 87 $str_points = $_tag[1]; 88 } else { 89 $str_points = ''; 90 } 91 // get the lat/lon for adding them to the metadata (used by geotag) 92 preg_match('(lat[:|=]\"-?\d*\.?\d*\")', $match, $mainLat); 93 preg_match('(lon[:|=]\"-?\d*\.?\d*\")', $match, $mainLon); 94 $mainLat = substr($mainLat [0], 5, -1); 95 $mainLon = substr($mainLon [0], 5, -1); 96 if (!is_numeric($mainLat)) { 97 $mainLat = $this->dflt ['lat']; 98 } 99 if (!is_numeric($mainLon)) { 100 $mainLon = $this->dflt ['lon']; 101 } 102 103 $gmap = $this->extractParams($str_params); 104 $overlay = $this->extractPoints($str_points); 105 $_firstimageID = ''; 106 107 $_nocache = false; 108 // choose maptype based on the specified tag 109 $imgUrl = "{{"; 110 if (stripos($gmap ['baselyr'], 'google') !== false) { 111 // Google 112 $imgUrl .= $this->getGoogle($gmap, $overlay); 113 $imgUrl .= "&.png"; 114 } elseif (stripos($gmap ['baselyr'], 'bing') !== false) { 115 // Bing 116 if (!$this->getConf('bingAPIKey')) { 117 // in case there is no Bing api key we'll use OSM 118 $_firstimageID = $this->getStaticOSM($gmap, $overlay); 119 $imgUrl .= $_firstimageID; 120 if ($this->getConf('optionStaticMapGenerator') == 'remote') { 121 $imgUrl .= "&.png"; 122 } 123 } else { 124 // seems that Bing doesn't like the DW client, turn off caching 125 $_nocache = true; 126 $imgUrl .= $this->getBing($gmap, $overlay) . "&.png"; 127 } 128 /* elseif (stripos ( $gmap ['baselyr'], 'mapquest' ) !== false) { 129 // MapQuest 130 if (! $this->getConf ( 'mapquestAPIKey' )) { 131 // no API key for MapQuest, use OSM 132 $_firstimageID = $this->getStaticOSM ( $gmap, $overlay ); 133 $imgUrl .= $_firstimageID; 134 if ($this->getConf ( 'optionStaticMapGenerator' ) == 'remote') { 135 $imgUrl .= "&.png"; 136 } 137 } else { 138 $imgUrl .= $this->_getMapQuest ( $gmap, $overlay ); 139 $imgUrl .= "&.png"; 140 } 141 } */ 142 } else { 143 // default OSM 144 $_firstimageID = $this->getStaticOSM($gmap, $overlay); 145 $imgUrl .= $_firstimageID; 146 if ($this->getConf('optionStaticMapGenerator') == 'remote') { 147 $imgUrl .= "&.png"; 148 } 149 } 150 151 // append dw p_render specific params and render 152 $imgUrl .= "?" . str_replace("px", "", $gmap ['width']) . "x" 153 . str_replace("px", "", $gmap ['height']); 154 $imgUrl .= "&nolink"; 155 156 // add nocache option for selected services 157 if ($_nocache) { 158 $imgUrl .= "&nocache"; 159 } 160 161 $imgUrl .= " |" . $gmap ['summary'] . " }}"; 162 163 $mapid = $gmap ['id']; 164 // create a javascript parameter string for the map 165 $param = ''; 166 foreach ($gmap as $key => $val) { 167 $param .= is_numeric($val) ? "$key: $val, " : "$key: '" . hsc($val) . "', "; 168 } 169 if (!empty($param)) { 170 $param = substr($param, 0, -2); 171 } 172 unset($gmap ['id']); 173 174 // create a javascript serialisation of the point data 175 $poi = ''; 176 $poitable = ''; 177 $rowId = 0; 178 if ($overlay !== []) { 179 foreach ($overlay as $data) { 180 [$lat, $lon, $text, $angle, $opacity, $img] = $data; 181 $rowId++; 182 $poi .= ", {lat:$lat,lon:$lon,txt:'$text',angle:$angle,opacity:$opacity,img:'$img',rowId: $rowId}"; 183 184 if ($this->getConf('displayformat') === 'DMS') { 185 $lat = $this->convertLat($lat); 186 $lon = $this->convertLon($lon); 187 } else { 188 $lat .= 'º'; 189 $lon .= 'º'; 190 } 191 192 $poitable .= ' 193 <tr> 194 <td class="rowId">' . $rowId . '</td> 195 <td class="icon"><img src="' . DOKU_BASE . 'lib/plugins/openlayersmap/icons/' . $img . '" alt="' 196 . substr($img, 0, -4) . $this->getlang('alt_legend_poi') . '" /></td> 197 <td class="lat" title="' . $this->getLang('olmapPOIlatTitle') . '">' . $lat . '</td> 198 <td class="lon" title="' . $this->getLang('olmapPOIlonTitle') . '">' . $lon . '</td> 199 <td class="txt">' . $text . '</td> 200 </tr>'; 201 } 202 $poi = substr($poi, 2); 203 } 204 if (!empty($gmap ['kmlfile'])) { 205 $poitable .= ' 206 <tr> 207 <td class="rowId"><img src="' . DOKU_BASE 208 . 'lib/plugins/openlayersmap/toolbar/kml_file.png" alt="KML file" /></td> 209 <td class="icon"><img src="' . DOKU_BASE . 'lib/plugins/openlayersmap/toolbar/kml_line.png" alt="' 210 . $this->getlang('alt_legend_kml') . '" /></td> 211 <td class="txt" colspan="3">KML track: ' . $this->getFileName($gmap ['kmlfile']) . '</td> 212 </tr>'; 213 } 214 if (!empty($gmap ['gpxfile'])) { 215 $poitable .= ' 216 <tr> 217 <td class="rowId"><img src="' . DOKU_BASE 218 . 'lib/plugins/openlayersmap/toolbar/gpx_file.png" alt="GPX file" /></td> 219 <td class="icon"><img src="' . DOKU_BASE 220 . 'lib/plugins/openlayersmap/toolbar/gpx_line.png" alt="' 221 . $this->getlang('alt_legend_gpx') . '" /></td> 222 <td class="txt" colspan="3">GPX track: ' . $this->getFileName($gmap ['gpxfile']) . '</td> 223 </tr>'; 224 } 225 if (!empty($gmap ['geojsonfile'])) { 226 $poitable .= ' 227 <tr> 228 <td class="rowId"><img src="' . DOKU_BASE 229 . 'lib/plugins/openlayersmap/toolbar/geojson_file.png" alt="GeoJSON file" /></td> 230 <td class="icon"><img src="' . DOKU_BASE 231 . 'lib/plugins/openlayersmap/toolbar/geojson_line.png" alt="' 232 . $this->getlang('alt_legend_geojson') . '" /></td> 233 <td class="txt" colspan="3">GeoJSON track: ' . $this->getFileName($gmap ['geojsonfile']) . '</td> 234 </tr>'; 235 } 236 237 $autozoom = empty($gmap ['autozoom']) ? $this->getConf('autoZoomMap') : $gmap ['autozoom']; 238 $js = "{mapOpts: {" . $param . ", displayformat: '" . $this->getConf('displayformat') 239 . "', autozoom: " . $autozoom . "}, poi: [$poi]};"; 240 // unescape the json 241 $poitable = stripslashes($poitable); 242 243 return [$mapid, $js, $mainLat, $mainLon, $poitable, $gmap ['summary'], $imgUrl, $_firstimageID]; 244 } 245 246 /** 247 * extract parameters for the map from the parameter string 248 * 249 * @param string $str_params 250 * string of key="value" pairs 251 * @return array associative array of parameters key=>value 252 */ 253 private function extractParams(string $str_params): array 254 { 255 $param = []; 256 preg_match_all('/(\w*)="(.*?)"/us', $str_params, $param, PREG_SET_ORDER); 257 // parse match for instructions, break into key value pairs 258 $gmap = $this->dflt; 259 foreach ($gmap as $key => &$value) { 260 $defval = $this->getConf('default_' . $key); 261 if ($defval !== '') { 262 $value = $defval; 263 } 264 } 265 unset($value); 266 foreach ($param as $kvpair) { 267 [$match, $key, $val] = $kvpair; 268 $key = strtolower($key); 269 if (isset($gmap [$key])) { 270 if ($key == 'summary') { 271 // preserve case for summary field 272 $gmap [$key] = $val; 273 } elseif ($key == 'id') { 274 // preserve case for id field 275 $gmap [$key] = $val; 276 } else { 277 $gmap [$key] = strtolower($val); 278 } 279 } 280 } 281 return $gmap; 282 } 283 284 /** 285 * extract overlay points for the map from the wiki syntax data 286 * 287 * @param string $str_points 288 * multi-line string of lat,lon,text triplets 289 * @return array multi-dimensional array of lat,lon,text triplets 290 */ 291 private function extractPoints(string $str_points): array 292 { 293 $point = []; 294 // preg_match_all('/^([+-]?[0-9].*?),\s*([+-]?[0-9].*?),(.*?),(.*?),(.*?),(.*)$/um', 295 // $str_points, $point, PREG_SET_ORDER); 296 /* 297 * group 1: ([+-]?[0-9]+(?:\.[0-9]*)?) 298 * group 2: ([+-]?[0-9]+(?:\.[0-9]*)?) 299 * group 3: (.*?) 300 * group 4: (.*?) 301 * group 5: (.*?) 302 * group 6: (.*) 303 */ 304 preg_match_all( 305 '/^([+-]?[0-9]+(?:\.[0-9]*)?),\s*([+-]?[0-9]+(?:\.[0-9]*)?),(.*?),(.*?),(.*?),(.*)$/um', 306 $str_points, 307 $point, 308 PREG_SET_ORDER 309 ); 310 // create poi array 311 $overlay = []; 312 foreach ($point as $pt) { 313 [$match, $lat, $lon, $angle, $opacity, $img, $text] = $pt; 314 $lat = is_numeric($lat) ? $lat : 0; 315 $lon = is_numeric($lon) ? $lon : 0; 316 $angle = is_numeric($angle) ? $angle : 0; 317 $opacity = is_numeric($opacity) ? $opacity : 0.8; 318 // TODO validate using exist & set up default img? 319 $img = trim($img); 320 $text = p_get_instructions($text); 321 // dbg ( $text ); 322 $text = p_render("xhtml", $text, $info); 323 // dbg ( $text ); 324 $text = addslashes(str_replace("\n", "", $text)); 325 $overlay [] = [$lat, $lon, $text, $angle, $opacity, $img]; 326 } 327 return $overlay; 328 } 329 330 /** 331 * Create a Google maps static image url w/ the poi. 332 * 333 * @param array $gmap 334 * @param array $overlay 335 */ 336 private function getGoogle(array $gmap, array $overlay): string 337 { 338 $sUrl = $this->getConf('iconUrlOverload'); 339 if (!$sUrl) { 340 $sUrl = DOKU_URL; 341 } 342 $maptype = match ($gmap ['baselyr']) { 343 'google hybrid' => 'hybrid', 344 'google sat' => 'satellite', 345 'terrain', 'google relief' => 'terrain', 346 default => 'roadmap', 347 }; 348 // TODO maybe use viewport / visible instead of center/zoom, 349 // see: https://developers.google.com/maps/documentation/staticmaps/index#Viewports 350 // http://maps.google.com/maps/api/staticmap?center=51.565690,5.456756&zoom=16&size=600x400&markers=icon:http://wild-water.nl/dokuwiki/lib/plugins/openlayersmap/icons/marker.png|label:1|51.565690,5.456756&markers=icon:http://wild-water.nl/dokuwiki/lib/plugins/openlayersmap/icons/marker-blue.png|51.566197,5.458966|label:2&markers=icon:http://wild-water.nl/dokuwiki/lib/plugins/openlayersmap/icons/parking.png|51.567177,5.457909|label:3&markers=icon:http://wild-water.nl/dokuwiki/lib/plugins/openlayersmap/icons/parking.png|51.566283,5.457330|label:4&markers=icon:http://wild-water.nl/dokuwiki/lib/plugins/openlayersmap/icons/parking.png|51.565630,5.457695|label:5&sensor=false&format=png&maptype=roadmap 351 $imgUrl = "https://maps.googleapis.com/maps/api/staticmap?"; 352 $imgUrl .= "&size=" . str_replace("px", "", $gmap ['width']) . "x" 353 . str_replace("px", "", $gmap ['height']); 354 //if (!$this->getConf( 'autoZoomMap')) { // no need for center & zoom params } 355 $imgUrl .= "¢er=" . $gmap ['lat'] . "," . $gmap ['lon']; 356 // max is 21 (== building scale), but that's overkill.. 357 if ($gmap ['zoom'] > 17) { 358 $imgUrl .= "&zoom=17"; 359 } else { 360 $imgUrl .= "&zoom=" . $gmap ['zoom']; 361 } 362 if ($overlay !== []) { 363 $rowId = 0; 364 foreach ($overlay as $data) { 365 [$lat, $lon, $text, $angle, $opacity, $img] = $data; 366 $imgUrl .= "&markers=icon%3a" . $sUrl . "lib/plugins/openlayersmap/icons/" . $img . "%7c" 367 . $lat . "," . $lon . "%7clabel%3a" . ++$rowId; 368 } 369 } 370 $imgUrl .= "&format=png&maptype=" . $maptype; 371 global $conf; 372 $imgUrl .= "&language=" . $conf ['lang']; 373 if ($this->getConf('googleAPIkey')) { 374 $imgUrl .= "&key=" . $this->getConf('googleAPIkey'); 375 } 376 return $imgUrl; 377 } 378 379 /** 380 * Create a MapQuest static map API image url. 381 * 382 * @param array $gmap 383 * @param array $overlay 384 */ 385 /* 386 private function _getMapQuest($gmap, $overlay) { 387 $sUrl = $this->getConf ( 'iconUrlOverload' ); 388 if (! $sUrl) { 389 $sUrl = DOKU_URL; 390 } 391 switch ($gmap ['baselyr']) { 392 case 'mapquest hybrid' : 393 $maptype = 'hyb'; 394 break; 395 case 'mapquest sat' : 396 // because sat coverage is very limited use 'hyb' instead of 'sat' so we don't get a blank map 397 $maptype = 'hyb'; 398 break; 399 case 'mapquest road' : 400 default : 401 $maptype = 'map'; 402 break; 403 } 404 $imgUrl = "http://open.mapquestapi.com/staticmap/v4/getmap?declutter=true&"; 405 if (count ( $overlay ) < 1) { 406 $imgUrl .= "?center=" . $gmap ['lat'] . "," . $gmap ['lon']; 407 // max level for mapquest is 16 408 if ($gmap ['zoom'] > 16) { 409 $imgUrl .= "&zoom=16"; 410 } else { 411 $imgUrl .= "&zoom=" . $gmap ['zoom']; 412 } 413 } 414 // use bestfit instead of center/zoom, needs upperleft/lowerright corners 415 // $bbox=$this->calcBBOX($overlay, $gmap['lat'], $gmap['lon']); 416 // $imgUrl .= "bestfit=".$bbox['minlat'].",".$bbox['maxlon'].",".$bbox['maxlat'].",".$bbox['minlon']; 417 418 // TODO declutter option works well for square maps but not for rectangular, maybe compensate for that 419 // or compensate the mbr.. 420 421 $imgUrl .= "&size=" . str_replace ( "px", "", $gmap ['width'] ) . "," . str_replace ("px","",$gmap['height']); 422 423 // TODO mapquest allows using one image url with a multiplier $NUMBER eg: 424 // $NUMBER = 2 425 // $imgUrl .= DOKU_URL."/".DOKU_PLUGIN."/".getPluginName()."/icons/".$img.",$NUMBER,C," 426 // .$lat1.",".$lon1.",0,0,0,0,C,".$lat2.",".$lon2.",0,0,0,0"; 427 if (! empty ( $overlay )) { 428 $imgUrl .= "&xis="; 429 foreach ( $overlay as $data ) { 430 list ( $lat, $lon, $text, $angle, $opacity, $img ) = $data; 431 // $imgUrl .= $sUrl."lib/plugins/openlayersmap/icons/".$img.",1,C,".$lat.",".$lon.",0,0,0,0,"; 432 $imgUrl .= $sUrl . "lib/plugins/openlayersmap/icons/" . $img . ",1,C," . $lat . "," . $lon . ","; 433 } 434 $imgUrl = substr ( $imgUrl, 0, - 1 ); 435 } 436 $imgUrl .= "&imageType=png&type=" . $maptype; 437 $imgUrl .= "&key=".$this->getConf ( 'mapquestAPIKey' ); 438 return $imgUrl; 439 } 440 */ 441 442 /** 443 * Create a static OSM map image url w/ the poi from http://staticmap.openstreetmap.de (staticMapLite) 444 * use http://staticmap.openstreetmap.de "staticMapLite" or a local version 445 * 446 * @param array $gmap 447 * @param array $overlay 448 * 449 * @return false|string 450 * @todo implementation for http://ojw.dev.openstreetmap.org/StaticMapDev/ 451 */ 452 private function getStaticOSM(array $gmap, array $overlay) 453 { 454 global $conf; 455 456 if ($this->getConf('optionStaticMapGenerator') === 'local') { 457 // using local basemap composer 458 if (($myMap = plugin_load('helper', 'openlayersmap_staticmap')) === null) { 459 Logger::error( 460 'openlayersmap_staticmap plugin is not available for use.', 461 $myMap 462 ); 463 } 464 if (($geophp = plugin_load('helper', 'geophp')) === null) { 465 Logger::debug('geophp plugin is not available for use.', $geophp); 466 } 467 $size = str_replace("px", "", $gmap ['width']) . "x" 468 . str_replace("px", "", $gmap ['height']); 469 470 $markers = []; 471 if ($overlay !== []) { 472 foreach ($overlay as $data) { 473 [$lat, $lon, $text, $angle, $opacity, $img] = $data; 474 $iconStyle = substr($img, 0, -4); 475 $markers [] = ['lat' => $lat, 'lon' => $lon, 'type' => $iconStyle]; 476 } 477 } 478 479 $apikey = ''; 480 switch ($gmap ['baselyr']) { 481 case 'mapnik': 482 case 'openstreetmap': 483 $maptype = 'openstreetmap'; 484 break; 485 case 'transport': 486 $maptype = 'transport'; 487 $apikey = '?apikey=' . $this->getConf('tfApiKey'); 488 break; 489 case 'landscape': 490 $maptype = 'landscape'; 491 $apikey = '?apikey=' . $this->getConf('tfApiKey'); 492 break; 493 case 'outdoors': 494 $maptype = 'outdoors'; 495 $apikey = '?apikey=' . $this->getConf('tfApiKey'); 496 break; 497 case 'cycle map': 498 $maptype = 'cycle'; 499 $apikey = '?apikey=' . $this->getConf('tfApiKey'); 500 break; 501 case 'hike and bike map': 502 $maptype = 'hikeandbike'; 503 break; 504 case 'mapquest hybrid': 505 case 'mapquest road': 506 case 'mapquest sat': 507 $maptype = 'mapquest'; 508 break; 509 default: 510 $maptype = ''; 511 break; 512 } 513 514 $result = $myMap->getMap( 515 $gmap ['lat'], 516 $gmap ['lon'], 517 $gmap ['zoom'], 518 $size, 519 $maptype, 520 $markers, 521 $gmap ['gpxfile'], 522 $gmap ['kmlfile'], 523 $gmap ['geojsonfile'], 524 $apikey 525 ); 526 } else { 527 // using external basemap composer 528 529 // https://staticmap.openstreetmap.de/staticmap.php?center=47.000622235634,10 530 //.117187497601&zoom=5&size=500x350 531 // &markers=48.999812532766,8.3593749976708,lightblue1|43.154850037315,17.499999997306, 532 // lightblue1|49.487527053077,10.820312497573,ltblu-pushpin|47.951071133739,15.917968747369, 533 // ol-marker|47.921629720114,18.027343747285,ol-marker-gold|47.951071133739,19.257812497236, 534 // ol-marker-blue|47.180141361692,19.257812497236,ol-marker-green 535 $imgUrl = "https://staticmap.openstreetmap.de/staticmap.php"; 536 $imgUrl .= "?center=" . $gmap ['lat'] . "," . $gmap ['lon']; 537 $imgUrl .= "&size=" . str_replace("px", "", $gmap ['width']) . "x" 538 . str_replace("px", "", $gmap ['height']); 539 540 if ($gmap ['zoom'] > 16) { 541 // actually this could even be 18, but that seems overkill 542 $imgUrl .= "&zoom=16"; 543 } else { 544 $imgUrl .= "&zoom=" . $gmap ['zoom']; 545 } 546 547 if ($overlay !== []) { 548 $rowId = 0; 549 $imgUrl .= "&markers="; 550 foreach ($overlay as $data) { 551 [$lat, $lon, $text, $angle, $opacity, $img] = $data; 552 $rowId++; 553 $iconStyle = "lightblue$rowId"; 554 $imgUrl .= "$lat,$lon,$iconStyle%7c"; 555 } 556 $imgUrl = substr($imgUrl, 0, -3); 557 } 558 559 $result = $imgUrl; 560 } 561 return $result; 562 } 563 564 /** 565 * Create a Bing maps static image url w/ the poi. 566 * 567 * @param array $gmap 568 * @param array $overlay 569 */ 570 private function getBing(array $gmap, array $overlay): string 571 { 572 $maptype = match ($gmap ['baselyr']) { 573 've hybrid', 'bing hybrid' => 'AerialWithLabels', 574 've sat', 'bing sat' => 'Aerial', 575 default => 'Road', 576 }; 577 $imgUrl = "https://dev.virtualearth.net/REST/v1/Imagery/Map/" . $maptype;// . "/"; 578 if ($this->getConf('autoZoomMap')) { 579 $bbox = $this->calcBBOX($overlay, $gmap ['lat'], $gmap ['lon']); 580 //$imgUrl .= "?ma=" . $bbox ['minlat'] . "," . $bbox ['minlon'] . "," 581 // . $bbox ['maxlat'] . "," . $bbox ['maxlon']; 582 $imgUrl .= "?ma=" . $bbox ['minlat'] . "%2C" . $bbox ['minlon'] . "%2C" . $bbox ['maxlat'] 583 . "%2C" . $bbox ['maxlon']; 584 $imgUrl .= "&dcl=1"; 585 } 586 if (!str_contains($imgUrl, "?")) 587 $imgUrl .= "?"; 588 589 //$imgUrl .= "&ms=" . str_replace ( "px", "", $gmap ['width'] ) . "," 590 // . str_replace ( "px", "", $gmap ['height'] ); 591 $imgUrl .= "&ms=" . str_replace("px", "", $gmap ['width']) . "%2C" 592 . str_replace("px", "", $gmap ['height']); 593 $imgUrl .= "&key=" . $this->getConf('bingAPIKey'); 594 if ($overlay !== []) { 595 $rowId = 0; 596 foreach ($overlay as $data) { 597 [$lat, $lon, $text, $angle, $opacity, $img] = $data; 598 // TODO icon style lookup, see: http://msdn.microsoft.com/en-us/library/ff701719.aspx for iconStyle 599 $iconStyle = 32; 600 $rowId++; 601 // NOTE: the max number of pushpins is 18! or we have to use POST 602 // (http://msdn.microsoft.com/en-us/library/ff701724.aspx) 603 if ($rowId == 18) { 604 break; 605 } 606 //$imgUrl .= "&pp=$lat,$lon;$iconStyle;$rowId"; 607 $imgUrl .= "&pp=$lat%2C$lon%3B$iconStyle%3B$rowId"; 608 } 609 } 610 global $conf; 611 $imgUrl .= "&fmt=png"; 612 $imgUrl .= "&c=" . $conf ['lang']; 613 return $imgUrl; 614 } 615 616 /** 617 * Calculate the minimum bbox for a start location + poi. 618 * 619 * @param array $overlay 620 * multi-dimensional array of array($lat, $lon, $text, $angle, $opacity, $img) 621 * @param float $lat 622 * latitude for map center 623 * @param float $lon 624 * longitude for map center 625 * @return array :float array describing the mbr and center point 626 */ 627 private function calcBBOX(array $overlay, float $lat, float $lon): array 628 { 629 $lats = [$lat]; 630 $lons = [$lon]; 631 foreach ($overlay as $data) { 632 [$lat, $lon, $text, $angle, $opacity, $img] = $data; 633 $lats [] = $lat; 634 $lons [] = $lon; 635 } 636 sort($lats); 637 sort($lons); 638 // TODO: make edge/wrap around cases work 639 $centerlat = $lats [0] + ($lats [count($lats) - 1] - $lats [0]); 640 $centerlon = $lons [0] + ($lons [count($lats) - 1] - $lons [0]); 641 return ['minlat' => $lats [0], 'minlon' => $lons [0], 'maxlat' => $lats [count($lats) - 1], 'maxlon' => $lons [count($lats) - 1], 'centerlat' => $centerlat, 'centerlon' => $centerlon]; 642 } 643 644 /** 645 * convert latitude in decimal degrees to DMS+hemisphere. 646 * 647 * @param float $decimaldegrees 648 * @todo move this into a shared library 649 */ 650 private function convertLat(float $decimaldegrees): string 651 { 652 if (str_contains($decimaldegrees, '-')) { 653 $latPos = "S"; 654 } else { 655 $latPos = "N"; 656 } 657 $dms = $this->convertDDtoDMS(abs($decimaldegrees)); 658 return hsc($dms . $latPos); 659 } 660 661 /** 662 * Convert decimal degrees to degrees, minutes, seconds format 663 * 664 * @param float $decimaldegrees 665 * @return string dms 666 * @todo move this into a shared library 667 */ 668 private function convertDDtoDMS(float $decimaldegrees): string 669 { 670 $dms = floor($decimaldegrees); 671 $secs = ($decimaldegrees - $dms) * 3600; 672 $min = floor($secs / 60); 673 $sec = round($secs - ($min * 60), 3); 674 $dms .= 'º' . $min . '\'' . $sec . '"'; 675 return $dms; 676 } 677 678 /** 679 * convert longitude in decimal degrees to DMS+hemisphere. 680 * 681 * @param float $decimaldegrees 682 * @todo move this into a shared library 683 */ 684 private function convertLon(float $decimaldegrees): string 685 { 686 if (str_contains($decimaldegrees, '-')) { 687 $lonPos = "W"; 688 } else { 689 $lonPos = "E"; 690 } 691 $dms = $this->convertDDtoDMS(abs($decimaldegrees)); 692 return hsc($dms . $lonPos); 693 } 694 695 /** 696 * Figures out the base filename of a media path. 697 * 698 * @param string $mediaLink 699 */ 700 private function getFileName(string $mediaLink): string 701 { 702 $mediaLink = str_replace('[[', '', $mediaLink); 703 $mediaLink = str_replace(']]', '', $mediaLink); 704 $mediaLink = substr($mediaLink, 0, -4); 705 706 $parts = explode(':', $mediaLink); 707 $mediaLink = end($parts); 708 return str_replace('_', ' ', $mediaLink); 709 } 710 711 /** 712 * 713 * @see DokuWiki_Syntax_Plugin::render() 714 */ 715 public function render($format, Doku_Renderer $renderer, $data): bool 716 { 717 // set to true after external scripts tags are written 718 static $initialised = false; 719 // incremented for each map tag in the page source so we can keep track of each map in this page 720 static $mapnumber = 0; 721 722 [$mapid, $param, $mainLat, $mainLon, $poitable, $poitabledesc, $staticImgUrl, $_firstimage] = $data; 723 724 if ($format === 'xhtml') { 725 $olscript = ''; 726 $stadiaEnable = $this->getConf('enableStadia'); 727 $osmEnable = $this->getConf('enableOSM'); 728 $enableBing = $this->getConf('enableBing'); 729 730 $scriptEnable = ''; 731 if (!$initialised) { 732 $initialised = true; 733 // render necessary script tags only once 734 $olscript = '<script defer="defer" src="' . DOKU_BASE . 'lib/plugins/openlayersmap/ol/ol.js"></script> 735<script defer="defer" src="' . DOKU_BASE . 'lib/plugins/openlayersmap/ol/ol-layerswitcher.js"></script>'; 736 737 $scriptEnable = '<script defer="defer" src="data:text/javascript;base64,'; 738 $scriptSrc = $olscript ? 'const olEnable=true;' : 'const olEnable=false;'; 739 $scriptSrc .= 'const osmEnable=' . ($osmEnable ? 'true' : 'false') . ';'; 740 $scriptSrc .= 'const stadiaEnable=' . ($stadiaEnable ? 'true' : 'false') . ';'; 741 $scriptSrc .= 'const bEnable=' . ($enableBing ? 'true' : 'false') . ';'; 742 $scriptSrc .= 'const bApiKey="' . $this->getConf('bingAPIKey') . '";'; 743 $scriptSrc .= 'const tfApiKey="' . $this->getConf('tfApiKey') . '";'; 744 $scriptSrc .= 'const gApiKey="' . $this->getConf('googleAPIkey') . '";'; 745 $scriptSrc .= 'olMapData = []; let olMaps = {}; let olMapOverlays = {};'; 746 $scriptEnable .= base64_encode($scriptSrc); 747 $scriptEnable .= '"></script>'; 748 } 749 $renderer->doc .= "$olscript\n$scriptEnable"; 750 $renderer->doc .= '<div class="olMapHelp">' . $this->locale_xhtml("help") . '</div>'; 751 if ($this->getConf('enableA11y')) { 752 $renderer->doc .= '<div id="' . $mapid . '-static" class="olStaticMap">' 753 . p_render($format, p_get_instructions($staticImgUrl), $info) . '</div>'; 754 } 755 $renderer->doc .= '<div id="' . $mapid . '-clearer" class="clearer"><p> </p></div>'; 756 if ($this->getConf('enableA11y')) { 757 // render a table of the POI for the print and a11y presentation, it is hidden using javascript 758 $renderer->doc .= ' 759 <div id="' . $mapid . '-table-span" class="olPOItableSpan"> 760 <table id="' . $mapid . '-table" class="olPOItable"> 761 <caption class="olPOITblCaption">' . $this->getLang('olmapPOItitle') . '</caption> 762 <thead class="olPOITblHeader"> 763 <tr> 764 <th class="rowId" scope="col">id</th> 765 <th class="icon" scope="col">' . $this->getLang('olmapPOIicon') . '</th> 766 <th class="lat" scope="col" title="' . $this->getLang('olmapPOIlatTitle') . '">' 767 . $this->getLang('olmapPOIlat') . '</th> 768 <th class="lon" scope="col" title="' . $this->getLang('olmapPOIlonTitle') . '">' 769 . $this->getLang('olmapPOIlon') . '</th> 770 <th class="txt" scope="col">' . $this->getLang('olmapPOItxt') . '</th> 771 </tr> 772 </thead>'; 773 if ($poitabledesc != '') { 774 $renderer->doc .= '<tfoot class="olPOITblFooter"><tr><td colspan="5">' . $poitabledesc 775 . '</td></tr></tfoot>'; 776 } 777 $renderer->doc .= '<tbody class="olPOITblBody">' . $poitable . '</tbody> 778 </table> 779 </div>'; 780 $renderer->doc .= "\n"; 781 } 782 // render inline mapscript parts 783 $renderer->doc .= '<script defer="defer" src="data:text/javascript;base64,'; 784 $renderer->doc .= base64_encode("olMapData[$mapnumber] = $param"); 785 $renderer->doc .= '"></script>'; 786 $mapnumber++; 787 return true; 788 } elseif ($format === 'metadata') { 789 if (!(($this->dflt ['lat'] == $mainLat) && ($this->dflt ['lon'] == $mainLon))) { 790 // render geo metadata, unless they are the default 791 $renderer->meta ['geo'] ['lat'] = $mainLat; 792 $renderer->meta ['geo'] ['lon'] = $mainLon; 793 if (($geophp = plugin_load('helper', 'geophp')) !== null) { 794 // if we have the geoPHP helper, add the geohash 795 try { 796 $renderer->meta['geo']['geohash'] = (new Point($mainLon, $mainLat))->out('geohash'); 797 } catch (Exception) { 798 Logger::error("Failed to create geohash for: $mainLat, $mainLon"); 799 } 800 } 801 } 802 803 if (($this->getConf('enableA11y')) && (!empty($_firstimage))) { 804 // add map local image into relation/firstimage if not already filled and when it is a local image 805 806 global $ID; 807 $rel = p_get_metadata($ID, 'relation', METADATA_RENDER_USING_CACHE); 808 // $img = $rel ['firstimage']; 809 if (empty($rel ['firstimage']) /* || $img == $_firstimage*/) { 810 //Logger::debug( 811 // 'olmap::render#rendering image relation metadata for _firstimage as $img was empty or same.', 812 // $_firstimage); 813 814 // This seems to never work; the firstimage entry in the .meta file is empty 815 // $renderer->meta['relation']['firstimage'] = $_firstimage; 816 // ... and neither does this; the firstimage entry in the .meta file is empty 817 // $relation = array('relation'=>array('firstimage'=>$_firstimage)); 818 // p_set_metadata($ID, $relation, false, false); 819 // ... this works 820 $renderer->internalmedia($_firstimage, $poitabledesc); 821 } 822 } 823 return true; 824 } 825 return false; 826 } 827} 828