]> ToastFreeware Gitweb - philipp/winterrodeln/mediawiki_extensions/wrmap.git/blob - wrmap.body.php
Remove linter warnings.
[philipp/winterrodeln/mediawiki_extensions/wrmap.git] / wrmap.body.php
1 <?php
2 /* This extension creates a map using OpenLayers to show sledrun details and sledrun overviews.
3 This extension depends on no other extension.
4
5
6 Example 1
7 ---------
8
9 <wrgmap lat="47.267648" lon="11.40465" zoom="10"/>
10
11 (Shows icons for all sledruns. lat, lon and zoom are optional.)
12
13
14 Example 2
15 ---------
16
17 <wrmap lat="47.2417134" lon="11.21408895" zoom="14" width="700" height="400">
18
19 <gasthaus name="Rosskogelhütte" wiki="Rosskogelhütte">47.240689 11.190454</gasthaus>
20 <gasthaus name="Stiglreith">47.238186 11.221940</gasthaus>
21 <gasthaus name="Sulzstich">47.240287 11.203006</gasthaus>
22 <parkplatz>47.245789 11.238971</parkplatz>
23 <parkplatz>47.237627 11.218886</parkplatz>
24 <haltestelle name="Oberperfuss Rangger Köpfl Lift">47.245711 11.238283</haltestelle>
25 <achtung name="Kreuzung mit Schipiste">47.2383200 11.2235592</achtung>
26
27 <gehweg>
28 47.238587 11.203360
29 47.239743 11.203522
30 47.240135 11.203247
31 </gehweg>
32
33 <gehweg>
34 47.238442 11.203263
35 47.237799 11.203511
36 47.237133 11.202988
37 47.238091 11.206642
38 47.237273 11.211675
39 47.237133 11.214466
40 47.237513 11.218199
41 </gehweg>
42
43 <alternative>
44 47.240487 11.190169
45 47.238996 11.188628
46 47.238987 11.188018
47 47.238267 11.187075
48 47.238461 11.190511
49 47.239751 11.191795
50 47.240037 11.192702
51 47.239525 11.193535
52 47.239688 11.194272
53 47.239017 11.193925
54 47.239536 11.195457
55 47.240063 11.196230
56 47.240747 11.196658
57 47.239734 11.198295
58 47.238857 11.198346
59 47.237743 11.199778
60 47.238250 11.202755
61 47.238587 11.203360
62 </alternative>
63
64 <rodelbahn>
65 47.238587 11.203360
66 47.238185 11.203982
67 47.238297 11.204381
68 47.239417 11.204972
69 47.239210 11.208772
70 47.238999 11.209523
71 47.239126 11.209839
72 47.238933 11.210641
73 47.239102 11.210739
74 47.238666 11.215042
75 47.238203 11.216089
76 47.238183 11.218151
77 47.237851 11.218599
78 47.238055 11.219755
79 47.237686 11.222441
80 47.238000 11.223367
81 47.238625 11.223687
82 47.239915 11.223118
83 47.240992 11.219781
84 47.243412 11.214141
85 47.243207 11.218331
86 47.243990 11.216205
87 47.243785 11.223251
88 47.242845 11.228510
89 47.242917 11.232501
90 47.242524 11.235001
91 47.244737 11.231791
92 47.244951 11.230868
93 47.245470 11.237853
94 </rodelbahn>
95
96 <lift>
97 47.245656 11.237286
98 47.238189 11.221344
99 </lift>
100
101 </wrmap>
102
103
104
105 Definition
106 ----------
107
108 * <wrmap>...</wrmap> has to be valid XML.
109 * All coordinates are in WGS84 coordinate system.
110 * Coordinates have the preferred format "latitude N longitude E",
111   however for parsing the N and E can be omitted.
112 * <wrmap> has the following attributes:
113         * lat (float): latitude of map-center, optional.
114         * lon (float): longitude of map-center, optional.
115         * zoom (integer): zoom level of the map (google zoom levels). optional.
116         * width (integer): width of the map in pixel. optional (100% if omitted)
117         * height (integer): height of the map in pixel. optional.
118         * <wrmap> can have any number of the following sub-elements:
119                 * <gasthaus>
120                 * <haltestelle>
121                 * <parkplatz>
122                 * <achtung>
123                 * <foto>
124                 * <verleih>
125                 * <punkt>
126                 * <rodelbahn>
127                 * <alternative>
128                 * <gehweg>
129                 * <lift>
130                 * <anfahrt>
131                 * <linie>
132         * The order may be used by the renderer to determine in which order the
133           elements should be drawn: First mentioned elements are drawn first.
134 * <gasthaus>, <haltestelle>, <parkplatz>, <achtung>, <foto>, <verleih> and <punkt> define points
135         * The elements may have the following attributes:
136                 * name (string): defines the name (not the label) of the element
137                 * wiki (string): name of a MediaWiki page the point refers to
138         * The content is exactly one coordinate pair.
139 * <rodelbahn>, <alternative>, <gehweg>, <lift>, <anfahrt> and <linie>
140   define non-closed polygons.
141         * They may have the following attributes:
142                 farbe (hex format, e.g. #12a50f): color of the line
143                 dicke (int): width of the line in pixel
144         * The content of the elements are a whitespace separated list of
145                 coordinates.
146
147
148 For transmitting the map to javascript, geojson is used in the <div> element of the map.
149 This way, an extra request is avoided. The geojson format used here consists of a single
150 "FeatureCollection" (representing the <wrmap>) containing the sub-elements of wrmap
151 as features.
152 The features have an properties key that has a hash as values with the properties of
153 the XML subelements of wrmap. Optional attributes/properties can be omitted.
154 Additionally one mandatory property key is called 'type' and has the sub-element's name
155 as value.
156 The featurecollection itself has a properties key as well containing the attributes of
157 the wrmap element.
158 */
159 use MediaWiki\MediaWikiServices;
160
161
162 // DOM helper classes
163 // ------------------
164
165 // The following two classes are "duplicated" from the wrreport extension to keep them separate.
166 // Put improvements in both classes.
167 class WrMapDOMDocument extends DOMDocument {
168         function __construct() {
169                 parent::__construct('1.0', 'utf-8');
170                 $this->registerNodeClass('DOMElement', 'WrMapDOMElement');
171         }
172
173         /// Creates and adds the element with the given tag name and returns it.
174         /// Additionally, it calls setAttribute($key, $value) for every entry
175         /// in $attributes.
176         function appendElement(string $tagName, $attributes=array()): WrMapDOMElement {
177                 $child = $this->appendChild($this->createElement($tagName));
178                 foreach ($attributes as $key => $value) $child->setAttribute($key, $value);
179                 return $child;
180         }
181 }
182
183
184 class WrMapDOMElement extends DOMElement {
185
186         /// Creates and adds the element with the given tag name and returns it
187         /// Additionally, it calls setAttribute($key, $value) for every entry
188         /// in $attributes.
189         function appendElement(string $tagName, $attributes=array()): WrMapDOMElement {
190                 $child = $this->appendChild($this->ownerDocument->createElement($tagName));
191                 foreach ($attributes as $key => $value) $child->setAttribute($key, $value);
192                 return $child;
193         }
194
195         /// Adds any UTF-8 string as content of the element - it will be escaped.
196         function appendText(string $text) {
197                 return $this->appendChild($this->ownerDocument->createTextNode($text));
198         }
199
200         // Appends a CDATASections to the element. This can be used to include
201         // raw (unparsed) HTML to the DOM tree as it is necessary because
202         // $parser->recursiveTagParse does not always escape & characters.
203         // (see https://bugzilla.wikimedia.org/show_bug.cgi?id=55526 )
204         // Workaround: Use a CDATA section. When serializing with $doc->saveHTML,
205         // the <![CDATA[...]]> is returned as ... .
206         // However, we end up having unescaped & in the output due to this bug in recursiveTagParse.
207         function appendCDATA($data) {
208                 return $this->appendChild($this->ownerDocument->createCDATASection($data));
209         }
210 }
211
212
213 // WrBaseMap
214 // ---------
215
216 class WrBaseMap {
217         // gets coordinates and returns an array of lon/lat coordinate pairs, e.g.
218         // 47.12 N 11.87 E
219         // 47.13 N 11.70 E
220         // ->
221         // array(array(11.87, 47.12), array(11.70, 47.13))
222         public static function geo_to_coordinates($input) {
223                 $matches = array();
224                 $num_matches = preg_match_all('/\s*(\d+\.?\d*)\s*N?\s+(\d+\.?\d*)\s*E?\s*/', $input, $matches);
225                 $result = array();
226                 for ($i=0; $i!=$num_matches; ++$i) {
227                         $result[] = array(floatval($matches[2][$i]), floatval($matches[1][$i]));
228                 }
229                 if (implode($matches[0]) != $input) throw new Exception(wfMessage('wrmap-error-coordinate-format', $input)->text());
230                 return $result;
231         }
232
233
234         /// Takes a page title from the wiki and returns an image (if available)
235         /// or Null. For image wiki pages, the image is the corresponding image,
236         /// for inns it's the image of the "Gasthausbox".
237         public static function wikipage_to_image(Title $title, int $width) {
238                 $file = false; // File class or false
239                 // for NS_FILE titles, use the corresponding file as image
240                 if ($title->inNamespace(NS_FILE)) {
241                         $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile($title); // $file is a mediawiki File class or false
242                 } else {
243                         $categories = $title->getParentCategories(); // e.g. array('Kategorie:Rodelbahn' => 'Juifenalm')
244                         $wgContLang = MediaWikiServices::getInstance()->getContentLanguage();
245                         $key_sledrun = $wgContLang->getNSText(NS_CATEGORY) . ':Rodelbahn';
246                         if (array_key_exists($key_sledrun, $categories)) {
247                                 // for sledrun titles use the image from the rodelbahnbox
248                                 $dbr = wfGetDB(DB_REPLICA);
249                                 $res = $dbr->select('wrsledruncache', 'image', array('page_id' => $title->getArticleID()), __METHOD__);
250                                 $image = $res->fetchRow();
251                                 if ($image && !is_null($image['image'])) $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile($image['image']);
252                         }
253                         $key_inn = $wgContLang->getNSText(NS_CATEGORY) . ':Gasthaus';
254                         if (array_key_exists($key_inn, $categories)) {
255                                 // for inn titles use the image from the gasthausbox
256                                 $dbr = wfGetDB(DB_REPLICA);
257                                 $res = $dbr->select('wrinncache', 'image', array('page_id' => $title->getArticleID()), __METHOD__);
258                                 $image = $res->fetchRow();
259                                 if ($image && !is_null($image['image'])) $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile($image['image']);
260                         }
261                 }
262                 if ($file === false) return Null;
263                 if (!$file->canRender()) return Null;
264                 $thumb_url = $file->createThumb($width, $width); // limit width and hight to $width
265                 if (strlen($thumb_url) == 0) return Null;
266                 return $thumb_url;
267         }
268
269
270         // convert sledruns to geojson (https://datatracker.ietf.org/doc/html/rfc7946)
271         // Returns an array of features
272         public static function sledruns_to_json_features() {
273                 $json_features = array(); // result
274                 $dbr = wfGetDB(DB_REPLICA);
275                 $res = $dbr->select(array('wrsledruncache', 'wrreportcache'), array('wrsledruncache.page_title', 'position_latitude', 'position_longitude', 'date_report', '`condition`'), array('show_in_overview', 'not under_construction'), __METHOD__, array(), array('wrreportcache' => array('left outer join', 'wrsledruncache.page_id=wrreportcache.page_id')));
276                 while ($sledrun = $res->fetchRow()) {
277                         $lat = $sledrun['position_latitude'];
278                         $lon = $sledrun['position_longitude'];
279                         if (is_null($lat) || is_null($lon)) continue;
280                         $lat = floatval($lat);
281                         $lon = floatval($lon);
282                         $title = Title::newFromText($sledrun['page_title']);
283                         $properties = array('type' => 'sledrun', 'name' => $title->getText(), 'wiki' => $sledrun['page_title']);
284                         if (!is_null($sledrun['date_report'])) $properties['date_report'] = $sledrun['date_report'];
285                         if (!is_null($sledrun['condition'])) $properties['condition'] = intval($sledrun['condition']);
286                         $image_url = WrBaseMap::wikipage_to_image($title, 150);
287                         if (!is_null($image_url)) $properties['thumb_url'] = $image_url;
288                         $json_feature = array(
289                                 'type' => 'Feature',
290                                 'geometry' => array(
291                                         'type' => 'Point',
292                                         'coordinates' => array($lon, $lat)
293                                 ),
294                                 'properties' => $properties
295                         );
296                         $json_features[] = $json_feature;
297                 }
298                 return $json_features;
299         }
300
301
302         // convert XML to geojson (http://www.geojson.org/geojson-spec.html)
303         // Returns an array of features
304         public static function xml_to_json_features($input) {
305                 libxml_use_internal_errors(true); // without that, we get PHP Warnings if the $input is not well-formed
306                 $xml = new SimpleXMLElement($input); // input
307                 $whitespace = (string) $xml; // everything between <wrmap> and </wrmap> that's not a sub-element
308                 if (strlen($whitespace) > 0 && !ctype_space($whitespace)) { // there must not be anythin except sub-elements or whitespace
309                         throw new Exception(wfMessage('wrmap-error-invalid-text', trim($xml))->text());
310                 }
311                 $json_features = array(); // output
312                 $point_types = array('gasthaus', 'haltestelle', 'parkplatz', 'achtung', 'foto', 'verleih', 'punkt');
313                 $line_types = array('rodelbahn', 'gehweg', 'alternative', 'lift', 'anfahrt', 'linie');
314                 foreach ($xml as $feature) {
315                         $given_properties = array();
316                         foreach ($feature->attributes() as $key => $value) $given_properties[] = $key;
317
318                         // determine feature type
319                         $is_point = in_array($feature->getName(), $point_types);
320                         $is_line = in_array($feature->getName(), $line_types);
321                         if (!$is_point && !$is_line) {
322                                 throw new Exception(wfMessage('wrmap-error-invalid-element', $feature->getName(), '<' . implode('>, <', array_merge($point_types, $line_types)) . '>')->text());
323                         }
324
325                         // point
326                         if ($is_point) {
327                                 $properties = array('type' => $feature->getName());
328                                 $allowed_properties = array('name', 'wiki');
329                                 $wrong_properties = array_diff($given_properties, $allowed_properties);
330                                 if (count($wrong_properties) > 0) throw new Exception(wfMessage('wrmap-error-invalid-attribute',  reset($wrong_properties), $feature->getName(), "'" . implode("', '", $allowed_properties) . "'")->text());
331                                 foreach ($given_properties as $property) {
332                                         $propval = (string) $feature[$property];
333                                         if ($property == 'wiki') {
334                                                 $title = Title::newFromText($propval);
335                                                 $file_url = WrBaseMap::wikipage_to_image($title, 200);
336                                                 if (!is_null($file_url)) $properties['thumb_url'] = $file_url;
337                                         }
338                                         $properties[$property] = $propval;
339                                 }
340                                 $coordinates = WrBaseMap::geo_to_coordinates($feature);
341                                 if (count($coordinates) != 1) throw new Exception(wfMessage('wrmap-error-coordinate-count', $feature->getName())->text());
342                                 $json_feature = array(
343                                         'type' => 'Feature',
344                                         'geometry' => array(
345                                                 'type' => 'Point',
346                                                 'coordinates' => reset($coordinates)
347                                         ),
348                                         'properties' => $properties
349                                 );
350                                 $json_features[] = $json_feature;
351                         }
352                         // line
353                         if ($is_line) {
354                                 $properties = array('type' => $feature->getName());
355                                 $allowed_properties = array('farbe', 'dicke');
356                                 $wrong_properties = array_diff($given_properties, $allowed_properties);
357                                 if (count($wrong_properties) > 0) throw new Exception(wfMessage('wrmap-error-invalid-attribute',  reset($wrong_properties), $feature->getName(), "'" . implode("', '", $allowed_properties) . "'")->text());
358                                 if (isset($feature['farbe'])) {
359                                         $color = (string) $feature['farbe']; // e.g. #a200b7
360                                         if (preg_match('/^#[0-9a-f]{6}$/i', $color) != 1)
361                                                 throw new Exception(wfMessage('wrmap-error-line-color')->text());
362                                         $properties['strokeColor'] = $color;
363                                 }
364                                 if (isset($feature['dicke'])) {
365                                         $stroke_width = (int) $feature['dicke']; // e.g. 6
366                                         if (((string) $stroke_width) !== (string) $feature['dicke'])
367                                                 throw new Exception(wfMessage('wrmap-error-line-width')->text());
368                                         $properties['strokeWidth'] = $stroke_width;
369                                 }
370                                 $json_feature = array(
371                                         'type' => 'Feature',
372                                         'geometry' => array(
373                                                 'type' => 'LineString',
374                                                 'coordinates' => WrBaseMap::geo_to_coordinates($feature)
375                                         ),
376                                         'properties' => $properties
377                                 );
378                                 $json_features[] = $json_feature;
379                         }
380                 }
381                 return $json_features;
382         }
383
384
385         /// Renders the <wrgmap> tag and the <wrmap> tag.
386         /// The WrBaseMap class would be the only class needed but as the function render() does not provide an argument
387         /// telling which tag name called the function, a trick with two inherited classes has to be used.
388         /// @param $content string - the content of the <wrgmap> tag
389         /// @param $args array - the array of attribute name/value pairs for the tag
390         /// @param $parser Parser - the MW Parser object for the current page
391         ///
392         /// @return string - the html for rendering the map
393         public static function render($content, $args, $parser, $frame) {
394                 // Unfortunately, $tagname is no argument of this function, therefore we have to use a trick with derived classes.
395                 $tagname = strtolower(get_called_class()); // either wrmap or wrgmap
396                 assert(in_array($tagname, array('wrmap', 'wrgmap')));
397
398                 $parserOutput = $parser->getOutput();
399                 $parserOutput->addModules(array('ext.wrmap'));
400
401                 // append all sledruns as icon
402                 $json_features = array();
403                 $show_sledruns = ($tagname == 'wrgmap');
404                 if ($show_sledruns) {
405                         $json_features = array_merge($json_features, WrBaseMap::sledruns_to_json_features());
406                 }
407
408                 try {
409                         // map properties
410                         $properties = array();
411                         if (isset($args['lat'])) $properties['lat'] = (float) $args['lat']; // latitude as float value
412                         if (isset($args['lon'])) $properties['lon'] = (float) $args['lon']; // longitude as float value
413                         if (isset($args['zoom'])) $properties['zoom'] = (int) $args['zoom']; // zoom as int value
414                         if (isset($args['width'])) $properties['width'] = (int) $args['width']; // width as int value
415                         if (isset($args['height'])) $properties['height'] = (int) $args['height']; // height as int value
416
417                         // append all elements in the XML
418                         $json_features = array_merge($json_features, WrBaseMap::xml_to_json_features('<wrmap>' . $content . '</wrmap>'));
419                 } catch (Exception $e) {
420                         $doc = new WrMapDOMDocument();
421                         $doc->appendElement('div', array('class' => 'error'))->appendText('Fehler beim Parsen der Landkarte: ' . $e->getMessage());
422                         return array($doc->saveHTML($doc->firstChild), 'markerType' => 'nowiki');
423                 }
424
425                 // create final geojson
426                 $json = array(
427                         'type' => 'FeatureCollection',
428                         'features' => $json_features,
429                         'properties' => $properties
430                 );
431                 $json_string = json_encode($json);
432
433                 // Create <div/> element where the map is placed in
434                 global $wgExtensionAssetsPath;
435                 $doc = new WrMapDOMDocument();
436                 $div_map = $doc->appendElement('div', array('class' => 'wrmap', 'style' => 'border-style:none;', 'data-ext-path' => "$wgExtensionAssetsPath/wrmap"));
437                 // progress message
438                 $div_map->appendElement('div', array())->appendText(wfMessage('wrmap-loading')->text());
439                 // data
440                 $div_map->appendElement('div', array('style' => 'height: 0px; display:none;'))->appendText($json_string);
441                 // popup
442                 $div_popup = $doc->appendElement('div', array('id' => 'popup', 'class' => 'ol-popup'));
443                 $div_popup->appendElement('a', array('id' => 'popup-closer', 'href' => '#', 'class' => 'ol-popup-closer'));
444                 $div_popup->appendElement('div', array('id' => 'popup-content'));
445                 return array($doc->saveHTML($div_map) . $doc->saveHTML($div_popup), 'markerType' => 'nowiki');
446         }
447 }
448
449
450 // <wrmap> tag
451 class WrMap extends WrBaseMap {
452         public static function onParserFirstCallInit(Parser $parser) {
453                 $parser->setHook('wrmap', 'WrMap::render');
454                 return true;
455         }
456 }
457
458
459 // <wrgmap> tag
460 class WrGMap extends WrBaseMap {
461         public static function onParserFirstCallInit(Parser $parser) {
462                 $parser->setHook('wrgmap', 'WrGMap::render');
463                 return true;
464         }
465 }
466
467 ?>