]> ToastFreeware Gitweb - philipp/winterrodeln/mediawiki_extensions/wrmap.git/blob - wrmap.body.php
Use ILoadBalancer::getConnection() instead of deprecated wfGetDB().
[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                         $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
244                         $categories = $title->getParentCategories(); // e.g. array('Kategorie:Rodelbahn' => 'Juifenalm')
245                         $wgContLang = MediaWikiServices::getInstance()->getContentLanguage();
246                         $key_sledrun = $wgContLang->getNSText(NS_CATEGORY) . ':Rodelbahn';
247                         if (array_key_exists($key_sledrun, $categories)) {
248                                 // for sledrun titles use the image from the rodelbahnbox
249                                 $dbr = $lb->getConnection( DB_REPLICA );
250                                 $res = $dbr->select('wrsledruncache', 'image', array('page_id' => $title->getArticleID()), __METHOD__);
251                                 $image = $res->fetchRow();
252                                 if ($image && !is_null($image['image'])) $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile($image['image']);
253                         }
254                         $key_inn = $wgContLang->getNSText(NS_CATEGORY) . ':Gasthaus';
255                         if (array_key_exists($key_inn, $categories)) {
256                                 // for inn titles use the image from the gasthausbox
257                                 $dbr = $lb->getConnection( DB_REPLICA );
258                                 $res = $dbr->select('wrinncache', 'image', array('page_id' => $title->getArticleID()), __METHOD__);
259                                 $image = $res->fetchRow();
260                                 if ($image && !is_null($image['image'])) $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile($image['image']);
261                         }
262                 }
263                 if ($file === false) return Null;
264                 if (!$file->canRender()) return Null;
265                 $thumb_url = $file->createThumb($width, $width); // limit width and hight to $width
266                 if (strlen($thumb_url) == 0) return Null;
267                 return $thumb_url;
268         }
269
270
271         // convert sledruns to geojson (https://datatracker.ietf.org/doc/html/rfc7946)
272         // Returns an array of features
273         public static function sledruns_to_json_features() {
274                 $json_features = array(); // result
275                 $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
276                 $dbr = $lb->getConnection( DB_REPLICA );
277                 $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')));
278                 while ($sledrun = $res->fetchRow()) {
279                         $lat = $sledrun['position_latitude'];
280                         $lon = $sledrun['position_longitude'];
281                         if (is_null($lat) || is_null($lon)) continue;
282                         $lat = floatval($lat);
283                         $lon = floatval($lon);
284                         $title = Title::newFromText($sledrun['page_title']);
285                         $properties = array('type' => 'sledrun', 'name' => $title->getText(), 'wiki' => $sledrun['page_title']);
286                         if (!is_null($sledrun['date_report'])) $properties['date_report'] = $sledrun['date_report'];
287                         if (!is_null($sledrun['condition'])) $properties['condition'] = intval($sledrun['condition']);
288                         $image_url = WrBaseMap::wikipage_to_image($title, 150);
289                         if (!is_null($image_url)) $properties['thumb_url'] = $image_url;
290                         $json_feature = array(
291                                 'type' => 'Feature',
292                                 'geometry' => array(
293                                         'type' => 'Point',
294                                         'coordinates' => array($lon, $lat)
295                                 ),
296                                 'properties' => $properties
297                         );
298                         $json_features[] = $json_feature;
299                 }
300                 return $json_features;
301         }
302
303
304         // convert XML to geojson (http://www.geojson.org/geojson-spec.html)
305         // Returns an array of features
306         public static function xml_to_json_features($input) {
307                 libxml_use_internal_errors(true); // without that, we get PHP Warnings if the $input is not well-formed
308                 $xml = new SimpleXMLElement($input); // input
309                 $whitespace = (string) $xml; // everything between <wrmap> and </wrmap> that's not a sub-element
310                 if (strlen($whitespace) > 0 && !ctype_space($whitespace)) { // there must not be anythin except sub-elements or whitespace
311                         throw new Exception(wfMessage('wrmap-error-invalid-text', trim($xml))->text());
312                 }
313                 $json_features = array(); // output
314                 $point_types = array('gasthaus', 'haltestelle', 'parkplatz', 'achtung', 'foto', 'verleih', 'punkt');
315                 $line_types = array('rodelbahn', 'gehweg', 'alternative', 'lift', 'anfahrt', 'linie');
316                 foreach ($xml as $feature) {
317                         $given_properties = array();
318                         foreach ($feature->attributes() as $key => $value) $given_properties[] = $key;
319
320                         // determine feature type
321                         $is_point = in_array($feature->getName(), $point_types);
322                         $is_line = in_array($feature->getName(), $line_types);
323                         if (!$is_point && !$is_line) {
324                                 throw new Exception(wfMessage('wrmap-error-invalid-element', $feature->getName(), '<' . implode('>, <', array_merge($point_types, $line_types)) . '>')->text());
325                         }
326
327                         // point
328                         if ($is_point) {
329                                 $properties = array('type' => $feature->getName());
330                                 $allowed_properties = array('name', 'wiki');
331                                 $wrong_properties = array_diff($given_properties, $allowed_properties);
332                                 if (count($wrong_properties) > 0) throw new Exception(wfMessage('wrmap-error-invalid-attribute',  reset($wrong_properties), $feature->getName(), "'" . implode("', '", $allowed_properties) . "'")->text());
333                                 foreach ($given_properties as $property) {
334                                         $propval = (string) $feature[$property];
335                                         if ($property == 'wiki') {
336                                                 $title = Title::newFromText($propval);
337                                                 $file_url = WrBaseMap::wikipage_to_image($title, 200);
338                                                 if (!is_null($file_url)) $properties['thumb_url'] = $file_url;
339                                         }
340                                         $properties[$property] = $propval;
341                                 }
342                                 $coordinates = WrBaseMap::geo_to_coordinates($feature);
343                                 if (count($coordinates) != 1) throw new Exception(wfMessage('wrmap-error-coordinate-count', $feature->getName())->text());
344                                 $json_feature = array(
345                                         'type' => 'Feature',
346                                         'geometry' => array(
347                                                 'type' => 'Point',
348                                                 'coordinates' => reset($coordinates)
349                                         ),
350                                         'properties' => $properties
351                                 );
352                                 $json_features[] = $json_feature;
353                         }
354                         // line
355                         if ($is_line) {
356                                 $properties = array('type' => $feature->getName());
357                                 $allowed_properties = array('farbe', 'dicke');
358                                 $wrong_properties = array_diff($given_properties, $allowed_properties);
359                                 if (count($wrong_properties) > 0) throw new Exception(wfMessage('wrmap-error-invalid-attribute',  reset($wrong_properties), $feature->getName(), "'" . implode("', '", $allowed_properties) . "'")->text());
360                                 if (isset($feature['farbe'])) {
361                                         $color = (string) $feature['farbe']; // e.g. #a200b7
362                                         if (preg_match('/^#[0-9a-f]{6}$/i', $color) != 1)
363                                                 throw new Exception(wfMessage('wrmap-error-line-color')->text());
364                                         $properties['strokeColor'] = $color;
365                                 }
366                                 if (isset($feature['dicke'])) {
367                                         $stroke_width = (int) $feature['dicke']; // e.g. 6
368                                         if (((string) $stroke_width) !== (string) $feature['dicke'])
369                                                 throw new Exception(wfMessage('wrmap-error-line-width')->text());
370                                         $properties['strokeWidth'] = $stroke_width;
371                                 }
372                                 $json_feature = array(
373                                         'type' => 'Feature',
374                                         'geometry' => array(
375                                                 'type' => 'LineString',
376                                                 'coordinates' => WrBaseMap::geo_to_coordinates($feature)
377                                         ),
378                                         'properties' => $properties
379                                 );
380                                 $json_features[] = $json_feature;
381                         }
382                 }
383                 return $json_features;
384         }
385
386
387         /// Renders the <wrgmap> tag and the <wrmap> tag.
388         /// The WrBaseMap class would be the only class needed but as the function render() does not provide an argument
389         /// telling which tag name called the function, a trick with two inherited classes has to be used.
390         /// @param $content string - the content of the <wrgmap> tag
391         /// @param $args array - the array of attribute name/value pairs for the tag
392         /// @param $parser Parser - the MW Parser object for the current page
393         ///
394         /// @return string - the html for rendering the map
395         public static function render($content, $args, $parser, $frame) {
396                 // Unfortunately, $tagname is no argument of this function, therefore we have to use a trick with derived classes.
397                 $tagname = strtolower(get_called_class()); // either wrmap or wrgmap
398                 assert(in_array($tagname, array('wrmap', 'wrgmap')));
399
400                 $parserOutput = $parser->getOutput();
401                 $parserOutput->addModules(array('ext.wrmap'));
402
403                 // append all sledruns as icon
404                 $json_features = array();
405                 $show_sledruns = ($tagname == 'wrgmap');
406                 if ($show_sledruns) {
407                         $json_features = array_merge($json_features, WrBaseMap::sledruns_to_json_features());
408                 }
409
410                 try {
411                         // map properties
412                         $properties = array();
413                         if (isset($args['lat'])) $properties['lat'] = (float) $args['lat']; // latitude as float value
414                         if (isset($args['lon'])) $properties['lon'] = (float) $args['lon']; // longitude as float value
415                         if (isset($args['zoom'])) $properties['zoom'] = (int) $args['zoom']; // zoom as int value
416                         if (isset($args['width'])) $properties['width'] = (int) $args['width']; // width as int value
417                         if (isset($args['height'])) $properties['height'] = (int) $args['height']; // height as int value
418
419                         // append all elements in the XML
420                         $json_features = array_merge($json_features, WrBaseMap::xml_to_json_features('<wrmap>' . $content . '</wrmap>'));
421                 } catch (Exception $e) {
422                         $doc = new WrMapDOMDocument();
423                         $doc->appendElement('div', array('class' => 'error'))->appendText('Fehler beim Parsen der Landkarte: ' . $e->getMessage());
424                         return array($doc->saveHTML($doc->firstChild), 'markerType' => 'nowiki');
425                 }
426
427                 // create final geojson
428                 $json = array(
429                         'type' => 'FeatureCollection',
430                         'features' => $json_features,
431                         'properties' => $properties
432                 );
433                 $json_string = json_encode($json);
434
435                 // Create <div/> element where the map is placed in
436                 global $wgExtensionAssetsPath;
437                 $doc = new WrMapDOMDocument();
438                 $div_map = $doc->appendElement('div', array('class' => 'wrmap', 'style' => 'border-style:none;', 'data-ext-path' => "$wgExtensionAssetsPath/wrmap"));
439                 // progress message
440                 $div_map->appendElement('div', array())->appendText(wfMessage('wrmap-loading')->text());
441                 // data
442                 $div_map->appendElement('div', array('style' => 'height: 0px; display:none;'))->appendText($json_string);
443                 // popup
444                 $div_popup = $doc->appendElement('div', array('id' => 'popup', 'class' => 'ol-popup'));
445                 $div_popup->appendElement('a', array('id' => 'popup-closer', 'href' => '#', 'class' => 'ol-popup-closer'));
446                 $div_popup->appendElement('div', array('id' => 'popup-content'));
447                 return array($doc->saveHTML($div_map) . $doc->saveHTML($div_popup), 'markerType' => 'nowiki');
448         }
449 }
450
451
452 // <wrmap> tag
453 class WrMap extends WrBaseMap {
454         public static function onParserFirstCallInit(Parser $parser) {
455                 $parser->setHook('wrmap', 'WrMap::render');
456                 return true;
457         }
458 }
459
460
461 // <wrgmap> tag
462 class WrGMap extends WrBaseMap {
463         public static function onParserFirstCallInit(Parser $parser) {
464                 $parser->setHook('wrgmap', 'WrGMap::render');
465                 return true;
466         }
467 }
468
469 ?>