Completed
Push — smwtests ( 887d06...0d03d8 )
by Jeroen De
02:58
created

MapPrinter::elementsToJson()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 0
cts 8
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 2
1
<?php
2
3
namespace Maps\SemanticMW\ResultPrinters;
4
5
use FormatJson;
6
use Html;
7
use Linker;
8
use Maps\Elements\BaseElement;
9
use Maps\Elements\Location;
10
use Maps\FileUrlFinder;
11
use Maps\MappingService;
12
use Maps\MapsFunctions;
13
use Maps\Presentation\ElementJsonSerializer;
14
use Maps\Presentation\WikitextParser;
15
use Maps\Presentation\WikitextParsers\LocationParser;
16
use ParamProcessor\ParamDefinition;
17
use Parser;
18
use SMW\Query\ResultPrinters\ResultPrinter;
19
use SMWOutputs;
20
use SMWQueryResult;
21
use Title;
22
23
/**
24
 * @licence GNU GPL v2+
25
 * @author Jeroen De Dauw < [email protected] >
26
 * @author Peter Grassberger < [email protected] >
27
 */
28
class MapPrinter extends ResultPrinter {
29
30
	private static $services = [];
31
32
	/**
33
	 * @var LocationParser
34
	 */
35
	private $locationParser;
36
37
	/**
38
	 * @var FileUrlFinder
39
	 */
40
	private $fileUrlFinder;
41
42
	/**
43
	 * @var MappingService
44
	 */
45
	private $service;
46
47
	/**
48
	 * @var WikitextParser
49
	 */
50
	private $wikitextParser;
51
52
	/**
53
	 * @var ElementJsonSerializer
54
	 */
55
	private $elementSerializer;
56
57
	/**
58
	 * @var string|boolean
59
	 */
60
	private $fatalErrorMsg = false;
61
62
	/**
63
	 * @param string $format
64
	 * @param bool $inline
65
	 */
66
	public function __construct( $format, $inline = true ) {
67
		$this->service = self::$services[$format];
68
69
		parent::__construct( $format, $inline );
70
	}
71
72
	/**
73
	 * @since 3.4
74
	 * FIXME: this is a temporary hack that should be replaced when SMW allows for dependency
75
	 * injection in query printers.
76
	 *
77
	 * @param MappingService $service
78
	 */
79
	public static function registerService( MappingService $service ) {
80
		self::$services[$service->getName()] = $service;
81
	}
82
83
	public static function registerDefaultService( $serviceName ) {
84
		self::$services['map'] = self::$services[$serviceName];
85
	}
86
87
	private function getParser(): Parser {
88
		$parser = $GLOBALS['wgParser'];
89
90
		if ( $parser instanceof \StubObject ) {
0 ignored issues
show
Bug introduced by
The class StubObject does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
91
			return $parser->_newObject();
92
		}
93
94
		return $parser;
95
	}
96
97
	private function getParserClone(): Parser {
98
		$parser = $this->getParser();
99
		return clone $parser;
100
	}
101
102
	/**
103
	 * Builds up and returns the HTML for the map, with the queried coordinate data on it.
104
	 *
105
	 * @param SMWQueryResult $res
106
	 * @param int $outputMode
107
	 *
108
	 * @return string
109
	 */
110
	public final function getResultText( SMWQueryResult $res, $outputMode ) {
111
		if ( $this->fatalErrorMsg !== false ) {
112
			return $this->fatalErrorMsg;
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->fatalErrorMsg; of type string|boolean adds the type boolean to the return on line 112 which is incompatible with the return type documented by Maps\SemanticMW\ResultPr...pPrinter::getResultText of type string.
Loading history...
113
		}
114
115
		$factory = \Maps\MapsFactory::newDefault();
116
		$this->locationParser = $factory->newLocationParser();
117
		$this->fileUrlFinder = $factory->getFileUrlFinder();
118
119
		$this->wikitextParser = new WikitextParser( $this->getParserClone() );
120
		$this->elementSerializer = new ElementJsonSerializer( $this->wikitextParser );
121
122
		$this->addTrackingCategoryIfNeeded();
123
124
		$params = $this->params;
125
126
		$queryHandler = new QueryHandler( $res, $outputMode );
127
		$queryHandler->setLinkStyle( $params['link'] );
128
		$queryHandler->setHeaderStyle( $params['headers'] );
129
		$queryHandler->setShowSubject( $params['showtitle'] );
130
		$queryHandler->setTemplate( $params['template'] );
131
		$queryHandler->setUserParam( $params['userparam'] );
132
		$queryHandler->setHideNamespace( $params['hidenamespace'] );
133
		$queryHandler->setActiveIcon( $params['activeicon'] );
134
135
		$this->handleMarkerData( $params, $queryHandler );
136
137
		$params['lines'] = $this->elementsToJson( $params['lines'] );
138
		$params['polygons'] = $this->elementsToJson( $params['polygons'] );
139
		$params['circles'] = $this->elementsToJson( $params['circles'] );
140
		$params['rectangles'] = $this->elementsToJson( $params['rectangles'] );
141
142
		$params['ajaxquery'] = urlencode( $params['ajaxquery'] );
143
144
		if ( $params['locations'] === [] ) {
145
			return $params['default'];
146
		}
147
148
		// We can only take care of the zoom defaulting here,
149
		// as not all locations are available in whats passed to Validator.
150
		if ( $this->fullParams['zoom']->wasSetToDefault() && count( $params['locations'] ) > 1 ) {
151
			$params['zoom'] = false;
152
		}
153
154
		$mapName = $this->service->newMapId();
155
156
		SMWOutputs::requireHeadItem(
157
			$mapName,
158
			$this->service->getDependencyHtml( $params )
159
		);
160
161
		foreach ( $this->service->getResourceModules() as $resourceModule ) {
162
			SMWOutputs::requireResource( $resourceModule );
163
		}
164
165
		if ( array_key_exists( 'source', $params ) ) {
166
			unset( $params['source'] );
167
		}
168
169
		return $this->getMapHTML( $params, $mapName );
170
	}
171
172
	private function elementsToJson( array $elements ) {
173
		return array_map(
174
			function( BaseElement $element ) {
175
				return $this->elementSerializer->elementToJson( $element );
176
			},
177
			$elements
178
		);
179
	}
180
181
	private function addTrackingCategoryIfNeeded() {
182
		/**
183
		 * @var Parser $wgParser
184
		 */
185
		global $wgParser;
186
187
		if ( $GLOBALS['egMapsEnableCategory'] && $wgParser->getOutput() !== null ) {
188
			$wgParser->addTrackingCategory( 'maps-tracking-category' );
189
		}
190
	}
191
192
	/**
193
	 * Converts the data in the coordinates parameter to JSON-ready objects.
194
	 * These get stored in the locations parameter, and the coordinates on gets deleted.
195
	 *
196
	 * @param array &$params
197
	 * @param QueryHandler $queryHandler
198
	 */
199
	private function handleMarkerData( array &$params, QueryHandler $queryHandler ) {
200
		$params['centre'] = $this->getCenter( $params['centre'] );
201
202
		$iconUrl = $this->fileUrlFinder->getUrlForFileName( $params['icon'] );
203
		$visitedIconUrl = $this->fileUrlFinder->getUrlForFileName( $params['visitedicon'] );
204
205
		$params['locations'] = $this->getJsonForStaticLocations(
206
			$params['staticlocations'],
207
			$params,
208
			$iconUrl,
209
			$visitedIconUrl
210
		);
211
212
		unset( $params['staticlocations'] );
213
214
		$params['locations'] = array_merge(
215
			$params['locations'],
216
			$this->getJsonForLocations(
217
				$queryHandler->getLocations(),
218
				$params,
219
				$iconUrl,
220
				$visitedIconUrl
221
			)
222
		);
223
	}
224
225
	private function getCenter( $coordinatesOrAddress ) {
226
		if ( $coordinatesOrAddress === false ) {
227
			return false;
228
		}
229
230
		try {
231
			// FIXME: a Location makes no sense here, since the non-coordinate data is not used
232
			$location = $this->locationParser->parse( $coordinatesOrAddress );
233
		}
234
		catch ( \Exception $ex ) {
235
			// TODO: somehow report this to the user
236
			return false;
237
		}
238
239
		return $location->getJSONObject();
240
	}
241
242
	private function getJsonForStaticLocations( array $staticLocations, array $params, $iconUrl, $visitedIconUrl ) {
243
		$locationsJson = [];
244
245
		foreach ( $staticLocations as $location ) {
246
			$locationsJson[] = $this->getJsonForStaticLocation(
247
				$location,
248
				$params,
249
				$iconUrl,
250
				$visitedIconUrl
251
			);
252
		}
253
254
		return $locationsJson;
255
	}
256
257
	private function getJsonForStaticLocation( Location $location, array $params, $iconUrl, $visitedIconUrl ) {
258
		$jsonObj = $location->getJSONObject( $params['title'], $params['label'], $iconUrl, '', '', $visitedIconUrl );
259
260
		$this->elementSerializer->titleAndText( $jsonObj );
261
262
		if ( $params['pagelabel'] ) {
263
			$jsonObj['inlineLabel'] = Linker::link( Title::newFromText( $jsonObj['title'] ) );
264
		}
265
266
		return $jsonObj;
267
	}
268
269
	/**
270
	 * @param Location[] $locations
271
	 * @param array $params
272
	 * @param string $iconUrl
273
	 * @param string $visitedIconUrl
274
	 *
275
	 * @return array
276
	 */
277
	private function getJsonForLocations( iterable $locations, array $params, string $iconUrl, string $visitedIconUrl ): array {
278
		$locationsJson = [];
279
280
		foreach ( $locations as $location ) {
281
			$jsonObj = $location->getJSONObject(
282
				$params['title'],
283
				$params['label'],
284
				$iconUrl,
285
				'',
286
				'',
287
				$visitedIconUrl
288
			);
289
290
			$jsonObj['title'] = strip_tags( $jsonObj['title'] );
291
292
			$locationsJson[] = $jsonObj;
293
		}
294
295
		return $locationsJson;
296
	}
297
298
	/**
299
	 * Returns the HTML to display the map.
300
	 *
301
	 * @param array $params
302
	 * @param string $mapName
303
	 *
304
	 * @return string
305
	 */
306
	private function getMapHTML( array $params, string $mapName ): string {
307
		return Html::rawElement(
308
			'div',
309
			[
310
				'id' => $mapName,
311
				'style' => "width: {$params['width']}; height: {$params['height']}; background-color: #cccccc; overflow: hidden;",
312
				'class' => 'maps-map maps-' . $this->service->getName()
313
			],
314
			wfMessage( 'maps-loading-map' )->inContentLanguage()->escaped() .
315
			Html::element(
316
				'div',
317
				[ 'style' => 'display:none', 'class' => 'mapdata' ],
318
				FormatJson::encode( $params )
319
			)
320
		);
321
	}
322
323
	/**
324
	 * Returns the internationalized name of the mapping service.
325
	 *
326
	 * @return string
327
	 */
328
	public final function getName() {
329
		return wfMessage( 'maps_' . $this->service->getName() )->text();
330
	}
331
332
	/**
333
	 * Returns a list of parameter information, for usage by Special:Ask and others.
334
	 *
335
	 * @return array
336
	 */
337
	public function getParameters() {
338
		$params = parent::getParameters();
339
		$paramInfo = $this->getParameterInfo();
340
341
		// Do not display this as an option, as the format already determines it
342
		// TODO: this can probably be done cleaner with some changes in Maps
343
		unset( $paramInfo['mappingservice'] );
344
345
		$params = array_merge( $params, $paramInfo );
346
347
		return $params;
348
	}
349
350
	/**
351
	 * Returns an array containing the parameter info.
352
	 *
353
	 * @return array
354
	 */
355
	private function getParameterInfo() {
356
		global $smgQPShowTitle, $smgQPTemplate, $smgQPHideNamespace;
357
358
		$params = array_merge(
359
			ParamDefinition::getCleanDefinitions( MapsFunctions::getCommonParameters() ),
0 ignored issues
show
Documentation introduced by
\Maps\MapsFunctions::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...
360
			$this->service->getParameterInfo()
361
		);
362
363
		$params['staticlocations'] = [
364
			'type' => 'mapslocation',
365
			'aliases' => [ 'locations', 'points' ],
366
			'default' => [],
367
			'islist' => true,
368
			'delimiter' => ';',
369
			'message' => 'semanticmaps-par-staticlocations',
370
		];
371
372
		$params['showtitle'] = [
373
			'type' => 'boolean',
374
			'aliases' => 'show title',
375
			'default' => $smgQPShowTitle,
376
		];
377
378
		$params['hidenamespace'] = [
379
			'type' => 'boolean',
380
			'aliases' => 'hide namespace',
381
			'default' => $smgQPHideNamespace,
382
		];
383
384
		$params['template'] = [
385
			'default' => $smgQPTemplate,
386
		];
387
388
		$params['userparam'] = [
389
			'default' => '',
390
		];
391
392
		$params['activeicon'] = [
393
			'type' => 'string',
394
			'default' => '',
395
		];
396
397
		$params['pagelabel'] = [
398
			'type' => 'boolean',
399
			'default' => false,
400
		];
401
402
		$params['ajaxcoordproperty'] = [
403
			'default' => '',
404
		];
405
406
		$params['ajaxquery'] = [
407
			'default' => '',
408
			'type' => 'string'
409
		];
410
411
		// Messages:
412
		// semanticmaps-par-staticlocations, semanticmaps-par-showtitle, semanticmaps-par-hidenamespace,
413
		// semanticmaps-par-template, semanticmaps-par-userparam, semanticmaps-par-activeicon,
414
		// semanticmaps-par-pagelabel, semanticmaps-par-ajaxcoordproperty semanticmaps-par-ajaxquery
415
		foreach ( $params as $name => &$data ) {
416
			if ( is_array( $data ) && !array_key_exists( 'message', $data ) ) {
417
				$data['message'] = 'semanticmaps-par-' . $name;
418
			}
419
		}
420
421
		return $params;
422
	}
423
}
424