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