Completed
Push — address-as-title ( 9b6eeb...601934 )
by Peter
11:07
created

Geocoders::getValidGeocoderIdentifier()   C

Complexity

Conditions 7
Paths 8

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 7
eloc 15
c 1
b 1
f 0
nc 8
nop 1
dl 0
loc 25
rs 6.7272
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 MWException;
9
use ValueParsers\ParseException;
10
11
/**
12
 * Class for geocoder functionality of the Maps extension.
13
 *
14
 * FIXME: this is procedural spaghetti
15
 *
16
 * @since 0.4
17
 *
18
 * @licence GNU GPL v2+
19
 * @author Jeroen De Dauw < [email protected] >
20
 */
21
final class Geocoders {
22
23
	/**
24
	 * Associative with geoservice identifiers as keys containing instances of
25
	 * the geocoder classes.
26
	 *
27
	 * Note: This list only contains the instances, so is not to be used for
28
	 * looping over all available services, as not all of them are guaranteed
29
	 * to have an instance already, use $registeredServices for this purpouse.
30
	 *
31
	 * @since 0.7
32
	 *
33
	 * @var Geocoder[]
34
	 */
35
	protected static $geocoders = [];
36
37
	/**
38
	 * Associative with geoservice identifiers as keys containing the class
39
	 * name of the geocoders. This is used for registration of a geocoder
40
	 * without immediately instantiating it.
41
	 *
42
	 * @since 0.7
43
	 *
44
	 * @var array of string => string
45
	 */
46
	public static $registeredGeocoders = [];
47
48
	/**
49
	 * The global geocoder cache, holding geocoded data when enabled.
50
	 *
51
	 * @since 0.7
52
	 *
53
	 * @var array
54
	 */
55
	private static $globalGeocoderCache = [];
56
57
	/**
58
	 * Can geocoding happen, ie are there any geocoders available.
59
	 *
60
	 * @since 1.0.3
61
	 * @var boolean
62
	 */
63
	protected static $canGeocode = false;
64
65
	/**
66
	 * Returns if this class can do geocoding operations.
67
	 * Ie. if there are any geocoders available.
68
	 *
69
	 * @since 0.7
70
	 *
71
	 * @return boolean
72
	 */
73
	public static function canGeocode() {
74
		self::init();
75
		return self::$canGeocode;
76
	}
77
78
	/**
79
	 * Gets a list of available geocoders.
80
	 *
81
	 * @since 1.0.3
82
	 *
83
	 * @return array
84
	 */
85
	public static function getAvailableGeocoders() {
86
		self::init();
87
		return array_keys( self::$registeredGeocoders );
88
	}
89
90
	/**
91
	 * Initiate the geocoding functionality.
92
	 *
93
	 * @since 1.0.3
94
	 *
95
	 * @return boolean Indicates if init happened
96
	 */
97
	public static function init() {
98
		static $initiated = false;
99
100
		if ( $initiated ) {
101
			return false;
102
		}
103
104
		$initiated = true;
105
106
		// Register the geocoders.
107
		\Hooks::run( 'GeocoderFirstCallInit' );
108
109
		// Determine if there are any geocoders.
110
		self::$canGeocode = count( self::$registeredGeocoders ) > 0;
111
112
		return true;
113
	}
114
115
	/**
116
	 * This function first determines whether the provided string is a pair or coordinates
117
	 * or an address. If it's the later, an attempt to geocode will be made. The function will
118
	 * return the coordinates or false, in case a geocoding attempt was made but failed.
119
	 *
120
	 * @since 0.7
121
	 *
122
	 * @param string $coordsOrAddress
123
	 * @param string $geoservice
124
	 * @param string|false $mappingService
125
	 * @param boolean $checkForCoords
126
	 *
127
	 * @return LatLongValue|false
128
	 */
129
	public static function attemptToGeocode( $coordsOrAddress, $geoservice = '', $mappingService = false, $checkForCoords = true ) {
130
		if ( $checkForCoords ) {
131
			$coordinateParser = new GeoCoordinateParser( new \ValueParsers\ParserOptions() );
132
133
			try {
134
				return $coordinateParser->parse( $coordsOrAddress );
135
			}
136
			catch ( ParseException $parseException ) {
137
				return self::geocode( $coordsOrAddress, $geoservice, $mappingService );
138
			}
139
		} else {
140
			return self::geocode( $coordsOrAddress, $geoservice, $mappingService );
141
		}
142
	}
143
144
	/**
145
	 *
146
	 *
147
	 * @since 0.7
148
	 *
149
	 * @param string $coordsOrAddress
150
	 * @param string $geoService
151
	 * @param string|false $mappingService
152
	 *
153
	 * @return boolean
154
	 */
155
	public static function isLocation( $coordsOrAddress, $geoService = '', $mappingService = false ) {
156
		return self::attemptToGeocode( $coordsOrAddress, $geoService, $mappingService ) !== false;
157
	}
158
159
	/**
160
	 * Geocodes an address with the provided geocoding service and returns the result
161
	 * as a string with the optionally provided format, or false when the geocoding failed.
162
	 *
163
	 * @since 0.7
164
	 *
165
	 * @param string $coordsOrAddress
166
	 * @param string $service
167
	 * @param string $mappingService
168
	 * @param boolean $checkForCoords
169
	 * @param string $targetFormat The notation to which they should be formatted. Defaults to floats.
170
	 * @param boolean $directional Indicates if the target notation should be directional. Defaults to false.
171
	 *
172
	 * @return string|false
173
	 */
174
	public static function attemptToGeocodeToString( $coordsOrAddress, $service = '', $mappingService = false, $checkForCoords = true, $targetFormat = Maps_COORDS_FLOAT, $directional = false ) {
175
		$geoCoordinate = self::attemptToGeocode( $coordsOrAddress, $service, $mappingService, $checkForCoords );
176
177
		if ( $geoCoordinate === false ) {
178
			return false;
179
		}
180
181
		$options = new \ValueFormatters\FormatterOptions( [
182
			GeoCoordinateFormatter::OPT_FORMAT => $targetFormat,
183
			GeoCoordinateFormatter::OPT_DIRECTIONAL => $directional,
184
			GeoCoordinateFormatter::OPT_PRECISION => 1 / 360000
185
		] );
186
187
		$formatter = new GeoCoordinateFormatter( $options );
188
		return $formatter->format( $geoCoordinate );
189
	}
190
191
	/**
192
	 * Geocodes an address with the provided geocoding service and returns the result
193
	 * as an array, or false when the geocoding failed.
194
	 *
195
	 * FIXME: complexity
196
	 *
197
	 * @since 0.7
198
	 *
199
	 * @param string $address
200
	 * @param string $geoService
201
	 * @param string|false $mappingService
202
	 *
203
	 * @return LatLongValue|false
204
	 * @throws MWException
205
	 */
206
	public static function geocode( $address, $geoService = '', $mappingService = false ) {
0 ignored issues
show
Unused Code introduced by
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...
207
		if ( !is_string( $address ) ) {
208
			throw new MWException( 'Parameter $address must be a string at ' . __METHOD__ );
209
		}
210
211
		if ( !self::canGeocode() ) {
212
			return false;
213
		}
214
215
		$geocoder = self::getValidGeocoderInstance( $geoService );
216
217
		// This means there was no suitable geocoder found, so return false.
218
		if ( $geocoder === false ) {
219
			return false;
220
		}
221
222
		if ( $geocoder->hasGlobalCacheSupport() ) {
223
			$cacheResult = self::cacheRead( $address );
224
225
			// This means the cache returned an already computed set of coordinates.
226
			if ( $cacheResult !== false ) {
227
				assert( $cacheResult instanceof LatLongValue );
228
				return $cacheResult;
229
			}
230
		}
231
232
		$coordinates = self::getGeocoded( $geocoder, $address );
233
234
		if ( $coordinates === false ) {
235
			return false;
236
		}
237
238
		self::cacheWrite( $address, $coordinates );
239
240
		return $coordinates;
241
	}
242
243
	private static function getGeocoded( Geocoder $geocoder, $address ) {
244
		$coordinates = self::getGeocodedAsArray( $geocoder, $address );
245
246
		if ( $coordinates !== false ) {
247
			$coordinates = new LatLongValue(
248
				$coordinates['lat'],
249
				$coordinates['lon']
250
			);
251
		}
252
253
		return $coordinates;
254
	}
255
256
	private static function getGeocodedAsArray( Geocoder $geocoder, $address ) {
257
		// Do the actual geocoding via the geocoder.
258
		$coordinates = $geocoder->geocode( $address );
259
260
		// If there address could not be geocoded, and contains comma's, try again without the comma's.
261
		// This is cause several geocoding services such as geonames do not handle comma's well.
262
		if ( !$coordinates && strpos( $address, ',' ) !== false ) {
263
			$coordinates = $geocoder->geocode( str_replace( ',', '', $address ) );
264
		}
265
266
		return $coordinates;
267
	}
268
269
	/**
270
	 * Returns already coordinates already known from previous geocoding operations,
271
	 * or false if there is no match found in the cache.
272
	 *
273
	 * @since 0.7
274
	 *
275
	 * @param string $address
276
	 *
277
	 * @return LatLongValue|boolean false
278
	 */
279
	protected static function cacheRead( $address ) {
280
		global $egMapsEnableGeoCache;
281
282
		if ( $egMapsEnableGeoCache && array_key_exists( $address, self::$globalGeocoderCache ) ) {
283
			return self::$globalGeocoderCache[$address];
284
		}
285
		else {
286
			return false;
287
		}
288
	}
289
290
	/**
291
	 * Writes the geocoded result to the cache if the cache is on.
292
	 *
293
	 * @since 0.7
294
	 *
295
	 * @param string $address
296
	 * @param LatLongValue $coordinates
297
	 */
298
	protected static function cacheWrite( $address, LatLongValue $coordinates ) {
299
		global $egMapsEnableGeoCache;
300
301
		// Add the obtained coordinates to the cache when there is a result and the cache is enabled.
302
		if ( $egMapsEnableGeoCache && $coordinates ) {
303
			self::$globalGeocoderCache[$address] = $coordinates;
304
		}
305
	}
306
307
	/**
308
	 * Registers a geocoder linked to an identifier.
309
	 *
310
	 * @since 0.7
311
	 *
312
	 * @param string $geocoderIdentifier
313
	 * @param string $geocoderClassName
314
	 */
315
	public static function registerGeocoder( $geocoderIdentifier, $geocoderClassName ) {
316
		self::$registeredGeocoders[$geocoderIdentifier] = $geocoderClassName;
317
	}
318
319
	/**
320
	 * Returns the instance of the geocoder linked to the provided identifier
321
	 * or the default one when it's not valid. False is returned when there
322
	 * are no geocoders available.
323
	 *
324
	 * @since 0.7
325
	 *
326
	 * @param string $geocoderIdentifier
327
	 *
328
	 * @return Geocoder|bool
329
	 */
330
	protected static function getValidGeocoderInstance( $geocoderIdentifier ) {
331
		return self::getGeocoderInstance( self::getValidGeocoderIdentifier( $geocoderIdentifier ) );
332
	}
333
334
	/**
335
	 * Returns the instance of a geocoder. This function assumes there is a
336
	 * geocoder linked to the identifier you provide - if you are not sure
337
	 * it does, use getValidGeocoderInstance instead.
338
	 *
339
	 * @since 0.7
340
	 *
341
	 * @param string $geocoderIdentifier
342
	 *
343
	 * @return Geocoder|bool
344
	 * @throws MWException
345
	 */
346
	protected static function getGeocoderInstance( $geocoderIdentifier ) {
347
		if ( !array_key_exists( $geocoderIdentifier, self::$geocoders ) ) {
348
			if ( array_key_exists( $geocoderIdentifier, self::$registeredGeocoders ) ) {
349
				$geocoder = new self::$registeredGeocoders[$geocoderIdentifier]( $geocoderIdentifier );
350
351
				//if ( $service instanceof iMappingService ) {
352
					self::$geocoders[$geocoderIdentifier] = $geocoder;
353
				//}
354
				//else {
355
				//	throw new MWException( 'The geocoder linked to identifier ' . $geocoderIdentifier . ' does not implement .' );
356
				//}
357
			}
358
			else {
359
				throw new MWException( 'There is geocoder linked to identifier ' . $geocoderIdentifier . '.' );
360
			}
361
		}
362
363
		return self::$geocoders[$geocoderIdentifier];
364
	}
365
366
	/**
367
	 * Returns a valid geocoder idenifier. If the given one is a valid main identifier,
368
	 * it will simply be returned. If it's an alias, it will be turned into the correponding
369
	 * main identifier. If it's not recognized at all (or empty), the default will be used.
370
	 * Only call this function when there are geocoders available, else an erro will be thrown.
371
	 *
372
	 * @since 0.7
373
	 *
374
	 * @param string $geocoderIdentifier
375
	 *
376
	 * @return string|bool
377
	 * @throws MWException
378
	 */
379
	protected static function getValidGeocoderIdentifier( $geocoderIdentifier ) {
380
		global $egMapsDefaultGeoService;
381
		static $validatedDefault = false;
382
383
		if ( $geocoderIdentifier === '' || !array_key_exists( $geocoderIdentifier, self::$registeredGeocoders ) ) {
384
			if ( !$validatedDefault ) {
385
				if ( !array_key_exists( $egMapsDefaultGeoService, self::$registeredGeocoders ) ) {
386
					$services = array_keys( self::$registeredGeocoders );
387
					$egMapsDefaultGeoService = array_shift( $services );
388
					if ( is_null( $egMapsDefaultGeoService ) ) {
389
						throw new MWException( 'Tried to geocode while there are no geocoders available at ' . __METHOD__  );
390
					}
391
				}
392
			}
393
394
			if ( array_key_exists( $egMapsDefaultGeoService, self::$registeredGeocoders ) ) {
395
				$geocoderIdentifier = $egMapsDefaultGeoService;
396
			}
397
			else {
398
				return false;
399
			}
400
		}
401
402
		return $geocoderIdentifier;
403
	}
404
405
}
406