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