xref: /plugin/openlayersmap/syntax/olmap.php (revision d29478df98b8b62dbce5a269cf53c4deae926744)
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 .= "&center=" . $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>&nbsp;</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