Plugin Directory

source: leaflet-map/trunk/class.geocoder.php

Last change on this file was 3483997, checked in by bozdoz, 12 days ago

Update gsm/nominatim geocoder implementation with user agent support and fix cache bug (#287)

  • feat(geocoder): rewrite osm geocoder with new endpoint, user agent, and wp_remote_get
  • feat(geocoder): add accept-language based on gotcha tip in nominatim docs
  • fix(geocoder): throw exception on fail instead of polluting cache with false as an object
  • refactor(geocoder): don't reuse var in endpoint url
  • feat(geocoder): osm lazy cache self-healing
  • feat(plugin-option): add placeholder support to input text
  • feat(plugin-settings): add nominatim_contact_email support
  • feat(geocode): add leaflet_map_nominatim_contact_email filter
  • docs(readme): document setting and filter for the nominatim contact email sent in the geocoder user agent
  • fix(geocoder): only use site language otherwise the global cache can end up multilingual depending on the users language
File size: 8.7 KB
Line 
1<?php
2/**
3* Geocoder
4*
5* calls the specific geocoder function (chosen in admin or default: google_geocode)
6*
7*/
8
9class Leaflet_Geocoder {
10    /**
11    * Geocoder should return this on error/not found
12    * @var array $not_found
13    */
14    private $not_found = array('lat' => 0, 'lng' => 0);
15    /**
16    * Latitude
17    * @var float $lat
18    */
19    public $lat = 0;
20    /**
21    * Longitude
22    * @var float $lng
23    */
24    public $lng = 0;
25
26    /**
27    * new Geocoder from address
28    *
29    * handles url encoding and caching
30    *
31    * @param string $address the requested address to look up
32    * @return NOTHING
33    */
34    public function __construct ($address) {
35        $settings = Leaflet_Map_Plugin_Settings::init();
36        // trim all quotes (even smart) from address
37        $address = trim($address, '\'"”');
38        $address = urlencode( $address );
39       
40        $geocoder = $settings->get('geocoder');
41
42        $cached_address = 'leaflet_' . $geocoder . '_' . $address;
43
44        /* retrieve cached geocoded location */
45        $found_cache = get_option( $cached_address );
46
47        if ( $this->is_valid_cached_location( $found_cache ) ) {
48            $location = $found_cache;
49        } else {
50            if ( false !== $found_cache ) {
51                $this->remove_cache_key( $cached_address );
52            }
53
54            // try geocoding
55            $geocoding_method = $geocoder . '_geocode';
56
57            try {
58                $location = (Object) $this->$geocoding_method( $address );
59
60                if ( ! $this->is_valid_cached_location( $location ) ) {
61                    throw new Exception('Invalid geocoder response');
62                }
63
64                /* add location */
65                add_option($cached_address, $location);
66
67                /* add option key to locations for clean up purposes */
68                $locations = get_option('leaflet_geocoded_locations', array());
69                if ( ! in_array( $cached_address, $locations, true ) ) {
70                    array_push($locations, $cached_address);
71                    update_option('leaflet_geocoded_locations', $locations);
72                }
73            } catch (Exception $e) {
74                // failed
75                $location = $this->not_found;
76            }
77        }
78
79        if (isset($location->lat) && isset($location->lng)) {
80            $this->lat = $location->lat;
81            $this->lng = $location->lng;
82        }
83    }
84
85    /**
86    * Removes location caches
87    */
88    public static function remove_caches () {
89        $addresses = get_option('leaflet_geocoded_locations', array());
90        foreach ($addresses as $address) {
91            delete_option($address);
92        }
93        delete_option('leaflet_geocoded_locations');
94    }
95
96    /**
97    * Determines whether a cached geocode entry is valid.
98    *
99    * @param mixed $location Cached location value from WordPress options.
100    * @return bool
101    */
102    private function is_valid_cached_location( $location ) {
103        if ( ! is_object( $location ) ) {
104            return false;
105        }
106
107        if ( ! isset( $location->lat ) || ! isset( $location->lng ) ) {
108            return false;
109        }
110
111        return filter_var( $location->lat, FILTER_VALIDATE_FLOAT ) !== false
112            && filter_var( $location->lng, FILTER_VALIDATE_FLOAT ) !== false;
113    }
114
115    /**
116    * Removes a single geocode cache entry and its registry reference.
117    *
118    * @param string $cached_address Option key for the cached geocode.
119    * @return void
120    */
121    private function remove_cache_key( $cached_address ) {
122        delete_option( $cached_address );
123
124        $addresses = get_option( 'leaflet_geocoded_locations', array() );
125        $addresses = array_values(
126            array_filter(
127                $addresses,
128                function ( $address ) use ( $cached_address ) {
129                    return $address !== $cached_address;
130                }
131            )
132        );
133
134        if ( empty( $addresses ) ) {
135            delete_option( 'leaflet_geocoded_locations' );
136        } else {
137            update_option( 'leaflet_geocoded_locations', $addresses );
138        }
139    }
140
141    /**
142    * Used by geocoders to make requests via curl or file_get_contents
143    *
144    * includes a try/catch
145    *
146    * @param string $url    the urlencoded request url
147    * @return varies object from API or null (failed)
148    */
149    private function get_url( $url ) {
150        $referer = get_site_url();
151
152        if (in_array('curl', get_loaded_extensions())) {
153            /* try curl */
154            $ch = curl_init();
155
156            curl_setopt($ch, CURLOPT_AUTOREFERER, TRUE);
157            curl_setopt($ch, CURLOPT_HEADER, 0);
158                    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
159                    curl_setopt($ch, CURLOPT_REFERER, $referer);
160            curl_setopt($ch, CURLOPT_URL, $url);
161            curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE);
162
163            $data = curl_exec($ch);
164            curl_close($ch);
165
166            return $data;
167        } else if (ini_get('allow_url_fopen')) {
168                    /* try file get contents */
169
170                    $opts = array(
171                        'http' => array(
172                            'header' => array("Referer: $referer\r\n")
173                        )
174                    );
175                    $context = stream_context_create($opts);
176
177            return file_get_contents($url, false, $context);
178        }
179
180        $error_msg = 'Could not get url: ' . $url;
181        throw new Exception( $error_msg );
182    }
183
184    /**
185    * Google geocoder (https://developers.google.com/maps/documentation/geocoding/start)
186    *
187    * @param string $address    the urlencoded address to look up
188    * @return varies object from API or null (failed)
189    */
190
191    private function google_geocode ( $address ) {
192        // Leaflet_Map_Plugin_Settings
193        $settings = Leaflet_Map_Plugin_Settings::init();
194        $key = $settings->get('google_appkey');
195       
196        $geocode_url = 'https://maps.googleapis.com/maps/api/geocode/json?address=%s&key=%s';
197        $geocode_url = sprintf($geocode_url, $address, $key);
198       
199        $json = $this->get_url($geocode_url);
200        $json = json_decode($json);
201
202        /* found location */
203        if ($json->status == 'OK') {
204           
205            $location = $json->results[0]->geometry->location;
206
207            return (Object) $location;
208        }
209       
210        throw new Exception('No Address Found');
211    }
212
213    /**
214    * OpenStreetMap geocoder Nominatim (https://nominatim.openstreetmap.org/)
215    *
216    * @param string $address The URL-encoded address to look up.
217    * @return object Object containing lat and lng properties.
218    * @throws Exception When the request fails or no valid coordinates are returned.
219    */
220    private function osm_geocode( $address ) {
221        $request_url = sprintf(
222            'https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=%s',
223            $address
224        );
225
226        $accept_language = str_replace( '_', '-', get_locale() );
227       
228        $settings = Leaflet_Map_Plugin_Settings::init();
229        $contact_email = $settings->get( 'nominatim_contact_email' );
230
231        if ( empty( $contact_email ) ) {
232            $contact_email = get_bloginfo( 'admin_email' );
233        }
234
235        $contact_email = apply_filters( 'leaflet_map_nominatim_contact_email', $contact_email );
236
237        $agent = 'Nominatim query for ' . get_bloginfo( 'url' ) . '; contact ' . $contact_email;
238
239        $response = wp_remote_get(
240            $request_url,
241            array(
242                'user-agent' => $agent,
243                'headers' => array(
244                    'Accept-Language' => $accept_language,
245                ),
246            )
247        );
248
249        if ( ! is_wp_error( $response ) && isset( $response['body'] ) ) {
250            $json = json_decode( $response['body'] );
251
252            if ( isset( $json[0]->lat ) && isset( $json[0]->lon ) ) {
253                return (object) array(
254                    'lat' => $json[0]->lat,
255                    'lng' => $json[0]->lon,
256                );
257            }
258        }
259
260        throw new Exception('No Address Found');
261    }
262
263    /**
264     * TODO: does this still work?
265     * Danish Addresses Web Application
266     * (https://dawa.aws.dk)
267     *
268     * @param string $address    the urlencoded address to look up
269     * @return varies object from API or null (failed)
270     */
271    private function dawa_geocode ( $address ) {
272        $geocode_url = 'https://dawa.aws.dk/adresser?format=json&q=';
273        $geocode_url .= $address;
274        $json = $this->get_url($geocode_url);
275        $json = json_decode($json);
276       
277        /* found location */
278        return (Object) array(
279            'lat' => $json[0]->adgangsadresse->adgangspunkt->koordinater[1],
280            'lng' => $json[0]->adgangsadresse->adgangspunkt->koordinater[0]
281        );
282    }
283}
Note: See TracBrowser for help on using the repository browser.