(Shows icons for all sledruns. lat, lon and zoom are optional.) Example 2 --------- 47.240689 11.190454 47.238186 11.221940 47.240287 11.203006 47.245789 11.238971 47.237627 11.218886 47.245711 11.238283 47.2383200 11.2235592 47.238587 11.203360 47.239743 11.203522 47.240135 11.203247 47.238442 11.203263 47.237799 11.203511 47.237133 11.202988 47.238091 11.206642 47.237273 11.211675 47.237133 11.214466 47.237513 11.218199 47.240487 11.190169 47.238996 11.188628 47.238987 11.188018 47.238267 11.187075 47.238461 11.190511 47.239751 11.191795 47.240037 11.192702 47.239525 11.193535 47.239688 11.194272 47.239017 11.193925 47.239536 11.195457 47.240063 11.196230 47.240747 11.196658 47.239734 11.198295 47.238857 11.198346 47.237743 11.199778 47.238250 11.202755 47.238587 11.203360 47.238587 11.203360 47.238185 11.203982 47.238297 11.204381 47.239417 11.204972 47.239210 11.208772 47.238999 11.209523 47.239126 11.209839 47.238933 11.210641 47.239102 11.210739 47.238666 11.215042 47.238203 11.216089 47.238183 11.218151 47.237851 11.218599 47.238055 11.219755 47.237686 11.222441 47.238000 11.223367 47.238625 11.223687 47.239915 11.223118 47.240992 11.219781 47.243412 11.214141 47.243207 11.218331 47.243990 11.216205 47.243785 11.223251 47.242845 11.228510 47.242917 11.232501 47.242524 11.235001 47.244737 11.231791 47.244951 11.230868 47.245470 11.237853 47.245656 11.237286 47.238189 11.221344 Definition ---------- * ... has to be valid XML. * All coordinates are in WGS84 coordinate system. * Coordinates have the preferred format "latitude N longitude E", however for parsing the N and E can be omitted. * has the following attributes: * lat (float): latitude of map-center, optional. * lon (float): longitude of map-center, optional. * zoom (integer): zoom level of the map (google zoom levels). optional. * width (integer): width of the map in pixel. optional (100% if omitted) * height (integer): height of the map in pixel. optional. * can have any number of the following sub-elements: * * * * * * * * * * * * * * The order may be used by the renderer to determine in which order the elements should be drawn. * , , , , , and define points * The elements may have the following attributes: * name (string): defines the name (not the label) of the element * wiki (string): name of a MediaWiki page the point refers to * The content is exactly one coordinate pair. * , , , , and define non-closed polygons. * They may have the following attributes: farbe (hex format, e.g. #12a50f): color of the line dicke (int): width of the line in pixel * The content of the elements are a whitespace separated list of coordinates. For transmitting the map to javascript, geojson is used in the
element of the map. This way, an extra request is avoided. The geojson format used here consists of a single "FeatureCollection" (representing the ) containing the sub-elements of wrmap as features. The features have an properties key that has a hash as values with the properties of the XML subelements of wrmap. Optional attributes/properties can be omitted. Additionally one mandatory property key is called 'type' and has the sub-element's name as value. The featurecollection itself has a properties key as well containing the attributes of the wrmap element. */ // DOM helper classes // ------------------ // The following two classes are "duplicated" from the wrreport extension to keep them separate. // Put improvements in both classes. class WrMapDOMDocument extends DOMDocument { function __construct() { parent::__construct('1.0', 'utf-8'); $this->registerNodeClass('DOMElement', 'WrMapDOMElement'); } /// Creates and adds the element with the given tag name and returns it. /// Additionally, it calls setAttribute($key, $value) for every entry /// in $attributes. function appendElement($tagName, $attributes=array()) { $child = $this->appendChild($this->createElement($tagName)); foreach ($attributes as $key => $value) $child->setAttribute($key, $value); return $child; } } class WrMapDOMElement extends DOMElement { /// Creates and adds the element with the given tag name and returns it /// Additionally, it calls setAttribute($key, $value) for every entry /// in $attributes. function appendElement($tagName, $attributes=array()) { $child = $this->appendChild($this->ownerDocument->createElement($tagName)); foreach ($attributes as $key => $value) $child->setAttribute($key, $value); return $child; } /// Adds any UTF-8 string as content of the element - it will be escaped. function appendText($text) { return $this->appendChild($this->ownerDocument->createTextNode($text)); } // Appends a CDATASections to the element. This can be used to include // raw (unparsed) HTML to the DOM tree as it is necessary because // $parser->recursiveTagParse does not always escape & characters. // (see https://bugzilla.wikimedia.org/show_bug.cgi?id=55526 ) // Workaround: Use a CDATA section. When serializing with $doc->saveHTML, // the is returned as ... . // However, we end up having unescaped & in the output due to this bug in recursiveTagParse. function appendCDATA($data) { return $this->appendChild($this->ownerDocument->createCDATASection($data)); } } // WrBaseMap // --------- class WrBaseMap { // gets coordinates and returns an array of lon/lat coordinate pairs, e.g. // 47.12 N 11.87 E // 47.13 N 11.70 E // -> // array(array(11.87, 47.12), array(11.70, 47.13)) public static function geo_to_coordinates($input) { $matches = array(); $num_matches = preg_match_all('/\s*(\d+\.?\d*)\s*N?\s+(\d+\.?\d*)\s*E?\s*/', $input, $matches); $result = array(); for ($i=0; $i!=$num_matches; ++$i) { $result[] = array(floatval($matches[2][$i]), floatval($matches[1][$i])); } if (implode($matches[0]) != $input) throw new Exception(wfMessage('wrmap-error-coordinate-format', $input)->text()); return $result; } /// Takes a page title from the wiki and returns an image (if available) /// or Null. For image wiki pages, the image is the corresponding image, /// for inns it's the image of the "Gasthausbox". public static function wikipage_to_image($title, $width) { $file = false; // File class or false // for NS_FILE titles, use the corresponding file as image if ($title->getNamespace() == NS_FILE) { $file = wfFindFile($title); // $file is a mediawiki File class or false } else { $categories = $title->getParentCategories(); // e.g. array('Kategorie:Rodelbahn' => 'Juifenalm') global $wgContLang; $key_sledrun = $wgContLang->getNSText(NS_CATEGORY) . ':Rodelbahn'; if (array_key_exists($key_sledrun, $categories)) { // for sledrun titles use the image from the rodelbahnbox $dbr = wfGetDB(DB_REPLICA); $res = $dbr->select('wrsledruncache', 'image', array('page_id' => $title->getArticleID()), __METHOD__); $image = $dbr->fetchRow($res); if ($image && !is_null($image['image'])) $file = wfFindFile($image['image']); $dbr->freeResult($res); } $key_inn = $wgContLang->getNSText(NS_CATEGORY) . ':Gasthaus'; if (array_key_exists($key_inn, $categories)) { // for inn titles use the image from the gasthausbox $dbr = wfGetDB(DB_REPLICA); $res = $dbr->select('wrinncache', 'image', array('page_id' => $title->getArticleID()), __METHOD__); $image = $dbr->fetchRow($res); if ($image && !is_null($image['image'])) $file = wfFindFile($image['image']); $dbr->freeResult($res); } } if ($file === false) return Null; if (!$file->canRender()) return Null; $thumb_url = $file->createThumb($width, $width); // limit width and hight to $width if (strlen($thumb_url) == 0) return Null; return $thumb_url; } // convert sledruns to geojson (http://www.geojson.org/geojson-spec.html) // Returns an array of features public static function sledruns_to_json_features() { $json_features = array(); // result $dbr = wfGetDB(DB_REPLICA); $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'))); while ($sledrun = $dbr->fetchRow($res)) { $lat = $sledrun['position_latitude']; $lon = $sledrun['position_longitude']; if (is_null($lat) || is_null($lon)) continue; $lat = floatval($lat); $lon = floatval($lon); $title = Title::newFromText($sledrun['page_title']); $properties = array('type' => 'sledrun', 'name' => $title->getText(), 'wiki' => $title->getLocalUrl()); if (!is_null($sledrun['date_report'])) $properties['date_report'] = $sledrun['date_report']; if (!is_null($sledrun['condition'])) $properties['condition'] = intval($sledrun['condition']); $image_url = WrBaseMap::wikipage_to_image($title, 150); if (!is_null($image_url)) $properties['thumb_url'] = $image_url; $json_feature = array( 'type' => 'Feature', 'geometry' => array( 'type' => 'Point', 'coordinates' => array($lon, $lat) ), 'properties' => $properties ); $json_features[] = $json_feature; } $dbr->freeResult($res); return $json_features; } // convert XML to geojson (http://www.geojson.org/geojson-spec.html) // Returns an array of features public static function xml_to_json_features($input) { libxml_use_internal_errors(true); // without that, we get PHP Warnings if the $input is not well-formed $xml = new SimpleXMLElement($input); // input $whitespace = (string) $xml; // everything between and that's not a sub-element if (strlen($whitespace) > 0 && !ctype_space($whitespace)) { // there must not be anythin except sub-elements or whitespace throw new Exception(wfMessage('wrmap-error-invalid-text', trim($xml))->text()); } $json_features = array(); // output $point_types = array('gasthaus', 'haltestelle', 'parkplatz', 'achtung', 'foto', 'verleih', 'punkt'); $line_types = array('rodelbahn', 'gehweg', 'alternative', 'lift', 'anfahrt', 'linie'); foreach ($xml as $feature) { $given_properties = array(); foreach ($feature->attributes() as $key => $value) $given_properties[] = $key; // determine feature type $is_point = in_array($feature->getName(), $point_types); $is_line = in_array($feature->getName(), $line_types); if (!$is_point && !$is_line) { throw new Exception(wfMessage('wrmap-error-invalid-element', $feature->getName(), '<' . implode('>, <', array_merge($point_types, $line_types)) . '>')->text()); } // point if ($is_point) { $properties = array('type' => $feature->getName()); $allowed_properties = array('name', 'wiki'); $wrong_properties = array_diff($given_properties, $allowed_properties); if (count($wrong_properties) > 0) throw new Exception(wfMessage('wrmap-error-invalid-attribute', reset($wrong_properties), $feature->getName(), "'" . implode("', '", $allowed_properties) . "'")->text()); foreach ($given_properties as $property) { $propval = (string) $feature[$property]; if ($property == 'wiki') { $title = Title::newFromText($propval); $propval = $title->getLocalUrl(); $file_url = WrBaseMap::wikipage_to_image($title, 200); if (!is_null($file_url)) $properties['thumb_url'] = $file_url; } $properties[$property] = $propval; } $coordinates = WrBaseMap::geo_to_coordinates($feature); if (count($coordinates) != 1) throw new Exception(wfMessage('wrmap-error-coordinate-count', $feature->getName())->text()); $json_feature = array( 'type' => 'Feature', 'geometry' => array( 'type' => 'Point', 'coordinates' => reset($coordinates) ), 'properties' => $properties ); $json_features[] = $json_feature; } // line if ($is_line) { $properties = array('type' => $feature->getName()); $allowed_properties = array('farbe', 'dicke'); $wrong_properties = array_diff($given_properties, $allowed_properties); if (count($wrong_properties) > 0) throw new Exception(wfMessage('wrmap-error-invalid-attribute', reset($wrong_properties), $feature->getName(), "'" . implode("', '", $allowed_properties) . "'")->text()); if (isset($feature['farbe'])) { $color = (string) $feature['farbe']; // e.g. #a200b7 if (preg_match('/^#[0-9a-f]{6}$/i', $color) != 1) throw new Exception(wfMessage('wrmap-error-line-color')->text()); $properties['strokeColor'] = $color; } if (isset($feature['dicke'])) { $stroke_width = (int) $feature['dicke']; // e.g. 6 if (((string) $stroke_width) !== (string) $feature['dicke']) throw new Exception(wfMessage('wrmap-error-line-width')->text()); $properties['strokeWidth'] = $stroke_width; } $json_feature = array( 'type' => 'Feature', 'geometry' => array( 'type' => 'LineString', 'coordinates' => WrBaseMap::geo_to_coordinates($feature) ), 'properties' => $properties ); $json_features[] = $json_feature; } } return $json_features; } /// Renders the tag and the tag. /// The WrBaseMap class would be the only class needed but as the function render() does not provide an argument /// telling which tag name called the function, a trick with two inherited classes has to be used. /// @param $content string - the content of the tag /// @param $args array - the array of attribute name/value pairs for the tag /// @param $parser Parser - the MW Parser object for the current page /// /// @return string - the html for rendering the map public static function render($content, $args, $parser, $frame) { // Unfortunately, $tagname is no argument of this function, therefore we have to use a trick with derived classes. $tagname = strtolower(get_called_class()); // either wrmap or wrgmap assert(in_array($tagname, array('wrmap', 'wrgmap'))); $parserOutput = $parser->getOutput(); // Polyfill für Internet Explorer $parserOutput->addHeadItem('', 'polyfill_ie'); // OpenLayers 6.5.0 does not work with Internet Explorer, see https://github.com/openlayers/openlayers/issues/11870 $parserOutput->addHeadItem('', 'openlayers_js'); // CDN link for 6.4.3 doesn't work anymore $parserOutput->addHeadItem('', 'openlayers_css'); $parserOutput->addModules('ext.wrmap'); // append all sledruns as icon $json_features = array(); $show_sledruns = ($tagname == 'wrgmap'); if ($show_sledruns) { $json_features = array_merge($json_features, WrBaseMap::sledruns_to_json_features()); } try { // map properties $properties = array(); if (isset($args['lat'])) $properties['lat'] = (float) $args['lat']; // latitude as float value if (isset($args['lon'])) $properties['lon'] = (float) $args['lon']; // longitude as float value if (isset($args['zoom'])) $properties['zoom'] = (int) $args['zoom']; // zoom as int value if (isset($args['width'])) $properties['width'] = (int) $args['width']; // width as int value if (isset($args['height'])) $properties['height'] = (int) $args['height']; // height as int value // append all elements in the XML $json_features = array_merge($json_features, WrBaseMap::xml_to_json_features('' . $content . '')); } catch (Exception $e) { $doc = new WrMapDOMDocument(); $doc->appendElement('div', array('class' => 'error'))->appendText('Fehler beim Parsen der Landkarte: ' . $e->getMessage()); return array($doc->saveHTML($doc->firstChild), 'markerType' => 'nowiki'); } // create final geojson $json = array( 'type' => 'FeatureCollection', 'features' => $json_features, 'properties' => $properties ); $json_string = json_encode($json); // Create
element where the map is placed in global $wgExtensionAssetsPath; $doc = new WrMapDOMDocument(); $div_map = $doc->appendElement('div', array('class' => 'wrmap', 'style' => 'border-style:none;', 'data-ext-path' => "$wgExtensionAssetsPath/wrmap")); // progress message $div_map->appendElement('div', array())->appendText(wfMessage('wrmap-loading')->text()); // data $div_map->appendElement('div', array('style' => 'height: 0px; display:none;'))->appendText($json_string); // popup $div_popup = $doc->appendElement('div', array('id' => 'popup', 'class' => 'ol-popup')); $div_popup->appendElement('a', array('id' => 'popup-closer', 'href' => '#', 'class' => 'ol-popup-closer')); $div_popup->appendElement('div', array('id' => 'popup-content')); return array($doc->saveHTML($div_map) . $doc->saveHTML($div_popup), 'markerType' => 'nowiki'); } } // tag class WrMap extends WrBaseMap { public static function onParserFirstCallInit(Parser $parser) { $parser->setHook('wrmap', 'WrMap::render'); return true; } } // tag class WrGMap extends WrBaseMap { public static function onParserFirstCallInit(Parser $parser) { $parser->setHook('wrgmap', 'WrGMap::render'); return true; } } ?>