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