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