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