(Shows icons for all sledruns. lat, lon and zoom are optional.)
Example 2
---------
47.240689 11.19045447.238186 11.22194047.240287 11.20300647.245789 11.23897147.237627 11.21888647.245711 11.23828347.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_SLAVE);
$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_SLAVE);
$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_SLAVE);
$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;
}
}
?>