1<?php 2 3/* 4 * Copyright (c) 2011-2023 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\Plugin; 20use dokuwiki\Logger; 21use geoPHP\Adapter\GeoHash; 22use geoPHP\Geometry\LineString; 23use geoPHP\Geometry\Point; 24 25/** 26 * DokuWiki Plugin spatialhelper (Search component). 27 * 28 * @license BSD license 29 * @author Mark Prins 30 */ 31class helper_plugin_spatialhelper_search extends Plugin 32{ 33 /** 34 * spatial index. 35 * 36 * @var array 37 */ 38 protected $spatial_idx = []; 39 /** 40 * Precision, Distance of Adjacent Cell in Meters. 41 * 42 * @see https://stackoverflow.com/questions/13836416/geohash-and-max-distance 43 * 44 * @var float 45 */ 46 private $precision = [5_003_530, 625441, 123264, 19545, 3803, 610, 118, 19, 3.7, 0.6]; 47 48 /** 49 * constructor; initialize/load spatial index. 50 */ 51 public function __construct() 52 { 53 global $conf; 54 55 if (plugin_load('helper', 'geophp', false, true) === null) { 56 $message = 57 'helper_plugin_spatialhelper_search::spatialhelper_search: required geophp plugin is not available.'; 58 msg($message, -1); 59 } 60 61 $idx_dir = $conf ['indexdir']; 62 if (!@file_exists($idx_dir . '/spatial.idx')) { 63 plugin_load('helper', 'spatialhelper_index'); 64 } 65 66 $this->spatial_idx = unserialize(io_readFile($idx_dir . '/spatial.idx', false), ['allowed_classes' => false]); 67 } 68 69 /** 70 * Find locations based on the coordinate pair. 71 * 72 * @param float $lat 73 * The y coordinate (or latitude) 74 * @param float $lon 75 * The x coordinate (or longitude) 76 * @throws Exception 77 */ 78 final public function findNearbyLatLon(float $lat, float $lon): array 79 { 80 $geometry = new Point($lon, $lat); 81 return $this->findNearby($geometry->out('geohash'), $geometry); 82 } 83 84 /** 85 * finds nearby elements in the index based on the geohash. 86 * returns a list of documents and the bunding box. 87 * 88 * @param string $geohash 89 * @param Point|null $p 90 * optional point 91 * @return array of ... 92 * @throws Exception 93 */ 94 final public function findNearby(string $geohash, Point $p = null): array 95 { 96 $_geohashClass = new Geohash(); 97 if (!$p instanceof Point) { 98 $decodedPoint = $_geohashClass->read($geohash); 99 } else { 100 $decodedPoint = $p; 101 } 102 103 // find adjacent blocks 104 $adjacent = []; 105 $adjacent ['center'] = $geohash; 106 $adjacent ['top'] = Geohash::adjacent($adjacent ['center'], 'top'); 107 $adjacent ['bottom'] = Geohash::adjacent($adjacent ['center'], 'bottom'); 108 $adjacent ['right'] = Geohash::adjacent($adjacent ['center'], 'right'); 109 $adjacent ['left'] = Geohash::adjacent($adjacent ['center'], 'left'); 110 $adjacent ['topleft'] = Geohash::adjacent($adjacent ['left'], 'top'); 111 $adjacent ['topright'] = Geohash::adjacent($adjacent ['right'], 'top'); 112 $adjacent ['bottomright'] = Geohash::adjacent($adjacent ['right'], 'bottom'); 113 $adjacent ['bottomleft'] = Geohash::adjacent($adjacent ['left'], 'bottom'); 114 Logger::debug("adjacent geo hashes", $adjacent); 115 116 // find all the pages in the index that overlap with the adjacent hashes 117 $docIds = []; 118 foreach ($adjacent as $adjHash) { 119 if (is_array($this->spatial_idx)) { 120 foreach ($this->spatial_idx as $_geohash => $_docIds) { 121 if (strpos($_geohash, (string)$adjHash) !== false) { 122 // if $adjHash similar to geohash 123 $docIds = array_merge($docIds, $_docIds); 124 } 125 } 126 } 127 } 128 $docIds = array_unique($docIds); 129 Logger::debug("found docIDs", $docIds); 130 131 // create associative array of pages + calculate distance 132 $pages = []; 133 $media = []; 134 $indexer = plugin_load('helper', 'spatialhelper_index'); 135 136 foreach ($docIds as $id) { 137 if (strpos($id, 'media__') === 0) { 138 $id = substr($id, strlen('media__')); 139 if (auth_quickaclcheck($id) >= /*AUTH_READ*/ 1) { 140 $point = $indexer->getCoordsFromExif($id); 141 $line = new LineString( 142 [ 143 $decodedPoint, 144 $point 145 ] 146 ); 147 $media [] = ['id' => $id, 'distance' => (int)($line->greatCircleLength()), 148 'lat' => $point->y(), 'lon' => $point->x()]; 149 } 150 } elseif (auth_quickaclcheck($id) >= /*AUTH_READ*/ 1) { 151 $geotags = p_get_metadata($id, 'geo'); 152 $point = new Point($geotags ['lon'], $geotags ['lat']); 153 $line = new LineString( 154 [ 155 $decodedPoint, 156 $point 157 ] 158 ); 159 $pages [] = ['id' => $id, 'distance' => (int)($line->greatCircleLength()), 160 'description' => p_get_metadata($id, 'description')['abstract'], 161 'lat' => $geotags ['lat'], 'lon' => $geotags ['lon']]; 162 } 163 } 164 165 // sort all the pages/media using distance 166 usort( 167 $pages, 168 static fn($a, $b) => strnatcmp($a ['distance'], $b ['distance']) 169 ); 170 usort( 171 $media, 172 static fn($a, $b) => strnatcmp($a ['distance'], $b ['distance']) 173 ); 174 175 return [ 176 'pages' => $pages, 177 'media' => $media, 178 'lat' => $decodedPoint->y(), 179 'lon' => $decodedPoint->x(), 180 'geohash' => $geohash, 181 'precision' => $this->precision [strlen($geohash)] 182 ]; 183 } 184} 185