Completed
Push — master ( 8491da...d644dd )
by Jeroen De
9s
created

SMMapPrinter::getCenter()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
dl 0
loc 16
ccs 0
cts 12
cp 0
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 8
nc 3
nop 1
crap 12
1
<?php
2
3
use Maps\Elements\Location;
4
use Maps\Element;
5
use Maps\Elements\BaseElement;
6
use Maps\LocationParser;
7
use ParamProcessor\ParamDefinition;
8
use ValueParsers\ParserOptions as ValueParserOptions;
9
10
/**
11
 * Query printer for maps. Is invoked via SMMapper.
12
 * Can be overridden per service to have custom output.
13
 *
14
 * @ingroup SemanticMaps
15
 *
16
 * @licence GNU GPL v2+
17
 * @author Jeroen De Dauw < [email protected] >
18
 * @author Peter Grassberger < [email protected] >
19
 */
20
class SMMapPrinter extends SMW\ResultPrinter {
21
22
	private static $services = [];
23
24
	/**
25
	 * @var LocationParser
26
	 */
27
	private $locationParser;
28
29
	/**
30
	 * @since 3.4
31
	 * FIXME: this is a temporary hack that should be replaced when SMW allows for dependency
32
	 * injection in query printers.
33
	 *
34
	 * @param MapsMappingService $service
35
	 */
36
	public static function registerService( MapsMappingService $service ) {
37
		self::$services[$service->getName()] = $service;
38
	}
39
40
	public static function registerDefaultService( $serviceName ) {
41
		self::$services['map'] = self::$services[$serviceName];
42
	}
43
	
44
	/**
45
	 * @var MapsMappingService
46
	 */
47
	private $service;
48
	
49
	/**
50
	 * @var string|boolean
51
	 */
52
	private $fatalErrorMsg = false;
53
	
54
	/**
55
	 * @param string $format
56
	 * @param bool $inline
57
	 */
58
	public function __construct( $format, $inline = true ) {
59
		$this->service = self::$services[$format];
60
		
61
		parent::__construct( $format, $inline );
62
	}
63
64
	/**
65
	 * Returns an array containing the parameter info.
66
	 * 
67
	 * @return array
68
	 */
69
	private function getParameterInfo() {
70
		global $smgQPShowTitle, $smgQPTemplate, $smgQPHideNamespace;
71
		
72
		$params = ParamDefinition::getCleanDefinitions( MapsMapper::getCommonParameters() );
0 ignored issues
show
Documentation introduced by
\MapsMapper::getCommonParameters() is of type array<string,array<strin...efault\":\"false\"}>"}>, but the function expects a array<integer,object<Par...ssor\IParamDefinition>>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
73
74
		$this->service->addParameterInfo( $params );
75
76
		$params['staticlocations'] = [
77
			'type' => 'mapslocation', // FIXME: geoservice is not used
78
			'aliases' => [ 'locations', 'points' ],
79
			'default' => [],
80
			'islist' => true,
81
			'delimiter' => ';',
82
			'message' => 'semanticmaps-par-staticlocations',
83
		];
84
85
		$params['showtitle'] = [
86
			'type' => 'boolean',
87
			'aliases' => 'show title',
88
			'default' => $smgQPShowTitle,
89
		];
90
91
		$params['hidenamespace'] = [
92
			'type' => 'boolean',
93
			'aliases' => 'hide namespace',
94
			'default' => $smgQPHideNamespace,
95
		];
96
97
		$params['template'] = [
98
			'default' => $smgQPTemplate,
99
		];
100
101
		$params['userparam'] = [
102
			'default' => '',
103
		];
104
105
		$params['activeicon'] =  [
106
			'type' => 'string',
107
			'default' => '',
108
		];
109
110
		$params['pagelabel'] =  [
111
			'type' => 'boolean',
112
			'default' => false,
113
		];
114
115
		$params['ajaxcoordproperty'] = array(
116
			'default' => '',
117
		);
118
119
		$params['ajaxquery'] = array(
120
			'default' => '',
121
			'type' => 'string'
122
		);
123
124
		// Messages:
125
		// semanticmaps-par-staticlocations, semanticmaps-par-showtitle, semanticmaps-par-hidenamespace,
126
		// semanticmaps-par-template, semanticmaps-par-userparam, semanticmaps-par-activeicon,
127
		// semanticmaps-par-pagelabel, semanticmaps-par-ajaxcoordproperty semanticmaps-par-ajaxquery
128
		foreach ( $params as $name => &$data ) {
129
			if ( is_array( $data ) && !array_key_exists( 'message', $data ) ) {
130
				$data['message'] = 'semanticmaps-par-' . $name;
131
			}
132
		}
133
134
		return $params;
135
	}
136
	
137
	/**
138
	 * Builds up and returns the HTML for the map, with the queried coordinate data on it.
139
	 *
140
	 * @param SMWQueryResult $res
141
	 * @param $outputmode
142
	 * 
143
	 * @return array or string
144
	 */
145
	public final function getResultText( SMWQueryResult $res, $outputmode ) {
0 ignored issues
show
Coding Style introduced by
getResultText uses the super-global variable $GLOBALS which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
146
		if ( $this->fatalErrorMsg !== false ) {
147
			return $this->fatalErrorMsg;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->fatalErrorMsg; (string|boolean) is incompatible with the return type documented by SMMapPrinter::getResultText of type array.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
148
		}
149
150
		/**
151
		 * @var Parser $wgParser
152
		 */
153
		global $wgParser;
154
155
		if ( $GLOBALS['egMapsEnableCategory'] && $wgParser->getOutput() !== null ) {
156
			$wgParser->addTrackingCategory( 'maps-tracking-category' );
157
		}
158
159
		$params = $this->params;
160
161
		$this->initializeLocationParser( $params );
162
163
		$queryHandler = new SMQueryHandler( $res, $outputmode );
164
		$queryHandler->setLinkStyle( $params['link'] );
165
		$queryHandler->setHeaderStyle($params['headers']);
166
		$queryHandler->setShowSubject( $params['showtitle'] );
167
		$queryHandler->setTemplate( $params['template'] );
168
		$queryHandler->setUserParam( $params['userparam'] );
169
		$queryHandler->setHideNamespace( $params['hidenamespace'] );
170
		$queryHandler->setActiveIcon( $params['activeicon'] );
171
172
		$this->handleMarkerData( $params, $queryHandler );
173
174
		$params['ajaxquery'] = urlencode( $params['ajaxquery'] );
175
176
		$locationAmount = count( $params['locations'] );
177
178
		if ( $locationAmount > 0 ) {
179
			// We can only take care of the zoom defaulting here,
180
			// as not all locations are available in whats passed to Validator.
181
			if ( $this->fullParams['zoom']->wasSetToDefault() && $locationAmount > 1 ) {
182
				$params['zoom'] = false;
183
			}
184
185
			$mapName = $this->service->getMapId();
186
187
			SMWOutputs::requireHeadItem(
188
				$mapName,
189
				$this->service->getDependencyHtml() .
190
				$configVars = Skin::makeVariablesScript( $this->service->getConfigVariables() )
191
			);
192
193
			foreach ( $this->service->getResourceModules() as $resourceModule ) {
194
				SMWOutputs::requireResource( $resourceModule );
195
			}
196
197
			if ( array_key_exists( 'source', $params ) ) {
198
				unset( $params['source'] );
199
			}
200
201
			return $this->getMapHTML( $params, $wgParser, $mapName );
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->getMapHTML..., $wgParser, $mapName); (string) is incompatible with the return type documented by SMMapPrinter::getResultText of type array.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
202
		}
203
		else {
204
			return $params['default'];
205
		}
206
	}
207
208
	private function initializeLocationParser( array $params ) {
209
		$this->locationParser = new LocationParser( new ValueParserOptions( [
210
			'geoService' => $params['geoservice']
211
		] ) );
212
	}
213
214
	/**
215
	 * Converts the data in the coordinates parameter to JSON-ready objects.
216
	 * These get stored in the locations parameter, and the coordinates on gets deleted.
217
	 *
218
	 * @param array &$params
219
	 * @param SMQueryHandler $queryHandler
220
	 */
221
	private function handleMarkerData( array &$params, SMQueryHandler $queryHandler ) {
222
		$params['centre'] = $this->getCenter( $params['centre'] );
223
224
		$iconUrl = MapsMapper::getFileUrl( $params['icon'] );
0 ignored issues
show
Deprecated Code introduced by
The method MapsMapper::getFileUrl() has been deprecated.

This method has been deprecated.

Loading history...
225
		$visitedIconUrl = MapsMapper::getFileUrl( $params['visitedicon'] );
0 ignored issues
show
Deprecated Code introduced by
The method MapsMapper::getFileUrl() has been deprecated.

This method has been deprecated.

Loading history...
226
227
		$params['locations'] = $this->getJsonForStaticLocations(
228
			$params['staticlocations'],
229
			$params,
230
			$iconUrl,
231
			$visitedIconUrl
232
		);
233
234
		unset( $params['staticlocations'] );
235
236
		$this->addShapeData( $queryHandler->getShapes(), $params, $iconUrl, $visitedIconUrl );
237
238
		if ( $params['format'] === 'openlayers' ) {
239
			$params['layers'] = MapsDisplayMapRenderer::evilOpenLayersHack( $params['layers'] );
0 ignored issues
show
Deprecated Code introduced by
The method MapsDisplayMapRenderer::evilOpenLayersHack() has been deprecated.

This method has been deprecated.

Loading history...
240
		}
241
	}
242
243
	private function getCenter( $coordinatesOrAddress ) {
244
		if ( $coordinatesOrAddress === false ) {
245
			return false;
246
		}
247
248
		try {
249
			// FIXME: a Location makes no sense here, since the non-coordinate data is not used
250
			$location = $this->locationParser->stringParse( $coordinatesOrAddress );
251
		}
252
		catch ( \Exception $ex ) {
253
			// TODO: somehow report this to the user
254
			return false;
255
		}
256
257
		return $location->getJSONObject();
258
	}
259
260
	/**
261
	 * Returns the HTML to display the map.
262
	 *
263
	 * @param array $params
264
	 * @param Parser $parser
265
	 * @param string $mapName
266
	 *
267
	 * @return string
268
	 */
269
	private function getMapHTML( array $params, Parser $parser, $mapName ) {
270
		return Html::rawElement(
271
			'div',
272
			[
273
				'id' => $mapName,
274
				'style' => "width: {$params['width']}; height: {$params['height']}; background-color: #cccccc; overflow: hidden;",
275
				'class' => 'maps-map maps-' . $this->service->getName()
276
			],
277
			wfMessage( 'maps-loading-map' )->inContentLanguage()->escaped() .
278
				Html::element(
279
					'div',
280
					[ 'style' => 'display:none', 'class' => 'mapdata' ],
281
					FormatJson::encode( $this->getJSONObject( $params, $parser ) )
282
				)
283
		);
284
	}
285
286
	/**
287
	 * Returns a PHP object to encode to JSON with the map data.
288
	 *
289
	 * @param array $params
290
	 * @param Parser $parser
291
	 *
292
	 * @return mixed
293
	 */
294
	private function getJSONObject( array $params, Parser $parser ) {
295
		return $params;
296
	}
297
298
	private function getJsonForStaticLocations( array $staticLocations, array $params, $iconUrl, $visitedIconUrl ) {
0 ignored issues
show
Coding Style introduced by
getJsonForStaticLocations uses the super-global variable $GLOBALS which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
299
		/**
300
		 * @var Parser $wgParser
301
		 */
302
		global $wgParser;
303
304
		$parser = version_compare( $GLOBALS['wgVersion'], '1.18', '<' ) ? $wgParser : clone $wgParser;
305
306
		$locationsJson = [];
307
308
		foreach ( $staticLocations as $location ) {
309
			$locationsJson[] = $this->getJsonForStaticLocation(
310
				$location,
311
				$params,
312
				$iconUrl,
313
				$visitedIconUrl,
314
				$parser
315
			);
316
		}
317
318
		return $locationsJson;
319
	}
320
321
	private function getJsonForStaticLocation( Location $location, array $params, $iconUrl, $visitedIconUrl, Parser $parser ) {
322
		$jsonObj = $location->getJSONObject( $params['title'], $params['label'], $iconUrl, '', '', $visitedIconUrl );
323
324
		$jsonObj['title'] = $parser->parse( $jsonObj['title'], $parser->getTitle(), new ParserOptions() )->getText();
325
		$jsonObj['text'] = $parser->parse( $jsonObj['text'], $parser->getTitle(), new ParserOptions() )->getText();
326
327
		$hasTitleAndtext = $jsonObj['title'] !== '' && $jsonObj['text'] !== '';
328
		$jsonObj['text'] = ( $hasTitleAndtext ? '<b>' . $jsonObj['title'] . '</b><hr />' : $jsonObj['title'] ) . $jsonObj['text'];
329
		$jsonObj['title'] = strip_tags( $jsonObj['title'] );
330
331
		if ( $params['pagelabel'] ) {
332
			$jsonObj['inlineLabel'] = Linker::link( Title::newFromText( $jsonObj['title'] ) );
333
		}
334
335
		return $jsonObj;
336
	}
337
338
	/**
339
	 * @param Element[] $queryShapes
340
	 * @param array $params
341
	 * @param string $iconUrl
342
	 * @param string $visitedIconUrl
343
	 */
344
	private function addShapeData( array $queryShapes, array &$params, $iconUrl, $visitedIconUrl ) {
345
		$params['locations'] = array_merge(
346
			$params['locations'],
347
			$this->getJsonForLocations(
348
				$queryShapes['locations'],
349
				$params,
350
				$iconUrl,
351
				$visitedIconUrl
352
			)
353
		);
354
355
		$params['lines'] = $this->getElementJsonArray( $queryShapes['lines'], $params );
356
		$params['polygons'] = $this->getElementJsonArray( $queryShapes['polygons'], $params );
357
	}
358
359
	/**
360
	 * @param Location[] $locations
361
	 * @param array $params
362
	 * @param string $iconUrl
363
	 * @param string $visitedIconUrl
364
	 *
365
	 * @return array
366
	 */
367
	private function getJsonForLocations( array $locations, array $params, $iconUrl, $visitedIconUrl ) {
368
		$locationsJson = [];
369
370
		foreach ( $locations as $location ) {
371
			$jsonObj = $location->getJSONObject( $params['title'], $params['label'], $iconUrl, '', '', $visitedIconUrl );
372
373
			$jsonObj['title'] = strip_tags( $jsonObj['title'] );
374
375
			$locationsJson[] = $jsonObj;
376
		}
377
378
		return $locationsJson;
379
	}
380
381
	/**
382
	 * @param BaseElement[] $elements
383
	 * @param array $params
384
	 *
385
	 * @return array
386
	 */
387
	private function getElementJsonArray( array $elements, array $params ) {
388
		$elementsJson = [];
389
390
		foreach ( $elements as $element ) {
391
			$jsonObj = $element->getJSONObject( $params['title'], $params['label'] );
0 ignored issues
show
Deprecated Code introduced by
The method Maps\Elements\BaseElement::getJSONObject() has been deprecated.

This method has been deprecated.

Loading history...
392
			$elementsJson[] = $jsonObj;
393
		}
394
395
		return $elementsJson;
396
	}
397
398
	/**
399
	 * Returns the internationalized name of the mapping service.
400
	 * 
401
	 * @return string
402
	 */
403
	public final function getName() {
404
		return wfMessage( 'maps_' . $this->service->getName() )->text();
405
	}
406
	
407
	/**
408
	 * Returns a list of parameter information, for usage by Special:Ask and others.
409
	 * 
410
	 * @return array
411
	 */
412
    public function getParameters() {
413
        $params = parent::getParameters();
414
        $paramInfo = $this->getParameterInfo();
415
        
416
        // Do not display this as an option, as the format already determines it
417
        // TODO: this can probably be done cleaner with some changes in Maps
418
        unset( $paramInfo['mappingservice'] );
419
        
420
        $params = array_merge( $params, $paramInfo );
421
422
		return $params;
423
    }
424
}
425