Remove unnecessary whitespace.
[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.
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
160
161 // DOM helper classes
162 // ------------------
163
164 // The following two classes are "duplicated" from the wrreport extension to keep them separate.
165 // Put improvements in both classes.
166 class WrMapDOMDocument extends DOMDocument {
167         function __construct() {
168                 parent::__construct('1.0', 'utf-8');
169                 $this->registerNodeClass('DOMElement', 'WrMapDOMElement');
170         }
171
172         /// Creates and adds the element with the given tag name and returns it.
173         /// Additionally, it calls setAttribute($key, $value) for every entry
174         /// in $attributes.
175         function appendElement($tagName, $attributes=array()) {
176                 $child = $this->appendChild($this->createElement($tagName));
177                 foreach ($attributes as $key => $value) $child->setAttribute($key, $value);
178                 return $child;
179         }
180 }
181
182
183 class WrMapDOMElement extends DOMElement {
184
185         /// Creates and adds the element with the given tag name and returns it
186         /// Additionally, it calls setAttribute($key, $value) for every entry
187         /// in $attributes.
188         function appendElement($tagName, $attributes=array()) {
189                 $child = $this->appendChild($this->ownerDocument->createElement($tagName));
190                 foreach ($attributes as $key => $value) $child->setAttribute($key, $value);
191                 return $child;
192         }
193
194         /// Adds any UTF-8 string as content of the element - it will be escaped.
195         function appendText($text) {
196                 return $this->appendChild($this->ownerDocument->createTextNode($text));
197         }
198
199         // Appends a CDATASections to the element. This can be used to include
200         // raw (unparsed) HTML to the DOM tree as it is necessary because
201         // $parser->recursiveTagParse does not always escape & characters.
202         // (see https://bugzilla.wikimedia.org/show_bug.cgi?id=55526 )
203         // Workaround: Use a CDATA section. When serializing with $doc->saveHTML,
204         // the <![CDATA[...]]> is returned as ... .
205         // However, we end up having unescaped & in the output due to this bug in recursiveTagParse.
206         function appendCDATA($data) {
207                 return $this->appendChild($this->ownerDocument->createCDATASection($data));
208         }
209 }
210
211
212 // WrBaseMap
213 // ---------
214
215 class WrBaseMap {
216         // gets coordinates and returns an array of lon/lat coordinate pairs, e.g.
217         // 47.12 N 11.87 E
218         // 47.13 N 11.70 E
219         // ->
220         // array(array(11.87, 47.12), array(11.70, 47.13))
221         public static function geo_to_coordinates($input) {
222                 $matches = array();
223                 $num_matches = preg_match_all('/\s*(\d+\.?\d*)\s*N?\s+(\d+\.?\d*)\s*E?\s*/', $input, $matches);
224                 $result = array();
225                 for ($i=0; $i!=$num_matches; ++$i) {
226                         $result[] = array(floatval($matches[2][$i]), floatval($matches[1][$i]));
227                 }
228                 if (implode($matches[0]) != $input) throw new Exception(wfMessage('wrmap-error-coordinate-format', $input)->text());
229                 return $result;
230         }
231
232
233         /// Takes a page title from the wiki and returns an image (if available)
234         /// or Null. For image wiki pages, the image is the corresponding image,
235         /// for inns it's the image of the "Gasthausbox".
236         public static function wikipage_to_image($title, $width) {
237                 $file = false; // File class or false
238                 // for NS_FILE titles, use the corresponding file as image
239                 if ($title->getNamespace() == NS_FILE) {
240                         $file = wfFindFile($title); // $file is a mediawiki File class or false
241                 } else {
242                         $categories = $title->getParentCategories(); // e.g. array('Kategorie:Rodelbahn' => 'Juifenalm')
243                         global $wgContLang;
244                         $key_sledrun = $wgContLang->getNSText(NS_CATEGORY) . ':Rodelbahn';
245                         if (array_key_exists($key_sledrun, $categories)) {
246                                 // for sledrun titles use the image from the rodelbahnbox
247                                 $dbr = wfGetDB(DB_SLAVE);
248                                 $res = $dbr->select('wrsledruncache', 'image', array('page_id' => $title->getArticleID()), __METHOD__);
249                                 $image = $dbr->fetchRow($res);
250                                 if ($image && !is_null($image['image'])) $file = wfFindFile($image['image']);
251                                 $dbr->freeResult($res);
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_SLAVE);
257                                 $res = $dbr->select('wrinncache', 'image', array('page_id' => $title->getArticleID()), __METHOD__);
258                                 $image = $dbr->fetchRow($res);
259                                 if ($image && !is_null($image['image'])) $file = wfFindFile($image['image']);
260                                 $dbr->freeResult($res);
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 (http://www.geojson.org/geojson-spec.html)
272         // Returns an array of features
273         public static function sledruns_to_json_features() {
274                 $json_features = array(); // result
275                 $dbr = wfGetDB(DB_SLAVE);
276                 $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')));
277                 while ($sledrun = $dbr->fetchRow($res)) {
278                         $lat = $sledrun['position_latitude'];
279                         $lon = $sledrun['position_longitude'];
280                         if (is_null($lat) || is_null($lon)) continue;
281                         $lat = floatval($lat);
282                         $lon = floatval($lon);
283                         $title = Title::newFromText($sledrun['page_title']);
284                         $properties = array('type' => 'sledrun', 'name' => $title->getText(), 'wiki' => $title->getLocalUrl());
285                         if (!is_null($sledrun['date_report'])) $properties['date_report'] = $sledrun['date_report'];
286                         if (!is_null($sledrun['condition'])) $properties['condition'] = intval($sledrun['condition']);
287                         $image_url = WrBaseMap::wikipage_to_image($title, 150);
288                         if (!is_null($image_url)) $properties['thumb_url'] = $image_url;
289                         $json_feature = array(
290                                 'type' => 'feature',
291                                 'geometry' => array(
292                                         'type' => 'Point',
293                                         'coordinates' => array($lon, $lat)
294                                 ),
295                                 'properties' => $properties
296                         );
297                         $json_features[] = $json_feature;
298                 }
299                 $dbr->freeResult($res);
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                                                 $propval = $title->getLocalUrl();
338                                                 $file_url = WrBaseMap::wikipage_to_image($title, 200);
339                                                 if (!is_null($file_url)) $properties['thumb_url'] = $file_url;
340                                         }
341                                         $properties[$property] = $propval;
342                                 }
343                                 $coordinates = WrBaseMap::geo_to_coordinates($feature);
344                                 if (count($coordinates) != 1) throw new Exception(wfMessage('wrmap-error-coordinate-count', $feature->getName())->text());
345                                 $json_feature = array(
346                                         'type' => 'feature',
347                                         'geometry' => array(
348                                                 'type' => 'Point',
349                                                 'coordinates' => reset($coordinates)
350                                         ),
351                                         'properties' => $properties
352                                 );
353                                 $json_features[] = $json_feature;
354                         }
355                         // line
356                         if ($is_line) {
357                                 $properties = array('type' => $feature->getName());
358                                 $allowed_properties = array('farbe', 'dicke');
359                                 $wrong_properties = array_diff($given_properties, $allowed_properties);
360                                 if (count($wrong_properties) > 0) throw new Exception(wfMessage('wrmap-error-invalid-attribute',  reset($wrong_properties), $feature->getName(), "'" . implode("', '", $allowed_properties) . "'")->text());
361                                 if (isset($feature['farbe'])) {
362                                         $color = (string) $feature['farbe']; // e.g. #a200b7
363                                         if (preg_match('/^#[0-9a-f]{6}$/i', $color) != 1)
364                                                 throw new Exception(wfMessage('wrmap-error-line-color')->text());
365                                         $properties['strokeColor'] = $color;
366                                 }
367                                 if (isset($feature['dicke'])) {
368                                         $stroke_width = (int) $feature['dicke']; // e.g. 6
369                                         if (((string) $stroke_width) !== (string) $feature['dicke'])
370                                                 throw new Exception(wfMessage('wrmap-error-line-width')->text());
371                                         $properties['strokeWidth'] = $stroke_width;
372                                 }
373                                 $json_feature = array(
374                                         'type' => 'feature',
375                                         'geometry' => array(
376                                                 'type' => 'LineString',
377                                                 'coordinates' => WrBaseMap::geo_to_coordinates($feature)
378                                         ),
379                                         'properties' => $properties
380                                 );
381                                 $json_features[] = $json_feature;
382                         }
383                 }
384                 return $json_features;
385         }
386
387
388         /// Renders the <wrgmap> tag and the <wrmap> tag.
389         /// The WrBaseMap class would be the only class needed but as the function render() does not provide an argument
390         /// telling which tag name called the function, a trick with two inherited classes has to be used.
391         /// @param $content string - the content of the <wrgmap> tag
392         /// @param $args array - the array of attribute name/value pairs for the tag
393         /// @param $parser Parser - the MW Parser object for the current page
394         ///
395         /// @return string - the html for rendering the map
396         public static function render($content, $args, $parser, $frame) {
397                 // Unfortunately, $tagname is no argument of this function, therefore we have to use a trick with derived classes.
398                 $tagname = strtolower(get_called_class()); // either wrmap or wrgmap
399                 assert(in_array($tagname, array('wrmap', 'wrgmap')));
400
401                 $parserOutput = $parser->getOutput();
402                 $parserOutput->addHeadItem('<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?key=AIzaSyABu68dyE3dB_l-UfB_WuyoPAsSTthI4gc"></script>', 'googlemaps');
403                 $parserOutput->addModules('ext.wrmap');
404
405                 // append all sledruns as icon
406                 $json_features = array();
407                 $show_sledruns = ($tagname == 'wrgmap');
408                 if ($show_sledruns) {
409                         $json_features = array_merge($json_features, WrBaseMap::sledruns_to_json_features());
410                 }
411
412                 try {
413                         // map properties
414                         $properties = array();
415                         if (isset($args['lat'])) $properties['lat'] = (float) $args['lat']; // latitude as float value
416                         if (isset($args['lon'])) $properties['lon'] = (float) $args['lon']; // longitude as float value
417                         if (isset($args['zoom'])) $properties['zoom'] = (int) $args['zoom']; // zoom as int value
418                         if (isset($args['width'])) $properties['width'] = (int) $args['width']; // width as int value
419                         if (isset($args['height'])) $properties['height'] = (int) $args['height']; // height as int value
420
421                         // append all elements in the XML
422                         $json_features = array_merge($json_features, WrBaseMap::xml_to_json_features('<wrmap>' . $content . '</wrmap>'));
423                 } catch (Exception $e) {
424                         $doc = new WrMapDOMDocument();
425                         $doc->appendElement('div', array('class' => 'error'))->appendText('Fehler beim Parsen der Landkarte: ' . $e->getMessage());
426                         return array($doc->saveHTML($doc->firstChild), 'markerType' => 'nowiki');
427                 }
428
429                 // create final geojson
430                 $json = array(
431                         'type' => 'FeatureCollection',
432                         'features' => $json_features,
433                         'properties' => $properties
434                 );
435                 $json_string = json_encode($json);
436
437                 // Create <div/> element where the map is placed in
438                 global $wgExtensionAssetsPath;
439                 $doc = new WrMapDOMDocument();
440                 $div = $doc->appendElement('div', array('class' => 'wrmap', 'style' => 'border-style:none;', 'data-ext-path' => "$wgExtensionAssetsPath/wrmap"));
441                 // progress message
442                 $div->appendElement('div', array())->appendText(wfMessage('wrmap-loading')->text());
443                 // data
444                 $div->appendElement('div', array('style' => 'height: 0px; display:none;'))->appendText($json_string);
445                 return array($doc->saveHTML($div), 'markerType' => 'nowiki');
446         }
447
448
449         public static function onEnableMobileModules($out, $mode) {
450                 $out->addModules('ext.wrmap.mobile');
451                 return true;
452         }
453 }
454
455
456 // <wrmap> tag
457 class WrMap extends WrBaseMap {
458         public static function onParserFirstCallInit(Parser $parser) {
459                 $parser->setHook('wrmap', 'WrMap::render');
460                 return true;
461         }
462 }
463
464
465 // <wrgmap> tag
466 class WrGMap extends WrBaseMap {
467         public static function onParserFirstCallInit(Parser $parser) {
468                 $parser->setHook('wrgmap', 'WrGMap::render');
469                 return true;
470         }
471 }
472
473 ?>