Completed
Push — rmdc ( badd50 )
by Jeroen De
03:20
created

Geocoders::attemptToGeocodeToString()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 10
nc 2
nop 6
dl 0
loc 16
ccs 0
cts 11
cp 0
crap 6
rs 9.4285
c 0
b 0
f 0
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
	 * Geocodes an address with the provided geocoding service and returns the result
147
	 * as an array, or false when the geocoding failed.
148
	 *
149
	 * @since 0.7
150
	 *
151
	 * @param string $address
152
	 * @param string $geoService
153
	 * @param string|false $mappingService
154
	 *
155
	 * @return LatLongValue|false
156
	 * @throws MWException
157
	 */
158
	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...
159
		if ( !is_string( $address ) ) {
160
			throw new MWException( 'Parameter $address must be a string at ' . __METHOD__ );
161
		}
162
163
		if ( !self::canGeocode() ) {
164
			return false;
165
		}
166
167
		$geocoder = self::getValidGeocoderInstance( $geoService );
168
169
		// This means there was no suitable geocoder found, so return false.
170
		if ( $geocoder === false ) {
171
			return false;
172
		}
173
174
		if ( $geocoder->hasGlobalCacheSupport() ) {
175
			$cacheResult = self::cacheRead( $address );
176
177
			// This means the cache returned an already computed set of coordinates.
178
			if ( $cacheResult !== false ) {
179
				assert( $cacheResult instanceof LatLongValue );
180
				return $cacheResult;
181
			}
182
		}
183
184
		$coordinates = self::getGeocoded( $geocoder, $address );
185
186
		if ( $coordinates === false ) {
187
			return false;
188
		}
189
190
		self::cacheWrite( $address, $coordinates );
191
192
		return $coordinates;
193
	}
194
195
	private static function getGeocoded( Geocoder $geocoder, $address ) {
196
		$coordinates = self::getGeocodedAsArray( $geocoder, $address );
197
198
		if ( $coordinates !== false ) {
199
			$coordinates = new LatLongValue(
200
				$coordinates['lat'],
201
				$coordinates['lon']
202
			);
203
		}
204
205
		return $coordinates;
206
	}
207
208
	private static function getGeocodedAsArray( Geocoder $geocoder, $address ) {
209
		// Do the actual geocoding via the geocoder.
210
		$coordinates = $geocoder->geocode( $address );
211
212
		// If there address could not be geocoded, and contains comma's, try again without the comma's.
213
		// This is cause several geocoding services such as geonames do not handle comma's well.
214
		if ( !$coordinates && strpos( $address, ',' ) !== false ) {
215
			$coordinates = $geocoder->geocode( str_replace( ',', '', $address ) );
216
		}
217
218
		return $coordinates;
219
	}
220
221
	/**
222
	 * Returns already coordinates already known from previous geocoding operations,
223
	 * or false if there is no match found in the cache.
224
	 *
225
	 * @since 0.7
226
	 *
227
	 * @param string $address
228
	 *
229
	 * @return LatLongValue|boolean false
230
	 */
231
	private static function cacheRead( $address ) {
232
		global $egMapsEnableGeoCache;
233
234
		if ( $egMapsEnableGeoCache && array_key_exists( $address, self::$globalGeocoderCache ) ) {
235
			return self::$globalGeocoderCache[$address];
236
		}
237
		else {
238
			return false;
239
		}
240
	}
241
242
	/**
243
	 * Writes the geocoded result to the cache if the cache is on.
244
	 *
245
	 * @since 0.7
246
	 *
247
	 * @param string $address
248
	 * @param LatLongValue $coordinates
249
	 */
250
	private static function cacheWrite( $address, LatLongValue $coordinates ) {
251
		global $egMapsEnableGeoCache;
252
253
		// Add the obtained coordinates to the cache when there is a result and the cache is enabled.
254
		if ( $egMapsEnableGeoCache && $coordinates ) {
255
			self::$globalGeocoderCache[$address] = $coordinates;
256
		}
257
	}
258
259
	/**
260
	 * Registers a geocoder linked to an identifier.
261
	 *
262
	 * @since 0.7
263
	 *
264
	 * @param string $geocoderIdentifier
265
	 * @param string|\Maps\Geocoders\Geocoder $geocoder
266
	 */
267
	public static function registerGeocoder( $geocoderIdentifier, $geocoder ) {
268
		self::$registeredGeocoders[$geocoderIdentifier] = $geocoder;
269
	}
270
271
	/**
272
	 * Returns the instance of the geocoder linked to the provided identifier
273
	 * or the default one when it's not valid. False is returned when there
274
	 * are no geocoders available.
275
	 *
276
	 * @since 0.7
277
	 *
278
	 * @param string $geocoderIdentifier
279
	 *
280
	 * @return Geocoder|bool
281
	 */
282
	private static function getValidGeocoderInstance( $geocoderIdentifier ) {
283
		return self::getGeocoderInstance( self::getValidGeocoderIdentifier( $geocoderIdentifier ) );
284
	}
285
286
	/**
287
	 * Returns the instance of a geocoder. This function assumes there is a
288
	 * geocoder linked to the identifier you provide - if you are not sure
289
	 * it does, use getValidGeocoderInstance instead.
290
	 *
291
	 * @since 0.7
292
	 *
293
	 * @param string $geocoderIdentifier
294
	 *
295
	 * @return Geocoder|bool
296
	 * @throws MWException
297
	 */
298
	private static function getGeocoderInstance( $geocoderIdentifier ) {
299
		if ( !array_key_exists( $geocoderIdentifier, self::$geocoders ) ) {
300
			if ( array_key_exists( $geocoderIdentifier, self::$registeredGeocoders ) ) {
301
				if ( is_string( self::$registeredGeocoders[$geocoderIdentifier] ) ) {
302
					$geocoder = new self::$registeredGeocoders[$geocoderIdentifier]( $geocoderIdentifier );
303
				}
304
				elseif ( self::$registeredGeocoders[$geocoderIdentifier] instanceof \Maps\Geocoders\Geocoder ) {
305
					$geocoder = new MapsOldGeocoderAdapter(
306
						self::$registeredGeocoders[$geocoderIdentifier],
307
						$geocoderIdentifier
308
					);
309
				}
310
				else {
311
					throw new MWException( 'Need either class name or Geocoder instance' );
312
				}
313
314
315
				self::$geocoders[$geocoderIdentifier] = $geocoder;
316
			}
317
			else {
318
				throw new MWException( 'There is geocoder linked to identifier ' . $geocoderIdentifier . '.' );
319
			}
320
		}
321
322
		return self::$geocoders[$geocoderIdentifier];
323
	}
324
325
	/**
326
	 * Returns a valid geocoder idenifier. If the given one is a valid main identifier,
327
	 * it will simply be returned. If it's an alias, it will be turned into the correponding
328
	 * main identifier. If it's not recognized at all (or empty), the default will be used.
329
	 * Only call this function when there are geocoders available, else an erro will be thrown.
330
	 *
331
	 * @since 0.7
332
	 *
333
	 * @param string $geocoderIdentifier
334
	 *
335
	 * @return string|bool
336
	 * @throws MWException
337
	 */
338
	private static function getValidGeocoderIdentifier( $geocoderIdentifier ) {
339
		global $egMapsDefaultGeoService;
340
		static $validatedDefault = false;
341
342
		if ( $geocoderIdentifier === '' || !array_key_exists( $geocoderIdentifier, self::$registeredGeocoders ) ) {
343
			if ( !$validatedDefault ) {
344
				if ( !array_key_exists( $egMapsDefaultGeoService, self::$registeredGeocoders ) ) {
345
					$services = array_keys( self::$registeredGeocoders );
346
					$egMapsDefaultGeoService = array_shift( $services );
347
					if ( is_null( $egMapsDefaultGeoService ) ) {
348
						throw new MWException( 'Tried to geocode while there are no geocoders available at ' . __METHOD__  );
349
					}
350
				}
351
			}
352
353
			if ( array_key_exists( $egMapsDefaultGeoService, self::$registeredGeocoders ) ) {
354
				$geocoderIdentifier = $egMapsDefaultGeoService;
355
			}
356
			else {
357
				return false;
358
			}
359
		}
360
361
		return $geocoderIdentifier;
362
	}
363
364
}
365