1<?php
2
3/*
4 * Copyright (c) 2011-2023 Mark C. Prins <mprins@users.sf.net>
5 *
6 * Permission to use, copy, modify, and distribute this software for any
7 * purpose with or without fee is hereby granted, provided that the above
8 * copyright notice and this permission notice appear in all copies.
9 *
10 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17 */
18
19use dokuwiki\Extension\Plugin;
20use dokuwiki\Logger;
21use geoPHP\Adapter\GeoHash;
22use geoPHP\Geometry\LineString;
23use geoPHP\Geometry\Point;
24
25/**
26 * DokuWiki Plugin spatialhelper (Search component).
27 *
28 * @license BSD license
29 * @author  Mark Prins
30 */
31class helper_plugin_spatialhelper_search extends Plugin
32{
33    /**
34     * spatial index.
35     *
36     * @var array
37     */
38    protected $spatial_idx = [];
39    /**
40     * Precision, Distance of Adjacent Cell in Meters.
41     *
42     * @see https://stackoverflow.com/questions/13836416/geohash-and-max-distance
43     *
44     * @var float
45     */
46    private $precision = [5_003_530, 625441, 123264, 19545, 3803, 610, 118, 19, 3.7, 0.6];
47
48    /**
49     * constructor; initialize/load spatial index.
50     */
51    public function __construct()
52    {
53        global $conf;
54
55        if (plugin_load('helper', 'geophp', false, true) === null) {
56            $message =
57                'helper_plugin_spatialhelper_search::spatialhelper_search: required geophp plugin is not available.';
58            msg($message, -1);
59        }
60
61        $idx_dir = $conf ['indexdir'];
62        if (!@file_exists($idx_dir . '/spatial.idx')) {
63            plugin_load('helper', 'spatialhelper_index');
64        }
65
66        $this->spatial_idx = unserialize(io_readFile($idx_dir . '/spatial.idx', false), ['allowed_classes' => false]);
67    }
68
69    /**
70     * Find locations based on the coordinate pair.
71     *
72     * @param float $lat
73     *          The y coordinate (or latitude)
74     * @param float $lon
75     *          The x coordinate (or longitude)
76     * @throws Exception
77     */
78    final public function findNearbyLatLon(float $lat, float $lon): array
79    {
80        $geometry = new Point($lon, $lat);
81        return $this->findNearby($geometry->out('geohash'), $geometry);
82    }
83
84    /**
85     * finds nearby elements in the index based on the geohash.
86     * returns a list of documents and the bunding box.
87     *
88     * @param string $geohash
89     * @param Point|null $p
90     *          optional point
91     * @return array of ...
92     * @throws Exception
93     */
94    final public function findNearby(string $geohash, Point $p = null): array
95    {
96        $_geohashClass = new Geohash();
97        if (!$p instanceof Point) {
98            $decodedPoint = $_geohashClass->read($geohash);
99        } else {
100            $decodedPoint = $p;
101        }
102
103        // find adjacent blocks
104        $adjacent = [];
105        $adjacent ['center'] = $geohash;
106        $adjacent ['top'] = Geohash::adjacent($adjacent ['center'], 'top');
107        $adjacent ['bottom'] = Geohash::adjacent($adjacent ['center'], 'bottom');
108        $adjacent ['right'] = Geohash::adjacent($adjacent ['center'], 'right');
109        $adjacent ['left'] = Geohash::adjacent($adjacent ['center'], 'left');
110        $adjacent ['topleft'] = Geohash::adjacent($adjacent ['left'], 'top');
111        $adjacent ['topright'] = Geohash::adjacent($adjacent ['right'], 'top');
112        $adjacent ['bottomright'] = Geohash::adjacent($adjacent ['right'], 'bottom');
113        $adjacent ['bottomleft'] = Geohash::adjacent($adjacent ['left'], 'bottom');
114        Logger::debug("adjacent geo hashes", $adjacent);
115
116        // find all the pages in the index that overlap with the adjacent hashes
117        $docIds = [];
118        foreach ($adjacent as $adjHash) {
119            if (is_array($this->spatial_idx)) {
120                foreach ($this->spatial_idx as $_geohash => $_docIds) {
121                    if (strpos($_geohash, (string)$adjHash) !== false) {
122                        // if $adjHash similar to geohash
123                        $docIds = array_merge($docIds, $_docIds);
124                    }
125                }
126            }
127        }
128        $docIds = array_unique($docIds);
129        Logger::debug("found docIDs", $docIds);
130
131        // create associative array of pages + calculate distance
132        $pages = [];
133        $media = [];
134        $indexer = plugin_load('helper', 'spatialhelper_index');
135
136        foreach ($docIds as $id) {
137            if (strpos($id, 'media__') === 0) {
138                $id = substr($id, strlen('media__'));
139                if (auth_quickaclcheck($id) >= /*AUTH_READ*/ 1) {
140                    $point = $indexer->getCoordsFromExif($id);
141                    $line = new LineString(
142                        [
143                            $decodedPoint,
144                            $point
145                        ]
146                    );
147                    $media [] = ['id' => $id, 'distance' => (int)($line->greatCircleLength()),
148                        'lat' => $point->y(), 'lon' => $point->x()];
149                }
150            } elseif (auth_quickaclcheck($id) >= /*AUTH_READ*/ 1) {
151                $geotags = p_get_metadata($id, 'geo');
152                $point = new Point($geotags ['lon'], $geotags ['lat']);
153                $line = new LineString(
154                    [
155                        $decodedPoint,
156                        $point
157                    ]
158                );
159                $pages [] = ['id' => $id, 'distance' => (int)($line->greatCircleLength()),
160                    'description' => p_get_metadata($id, 'description')['abstract'],
161                    'lat' => $geotags ['lat'], 'lon' => $geotags ['lon']];
162            }
163        }
164
165        // sort all the pages/media using distance
166        usort(
167            $pages,
168            static fn($a, $b) => strnatcmp($a ['distance'], $b ['distance'])
169        );
170        usort(
171            $media,
172            static fn($a, $b) => strnatcmp($a ['distance'], $b ['distance'])
173        );
174
175        return [
176            'pages' => $pages,
177            'media' => $media,
178            'lat' => $decodedPoint->y(),
179            'lon' => $decodedPoint->x(),
180            'geohash' => $geohash,
181            'precision' => $this->precision [strlen($geohash)]
182        ];
183    }
184}
185