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