Completed
Push — master ( 8f3dec...787392 )
by Mark
18s queued 12s
created

helper_plugin_spatialhelper_index::saveIndex()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
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
18
/**
19
 * DokuWiki Plugin spatialhelper (index component).
20
 *
21
 * @license BSD license
22
 * @author Mark Prins
23
 */
24
class helper_plugin_spatialhelper_index extends DokuWiki_Plugin {
25
    /**
26
     * directory for index files.
27
     *
28
     * @var string
29
     */
30
    protected $idx_dir = '';
31
32
    /**
33
     * spatial index, well lookup list/array so we can do spatial queries.
34
     * entries should be: array("geohash" => {"id1","id3",})
35
     *
36
     * @var array
37
     */
38
    protected $spatial_idx = array();
39
40
    /**
41
     * handle to the geoPHP plugin.
42
     */
43
    protected $geophp;
44
45
    /**
46
     * Constructor, initialises the spatial index.
47
     */
48
    public function __construct() {
49 View Code Duplication
        if (!$geophp = &plugin_load('helper', 'geophp')) {
50
            $message = 'helper_plugin_spatialhelper_index::spatialhelper_index: geophp plugin is not available.';
51
            msg($message, - 1);
52
            return "";
0 ignored issues
show
Bug introduced by
Constructors do not have meaningful return values, anything that is returned from here is discarded. Are you sure this is correct?
Loading history...
53
        }
54
55
        global $conf;
56
        $this->idx_dir = $conf ['indexdir'];
57
        // test if there is a spatialindex, if not build one for the wiki
58
        if (!@file_exists($this->idx_dir . '/spatial.idx')) {
59
            // creates and stores the index
60
            $this->generateSpatialIndex();
61
        } else {
62
            $this->spatial_idx = unserialize(io_readFile($this->idx_dir . '/spatial.idx', false));
63
            dbglog($this->spatial_idx, 'done loading spatial index');
64
        }
65
    }
66
67
    /**
68
     * Update the spatial index for the page.
69
     *
70
     * @param string $id
71
     *          the document ID
72
     */
73
    public function updateSpatialIndex($id) {
74
        $geotags = p_get_metadata($id, 'geo');
75
        if (empty ($geotags)) {
76
            return false;
77
        }
78
        if (empty ($geotags ['lon']) || empty ($geotags ['lat'])) {
79
            return false;
80
        }
81
        dbglog($geotags, "Geo metadata found for page $id");
82
        $geometry = new Point($geotags ['lon'], $geotags ['lat']);
83
        $geohash = $geometry->out('geohash');
84
        dbglog('Update index for geohash: ' . $geohash);
85
        $succes = $this->addToIndex($geohash, $id);
0 ignored issues
show
Unused Code introduced by
$succes is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
86
    }
87
88
    /**
89
     * Looks up the geohash(es) for the document in the index.
90
     *
91
     * @param String $id
92
     *          document ID
93
     * @param array $index
94
     *          spatial index
95
     */
96
    public function findHashesForId($id, $index) {
97
        $hashes = array();
98
        foreach ($index as $hash => $docIds) {
99
            if (in_array($id, $docIds, false)) {
100
                $hashes [] = $hash;
101
            }
102
        }
103
        dbglog($hashes, "Found the following hashes for $id (should only be 1)");
104
        return $hashes;
105
    }
106
107
    /**
108
     * Deletes the page from the index.
109
     *
110
     * @param String $id
111
     *          document ID
112
     */
113
    public function deleteFromIndex($id) {
114
        // check the index for document
115
        $knownHashes = $this->findHashesForId($id, $this->spatial_idx);
116
        if (empty ($knownHashes)) {
117
            return;
118
        }
119
120
        // TODO shortcut, need to make sure there is only one element, if not the index is corrupt
121
        $knownHash = $knownHashes [0];
122
        $knownIds = $this->spatial_idx [$knownHash];
123
        $i = array_search($id, $knownIds);
124
        dbglog("removing: $knownIds[$i] from the index.");
125
        unset ($knownIds [$i]);
126
        $this->spatial_idx [$knownHash] = $knownIds;
127
        if (empty ($this->spatial_idx [$knownHash])) {
128
            // dbglog ( "removing key: $knownHash from the index." );
129
            unset ($this->spatial_idx [$knownHash]);
130
        }
131
        $succes = $this->saveIndex();
0 ignored issues
show
Unused Code introduced by
$succes is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
132
    }
133
134
    /**
135
     * Save spatial index.
136
     */
137
    private function saveIndex() {
138
        return io_saveFile($this->idx_dir . '/spatial.idx', serialize($this->spatial_idx));
139
    }
140
141
    /**
142
     * (re-)Generates the spatial index by running through all the pages in the wiki.
143
     *
144
     * @todo add an option to erase the old index
145
     */
146
    public function generateSpatialIndex() {
147
        global $conf;
148
        require_once (DOKU_INC . 'inc/search.php');
149
        $pages = array();
150
        search($pages, $conf ['datadir'], 'search_allpages', array());
151
        foreach ($pages as $page) {
152
            $this->updateSpatialIndex($page ['id']);
153
        }
154
        // media
155
        $media = array();
156
        search($media, $conf ['mediadir'], 'search_media', array());
157
        foreach ($media as $medium) {
158
            if ($medium ['isimg']) {
159
                $this->indexImage($medium);
160
            }
161
        }
162
        return true;
163
    }
164
165
    /**
166
     * Add an index entry for this file having EXIF / IPTC data.
167
     *
168
     * @param unknown_type $img Dokuwiki image
169
     * @see http://www.php.net/manual/en/function.iptcparse.php
170
     * @see http://php.net/manual/en/function.exif-read-data.php
171
     *
172
     * @return boolean true when image was succesfully added to the index.
173
     */
174
    public function indexImage($img) {
175
        // test for supported files (jpeg only)
176
        if (
177
            (substr($img ['file'], - strlen('.jpg')) !== '.jpg') and
178
            (substr($img ['file'], - strlen('.jpeg')) !== '.jpeg')) {
179
            return false;
180
        }
181
182
        $geometry = $this->getCoordsFromExif($img ['id']);
183
        if (!$geometry) {
184
            return false;
185
        }
186
        $geohash = $geometry->out('geohash');
187
        // TODO truncate the geohash to something reasonable, otherwise they are
188
        // useless as an indexing mechanism eg. u1h73weckdrmskdqec3c9 is far too
189
        // precise, limit at ~9 as most GPS are not submeter accurate
190
        return $this->addToIndex($geohash, 'media__' . $img ['id']);
191
    }
192
193
    /**
194
     * retrieve GPS decimal coordinates from exif.
195
     *
196
     * @param
197
     *          $id
198
     * @return Point or false
199
     */
200
    public function getCoordsFromExif($id) {
201
        $exif = exif_read_data(mediaFN($id), 0, true);
202
        if (empty ($exif ['GPS'])) {
203
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by helper_plugin_spatialhel...ndex::getCoordsFromExif of type Point.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
204
        }
205
206
        $lat = $this->convertDMStoD(array(
207
                $exif ['GPS'] ['GPSLatitude'] [0],
208
                $exif ['GPS'] ['GPSLatitude'] [1],
209
                $exif ['GPS'] ['GPSLatitude'] [2],
210
                $exif ['GPS'] ['GPSLatitudeRef']
211
        ));
212
213
        $lon = $this->convertDMStoD(array(
214
                $exif ['GPS'] ['GPSLongitude'] [0],
215
                $exif ['GPS'] ['GPSLongitude'] [1],
216
                $exif ['GPS'] ['GPSLongitude'] [2],
217
                $exif ['GPS'] ['GPSLongitudeRef']
218
        ));
219
220
        return new Point($lon, $lat);
221
    }
222
223
    /**
224
     * Store the hash/id entry in the index.
225
     *
226
     * @param string $geohash
227
     * @param string $id
228
     *          page or media id
229
     * @return boolean true when succesful
230
     */
231
    private function addToIndex($geohash, $id) {
232
        $pageIds = array();
233
        // check index for key/geohash
234
        if (!array_key_exists($geohash, $this->spatial_idx)) {
235
            dbglog("Geohash $geohash not in index, just add $id.");
236
            $pageIds [] = $id;
237
        } else {
238
            dbglog('Geohash for document is in index, find it.');
239
            // check the index for document
240
            $knownHashes = $this->findHashesForId($id, $this->spatial_idx);
241
            if (empty ($knownHashes)) {
242
                dbglog("No index record found for document $id, just add");
243
                $pageIds = $this->spatial_idx [$geohash];
244
                $pageIds [] = $id;
245
            }
246
            // TODO shortcut, need to make sure there is only one element, if not the index is corrupt
247
            $knownHash = $knownHashes [0];
248
249
            if ($knownHash == $geohash) {
250
                dbglog("Document $id was found in index and has the same geohash, nothing to do.");
251
                return true;
252
            }
253
254
            if (!empty ($knownHash)) {
255
                dbglog("Document/media $id was found in index but has different geohash (it moved).");
256
                $knownIds = $this->spatial_idx [$knownHash];
257
                dbglog($knownIds, "Known id's for this hash:");
258
                // remove it from the old geohash element
259
                $i = array_search($id, $knownIds);
260
                dbglog('Unsetting:' . $knownIds [$i]);
261
                unset ($knownIds [$i]);
262
                $this->spatial_idx [$knownHash] = $knownIds;
263
                // set on new geohash element
264
                $pageIds = $this->spatial_idx [$geohash];
265
                $pageIds [] = $id;
266
            }
267
        }
268
        // store and save
269
        $this->spatial_idx [$geohash] = $pageIds;
270
        return $this->saveIndex();
271
    }
272
273
    /**
274
     * convert DegreesMinutesSeconds to Decimal degrees.
275
     *
276
     * @param array $param array of rational DMS
277
     * @return number
278
     */
279
    public function convertDMStoD($param) {
280
        if (!is_array($param)) {
281
            $param = array($param);
282
        }
283
        $deg = $this->convertRationaltoFloat($param [0]);
284
        $min = $this->convertRationaltoFloat($param [1]) / 60;
285
        $sec = $this->convertRationaltoFloat($param [2]) / 60 / 60;
286
        // Hemisphere (N, S, W or E)
287
        $hem = ($param [3] === 'N' || $param [3] === 'E') ? 1 : -1;
288
        return $hem * ($deg + $min + $sec);
289
    }
290
291
    public function convertRationaltoFloat($param) {
292
        // rational64u
293
        $nums = explode('/', $param);
294
        if (intval($nums[1]) > 0) {
295
            return intval($nums[0]) / intval($nums[1]);
296
        } else {
297
            return intval($nums[0]);
298
        }
299
    }
300
}
301