1<?php
2
3/*
4 * Copyright (c) 2011 Mark C. Prins <mprins@users.sf.net>
5 *
6 * Permission to use, copy, modify, and distribute this software for any
7 * purpose with or without fee is hereby granted, provided that the above
8 * copyright notice and this permission notice appear in all copies.
9 *
10 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17 */
18
19use dokuwiki\Extension\SyntaxPlugin;
20use geoPHP\Geometry\Point;
21
22/**
23 * DokuWiki Plugin geotag (Syntax Component).
24 *
25 * Handles the rendering part of the geotag plugin.
26 *
27 * @license BSD license
28 * @author  Mark C. Prins <mprins@users.sf.net>
29 */
30class syntax_plugin_geotag_geotag extends SyntaxPlugin
31{
32    /**
33     *
34     * @see DokuWiki_Syntax_Plugin::getType()
35     */
36    final public function getType(): string
37    {
38        return 'substition';
39    }
40
41    /**
42     *
43     * @see DokuWiki_Syntax_Plugin::getPType()
44     */
45    final public function getPType(): string
46    {
47        return 'block';
48    }
49
50    /**
51     *
52     * @see Doku_Parser_Mode::getSort()
53     */
54    final public function getSort(): int
55    {
56        return 305;
57    }
58
59    /**
60     *
61     * @see Doku_Parser_Mode::connectTo()
62     */
63    final public function connectTo($mode): void
64    {
65        $this->Lexer->addSpecialPattern('\{\{geotag>.*?\}\}', $mode, 'plugin_geotag_geotag');
66    }
67
68    /**
69     *
70     * @see DokuWiki_Syntax_Plugin::handle()
71     */
72    final public function handle($match, $state, $pos, Doku_Handler $handler): array
73    {
74        $tags = trim(substr($match, 9, -2));
75        // parse geotag content
76        $lat = $this->parseNumericParameter("lat", $tags);
77        $lon = $this->parseNumericParameter("lon", $tags);
78        $alt = $this->parseNumericParameter("alt", $tags);
79        preg_match("/(region[:|=][\p{L}\s\w'-]*)/u", $tags, $region);
80        preg_match("/(placename[:|=][\p{L}\s\w'-]*)/u", $tags, $placename);
81        preg_match("/(country[:|=][\p{L}\s\w'-]*)/u", $tags, $country);
82        preg_match("(hide|unhide)", $tags, $hide);
83
84        $showlocation = $this->getConf('geotag_location_prefix');
85        if ($this->getConf('geotag_showlocation')) {
86            $showlocation = trim(substr($placename [0], 10));
87            if ($showlocation === '') {
88                $showlocation = $this->getConf('geotag_location_prefix');
89            }
90        }
91        // read config for system setting
92        $style = '';
93        if ($this->getConf('geotag_hide')) {
94            $style = ' style="display: none;"';
95        }
96        // override config for the current tag
97        if (array_key_exists(0, $hide) && trim($hide [0]) === 'hide') {
98            $style = ' style="display: none;"';
99        } elseif (array_key_exists(0, $hide) && trim($hide [0]) === 'unhide') {
100            $style = '';
101        }
102        return [
103            hsc($lat),
104            hsc($lon),
105            hsc($alt),
106            $this->geohash($lat, $lon),
107            hsc(trim(substr(($region[0] ?? ''), 7))),
108            hsc(trim(substr(($placename[0] ?? ''), 10))),
109            hsc(trim(substr(($country [0] ?? ''), 8))),
110            hsc($showlocation), $style
111        ];
112    }
113
114    /**
115     * parses numeric parameter with given name
116     *
117     * @param string $name name of the parameter
118     * @param string $input text to consume
119     * @return string parameter values as numeric string or empty string if nothing is found
120     */
121    private function parseNumericParameter(string $name, string $input): string
122    {
123        $output = '';
124        $pattern = "/" . $name . "\s*[:=]\s*(-?\d*\.?\d*)/";
125        if (preg_match($pattern, $input, $matches)) {
126            $output = $matches[1];
127        }
128        return $output;
129    }
130
131    /**
132     * Calculate the geohash for this lat/lon pair.
133     *
134     * @param float $lat
135     * @param float $lon
136     * @return string
137     * @throws Exception
138     */
139    private function geohash(float $lat, float $lon): string
140    {
141        if ((plugin_load('helper', 'geophp')) === null) {
142            return "";
143        }
144
145        return (new Point($lon, $lat))->out('geohash');
146    }
147
148    /**
149     *
150     * @see DokuWiki_Syntax_Plugin::render()
151     */
152    final public function render($format, Doku_Renderer $renderer, $data): bool
153    {
154        if ($data === false) {
155            return false;
156        }
157        [$lat, $lon, $alt, $geohash, $region, $placename, $country, $showlocation, $style] = $data;
158        $ddlat = $lat;
159        $ddlon = $lon;
160        if ($this->getConf('displayformat') === 'DMS') {
161            $lat = $this->convertLat($lat);
162            $lon = $this->convertLon($lon);
163        } else {
164            $lat .= 'º';
165            $lon .= 'º';
166        }
167
168        if ($format === 'xhtml') {
169            if ($this->getConf('geotag_prevent_microformat_render')) {
170                return true;
171            }
172            $searchPre = '';
173            $searchPost = '';
174            if ($this->getConf('geotag_showsearch')) {
175                if (($spHelper = plugin_load('helper', 'spatialhelper_search')) !== null) {
176                    $title = $this->getLang('findnearby') . '&nbsp;' . $placename;
177                    $url = wl(
178                        getID(),
179                        ['do' => 'findnearby', 'lat' => $ddlat, 'lon' => $ddlon]
180                    );
181                    $searchPre = '<a href="' . $url . '" title="' . $title . '">';
182                    $searchPost = '<span class="a11y">' . $title . '</span></a>';
183                }
184            }
185
186            // render geotag microformat/schema.org microdata
187            $renderer->doc .= '<span class="geotagPrint">' . $this->getLang('geotag_desc') . '</span>';
188            $renderer->doc .= '<div class="h-geo geo"' . $style . ' title="' . $this->getLang('geotag_desc')
189                . $placename . '" itemscope itemtype="https://schema.org/Place">';
190            $renderer->doc .= '<span itemprop="name">' . $showlocation . '</span>:&nbsp;' . $searchPre;
191            $renderer->doc .= '<span itemprop="geo" itemscope itemtype="https://schema.org/GeoCoordinates">';
192            $renderer->doc .= '<span class="p-latitude latitude" itemprop="latitude" data-latitude="' . $ddlat . '">'
193                . $lat . '</span>;';
194            $renderer->doc .= '<span class="p-longitude longitude" itemprop="longitude" data-longitude="' . $ddlon
195                . '">' . $lon . '</span>';
196            if (!empty($alt)) {
197                $renderer->doc .= ', <span class="p-altitude altitude" itemprop="elevation" data-altitude="' . $alt
198                    . '">' . $alt . 'm</span>';
199            }
200            $renderer->doc .= '</span>' . $searchPost . '</div>' . DOKU_LF;
201            return true;
202        } elseif ($format === 'metadata') {
203            // render metadata (our action plugin will put it in the page head)
204            $renderer->meta ['geo'] ['lat'] = $ddlat;
205            $renderer->meta ['geo'] ['lon'] = $ddlon;
206            $renderer->meta ['geo'] ['placename'] = $placename;
207            $renderer->meta ['geo'] ['region'] = $region;
208            $renderer->meta ['geo'] ['country'] = $country;
209            $renderer->meta ['geo'] ['geohash'] = $geohash;
210            if (!empty($alt)) {
211                $renderer->meta ['geo'] ['alt'] = $alt;
212            }
213            return true;
214        } elseif ($format === 'odt') {
215            if (!empty($alt)) {
216                $alt = ', ' . $alt . 'm';
217            }
218            $renderer->p_open();
219            $renderer->_odtAddImage(DOKU_PLUGIN . 'geotag/images/geotag.png', null, null, 'left', '');
220            $renderer->cdata($this->getLang('geotag_desc') . ' ' . $placename);
221            $renderer->monospace_open();
222            $renderer->cdata($lat . ';' . $lon . $alt);
223            $renderer->monospace_close();
224            $renderer->p_close();
225            return true;
226        } else {
227            return false;
228        }
229    }
230
231    /**
232     * convert latitude in decimal degrees to DMS+hemisphere.
233     *
234     * @param float $decimaldegrees
235     * @return string
236     * @todo move this into a shared library
237     */
238    private function convertLat(float $decimaldegrees): string
239    {
240        if (strpos($decimaldegrees, '-') !== false) {
241            $latPos = "S";
242        } else {
243            $latPos = "N";
244        }
245        $dms = $this->convertDDtoDMS(abs($decimaldegrees));
246        return hsc($dms . $latPos);
247    }
248
249    /**
250     * Convert decimal degrees to degrees, minutes, seconds format
251     *
252     * @param float $decimaldegrees
253     * @return string dms
254     * @todo move this into a shared library
255     */
256    private function convertDDtoDMS(float $decimaldegrees): string
257    {
258        $dms = floor($decimaldegrees);
259        $secs = ($decimaldegrees - $dms) * 3600;
260        $min = floor($secs / 60);
261        $sec = round($secs - ($min * 60), 3);
262        $dms .= 'º' . $min . '\'' . $sec . '"';
263        return $dms;
264    }
265
266    /**
267     * convert longitude in decimal degrees to DMS+hemisphere.
268     *
269     * @param float $decimaldegrees
270     * @todo move this into a shared library
271     */
272    private function convertLon(float $decimaldegrees): string
273    {
274        if (strpos($decimaldegrees, '-') !== false) {
275            $lonPos = "W";
276        } else {
277            $lonPos = "E";
278        }
279        $dms = $this->convertDDtoDMS(abs($decimaldegrees));
280        return hsc($dms . $lonPos);
281    }
282}
283