| 1 | <?php |
|---|
| 2 | /** |
|---|
| 3 | * Geocoder |
|---|
| 4 | * |
|---|
| 5 | * calls the specific geocoder function (chosen in admin or default: google_geocode) |
|---|
| 6 | * |
|---|
| 7 | */ |
|---|
| 8 | |
|---|
| 9 | class 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 | } |
|---|