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'], 'azure') !== false) { 115 if (!$this->getConf('azureAPIKey')) { 116 // in case there is no Azure api key we'll use OSM 117 $_firstimageID = $this->getStaticOSM($gmap, $overlay); 118 $imgUrl .= $_firstimageID; 119 if ($this->getConf('optionStaticMapGenerator') == 'remote') { 120 $imgUrl .= "&.png"; 121 } 122 } else { 123 $_nocache = true; 124 // NOTE the azure API key must be transmitted in the request header as `x-ms-client-id` as well as an 125 // Accept header with 'image/png'. 126 // So ultimately this will fail if we try to use the Azure maps API without local static map generator, 127 // but we'll do our best to generate a map URL for the user 128 $imgUrl .= $this->getAzure($gmap, $overlay) . "&.png"; 129 } 130 /* elseif (stripos ( $gmap ['baselyr'], 'mapquest' ) !== false) { 131 // MapQuest 132 if (! $this->getConf ( 'mapquestAPIKey' )) { 133 // no API key for MapQuest, use OSM 134 $_firstimageID = $this->getStaticOSM ( $gmap, $overlay ); 135 $imgUrl .= $_firstimageID; 136 if ($this->getConf ( 'optionStaticMapGenerator' ) == 'remote') { 137 $imgUrl .= "&.png"; 138 } 139 } else { 140 $imgUrl .= $this->_getMapQuest ( $gmap, $overlay ); 141 $imgUrl .= "&.png"; 142 } 143 } */ 144 } else { 145 // default OSM 146 $_firstimageID = $this->getStaticOSM($gmap, $overlay); 147 $imgUrl .= $_firstimageID; 148 if ($this->getConf('optionStaticMapGenerator') == 'remote') { 149 $imgUrl .= "&.png"; 150 } 151 } 152 153 // append dw p_render specific params and render 154 $imgUrl .= "?" . str_replace("px", "", $gmap ['width']) . "x" 155 . str_replace("px", "", $gmap ['height']); 156 $imgUrl .= "&nolink"; 157 158 // add nocache option for selected services 159 if ($_nocache) { 160 $imgUrl .= "&nocache"; 161 } 162 163 $imgUrl .= " |" . $gmap ['summary'] . " }}"; 164 165 $mapid = $gmap ['id']; 166 // create a javascript parameter string for the map 167 $param = ''; 168 foreach ($gmap as $key => $val) { 169 $param .= is_numeric($val) ? "$key: $val, " : "$key: '" . hsc($val) . "', "; 170 } 171 if (!empty($param)) { 172 $param = substr($param, 0, -2); 173 } 174 unset($gmap ['id']); 175 176 // create a javascript serialisation of the point data 177 $poi = ''; 178 $poitable = ''; 179 $rowId = 0; 180 if ($overlay !== []) { 181 foreach ($overlay as $data) { 182 [$lat, $lon, $text, $angle, $opacity, $img] = $data; 183 $rowId++; 184 $poi .= ", {lat:$lat,lon:$lon,txt:'$text',angle:$angle,opacity:$opacity,img:'$img',rowId: $rowId}"; 185 186 if ($this->getConf('displayformat') === 'DMS') { 187 $lat = $this->convertLat($lat); 188 $lon = $this->convertLon($lon); 189 } else { 190 $lat .= 'º'; 191 $lon .= 'º'; 192 } 193 194 $poitable .= ' 195 <tr> 196 <td class="rowId">' . $rowId . '</td> 197 <td class="icon"><img src="' . DOKU_BASE . 'lib/plugins/openlayersmap/icons/' . $img . '" alt="' 198 . substr($img, 0, -4) . $this->getlang('alt_legend_poi') . '" /></td> 199 <td class="lat" title="' . $this->getLang('olmapPOIlatTitle') . '">' . $lat . '</td> 200 <td class="lon" title="' . $this->getLang('olmapPOIlonTitle') . '">' . $lon . '</td> 201 <td class="txt">' . $text . '</td> 202 </tr>'; 203 } 204 $poi = substr($poi, 2); 205 } 206 if (!empty($gmap ['kmlfile'])) { 207 $poitable .= ' 208 <tr> 209 <td class="rowId"><img src="' . DOKU_BASE 210 . 'lib/plugins/openlayersmap/toolbar/kml_file.png" alt="KML file" /></td> 211 <td class="icon"><img src="' . DOKU_BASE . 'lib/plugins/openlayersmap/toolbar/kml_line.png" alt="' 212 . $this->getlang('alt_legend_kml') . '" /></td> 213 <td class="txt" colspan="3">KML track: ' . $this->getFileName($gmap ['kmlfile']) . '</td> 214 </tr>'; 215 } 216 if (!empty($gmap ['gpxfile'])) { 217 $poitable .= ' 218 <tr> 219 <td class="rowId"><img src="' . DOKU_BASE 220 . 'lib/plugins/openlayersmap/toolbar/gpx_file.png" alt="GPX file" /></td> 221 <td class="icon"><img src="' . DOKU_BASE 222 . 'lib/plugins/openlayersmap/toolbar/gpx_line.png" alt="' 223 . $this->getlang('alt_legend_gpx') . '" /></td> 224 <td class="txt" colspan="3">GPX track: ' . $this->getFileName($gmap ['gpxfile']) . '</td> 225 </tr>'; 226 } 227 if (!empty($gmap ['geojsonfile'])) { 228 $poitable .= ' 229 <tr> 230 <td class="rowId"><img src="' . DOKU_BASE 231 . 'lib/plugins/openlayersmap/toolbar/geojson_file.png" alt="GeoJSON file" /></td> 232 <td class="icon"><img src="' . DOKU_BASE 233 . 'lib/plugins/openlayersmap/toolbar/geojson_line.png" alt="' 234 . $this->getlang('alt_legend_geojson') . '" /></td> 235 <td class="txt" colspan="3">GeoJSON track: ' . $this->getFileName($gmap ['geojsonfile']) . '</td> 236 </tr>'; 237 } 238 239 $autozoom = empty($gmap ['autozoom']) ? $this->getConf('autoZoomMap') : $gmap ['autozoom']; 240 $js = "{mapOpts: {" . $param . ", displayformat: '" . $this->getConf('displayformat') 241 . "', autozoom: " . $autozoom . "}, poi: [$poi]};"; 242 // unescape the json 243 $poitable = stripslashes($poitable); 244 245 return [$mapid, $js, $mainLat, $mainLon, $poitable, $gmap ['summary'], $imgUrl, $_firstimageID]; 246 } 247 248 /** 249 * extract parameters for the map from the parameter string 250 * 251 * @param string $str_params 252 * string of key="value" pairs 253 * @return array associative array of parameters key=>value 254 */ 255 private function extractParams(string $str_params): array 256 { 257 $param = []; 258 preg_match_all('/(\w*)="(.*?)"/us', $str_params, $param, PREG_SET_ORDER); 259 // parse match for instructions, break into key value pairs 260 $gmap = $this->dflt; 261 foreach ($gmap as $key => &$value) { 262 $defval = $this->getConf('default_' . $key); 263 if ($defval !== '') { 264 $value = $defval; 265 } 266 } 267 unset($value); 268 foreach ($param as $kvpair) { 269 [$match, $key, $val] = $kvpair; 270 $key = strtolower($key); 271 if (isset($gmap [$key])) { 272 if ($key == 'summary') { 273 // preserve case for summary field 274 $gmap [$key] = $val; 275 } elseif ($key == 'id') { 276 // preserve case for id field 277 $gmap [$key] = $val; 278 } else { 279 $gmap [$key] = strtolower($val); 280 } 281 } 282 } 283 return $gmap; 284 } 285 286 /** 287 * extract overlay points for the map from the wiki syntax data 288 * 289 * @param string $str_points 290 * multi-line string of lat,lon,text triplets 291 * @return array multi-dimensional array of lat,lon,text triplets 292 */ 293 private function extractPoints(string $str_points): array 294 { 295 $point = []; 296 // preg_match_all('/^([+-]?[0-9].*?),\s*([+-]?[0-9].*?),(.*?),(.*?),(.*?),(.*)$/um', 297 // $str_points, $point, PREG_SET_ORDER); 298 /* 299 * group 1: ([+-]?[0-9]+(?:\.[0-9]*)?) 300 * group 2: ([+-]?[0-9]+(?:\.[0-9]*)?) 301 * group 3: (.*?) 302 * group 4: (.*?) 303 * group 5: (.*?) 304 * group 6: (.*) 305 */ 306 preg_match_all( 307 '/^([+-]?[0-9]+(?:\.[0-9]*)?),\s*([+-]?[0-9]+(?:\.[0-9]*)?),(.*?),(.*?),(.*?),(.*)$/um', 308 $str_points, 309 $point, 310 PREG_SET_ORDER 311 ); 312 // create poi array 313 $overlay = []; 314 foreach ($point as $pt) { 315 [$match, $lat, $lon, $angle, $opacity, $img, $text] = $pt; 316 $lat = is_numeric($lat) ? $lat : 0; 317 $lon = is_numeric($lon) ? $lon : 0; 318 $angle = is_numeric($angle) ? $angle : 0; 319 $opacity = is_numeric($opacity) ? $opacity : 0.8; 320 // TODO validate using exist & set up default img? 321 $img = trim($img); 322 $text = p_get_instructions($text); 323 // dbg ( $text ); 324 $text = p_render("xhtml", $text, $info); 325 // dbg ( $text ); 326 $text = addslashes(str_replace("\n", "", $text)); 327 $overlay [] = [$lat, $lon, $text, $angle, $opacity, $img]; 328 } 329 return $overlay; 330 } 331 332 /** 333 * Create a Google maps static image url w/ the poi. 334 * 335 * @param array $gmap 336 * @param array $overlay 337 */ 338 private function getGoogle(array $gmap, array $overlay): string 339 { 340 $sUrl = $this->getConf('iconUrlOverload'); 341 if (!$sUrl) { 342 $sUrl = DOKU_URL; 343 } 344 $maptype = match ($gmap ['baselyr']) { 345 'google hybrid' => 'hybrid', 346 'google sat' => 'satellite', 347 'terrain', 'google relief' => 'terrain', 348 default => 'roadmap', 349 }; 350 // TODO maybe use viewport / visible instead of center/zoom, 351 // see: https://developers.google.com/maps/documentation/staticmaps/index#Viewports 352 // 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 353 $imgUrl = "https://maps.googleapis.com/maps/api/staticmap?"; 354 $imgUrl .= "&size=" . str_replace("px", "", $gmap ['width']) . "x" 355 . str_replace("px", "", $gmap ['height']); 356 //if (!$this->getConf( 'autoZoomMap')) { // no need for center & zoom params } 357 $imgUrl .= "¢er=" . $gmap ['lat'] . "," . $gmap ['lon']; 358 // max is 21 (== building scale), but that's overkill.. 359 if ($gmap ['zoom'] > 17) { 360 $imgUrl .= "&zoom=17"; 361 } else { 362 $imgUrl .= "&zoom=" . $gmap ['zoom']; 363 } 364 if ($overlay !== []) { 365 $rowId = 0; 366 foreach ($overlay as $data) { 367 [$lat, $lon, $text, $angle, $opacity, $img] = $data; 368 $imgUrl .= "&markers=icon%3a" . $sUrl . "lib/plugins/openlayersmap/icons/" . $img . "%7c" 369 . $lat . "," . $lon . "%7clabel%3a" . ++$rowId; 370 } 371 } 372 $imgUrl .= "&format=png&maptype=" . $maptype; 373 global $conf; 374 $imgUrl .= "&language=" . $conf ['lang']; 375 if ($this->getConf('googleAPIkey')) { 376 $imgUrl .= "&key=" . $this->getConf('googleAPIkey'); 377 } 378 return $imgUrl; 379 } 380 381 /** 382 * Create a MapQuest static map API image url. 383 * 384 * @param array $gmap 385 * @param array $overlay 386 */ 387 /* 388 private function _getMapQuest($gmap, $overlay) { 389 $sUrl = $this->getConf ( 'iconUrlOverload' ); 390 if (! $sUrl) { 391 $sUrl = DOKU_URL; 392 } 393 switch ($gmap ['baselyr']) { 394 case 'mapquest hybrid' : 395 $maptype = 'hyb'; 396 break; 397 case 'mapquest sat' : 398 // because sat coverage is very limited use 'hyb' instead of 'sat' so we don't get a blank map 399 $maptype = 'hyb'; 400 break; 401 case 'mapquest road' : 402 default : 403 $maptype = 'map'; 404 break; 405 } 406 $imgUrl = "http://open.mapquestapi.com/staticmap/v4/getmap?declutter=true&"; 407 if (count ( $overlay ) < 1) { 408 $imgUrl .= "?center=" . $gmap ['lat'] . "," . $gmap ['lon']; 409 // max level for mapquest is 16 410 if ($gmap ['zoom'] > 16) { 411 $imgUrl .= "&zoom=16"; 412 } else { 413 $imgUrl .= "&zoom=" . $gmap ['zoom']; 414 } 415 } 416 // use bestfit instead of center/zoom, needs upperleft/lowerright corners 417 // $bbox=$this->calcBBOX($overlay, $gmap['lat'], $gmap['lon']); 418 // $imgUrl .= "bestfit=".$bbox['minlat'].",".$bbox['maxlon'].",".$bbox['maxlat'].",".$bbox['minlon']; 419 420 // TODO declutter option works well for square maps but not for rectangular, maybe compensate for that 421 // or compensate the mbr.. 422 423 $imgUrl .= "&size=" . str_replace ( "px", "", $gmap ['width'] ) . "," . str_replace ("px","",$gmap['height']); 424 425 // TODO mapquest allows using one image url with a multiplier $NUMBER eg: 426 // $NUMBER = 2 427 // $imgUrl .= DOKU_URL."/".DOKU_PLUGIN."/".getPluginName()."/icons/".$img.",$NUMBER,C," 428 // .$lat1.",".$lon1.",0,0,0,0,C,".$lat2.",".$lon2.",0,0,0,0"; 429 if (! empty ( $overlay )) { 430 $imgUrl .= "&xis="; 431 foreach ( $overlay as $data ) { 432 list ( $lat, $lon, $text, $angle, $opacity, $img ) = $data; 433 // $imgUrl .= $sUrl."lib/plugins/openlayersmap/icons/".$img.",1,C,".$lat.",".$lon.",0,0,0,0,"; 434 $imgUrl .= $sUrl . "lib/plugins/openlayersmap/icons/" . $img . ",1,C," . $lat . "," . $lon . ","; 435 } 436 $imgUrl = substr ( $imgUrl, 0, - 1 ); 437 } 438 $imgUrl .= "&imageType=png&type=" . $maptype; 439 $imgUrl .= "&key=".$this->getConf ( 'mapquestAPIKey' ); 440 return $imgUrl; 441 } 442 */ 443 444 /** 445 * Create a static OSM map image url w/ the poi from http://staticmap.openstreetmap.de (staticMapLite) 446 * use http://staticmap.openstreetmap.de "staticMapLite" or a local version 447 * 448 * @param array $gmap 449 * @param array $overlay 450 * 451 * @return false|string 452 * @todo implementation for http://ojw.dev.openstreetmap.org/StaticMapDev/ 453 */ 454 private function getStaticOSM(array $gmap, array $overlay) 455 { 456 global $conf; 457 458 if ($this->getConf('optionStaticMapGenerator') === 'local') { 459 // using local basemap composer 460 if (($myMap = plugin_load('helper', 'openlayersmap_staticmap')) === null) { 461 Logger::error( 462 'openlayersmap_staticmap plugin is not available for use.', 463 $myMap 464 ); 465 } 466 if (($geophp = plugin_load('helper', 'geophp')) === null) { 467 Logger::debug('geophp plugin is not available for use.', $geophp); 468 } 469 $size = str_replace("px", "", $gmap ['width']) . "x" 470 . str_replace("px", "", $gmap ['height']); 471 472 $markers = []; 473 if ($overlay !== []) { 474 foreach ($overlay as $data) { 475 [$lat, $lon, $text, $angle, $opacity, $img] = $data; 476 $iconStyle = substr($img, 0, -4); 477 $markers [] = ['lat' => $lat, 'lon' => $lon, 'type' => $iconStyle]; 478 } 479 } 480 481 $apikey = ''; 482 switch ($gmap ['baselyr']) { 483 case 'mapnik': 484 case 'openstreetmap': 485 $maptype = 'openstreetmap'; 486 break; 487 case 'transport': 488 $maptype = 'transport'; 489 $apikey = '?apikey=' . $this->getConf('tfApiKey'); 490 break; 491 case 'landscape': 492 $maptype = 'landscape'; 493 $apikey = '?apikey=' . $this->getConf('tfApiKey'); 494 break; 495 case 'outdoors': 496 $maptype = 'outdoors'; 497 $apikey = '?apikey=' . $this->getConf('tfApiKey'); 498 break; 499 case 'cycle map': 500 $maptype = 'cycle'; 501 $apikey = '?apikey=' . $this->getConf('tfApiKey'); 502 break; 503 case 'hike and bike map': 504 $maptype = 'hikeandbike'; 505 break; 506 case 'mapquest hybrid': 507 case 'mapquest road': 508 case 'mapquest sat': 509 $maptype = 'mapquest'; 510 break; 511 default: 512 $maptype = ''; 513 break; 514 } 515 516 $result = $myMap->getMap( 517 $gmap ['lat'], 518 $gmap ['lon'], 519 $gmap ['zoom'], 520 $size, 521 $maptype, 522 $markers, 523 $gmap ['gpxfile'], 524 $gmap ['kmlfile'], 525 $gmap ['geojsonfile'], 526 $apikey 527 ); 528 } else { 529 // using external basemap composer 530 531 // https://staticmap.openstreetmap.de/staticmap.php?center=47.000622235634,10 532 //.117187497601&zoom=5&size=500x350 533 // &markers=48.999812532766,8.3593749976708,lightblue1|43.154850037315,17.499999997306, 534 // lightblue1|49.487527053077,10.820312497573,ltblu-pushpin|47.951071133739,15.917968747369, 535 // ol-marker|47.921629720114,18.027343747285,ol-marker-gold|47.951071133739,19.257812497236, 536 // ol-marker-blue|47.180141361692,19.257812497236,ol-marker-green 537 $imgUrl = "https://staticmap.openstreetmap.de/staticmap.php"; 538 $imgUrl .= "?center=" . $gmap ['lat'] . "," . $gmap ['lon']; 539 $imgUrl .= "&size=" . str_replace("px", "", $gmap ['width']) . "x" 540 . str_replace("px", "", $gmap ['height']); 541 542 if ($gmap ['zoom'] > 16) { 543 // actually this could even be 18, but that seems overkill 544 $imgUrl .= "&zoom=16"; 545 } else { 546 $imgUrl .= "&zoom=" . $gmap ['zoom']; 547 } 548 549 if ($overlay !== []) { 550 $rowId = 0; 551 $imgUrl .= "&markers="; 552 foreach ($overlay as $data) { 553 [$lat, $lon, $text, $angle, $opacity, $img] = $data; 554 $rowId++; 555 $iconStyle = "lightblue$rowId"; 556 $imgUrl .= "$lat,$lon,$iconStyle%7c"; 557 } 558 $imgUrl = substr($imgUrl, 0, -3); 559 } 560 561 $result = $imgUrl; 562 } 563 return $result; 564 } 565 566 /** 567 * Create an Azure maps static image url w/ the poi. 568 * 569 * @param array $gmap 570 * @param array $overlay 571 * @return string 572 * 573 * @see https://learn.microsoft.com/en-us/rest/api/maps/render/get-map-static-image?view=rest-maps-2026-01-01&tabs=HTTP 574 */ 575 private function getAzure(array $gmap, array $overlay): string 576 { 577 $maptype = match ($gmap ['baselyr']) { 578 've sat', 'azure sat' => 'microsoft.imagery', 579 default => 'microsoft.base.road', 580 }; 581 $imgUrl = "https://atlas.microsoft.com/map/static?api-version=2024-04-01&tilesetId=" . $maptype; 582 // unlikely to work as it should be in the req-header, but we'll do our best to generate a map URL for the user 583 $imgUrl .= "&subscription-key=" . $this->getConf('azureAPIKey'); 584 if ($this->getConf('autoZoomMap')) { 585 $bbox = $this->calcBBOX($overlay, $gmap ['lat'], $gmap ['lon']); 586 $imgUrl .= "&bbox=" . $bbox ['minlon'] . "%2C" . $bbox ['minlat'] . "%2C" . $bbox ['maxlon'] . "%2C" . $bbox ['maxlat']; 587 } else { 588 $imgUrl .= "¢er=" . $gmap ['lon'] . "%2C" . $gmap ['lat']; 589 $imgUrl .= "&zoom=" . $gmap ['zoom']; 590 } 591 $imgUrl .= "&width=" . str_replace("px", "", $gmap ['width']); 592 $imgUrl .= "&height=" . str_replace("px", "", $gmap ['height']); 593 if ($overlay !== []) { 594 $rowId = 0; 595 $imgUrl .= "&pins=default%7C"; 596 foreach ($overlay as $data) { 597 [$lat, $lon, $text, $angle, $opacity, $img] = $data; 598 $rowId++; 599 // The Azure Maps account S0 SKU only supports a single instance of the pins parameter and the number 600 // of locations is limited to 5 per pin. Other SKUs allow up to 25 instances of the pins parameter 601 // to specify multiple pin styles, and the number of locations is limited to 50 per pin. 602 if ($rowId == 6) { 603 break; 604 } 605 $imgUrl .="%7C'$rowId'$lon%20$lat"; 606 } 607 } 608 global $conf; 609 $imgUrl .= "&language=" . $conf ['lang']; 610 return $imgUrl; 611 } 612 613 /** 614 * Calculate the minimum bbox for a start location + poi. 615 * 616 * @param array $overlay 617 * multi-dimensional array of array($lat, $lon, $text, $angle, $opacity, $img) 618 * @param float $lat 619 * latitude for map center 620 * @param float $lon 621 * longitude for map center 622 * @return array :float array describing the mbr and center point 623 */ 624 private function calcBBOX(array $overlay, float $lat, float $lon): array 625 { 626 $lats = [$lat]; 627 $lons = [$lon]; 628 foreach ($overlay as $data) { 629 [$lat, $lon, $text, $angle, $opacity, $img] = $data; 630 $lats [] = $lat; 631 $lons [] = $lon; 632 } 633 sort($lats); 634 sort($lons); 635 // TODO: make edge/wrap around cases work 636 $centerlat = $lats [0] + ($lats [count($lats) - 1] - $lats [0]); 637 $centerlon = $lons [0] + ($lons [count($lats) - 1] - $lons [0]); 638 return ['minlat' => $lats [0], 'minlon' => $lons [0], 'maxlat' => $lats [count($lats) - 1], 'maxlon' => $lons [count($lats) - 1], 'centerlat' => $centerlat, 'centerlon' => $centerlon]; 639 } 640 641 /** 642 * convert latitude in decimal degrees to DMS+hemisphere. 643 * 644 * @param float $decimaldegrees 645 * @todo move this into a shared library 646 */ 647 private function convertLat(float $decimaldegrees): string 648 { 649 if (str_contains($decimaldegrees, '-')) { 650 $latPos = "S"; 651 } else { 652 $latPos = "N"; 653 } 654 $dms = $this->convertDDtoDMS(abs($decimaldegrees)); 655 return hsc($dms . $latPos); 656 } 657 658 /** 659 * Convert decimal degrees to degrees, minutes, seconds format 660 * 661 * @param float $decimaldegrees 662 * @return string dms 663 * @todo move this into a shared library 664 */ 665 private function convertDDtoDMS(float $decimaldegrees): string 666 { 667 $dms = floor($decimaldegrees); 668 $secs = ($decimaldegrees - $dms) * 3600; 669 $min = floor($secs / 60); 670 $sec = round($secs - ($min * 60), 3); 671 $dms .= 'º' . $min . '\'' . $sec . '"'; 672 return $dms; 673 } 674 675 /** 676 * convert longitude in decimal degrees to DMS+hemisphere. 677 * 678 * @param float $decimaldegrees 679 * @todo move this into a shared library 680 */ 681 private function convertLon(float $decimaldegrees): string 682 { 683 if (str_contains($decimaldegrees, '-')) { 684 $lonPos = "W"; 685 } else { 686 $lonPos = "E"; 687 } 688 $dms = $this->convertDDtoDMS(abs($decimaldegrees)); 689 return hsc($dms . $lonPos); 690 } 691 692 /** 693 * Figures out the base filename of a media path. 694 * 695 * @param string $mediaLink 696 */ 697 private function getFileName(string $mediaLink): string 698 { 699 $mediaLink = str_replace('[[', '', $mediaLink); 700 $mediaLink = str_replace(']]', '', $mediaLink); 701 $mediaLink = substr($mediaLink, 0, -4); 702 703 $parts = explode(':', $mediaLink); 704 $mediaLink = end($parts); 705 return str_replace('_', ' ', $mediaLink); 706 } 707 708 /** 709 * 710 * @see DokuWiki_Syntax_Plugin::render() 711 */ 712 public function render($format, Doku_Renderer $renderer, $data): bool 713 { 714 // set to true after external scripts tags are written 715 static $initialised = false; 716 // incremented for each map tag in the page source so we can keep track of each map in this page 717 static $mapnumber = 0; 718 719 [$mapid, $param, $mainLat, $mainLon, $poitable, $poitabledesc, $staticImgUrl, $_firstimage] = $data; 720 721 if ($format === 'xhtml') { 722 $olscript = ''; 723 $stadiaEnable = $this->getConf('enableStadia'); 724 $osmEnable = $this->getConf('enableOSM'); 725 $enableAzure = $this->getConf('enableAzure'); 726 727 $scriptEnable = ''; 728 if (!$initialised) { 729 $initialised = true; 730 // render necessary script tags only once 731 $olscript = '<script defer="defer" src="' . DOKU_BASE . 'lib/plugins/openlayersmap/ol/ol.js"></script> 732<script defer="defer" src="' . DOKU_BASE . 'lib/plugins/openlayersmap/ol/ol-layerswitcher.js"></script>'; 733 734 $scriptEnable = '<script defer="defer" src="data:text/javascript;base64,'; 735 $scriptSrc = $olscript ? 'const olEnable=true;' : 'const olEnable=false;'; 736 $scriptSrc .= 'const osmEnable=' . ($osmEnable ? 'true' : 'false') . ';'; 737 $scriptSrc .= 'const stadiaEnable=' . ($stadiaEnable ? 'true' : 'false') . ';'; 738 $scriptSrc .= 'const aEnable=' . ($enableAzure ? 'true' : 'false') . ';'; 739 $scriptSrc .= 'const aApiKey="' . $this->getConf('azureAPIKey') . '";'; 740 $scriptSrc .= 'const tfApiKey="' . $this->getConf('tfApiKey') . '";'; 741 $scriptSrc .= 'const gApiKey="' . $this->getConf('googleAPIkey') . '";'; 742 $scriptSrc .= 'olMapData = []; let olMaps = {}; let olMapOverlays = {};'; 743 $scriptEnable .= base64_encode($scriptSrc); 744 $scriptEnable .= '"></script>'; 745 } 746 $renderer->doc .= "$olscript\n$scriptEnable"; 747 $renderer->doc .= '<div class="olMapHelp">' . $this->locale_xhtml("help") . '</div>'; 748 if ($this->getConf('enableA11y')) { 749 $renderer->doc .= '<div id="' . $mapid . '-static" class="olStaticMap">' 750 . p_render($format, p_get_instructions($staticImgUrl), $info) . '</div>'; 751 } 752 $renderer->doc .= '<div id="' . $mapid . '-clearer" class="clearer"><p> </p></div>'; 753 if ($this->getConf('enableA11y')) { 754 // render a table of the POI for the print and a11y presentation, it is hidden using javascript 755 $renderer->doc .= ' 756 <div id="' . $mapid . '-table-span" class="olPOItableSpan"> 757 <table id="' . $mapid . '-table" class="olPOItable"> 758 <caption class="olPOITblCaption">' . $this->getLang('olmapPOItitle') . '</caption> 759 <thead class="olPOITblHeader"> 760 <tr> 761 <th class="rowId" scope="col">id</th> 762 <th class="icon" scope="col">' . $this->getLang('olmapPOIicon') . '</th> 763 <th class="lat" scope="col" title="' . $this->getLang('olmapPOIlatTitle') . '">' 764 . $this->getLang('olmapPOIlat') . '</th> 765 <th class="lon" scope="col" title="' . $this->getLang('olmapPOIlonTitle') . '">' 766 . $this->getLang('olmapPOIlon') . '</th> 767 <th class="txt" scope="col">' . $this->getLang('olmapPOItxt') . '</th> 768 </tr> 769 </thead>'; 770 if ($poitabledesc != '') { 771 $renderer->doc .= '<tfoot class="olPOITblFooter"><tr><td colspan="5">' . $poitabledesc 772 . '</td></tr></tfoot>'; 773 } 774 $renderer->doc .= '<tbody class="olPOITblBody">' . $poitable . '</tbody> 775 </table> 776 </div>'; 777 $renderer->doc .= "\n"; 778 } 779 // render inline mapscript parts 780 $renderer->doc .= '<script defer="defer" src="data:text/javascript;base64,'; 781 $renderer->doc .= base64_encode("olMapData[$mapnumber] = $param"); 782 $renderer->doc .= '"></script>'; 783 $mapnumber++; 784 return true; 785 } elseif ($format === 'metadata') { 786 if (!(($this->dflt ['lat'] == $mainLat) && ($this->dflt ['lon'] == $mainLon))) { 787 // render geo metadata, unless they are the default 788 $renderer->meta ['geo'] ['lat'] = $mainLat; 789 $renderer->meta ['geo'] ['lon'] = $mainLon; 790 if (($geophp = plugin_load('helper', 'geophp')) !== null) { 791 // if we have the geoPHP helper, add the geohash 792 try { 793 $renderer->meta['geo']['geohash'] = (new Point($mainLon, $mainLat))->out('geohash'); 794 } catch (Exception) { 795 Logger::error("Failed to create geohash for: $mainLat, $mainLon"); 796 } 797 } 798 } 799 800 if (($this->getConf('enableA11y')) && (!empty($_firstimage))) { 801 // add map local image into relation/firstimage if not already filled and when it is a local image 802 803 global $ID; 804 $rel = p_get_metadata($ID, 'relation', METADATA_RENDER_USING_CACHE); 805 // $img = $rel ['firstimage']; 806 if (empty($rel ['firstimage']) /* || $img == $_firstimage*/) { 807 //Logger::debug( 808 // 'olmap::render#rendering image relation metadata for _firstimage as $img was empty or same.', 809 // $_firstimage); 810 811 // This seems to never work; the firstimage entry in the .meta file is empty 812 // $renderer->meta['relation']['firstimage'] = $_firstimage; 813 // ... and neither does this; the firstimage entry in the .meta file is empty 814 // $relation = array('relation'=>array('firstimage'=>$_firstimage)); 815 // p_set_metadata($ID, $relation, false, false); 816 // ... this works 817 $renderer->internalmedia($_firstimage, $poitabledesc); 818 } 819 } 820 return true; 821 } 822 return false; 823 } 824} 825