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, strlen($img) - 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        // dbglog ( $result, 'syntax_plugin_openlayersmap_olmap::getStaticOSM: osm image url is:' );
588        return $result;
589    }
590
591    /**
592     * Create a Bing maps static image url w/ the poi.
593     *
594     * @param array $gmap
595     * @param array $overlay
596     * @return string
597     */
598    private function getBing(array $gmap, array $overlay): string {
599        switch($gmap ['baselyr']) {
600            case 've hybrid' :
601            case 'bing hybrid' :
602                $maptype = 'AerialWithLabels';
603                break;
604            case 've sat' :
605            case 'bing sat' :
606                $maptype = 'Aerial';
607                break;
608            case 've normal' :
609            case 've road' :
610            case 've' :
611            case 'bing road' :
612            default :
613                $maptype = 'Road';
614                break;
615        }
616        $imgUrl = "https://dev.virtualearth.net/REST/v1/Imagery/Map/" . $maptype;// . "/";
617        if($this->getConf('autoZoomMap')) {
618            $bbox = $this->calcBBOX($overlay, $gmap ['lat'], $gmap ['lon']);
619            //$imgUrl .= "?ma=" . $bbox ['minlat'] . "," . $bbox ['minlon'] . ","
620            //          . $bbox ['maxlat'] . "," . $bbox ['maxlon'];
621            $imgUrl .= "?ma=" . $bbox ['minlat'] . "%2C" . $bbox ['minlon'] . "%2C" . $bbox ['maxlat']
622                . "%2C" . $bbox ['maxlon'];
623            $imgUrl .= "&dcl=1";
624        }
625        if(strpos($imgUrl, "?") === false)
626            $imgUrl .= "?";
627
628        //$imgUrl .= "&ms=" . str_replace ( "px", "", $gmap ['width'] ) . ","
629        //          . str_replace ( "px", "", $gmap ['height'] );
630        $imgUrl .= "&ms=" . str_replace("px", "", $gmap ['width']) . "%2C"
631            . str_replace("px", "", $gmap ['height']);
632        $imgUrl .= "&key=" . $this->getConf('bingAPIKey');
633        if(!empty ($overlay)) {
634            $rowId = 0;
635            foreach($overlay as $data) {
636                list ($lat, $lon, $text, $angle, $opacity, $img) = $data;
637                // TODO icon style lookup, see: http://msdn.microsoft.com/en-us/library/ff701719.aspx for iconStyle
638                $iconStyle = 32;
639                $rowId++;
640                // NOTE: the max number of pushpins is 18! or we have to use POST
641                //  (http://msdn.microsoft.com/en-us/library/ff701724.aspx)
642                if($rowId == 18) {
643                    break;
644                }
645                //$imgUrl .= "&pp=$lat,$lon;$iconStyle;$rowId";
646                $imgUrl .= "&pp=$lat%2C$lon%3B$iconStyle%3B$rowId";
647
648            }
649        }
650        global $conf;
651        $imgUrl .= "&fmt=png";
652        $imgUrl .= "&c=" . $conf ['lang'];
653        // dbglog($imgUrl,'syntax_plugin_openlayersmap_olmap::getBing: bing image url is:');
654        return $imgUrl;
655    }
656
657    /**
658     * Calculate the minimum bbox for a start location + poi.
659     *
660     * @param array $overlay
661     *            multi-dimensional array of array($lat, $lon, $text, $angle, $opacity, $img)
662     * @param float $lat
663     *            latitude for map center
664     * @param float $lon
665     *            longitude for map center
666     * @return array :float array describing the mbr and center point
667     */
668    private function calcBBOX(array $overlay, float $lat, float $lon): array {
669        $lats = array($lat);
670        $lons = array($lon);
671        foreach($overlay as $data) {
672            list ($lat, $lon, $text, $angle, $opacity, $img) = $data;
673            $lats [] = $lat;
674            $lons [] = $lon;
675        }
676        sort($lats);
677        sort($lons);
678        // TODO: make edge/wrap around cases work
679        $centerlat = $lats [0] + ($lats [count($lats) - 1] - $lats [0]);
680        $centerlon = $lons [0] + ($lons [count($lats) - 1] - $lons [0]);
681        return array(
682            'minlat'    => $lats [0],
683            'minlon'    => $lons [0],
684            'maxlat'    => $lats [count($lats) - 1],
685            'maxlon'    => $lons [count($lats) - 1],
686            'centerlat' => $centerlat,
687            'centerlon' => $centerlon
688        );
689    }
690
691    /**
692     * convert latitude in decimal degrees to DMS+hemisphere.
693     *
694     * @param float $decimaldegrees
695     * @return string
696     * @todo move this into a shared library
697     */
698    private function convertLat(float $decimaldegrees): string {
699        if(strpos($decimaldegrees, '-') !== false) {
700            $latPos = "S";
701        } else {
702            $latPos = "N";
703        }
704        $dms = $this->convertDDtoDMS(abs($decimaldegrees));
705        return hsc($dms . $latPos);
706    }
707
708    /**
709     * Convert decimal degrees to degrees, minutes, seconds format
710     *
711     * @param float $decimaldegrees
712     * @return string dms
713     * @todo move this into a shared library
714     */
715    private function convertDDtoDMS(float $decimaldegrees): string {
716        $dms  = floor($decimaldegrees);
717        $secs = ($decimaldegrees - $dms) * 3600;
718        $min  = floor($secs / 60);
719        $sec  = round($secs - ($min * 60), 3);
720        $dms  .= 'º' . $min . '\'' . $sec . '"';
721        return $dms;
722    }
723
724    /**
725     * convert longitude in decimal degrees to DMS+hemisphere.
726     *
727     * @param float $decimaldegrees
728     * @return string
729     * @todo move this into a shared library
730     */
731    private function convertLon(float $decimaldegrees): string {
732        if(strpos($decimaldegrees, '-') !== false) {
733            $lonPos = "W";
734        } else {
735            $lonPos = "E";
736        }
737        $dms = $this->convertDDtoDMS(abs($decimaldegrees));
738        return hsc($dms . $lonPos);
739    }
740
741    /**
742     * Figures out the base filename of a media path.
743     *
744     * @param string $mediaLink
745     * @return string
746     */
747    private function getFileName(string $mediaLink): string {
748        $mediaLink = str_replace('[[', '', $mediaLink);
749        $mediaLink = str_replace(']]', '', $mediaLink);
750        $mediaLink = substr($mediaLink, 0, -4);
751        $parts     = explode(':', $mediaLink);
752        $mediaLink = end($parts);
753        return str_replace('_', ' ', $mediaLink);
754    }
755
756    /**
757     *
758     * @see DokuWiki_Syntax_Plugin::render()
759     */
760    public function render($format, Doku_Renderer $renderer, $data): bool {
761        // set to true after external scripts tags are written
762        static $initialised = false;
763        // incremented for each map tag in the page source so we can keep track of each map in this page
764        static $mapnumber = 0;
765
766        // dbglog($data, 'olmap::render() data.');
767        list ($mapid, $param, $mainLat, $mainLon, $poitable, $poitabledesc, $staticImgUrl, $_firstimage) = $data;
768
769        if($format == 'xhtml') {
770            $olscript     = '';
771            $stamenEnable = $this->getConf('enableStamen');
772            $osmEnable    = $this->getConf('enableOSM');
773            $enableBing   = $this->getConf('enableBing');
774
775            $scriptEnable = '';
776            if(!$initialised) {
777                $initialised = true;
778                // render necessary script tags only once
779                $olscript = '<script defer="defer" src="' . DOKU_BASE . 'lib/plugins/openlayersmap/ol7/ol.js"></script>
780<script defer="defer" src="' . DOKU_BASE . 'lib/plugins/openlayersmap/ol7/ol-layerswitcher.js"></script>';
781
782                $scriptEnable = '<script defer="defer" src="data:text/javascript;base64,';
783                $scriptSrc    = $olscript ? 'const olEnable=true;' : 'const olEnable=false;';
784                $scriptSrc    .= 'const osmEnable=' . ($osmEnable ? 'true' : 'false') . ';';
785                $scriptSrc    .= 'const stamenEnable=' . ($stamenEnable ? 'true' : 'false') . ';';
786                $scriptSrc    .= 'const bEnable=' . ($enableBing ? 'true' : 'false') . ';';
787                $scriptSrc    .= 'const bApiKey="' . $this->getConf('bingAPIKey') . '";';
788                $scriptSrc    .= 'const tfApiKey="' . $this->getConf('tfApiKey') . '";';
789                $scriptSrc    .= 'const gApiKey="' . $this->getConf('googleAPIkey') . '";';
790                $scriptSrc    .= 'olMapData = []; let olMaps = {}; let olMapOverlays = {};';
791                $scriptEnable .= base64_encode($scriptSrc);
792                $scriptEnable .= '"></script>';
793            }
794            $renderer->doc .= "$olscript\n$scriptEnable";
795            $renderer->doc .= '<div class="olMapHelp">' . $this->locale_xhtml("help") . '</div>';
796            if($this->getConf('enableA11y')) {
797                $renderer->doc .= '<div id="' . $mapid . '-static" class="olStaticMap">'
798                    . p_render($format, p_get_instructions($staticImgUrl), $info) . '</div>';
799            }
800            $renderer->doc .= '<div id="' . $mapid . '-clearer" class="clearer"><p>&nbsp;</p></div>';
801            if($this->getConf('enableA11y')) {
802                // render a table of the POI for the print and a11y presentation, it is hidden using javascript
803                $renderer->doc .= '
804                <div id="' . $mapid . '-table-span" class="olPOItableSpan">
805                    <table id="' . $mapid . '-table" class="olPOItable">
806                    <caption class="olPOITblCaption">' . $this->getLang('olmapPOItitle') . '</caption>
807                    <thead class="olPOITblHeader">
808                    <tr>
809                    <th class="rowId" scope="col">id</th>
810                    <th class="icon" scope="col">' . $this->getLang('olmapPOIicon') . '</th>
811                    <th class="lat" scope="col" title="' . $this->getLang('olmapPOIlatTitle') . '">'
812                    . $this->getLang('olmapPOIlat') . '</th>
813                    <th class="lon" scope="col" title="' . $this->getLang('olmapPOIlonTitle') . '">'
814                    . $this->getLang('olmapPOIlon') . '</th>
815                    <th class="txt" scope="col">' . $this->getLang('olmapPOItxt') . '</th>
816                    </tr>
817                    </thead>';
818                if($poitabledesc != '') {
819                    $renderer->doc .= '<tfoot class="olPOITblFooter"><tr><td colspan="5">' . $poitabledesc
820                        . '</td></tr></tfoot>';
821                }
822                $renderer->doc .= '<tbody class="olPOITblBody">' . $poitable . '</tbody>
823                    </table>
824                </div>';
825                $renderer->doc .= "\n";
826            }
827            // render inline mapscript parts
828            $renderer->doc .= '<script defer="defer" src="data:text/javascript;base64,';
829            $renderer->doc .= base64_encode("olMapData[$mapnumber] = $param");
830            $renderer->doc .= '"></script>';
831            $mapnumber++;
832            return true;
833        } elseif($format == 'metadata') {
834            if(!(($this->dflt ['lat'] == $mainLat) && ($this->dflt ['lon'] == $mainLon))) {
835                // render geo metadata, unless they are the default
836                $renderer->meta ['geo'] ['lat'] = $mainLat;
837                $renderer->meta ['geo'] ['lon'] = $mainLon;
838                if($geophp = plugin_load('helper', 'geophp')) {
839                    // if we have the geoPHP helper, add the geohash
840
841                    // fails with older php versions..
842                    // $renderer->meta['geo']['geohash'] = (new Point($mainLon,$mainLat))->out('geohash');
843                    $p                                  = new Point ($mainLon, $mainLat);
844                    $renderer->meta ['geo'] ['geohash'] = $p->out('geohash');
845                }
846            }
847
848            if(($this->getConf('enableA11y')) && (!empty ($_firstimage))) {
849                // add map local image into relation/firstimage if not already filled and when it is a local image
850
851                global $ID;
852                $rel = p_get_metadata($ID, 'relation', METADATA_RENDER_USING_CACHE);
853                // $img = $rel ['firstimage'];
854                if(empty ($rel ['firstimage']) /* || $img == $_firstimage*/) {
855                    //dbglog ( $_firstimage,
856                    // 'olmap::render#rendering image relation metadata for _firstimage as $img was empty or same.' );
857                    // This seems to never work; the firstimage entry in the .meta file is empty
858                    // $renderer->meta['relation']['firstimage'] = $_firstimage;
859
860                    // ... and neither does this; the firstimage entry in the .meta file is empty
861                    // $relation = array('relation'=>array('firstimage'=>$_firstimage));
862                    // p_set_metadata($ID, $relation, false, false);
863
864                    // ... this works
865                    $renderer->internalmedia($_firstimage, $poitabledesc);
866                }
867            }
868            return true;
869        }
870        return false;
871    }
872}
873