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