* * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ use dokuwiki\Extension\Plugin; use dokuwiki\Logger; use geoPHP\Geometry\Point; /** * DokuWiki Plugin spatialhelper (index component). * * @license BSD license * @author Mark Prins */ class helper_plugin_spatialhelper_index extends Plugin { /** * directory for index files. * * @var string */ protected string $idx_dir = ''; /** * spatial index, we'll look up list/array so we can do spatial queries. * entries should be: array("geohash" => {"id1","id3",}) * * @var array */ protected array $spatial_idx = []; /** * Constructor, initialises the spatial index. * @throws Exception */ public function __construct() { if (plugin_load('helper', 'geophp') === null) { $message = 'Required geophp plugin is not available'; msg($message, -1); } global $conf; $this->idx_dir = $conf ['indexdir']; // test if there is a spatialindex, if not build one for the wiki if (!@file_exists($this->idx_dir . '/spatial.idx')) { // creates and stores the index $this->generateSpatialIndex(); } else { $this->spatial_idx = unserialize( io_readFile($this->idx_dir . '/spatial.idx', false), ['allowed_classes' => false] ); Logger::debug('done loading spatial index', $this->spatial_idx); } } /** * (re-)Generates the spatial index by running through all the pages in the wiki. * * @throws Exception * @todo add an option to erase the old index */ final public function generateSpatialIndex(): bool { global $conf; require_once(DOKU_INC . 'inc/search.php'); $pages = []; search($pages, $conf ['datadir'], 'search_allpages', []); foreach ($pages as $page) { $this->updateSpatialIndex($page ['id']); } // media $media = []; search($media, $conf['mediadir'], 'search_media', []); foreach ($media as $medium) { if ($medium['isimg']) { $this->indexImage($medium['id']); } } return true; } /** * Update the spatial index for the page. * * @param string $id * the document ID * @throws Exception */ final public function updateSpatialIndex(string $id): bool { $geotags = p_get_metadata($id, 'geo'); if (empty($geotags)) { return false; } if (empty($geotags ['lon']) || empty($geotags ['lat'])) { return false; } Logger::debug("Geo metadata found for page $id", $geotags); $geometry = new Point($geotags ['lon'], $geotags ['lat']); $geohash = $geometry->out('geohash'); Logger::debug('Update index for geohash: ' . $geohash); return $this->addToIndex($geohash, $id); } /** * Store the hash/id entry in the index. * * @param string $geohash * @param string $id * page or media id * @return bool true when succesful */ private function addToIndex(string $geohash, string $id): bool { $pageIds = []; // check index for key/geohash if (!array_key_exists($geohash, $this->spatial_idx)) { Logger::debug("Geohash $geohash not in index, just add $id."); $pageIds [] = $id; } else { Logger::debug('Geohash for document is in index, find it.'); // check the index for document $knownHashes = $this->findHashesForId($id, $this->spatial_idx); if ($knownHashes === []) { Logger::debug("No index record found for document $id, adding it to the index."); $pageIds = $this->spatial_idx [$geohash]; $pageIds [] = $id; } // TODO shortcut, need to make sure there is only one element, if not the index is corrupt $knownHash = $knownHashes [0]; if ($knownHash === $geohash) { Logger::debug("Document $id was found in index and has the same geohash, nothing to do."); return true; } if (!empty($knownHash)) { Logger::debug("Document/media $id was found in index but has different geohash (it moved)."); $knownIds = $this->spatial_idx [$knownHash]; Logger::debug("Known id's for this hash:", $knownIds); // remove it from the old geohash element $i = array_search($id, $knownIds); Logger::debug('Unsetting:' . $knownIds [$i]); unset($knownIds [$i]); $this->spatial_idx [$knownHash] = $knownIds; // set on new geohash element $pageIds = $this->spatial_idx[$geohash]; $pageIds [] = $id; } } // store and save $this->spatial_idx [$geohash] = $pageIds; return $this->saveIndex(); } /** * Looks up the geohash(es) for the document in the index. * * @param String $id * document ID * @param array $index * spatial index */ final public function findHashesForId(string $id, array $index): array { $hashes = []; foreach ($index as $hash => $docIds) { if (in_array($id, $docIds)) { $hashes [] = $hash; } } Logger::debug("Found the following hashes for $id (should only be 1)", $hashes); return $hashes; } /** * Save spatial index. */ private function saveIndex(): bool { return io_saveFile($this->idx_dir . '/spatial.idx', serialize($this->spatial_idx)); } /** * Add an index entry for this file having EXIF / IPTC data. * * @param $imgId * a Dokuwiki image id * @return bool true when image was successfully added to the index. * @throws Exception * @see http://www.php.net/manual/en/function.iptcparse.php * @see http://php.net/manual/en/function.exif-read-data.php * */ final public function indexImage(string $imgId): bool { // test for supported files (jpeg only) if ( (!str_ends_with(strtolower($imgId), '.jpg')) && (!str_ends_with(strtolower($imgId), '.jpeg')) ) { Logger::debug("indexImage:: " . $imgId . " is not a supported image file."); return false; } $geometry = $this->getCoordsFromExif($imgId); if (!$geometry) { return false; } $geohash = $geometry->out('geohash'); // TODO truncate the geohash to something reasonable, otherwise they are // useless as an indexing mechanism eg. u1h73weckdrmskdqec3c9 is far too // precise, limit at ~9 as most GPS are not submeter accurate return $this->addToIndex($geohash, 'media__' . $imgId); } /** * retrieve GPS decimal coordinates from exif. * * @param string $id * @return Point|false * @throws Exception */ final public function getCoordsFromExif(string $id): Point|false { $exif = exif_read_data(mediaFN($id), 0, true); if (empty($exif ['GPS'])) { return false; } $lat = $this->convertDMStoD( [ $exif ['GPS'] ['GPSLatitude'] [0], $exif ['GPS'] ['GPSLatitude'] [1], $exif ['GPS'] ['GPSLatitude'] [2], $exif ['GPS'] ['GPSLatitudeRef'] ] ); $lon = $this->convertDMStoD( [ $exif ['GPS'] ['GPSLongitude'] [0], $exif ['GPS'] ['GPSLongitude'] [1], $exif ['GPS'] ['GPSLongitude'] [2], $exif ['GPS'] ['GPSLongitudeRef'] ] ); return new Point($lon, $lat); } /** * convert DegreesMinutesSeconds to Decimal degrees. * * @param array $param array of rational DMS */ final public function convertDMStoD(array $param): float { // if (!(is_array($param))) { // $param = [$param]; // } $deg = $this->convertRationaltoFloat($param [0]); $min = $this->convertRationaltoFloat($param [1]) / 60; $sec = $this->convertRationaltoFloat($param [2]) / 60 / 60; // Hemisphere (N, S, W or E) $hem = ($param [3] === 'N' || $param [3] === 'E') ? 1 : -1; return $hem * ($deg + $min + $sec); } final public function convertRationaltoFloat(string $param): float { // rational64u $nums = explode('/', $param); if ((int)$nums[1] > 0) { return (float)$nums[0] / (int)$nums[1]; } return (float)$nums[0]; } /** * Deletes the page from the index. * * @param string $id document ID */ final public function deleteFromIndex(string $id): void { // check the index for document $knownHashes = $this->findHashesForId($id, $this->spatial_idx); if ($knownHashes === []) { return; } // TODO shortcut, need to make sure there is only one element, if not the index is corrupt $knownHash = $knownHashes [0]; $knownIds = $this->spatial_idx [$knownHash]; $i = array_search($id, $knownIds); Logger::debug("removing: $knownIds[$i] from the index."); unset($knownIds [$i]); $this->spatial_idx [$knownHash] = $knownIds; if (empty($this->spatial_idx [$knownHash])) { unset($this->spatial_idx [$knownHash]); } $this->saveIndex(); } }