Completed
Push — master ( 684184...9a35aa )
by Jeroen De
08:07
created

includes/Geocoders.php (1 issue)

Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace Maps;
4
5
use DataValues\Geo\Formatters\GeoCoordinateFormatter;
6
use DataValues\Geo\Parsers\GeoCoordinateParser;
7
use DataValues\Geo\Values\LatLongValue;
8
use MapsOldGeocoderAdapter;
9
use MWException;
10
use ValueParsers\ParseException;
11
12
/**
13
 * Class for geocoder functionality of the Maps extension.
14
 *
15
 * FIXME: this is procedural spaghetti
16
 *
17
 * @since 0.4
18
 *
19
 * @licence GNU GPL v2+
20
 * @author Jeroen De Dauw < [email protected] >
21
 */
22
final class Geocoders {
23
24
	/**
25
	 * Associative with geoservice identifiers as keys containing instances of
26
	 * the geocoder classes.
27
	 *
28
	 * Note: This list only contains the instances, so is not to be used for
29
	 * looping over all available services, as not all of them are guaranteed
30
	 * to have an instance already, use $registeredServices for this purpouse.
31
	 *
32
	 * @since 0.7
33
	 *
34
	 * @var Geocoder[]
35
	 */
36
	protected static $geocoders = [];
37
38
	/**
39
	 * Associative with geoservice identifiers as keys containing the class
40
	 * name of the geocoders. This is used for registration of a geocoder
41
	 * without immediately instantiating it.
42
	 *
43
	 * @since 0.7
44
	 *
45
	 * @var array of string => string
46
	 */
47
	public static $registeredGeocoders = [];
48
49
	/**
50
	 * The global geocoder cache, holding geocoded data when enabled.
51
	 *
52
	 * @since 0.7
53
	 *
54
	 * @var array
55
	 */
56
	private static $globalGeocoderCache = [];
57
58
	/**
59
	 * Can geocoding happen, ie are there any geocoders available.
60
	 *
61
	 * @since 1.0.3
62
	 * @var boolean
63
	 */
64
	protected static $canGeocode = false;
65
66
	/**
67
	 * Returns if this class can do geocoding operations.
68
	 * Ie. if there are any geocoders available.
69
	 *
70
	 * @since 0.7
71
	 *
72
	 * @return boolean
73
	 */
74
	public static function canGeocode() {
75
		self::init();
76
		return self::$canGeocode;
77
	}
78
79
	/**
80
	 * Gets a list of available geocoders.
81
	 *
82
	 * @since 1.0.3
83
	 *
84
	 * @return array
85
	 */
86
	public static function getAvailableGeocoders() {
87
		self::init();
88
		return array_keys( self::$registeredGeocoders );
89
	}
90
91
	/**
92
	 * Initiate the geocoding functionality.
93
	 *
94
	 * @since 1.0.3
95
	 *
96
	 * @return boolean Indicates if init happened
97
	 */
98
	public static function init() {
99
		static $initiated = false;
100
101
		if ( $initiated ) {
102
			return false;
103
		}
104
105
		$initiated = true;
106
107
		// Register the geocoders.
108
		\Hooks::run( 'GeocoderFirstCallInit' );
109
110
		// Determine if there are any geocoders.
111
		self::$canGeocode = count( self::$registeredGeocoders ) > 0;
112
113
		return true;
114
	}
115
116
	/**
117
	 * This function first determines whether the provided string is a pair or coordinates
118
	 * or an address. If it's the later, an attempt to geocode will be made. The function will
119
	 * return the coordinates or false, in case a geocoding attempt was made but failed.
120
	 *
121
	 * @since 0.7
122
	 *
123
	 * @param string $coordsOrAddress
124
	 * @param string $geoservice
125
	 * @param string|false $mappingService
126
	 * @param boolean $checkForCoords
127
	 *
128
	 * @return LatLongValue|false
129
	 */
130
	public static function attemptToGeocode( $coordsOrAddress, $geoservice = '', $mappingService = false, $checkForCoords = true ) {
131
		if ( $checkForCoords ) {
132
			$coordinateParser = new GeoCoordinateParser( new \ValueParsers\ParserOptions() );
133
134
			try {
135
				return $coordinateParser->parse( $coordsOrAddress );
136
			}
137
			catch ( ParseException $parseException ) {
138
				return self::geocode( $coordsOrAddress, $geoservice, $mappingService );
139
			}
140
		} else {
141
			return self::geocode( $coordsOrAddress, $geoservice, $mappingService );
142
		}
143
	}
144
145
	/**
146
	 *
147
	 *
148
	 * @since 0.7
149
	 *
150
	 * @param string $coordsOrAddress
151
	 * @param string $geoService
152
	 * @param string|false $mappingService
153
	 *
154
	 * @return boolean
155
	 */
156
	public static function isLocation( $coordsOrAddress, $geoService = '', $mappingService = false ) {
157
		return self::attemptToGeocode( $coordsOrAddress, $geoService, $mappingService ) !== false;
158
	}
159
160
	/**
161
	 * Geocodes an address with the provided geocoding service and returns the result
162
	 * as a string with the optionally provided format, or false when the geocoding failed.
163
	 *
164
	 * @since 0.7
165
	 *
166
	 * @param string $coordsOrAddress
167
	 * @param string $service
168
	 * @param string $mappingService
169
	 * @param boolean $checkForCoords
170
	 * @param string $targetFormat The notation to which they should be formatted. Defaults to floats.
171
	 * @param boolean $directional Indicates if the target notation should be directional. Defaults to false.
172
	 *
173
	 * @return string|false
174
	 */
175
	public static function attemptToGeocodeToString( $coordsOrAddress, $service = '', $mappingService = false, $checkForCoords = true, $targetFormat = Maps_COORDS_FLOAT, $directional = false ) {
176
		$geoCoordinate = self::attemptToGeocode( $coordsOrAddress, $service, $mappingService, $checkForCoords );
177
178
		if ( $geoCoordinate === false ) {
179
			return false;
180
		}
181
182
		$options = new \ValueFormatters\FormatterOptions( [
183
			GeoCoordinateFormatter::OPT_FORMAT => $targetFormat,
184
			GeoCoordinateFormatter::OPT_DIRECTIONAL => $directional,
185
			GeoCoordinateFormatter::OPT_PRECISION => 1 / 360000
186
		] );
187
188
		$formatter = new GeoCoordinateFormatter( $options );
189
		return $formatter->format( $geoCoordinate );
190
	}
191
192
	/**
193
	 * Geocodes an address with the provided geocoding service and returns the result
194
	 * as an array, or false when the geocoding failed.
195
	 *
196
	 * FIXME: complexity
197
	 *
198
	 * @since 0.7
199
	 *
200
	 * @param string $address
201
	 * @param string $geoService
202
	 * @param string|false $mappingService
203
	 *
204
	 * @return LatLongValue|false
205
	 * @throws MWException
206
	 */
207
	public static function geocode( $address, $geoService = '', $mappingService = false ) {
0 ignored issues
show
The parameter $mappingService is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
208
		if ( !is_string( $address ) ) {
209
			throw new MWException( 'Parameter $address must be a string at ' . __METHOD__ );
210
		}
211
212
		if ( !self::canGeocode() ) {
213
			return false;
214
		}
215
216
		$geocoder = self::getValidGeocoderInstance( $geoService );
217
218
		// This means there was no suitable geocoder found, so return false.
219
		if ( $geocoder === false ) {
220
			return false;
221
		}
222
223
		if ( $geocoder->hasGlobalCacheSupport() ) {
224
			$cacheResult = self::cacheRead( $address );
225
226
			// This means the cache returned an already computed set of coordinates.
227
			if ( $cacheResult !== false ) {
228
				assert( $cacheResult instanceof LatLongValue );
229
				return $cacheResult;
230
			}
231
		}
232
233
		$coordinates = self::getGeocoded( $geocoder, $address );
234
235
		if ( $coordinates === false ) {
236
			return false;
237
		}
238
239
		self::cacheWrite( $address, $coordinates );
240
241
		return $coordinates;
242
	}
243
244
	private static function getGeocoded( Geocoder $geocoder, $address ) {
245
		$coordinates = self::getGeocodedAsArray( $geocoder, $address );
246
247
		if ( $coordinates !== false ) {
248
			$coordinates = new LatLongValue(
249
				$coordinates['lat'],
250
				$coordinates['lon']
251
			);
252
		}
253
254
		return $coordinates;
255
	}
256
257
	private static function getGeocodedAsArray( Geocoder $geocoder, $address ) {
258
		// Do the actual geocoding via the geocoder.
259
		$coordinates = $geocoder->geocode( $address );
260
261
		// If there address could not be geocoded, and contains comma's, try again without the comma's.
262
		// This is cause several geocoding services such as geonames do not handle comma's well.
263
		if ( !$coordinates && strpos( $address, ',' ) !== false ) {
264
			$coordinates = $geocoder->geocode( str_replace( ',', '', $address ) );
265
		}
266
267
		return $coordinates;
268
	}
269
270
	/**
271
	 * Returns already coordinates already known from previous geocoding operations,
272
	 * or false if there is no match found in the cache.
273
	 *
274
	 * @since 0.7
275
	 *
276
	 * @param string $address
277
	 *
278
	 * @return LatLongValue|boolean false
279
	 */
280
	protected static function cacheRead( $address ) {
281
		global $egMapsEnableGeoCache;
282
283
		if ( $egMapsEnableGeoCache && array_key_exists( $address, self::$globalGeocoderCache ) ) {
284
			return self::$globalGeocoderCache[$address];
285
		}
286
		else {
287
			return false;
288
		}
289
	}
290
291
	/**
292
	 * Writes the geocoded result to the cache if the cache is on.
293
	 *
294
	 * @since 0.7
295
	 *
296
	 * @param string $address
297
	 * @param LatLongValue $coordinates
298
	 */
299
	protected static function cacheWrite( $address, LatLongValue $coordinates ) {
300
		global $egMapsEnableGeoCache;
301
302
		// Add the obtained coordinates to the cache when there is a result and the cache is enabled.
303
		if ( $egMapsEnableGeoCache && $coordinates ) {
304
			self::$globalGeocoderCache[$address] = $coordinates;
305
		}
306
	}
307
308
	/**
309
	 * Registers a geocoder linked to an identifier.
310
	 *
311
	 * @since 0.7
312
	 *
313
	 * @param string $geocoderIdentifier
314
	 * @param string|\Maps\Geocoders\Geocoder $geocoder
315
	 */
316
	public static function registerGeocoder( $geocoderIdentifier, $geocoder ) {
317
		self::$registeredGeocoders[$geocoderIdentifier] = $geocoder;
318
	}
319
320
	/**
321
	 * Returns the instance of the geocoder linked to the provided identifier
322
	 * or the default one when it's not valid. False is returned when there
323
	 * are no geocoders available.
324
	 *
325
	 * @since 0.7
326
	 *
327
	 * @param string $geocoderIdentifier
328
	 *
329
	 * @return Geocoder|bool
330
	 */
331
	protected static function getValidGeocoderInstance( $geocoderIdentifier ) {
332
		return self::getGeocoderInstance( self::getValidGeocoderIdentifier( $geocoderIdentifier ) );
333
	}
334
335
	/**
336
	 * Returns the instance of a geocoder. This function assumes there is a
337
	 * geocoder linked to the identifier you provide - if you are not sure
338
	 * it does, use getValidGeocoderInstance instead.
339
	 *
340
	 * @since 0.7
341
	 *
342
	 * @param string $geocoderIdentifier
343
	 *
344
	 * @return Geocoder|bool
345
	 * @throws MWException
346
	 */
347
	protected static function getGeocoderInstance( $geocoderIdentifier ) {
348
		if ( !array_key_exists( $geocoderIdentifier, self::$geocoders ) ) {
349
			if ( array_key_exists( $geocoderIdentifier, self::$registeredGeocoders ) ) {
350
				if ( is_string( self::$registeredGeocoders[$geocoderIdentifier] ) ) {
351
					$geocoder = new self::$registeredGeocoders[$geocoderIdentifier]( $geocoderIdentifier );
352
				}
353
				elseif ( self::$registeredGeocoders[$geocoderIdentifier] instanceof \Maps\Geocoders\Geocoder ) {
354
					$geocoder = new MapsOldGeocoderAdapter(
355
						self::$registeredGeocoders[$geocoderIdentifier],
356
						$geocoderIdentifier
357
					);
358
				}
359
				else {
360
					throw new MWException( 'Need either class name or Geocoder instance' );
361
				}
362
363
364
				self::$geocoders[$geocoderIdentifier] = $geocoder;
365
			}
366
			else {
367
				throw new MWException( 'There is geocoder linked to identifier ' . $geocoderIdentifier . '.' );
368
			}
369
		}
370
371
		return self::$geocoders[$geocoderIdentifier];
372
	}
373
374
	/**
375
	 * Returns a valid geocoder idenifier. If the given one is a valid main identifier,
376
	 * it will simply be returned. If it's an alias, it will be turned into the correponding
377
	 * main identifier. If it's not recognized at all (or empty), the default will be used.
378
	 * Only call this function when there are geocoders available, else an erro will be thrown.
379
	 *
380
	 * @since 0.7
381
	 *
382
	 * @param string $geocoderIdentifier
383
	 *
384
	 * @return string|bool
385
	 * @throws MWException
386
	 */
387
	protected static function getValidGeocoderIdentifier( $geocoderIdentifier ) {
388
		global $egMapsDefaultGeoService;
389
		static $validatedDefault = false;
390
391
		if ( $geocoderIdentifier === '' || !array_key_exists( $geocoderIdentifier, self::$registeredGeocoders ) ) {
392
			if ( !$validatedDefault ) {
393
				if ( !array_key_exists( $egMapsDefaultGeoService, self::$registeredGeocoders ) ) {
394
					$services = array_keys( self::$registeredGeocoders );
395
					$egMapsDefaultGeoService = array_shift( $services );
396
					if ( is_null( $egMapsDefaultGeoService ) ) {
397
						throw new MWException( 'Tried to geocode while there are no geocoders available at ' . __METHOD__  );
398
					}
399
				}
400
			}
401
402
			if ( array_key_exists( $egMapsDefaultGeoService, self::$registeredGeocoders ) ) {
403
				$geocoderIdentifier = $egMapsDefaultGeoService;
404
			}
405
			else {
406
				return false;
407
			}
408
		}
409
410
		return $geocoderIdentifier;
411
	}
412
413
}
414