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') . ' ' . $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>: ' . $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