1
|
|
|
<?php |
2
|
|
|
/* |
3
|
|
|
* Copyright (c) 2011-2017 Mark C. Prins <[email protected]> |
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
|
|
|
if (!defined('DOKU_INC')) { |
18
|
|
|
die (); |
19
|
|
|
} |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* DokuWiki Plugin spatialhelper (index component). |
23
|
|
|
* |
24
|
|
|
* @license BSD license |
25
|
|
|
* @author Mark Prins |
26
|
|
|
*/ |
27
|
|
|
class helper_plugin_spatialhelper_index extends DokuWiki_Plugin { |
28
|
|
|
/** |
29
|
|
|
* directory for index files. |
30
|
|
|
* |
31
|
|
|
* @var string |
32
|
|
|
*/ |
33
|
|
|
protected $idx_dir = ''; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* spatial index, well lookup list/array so we can do spatial queries. |
37
|
|
|
* entries should be: array("geohash" => {"id1","id3",}) |
38
|
|
|
* |
39
|
|
|
* @var array |
40
|
|
|
*/ |
41
|
|
|
protected $spatial_idx = array(); |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* handle to the geoPHP plugin. |
45
|
|
|
*/ |
46
|
|
|
protected $geophp; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* Constructor, initialises the spatial index. |
50
|
|
|
*/ |
51
|
|
|
public function __construct() { |
52
|
|
|
// parent::__construct (); |
|
|
|
|
53
|
|
View Code Duplication |
if (!$geophp = &plugin_load('helper', 'geophp')) { |
54
|
|
|
$message = 'helper_plugin_spatialhelper_index::spatialhelper_index: geophp plugin is not available.'; |
55
|
|
|
msg($message, - 1); |
56
|
|
|
return ""; |
|
|
|
|
57
|
|
|
} |
58
|
|
|
|
59
|
|
|
global $conf; |
60
|
|
|
$this->idx_dir = $conf ['indexdir']; |
61
|
|
|
// test if there is a spatialindex, if not build one for the wiki |
62
|
|
|
if (!@file_exists($this->idx_dir . '/spatial.idx')) { |
63
|
|
|
// creates and stores the index |
64
|
|
|
$this->generateSpatialIndex(); |
65
|
|
|
} else { |
66
|
|
|
$this->spatial_idx = unserialize(io_readFile($this->idx_dir . '/spatial.idx', false)); |
67
|
|
|
dbglog($this->spatial_idx, 'done loading spatial index'); |
68
|
|
|
} |
69
|
|
|
} |
70
|
|
|
|
71
|
|
|
/** |
72
|
|
|
* Update the spatial index for the page. |
73
|
|
|
* |
74
|
|
|
* @param string $id |
75
|
|
|
* the document ID |
76
|
|
|
*/ |
77
|
|
|
public function updateSpatialIndex($id) { |
78
|
|
|
$geotags = p_get_metadata($id, 'geo'); |
79
|
|
|
if (empty ($geotags)) { |
80
|
|
|
return false; |
81
|
|
|
} |
82
|
|
|
if (empty ($geotags ['lon']) || empty ($geotags ['lat'])) { |
83
|
|
|
return false; |
84
|
|
|
} |
85
|
|
|
dbglog($geotags, "Geo metadata found for page $id"); |
86
|
|
|
$geometry = new Point($geotags ['lon'], $geotags ['lat']); |
87
|
|
|
$geohash = $geometry->out('geohash'); |
88
|
|
|
dbglog('Update index for geohash: ' . $geohash); |
89
|
|
|
$succes = $this->_addToIndex($geohash, $id); |
|
|
|
|
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
/** |
93
|
|
|
* Looks up the geohash(es) for the document in the index. |
94
|
|
|
* |
95
|
|
|
* @param String $id |
96
|
|
|
* document ID |
97
|
|
|
* @param array $index |
98
|
|
|
* spatial index |
99
|
|
|
*/ |
100
|
|
|
public function findHashesForId($id, $index) { |
101
|
|
|
$hashes = array(); |
102
|
|
|
foreach ($index as $hash => $docIds) { |
103
|
|
|
if (in_array($id, $docIds, false)) { |
104
|
|
|
$hashes [] = $hash; |
105
|
|
|
} |
106
|
|
|
} |
107
|
|
|
dbglog($hashes, "Found the following hashes for $id (should only be 1)"); |
108
|
|
|
return $hashes; |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
/** |
112
|
|
|
* Deletes the page from the index. |
113
|
|
|
* |
114
|
|
|
* @param String $id |
115
|
|
|
* document ID |
116
|
|
|
*/ |
117
|
|
|
public function deleteFromIndex($id) { |
118
|
|
|
// check the index for document |
119
|
|
|
$knownHashes = $this->findHashesForId($id, $this->spatial_idx); |
120
|
|
|
if (empty ($knownHashes)) { |
121
|
|
|
return; |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
// TODO shortcut, need to make sure there is only one element, if not the index is corrupt |
125
|
|
|
$knownHash = $knownHashes [0]; |
126
|
|
|
$knownIds = $this->spatial_idx [$knownHash]; |
127
|
|
|
$i = array_search($id, $knownIds); |
128
|
|
|
dbglog("removing: $knownIds[$i] from the index."); |
129
|
|
|
unset ($knownIds [$i]); |
130
|
|
|
$this->spatial_idx [$knownHash] = $knownIds; |
131
|
|
|
if (empty ($this->spatial_idx [$knownHash])) { |
132
|
|
|
// dbglog ( "removing key: $knownHash from the index." ); |
|
|
|
|
133
|
|
|
unset ($this->spatial_idx [$knownHash]); |
134
|
|
|
} |
135
|
|
|
$succes = $this->_saveIndex(); |
|
|
|
|
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
/** |
139
|
|
|
* Save spatial index. |
140
|
|
|
*/ |
141
|
|
|
private function _saveIndex() { |
142
|
|
|
return io_saveFile($this->idx_dir . '/spatial.idx', serialize($this->spatial_idx)); |
143
|
|
|
} |
144
|
|
|
|
145
|
|
|
/** |
146
|
|
|
* (re-)Generates the spatial index by running through all the pages in the wiki. |
147
|
|
|
* |
148
|
|
|
* @todo add an option to erase the old index |
149
|
|
|
*/ |
150
|
|
|
public function generateSpatialIndex() { |
151
|
|
|
global $conf; |
152
|
|
|
require_once (DOKU_INC . 'inc/search.php'); |
153
|
|
|
$pages = array(); |
154
|
|
|
search($pages, $conf ['datadir'], 'search_allpages', array()); |
155
|
|
|
foreach ($pages as $page) { |
156
|
|
|
$this->updateSpatialIndex($page ['id']); |
157
|
|
|
} |
158
|
|
|
// media |
159
|
|
|
$media = array(); |
160
|
|
|
search($media, $conf ['mediadir'], 'search_media', array()); |
161
|
|
|
foreach ($media as $medium) { |
162
|
|
|
if ($medium ['isimg']) { |
163
|
|
|
$this->indexImage($medium); |
164
|
|
|
} |
165
|
|
|
} |
166
|
|
|
return true; |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
/** |
170
|
|
|
* Add an index entry for this file having EXIF / IPTC data. |
171
|
|
|
* |
172
|
|
|
* @param unknown_type $img Dokuwiki image |
173
|
|
|
* @see http://www.php.net/manual/en/function.iptcparse.php |
174
|
|
|
* @see http://php.net/manual/en/function.exif-read-data.php |
175
|
|
|
* |
176
|
|
|
* @return boolean true when image was succesfully added to the index. |
177
|
|
|
*/ |
178
|
|
|
public function indexImage($img) { |
179
|
|
|
// test for supported files (jpeg only) |
180
|
|
|
if ((substr($img ['file'], - strlen('.jpg')) !== '.jpg') and (substr($img ['file'], - strlen('.jpeg')) !== '.jpeg')) { |
181
|
|
|
return false; |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
$geometry = $this->getCoordsFromExif($img ['id']); |
185
|
|
|
if (!$geometry) { |
186
|
|
|
return false; |
187
|
|
|
} |
188
|
|
|
$geohash = $geometry->out('geohash'); |
189
|
|
|
// TODO truncate the geohash to something reasonable, otherwise they are |
190
|
|
|
// useless as an indexing mechanism eg. u1h73weckdrmskdqec3c9 is far too |
191
|
|
|
// precise, limit at ~9 as most GPS are not submeter accurate |
192
|
|
|
return $this->_addToIndex($geohash, 'media__' . $img ['id']); |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
/** |
196
|
|
|
* retrieve GPS decimal coordinates from exif. |
197
|
|
|
* |
198
|
|
|
* @param |
199
|
|
|
* $id |
200
|
|
|
* @return Point or false |
201
|
|
|
*/ |
202
|
|
|
public function getCoordsFromExif($id) { |
203
|
|
|
$exif = exif_read_data(mediaFN($id), 0, true); |
204
|
|
|
if (empty ($exif ['GPS'])) { |
205
|
|
|
return false; |
|
|
|
|
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
$lat = $this->convertDMStoD(array( |
209
|
|
|
$exif ['GPS'] ['GPSLatitude'] [0], |
210
|
|
|
$exif ['GPS'] ['GPSLatitude'] [1], |
211
|
|
|
$exif ['GPS'] ['GPSLatitude'] [2], |
212
|
|
|
$exif ['GPS'] ['GPSLatitudeRef'] |
213
|
|
|
)); |
214
|
|
|
|
215
|
|
|
$lon = $this->convertDMStoD(array( |
216
|
|
|
$exif ['GPS'] ['GPSLongitude'] [0], |
217
|
|
|
$exif ['GPS'] ['GPSLongitude'] [1], |
218
|
|
|
$exif ['GPS'] ['GPSLongitude'] [2], |
219
|
|
|
$exif ['GPS'] ['GPSLongitudeRef'] |
220
|
|
|
)); |
221
|
|
|
|
222
|
|
|
return new Point($lon, $lat); |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
/** |
226
|
|
|
* Store the hash/id entry in the index. |
227
|
|
|
* |
228
|
|
|
* @param string $geohash |
229
|
|
|
* @param string $id |
230
|
|
|
* page or media id |
231
|
|
|
* @return boolean true when succesful |
232
|
|
|
*/ |
233
|
|
|
private function _addToIndex($geohash, $id) { |
234
|
|
|
$pageIds = array(); |
235
|
|
|
// check index for key/geohash |
236
|
|
|
if (!array_key_exists($geohash, $this->spatial_idx)) { |
237
|
|
|
dbglog("Geohash $geohash not in index, just add $id."); |
238
|
|
|
$pageIds [] = $id; |
239
|
|
|
} else { |
240
|
|
|
dbglog('Geohash for document is in index, find it.'); |
241
|
|
|
// check the index for document |
242
|
|
|
$knownHashes = $this->findHashesForId($id, $this->spatial_idx); |
243
|
|
|
if (empty ($knownHashes)) { |
244
|
|
|
dbglog("No index record found for document $id, just add"); |
245
|
|
|
$pageIds = $this->spatial_idx [$geohash]; |
246
|
|
|
$pageIds [] = $id; |
247
|
|
|
} |
248
|
|
|
// TODO shortcut, need to make sure there is only one element, if not the index is corrupt |
249
|
|
|
$knownHash = $knownHashes [0]; |
250
|
|
|
|
251
|
|
|
if ($knownHash == $geohash) { |
252
|
|
|
dbglog("Document $id was found in index and has the same geohash, nothing to do."); |
253
|
|
|
return true; |
254
|
|
|
} |
255
|
|
|
|
256
|
|
|
if (!empty ($knownHash)) { |
257
|
|
|
dbglog("Document/media $id was found in index but has different geohash (it moved)."); |
258
|
|
|
$knownIds = $this->spatial_idx [$knownHash]; |
259
|
|
|
dbglog($knownIds, "Known id's for this hash:"); |
260
|
|
|
// remove it from the old geohash element |
261
|
|
|
$i = array_search($id, $knownIds); |
262
|
|
|
dbglog('Unsetting:' . $knownIds [$i]); |
263
|
|
|
unset ($knownIds [$i]); |
264
|
|
|
$this->spatial_idx [$knownHash] = $knownIds; |
265
|
|
|
// set on new geohash element |
266
|
|
|
$pageIds = $this->spatial_idx [$geohash]; |
267
|
|
|
$pageIds [] = $id; |
268
|
|
|
} |
269
|
|
|
} |
270
|
|
|
// store and save |
271
|
|
|
$this->spatial_idx [$geohash] = $pageIds; |
272
|
|
|
return $this->_saveIndex(); |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
/** |
276
|
|
|
* convert DegreesMinutesSeconds to Decimal degrees. |
277
|
|
|
* |
278
|
|
|
* @param array $param array of rational DMS |
279
|
|
|
* @return number |
280
|
|
|
*/ |
281
|
|
|
public function convertDMStoD($param) { |
282
|
|
|
if (!is_array($param)) { |
283
|
|
|
$param = array($param); |
284
|
|
|
} |
285
|
|
|
$deg = $this->convertRationaltoFloat($param [0]); |
286
|
|
|
$min = $this->convertRationaltoFloat($param [1]) / 60; |
287
|
|
|
$sec = $this->convertRationaltoFloat($param [2]) / 60 / 60; |
288
|
|
|
// Hemisphere (N, S, W or E) |
289
|
|
|
$hem = ($param [3] === 'N' || $param [3] === 'E') ? 1 : -1; |
290
|
|
|
return $hem * ($deg + $min + $sec); |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
public function convertRationaltoFloat($param) { |
294
|
|
|
// rational64u |
295
|
|
|
$nums = explode('/', $param); |
296
|
|
|
if (intval($nums[1]) > 0) { |
297
|
|
|
return intval($nums[0]) / intval($nums[1]); |
298
|
|
|
} else { |
299
|
|
|
return intval($nums[0]); |
300
|
|
|
} |
301
|
|
|
} |
302
|
|
|
} |
303
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.