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