"""This module contains winterrodeln specific functions that are processing the MediaWiki markup.
"""
import re
import xml.etree.ElementTree
import collections
from typing import Tuple, Optional, List, OrderedDict, Union, Dict, Any
import jinja2
from mwparserfromhell.nodes import Template, Wikilink
import wrpylib.wrvalidators
import wrpylib.mwmarkup
import wrpylib.wrmwdb
from wrpylib.lib_sledrun_wikitext_from_json import strip_eol
from wrpylib.wrvalidators import LonLat, opt_lonlat_from_str, opt_lonlat_to_str, opt_uint_from_str, opt_uint_to_str, \
opt_str_opt_comment_enum_to_str, lift_german_to_str, webauskunft_to_str, cachet_german_to_str, \
opt_phone_comment_enum_to_str, lift_german_from_str, GASTHAUSBOX_DICT, opt_difficulty_german_from_str, \
opt_avalanches_german_from_str, opt_public_transport_german_from_str, \
opt_tristate_german_comment_from_str, rodelbahnbox_to_str, lonlat_to_str, opt_no_or_str_to_str, \
opt_no_or_str_from_str, opt_tristate_german_from_str, tristate_german_from_str
def split_lon_lat(value: Optional[LonLat]) -> Union[LonLat, Tuple[None, None]]:
if value is None:
return None, None
return value
def join_lon_lat(lon: Optional[float], lat: Optional[float]) -> Optional[LonLat]:
if lon is None or lat is None:
return None
return LonLat(lon, lat)
def sledrun_from_rodelbahnbox(value: OrderedDict, sledrun: object):
"""Takes a Rodelbahnbox as returned by rodelbahnbox_from_str (that is, an OrderedDict) and
updates the sledrun instance with all values present in the Rodelbahnbox. Other values are not
updated. Does not validate the arguments."""
# sledrun.page_id = None # this field is not updated because it is not present in the RodelbahnBox
# sledrun.page_title = None # this field is not updated because it is not present in the RodelbahnBox
# sledrun.name_url = None # this field is not updated because it is not present in the RodelbahnBox
sledrun.position_longitude, sledrun.position_latitude = split_lon_lat(value['Position'])
sledrun.top_longitude, sledrun.top_latitude = split_lon_lat(value['Position oben'])
sledrun.top_elevation = value['Höhe oben']
sledrun.bottom_longitude, sledrun.bottom_latitude = split_lon_lat(value['Position unten'])
sledrun.bottom_elevation = value['Höhe unten']
sledrun.length = value['Länge']
sledrun.difficulty = value['Schwierigkeit']
sledrun.avalanches = value['Lawinen']
sledrun.operator = opt_no_or_str_to_str(value['Betreiber'])
sledrun.public_transport = value['Öffentliche Anreise']
sledrun.walkup_possible = value['Aufstieg möglich']
sledrun.walkup_time = value['Gehzeit']
sledrun.walkup_separate, sledrun.walkup_separate_comment = value['Aufstieg getrennt']
sledrun.lift = None if value['Aufstiegshilfe'] is None else len(value['Aufstiegshilfe']) > 0
sledrun.lift_details = lift_german_to_str(value['Aufstiegshilfe'])
sledrun.night_light, sledrun.night_light_comment = value['Beleuchtungsanlage']
sledrun.night_light_days, sledrun.night_light_days_comment = value['Beleuchtungstage']
sledrun.sled_rental = None if value['Rodelverleih'] is None else len(value['Rodelverleih']) > 0
sledrun.sled_rental_comment = opt_str_opt_comment_enum_to_str(value['Rodelverleih'])
sledrun.cachet = cachet_german_to_str(value['Gütesiegel'])
sledrun.information_web = webauskunft_to_str(value['Webauskunft'])
sledrun.information_phone = opt_phone_comment_enum_to_str(value['Telefonauskunft'])
sledrun.image = value['Bild']
sledrun.show_in_overview = value['In Übersichtskarte']
sledrun.forum_id = value['Forumid']
# sledrun.under_construction = None # this field is not updated because it is not present in the RodelbahnBox
return sledrun
def sledrun_to_rodelbahnbox(sledrun) -> collections.OrderedDict:
"""Takes a sledrun instance that might come from the database and converts it to a OrderedDict ready
to be formatted as RodelbahnBox."""
value = collections.OrderedDict()
value['Position'] = join_lon_lat(sledrun.position_longitude, sledrun.position_latitude)
value['Position oben'] = join_lon_lat(sledrun.top_longitude, sledrun.top_latitude)
value['Höhe oben'] = sledrun.top_elevation
value['Position unten'] = join_lon_lat(sledrun.bottom_longitude, sledrun.bottom_latitude)
value['Höhe unten'] = sledrun.bottom_elevation
value['Länge'] = sledrun.length
value['Schwierigkeit'] = sledrun.difficulty
value['Lawinen'] = sledrun.avalanches
value['Betreiber'] = opt_no_or_str_from_str(sledrun.operator)
value['Öffentliche Anreise'] = sledrun.public_transport
value['Aufstieg möglich'] = sledrun.walkup_possible
value['Gehzeit'] = sledrun.walkup_time
value['Aufstieg getrennt'] = sledrun.walkup_separate, sledrun.walkup_separate_comment
value['Aufstiegshilfe'] = lift_german_from_str(sledrun.lift_details)
value['Beleuchtungsanlage'] = sledrun.night_light, sledrun.night_light_comment
value['Beleuchtungstage'] = sledrun.night_light_days, sledrun.night_light_days_comment
value['Rodelverleih'] = sledrun.sled_rental, sledrun.sled_rental_comment
value['Gütesiegel'] = sledrun.cachet
value['Webauskunft'] = sledrun.information_web
value['Telefonauskunft'] = sledrun.information_phone
value['Bild'] = sledrun.image
value['In Übersichtskarte'] = sledrun.show_in_overview
value['Forumid'] = sledrun.forum_id
return value
def inn_from_gasthausbox(value, inn):
"""Converts a dict with Gasthausbox properties to a Inn class. Does no validation.
value is a dict of properties as returned by gasthausbox_from_str."""
# page_id = None # this field is not updated because it is not present in the Gasthausbox
# page_title = None # this field is not updated because it is not present in the Gasthausbox
def convtodb(val, key):
v = GASTHAUSBOX_DICT[key].to_str(val[key])
if v == '':
return None
return v
inn.position_longitude, inn.position_latitude = split_lon_lat(value['Position'])
inn.position_elevation = value['Höhe']
inn.operator = value['Betreiber']
inn.seats = value['Sitzplätze']
inn.overnight, inn.overnight_comment = value['Übernachtung']
inn.smoker_area = None if value['Rauchfrei'] is None else value['Rauchfrei'] < 0.9
inn.nonsmoker_area = None if value['Rauchfrei'] is None else value['Rauchfrei'] > 0.1
inn.sled_rental, inn.sled_rental_comment = value['Rodelverleih']
inn.mobile_provider = convtodb(value, 'Handyempfang')
inn.homepage = convtodb(value, 'Homepage')
inn.email_list = convtodb(value, 'E-Mail')
inn.phone_list = convtodb(value, 'Telefon')
inn.image = value['Bild']
inn.sledding_list = convtodb(value, 'Rodelbahnen')
# under_construction = None # this field is not updated because it is not present in the GasthausBox
return inn
def inn_to_gasthausbox(inn) -> collections.OrderedDict:
"""Converts an inn class to a dict of Gasthausbox properties. inn is an Inn instance."""
def convfromdb(val, key):
v = '' if val is None else val
return GASTHAUSBOX_DICT[key].from_str(v)
value = collections.OrderedDict()
value['Position'] = join_lon_lat(inn.position_longitude, inn.position_latitude)
value['Höhe'] = inn.position_elevation
value['Betreiber'] = inn.operator
value['Sitzplätze'] = inn.seats
value['Übernachtung'] = (inn.overnight, inn.overnight_comment)
value['Rauchfrei'] = {(False, True): 0.0, (True, True): 0.5, (True, False): 1.0} \
.get((inn.nonsmoker_area, inn.smoker_area), None)
value['Rodelverleih'] = (inn.sled_rental, inn.sled_rental_comment)
value['Handyempfang'] = convfromdb(inn.mobile_provider, 'Handyempfang')
value['Homepage'] = convfromdb(inn.homepage, 'Homepage')
value['E-Mail'] = convfromdb(inn.email_list, 'E-Mail')
value['Telefon'] = convfromdb(inn.phone_list, 'Telefon')
value['Bild'] = inn.image
value['Rodelbahnen'] = convfromdb(inn.sledding_list, 'Rodelbahnen')
return value
def lonlat_ele_from_template(template) -> Tuple[Optional[LonLat], Optional[int]]:
"""Template is a `mwparserfromhell.nodes.template.Template` instance. Returns (lonlat, ele)."""
lonlat = opt_lonlat_from_str(template.params[0].strip())
ele = opt_uint_from_str(template.params[1].strip())
return lonlat, ele
def latlon_ele_to_template(lonlat_ele: Tuple[Optional[LonLat], Optional[int]], name: str) -> Template:
lonlat, ele = lonlat_ele
template = Template(name)
template.add(1, opt_lonlat_to_str(lonlat))
template.add(2, opt_uint_to_str(ele))
wrpylib.mwmarkup.format_template_oneline(template)
return template
def lonlat_to_json(lonlat: LonLat) -> dict:
return {'longitude': lonlat.lon, 'latitude': lonlat.lat}
def lonlat_ele_to_json(lonlat: Optional[LonLat], ele: Optional[int]) -> dict:
result = {}
if lonlat is not None:
result['position'] = lonlat_to_json(lonlat)
if ele is not None:
result['elevation'] = ele
return result
class ParseError(RuntimeError):
"""Exception used by some of the functions"""
pass
def parse_wrmap_coordinates(coords: str) -> List[List[float]]:
"""gets a string coordinates and returns an array of lon/lat coordinate pairs, e.g.
47.12 N 11.87 E
47.13 N 11.70 E
->
[[11.87, 47.12], [11.70, 47.13]]"""
result = []
pos = 0
for match in re.finditer(r'\s*(\d+\.?\d*)\s*N?\s+(\d+\.?\d*)\s*E?\s*', coords):
if match.start() != pos:
break
result.append([float(match.groups()[1]), float(match.groups()[0])])
pos = match.end()
else:
if pos == len(coords):
return result
raise RuntimeError(f'Wrong coordinate format: {coords}')
WRMAP_POINT_TYPES = ['gasthaus', 'haltestelle', 'parkplatz', 'achtung', 'foto', 'verleih', 'punkt']
WRMAP_LINE_TYPES = ['rodelbahn', 'gehweg', 'alternative', 'lift', 'anfahrt', 'linie']
def parse_wrmap(wikitext: str) -> dict:
"""Parses the 'content' of the Winterrodeln wrmap extension.
If wikitext does not contain the tag or if the tag contains
invalid formatted lines, a ParseError is raised.
Use wrpylib.mwmarkup.find_tag(wikitext, 'wrmap') to find the wrmap tag within an arbitrary
wikitext before using this function.
:param wikitext: wikitext containing only the template. Example:
wikitext = '''
47.240689 11.190454
47.245789 11.238971
47.245711 11.238283
47.238587 11.203360
47.244951 11.230868
47.245470 11.237853
'''
:returns: GeoJSON as nested Python datatype
"""
# parse XML
try:
wrmap_xml = xml.etree.ElementTree.fromstring(wikitext)
except xml.etree.ElementTree.ParseError as e:
row, column = e.position
raise ParseError(f"XML parse error on row {row}, column {column}: {e}")
if wrmap_xml.tag not in ['wrmap', 'wrgmap']:
raise ParseError('No valid tag name')
# convert XML to geojson (http://www.geojson.org/geojson-spec.html)
json_features = []
for feature in wrmap_xml:
# determine feature type
is_point = feature.tag in WRMAP_POINT_TYPES
is_line = feature.tag in WRMAP_LINE_TYPES
if not is_point and not is_line:
raise ParseError(f'Unknown element <{feature.tag}>.')
# point
if is_point:
properties = {'type': feature.tag}
allowed_properties = {'name', 'wiki'}
wrong_properties = set(feature.attrib.keys()) - allowed_properties
if len(wrong_properties) > 0:
raise ParseError(f"The attribute '{list(wrong_properties)[0]}' is not allowed at <{feature.tag}>.")
properties.update(feature.attrib)
coordinates = parse_wrmap_coordinates(feature.text)
if len(coordinates) != 1:
raise ParseError(f'The element <{feature.tag}> has to have exactly one coordinate pair.')
json_features.append({
'type': 'Feature',
'geometry': {'type': 'Point', 'coordinates': coordinates[0]},
'properties': properties})
# line
if is_line:
properties = {'type': feature.tag}
allowed_properties = {'farbe', 'dicke'}
wrong_properties = set(feature.attrib.keys()) - allowed_properties
if len(wrong_properties) > 0:
raise ParseError(f"The attribute '{list(wrong_properties)[0]}' is not allowed at <{feature.tag}>.")
if 'farbe' in feature.attrib:
if not re.match('#[0-9a-fA-F]{6}$', feature.attrib['farbe']):
raise ParseError('The attribute "farbe" has to have a format like "#a0bb43".')
properties['strokeColor'] = feature.attrib['farbe'] # e.g. #a200b7
if 'dicke' in feature.attrib:
try:
properties['strokeWidth'] = int(feature.attrib['dicke']) # e.g. 6
except ValueError:
raise ParseError('The attribute "dicke" has to be an integer.')
json_features.append({
'type': 'Feature',
'geometry': {'type': 'LineString', 'coordinates': parse_wrmap_coordinates(feature.text)},
'properties': properties})
# attributes
properties = {}
for k, v in wrmap_xml.attrib.items():
if k in ['lat', 'lon']:
try:
properties[k] = float(v)
except ValueError:
raise ParseError(f'Attribute "{k}" has to be a float value.')
elif k in ['zoom', 'width', 'height']:
try:
properties[k] = int(v)
except ValueError:
raise ParseError(f'Attribute "{k}" has to be an integer value.')
else:
raise ParseError(f'Unknown attribute "{k}".')
geojson = {
'type': 'FeatureCollection',
'features': json_features,
'properties': properties}
return geojson
def create_wrmap_coordinates(coords):
result = []
for coord in coords:
result.append(f'{coord[1]:.6f} N {coord[0]:.6f} E')
return '\n'.join(result)
def create_wrmap(geojson: Dict) -> str:
"""Creates a wikitext from geojson (as python types)."""
wrmap_xml = xml.etree.ElementTree.Element('wrmap')
wrmap_xml.text = '\n\n'
for k, v in geojson['properties'].items():
if k in ['lon', 'lat']:
wrmap_xml.attrib[k] = f'{v:.6f}'
else:
wrmap_xml.attrib[k] = str(v)
assert geojson['type'] == 'FeatureCollection'
json_features = geojson['features']
last_json_feature = None
for json_feature in json_features:
feature_xml = xml.etree.ElementTree.SubElement(wrmap_xml, json_feature['properties']['type'])
geo = json_feature['geometry']
if geo['type'] == 'Point':
feature_xml.text = create_wrmap_coordinates([geo['coordinates']])
if last_json_feature is not None:
last_json_feature.tail = '\n'
else:
if last_json_feature is not None:
last_json_feature.tail = '\n\n'
feature_xml.text = '\n' + create_wrmap_coordinates(geo['coordinates']) + '\n'
last_json_feature = feature_xml
feature_xml.attrib = json_feature['properties'].copy()
del feature_xml.attrib['type']
if last_json_feature is not None:
last_json_feature.tail = '\n\n'
return xml.etree.ElementTree.tostring(wrmap_xml, encoding='utf-8').decode('utf-8')
def german_bool(value: Union[bool, jinja2.Undefined]) -> Union[str, jinja2.Undefined]:
if jinja2.is_undefined(value):
return value
return wrpylib.wrvalidators.bool_german_to_str(value)
class Jinja2Tools:
def create_wrmap(self, geojson: Dict) -> str:
return create_wrmap(geojson)
def json_position(self, value: dict) -> str:
lon_lat = LonLat(value['longitude'], value['latitude'])
return lonlat_to_str(lon_lat)
def json_pos_ele_position(self, value: dict) -> str:
pos = value.get('position')
if pos is None:
return ''
return self.json_position(pos)
def json_pos_ele_elevation(self, value: dict) -> str:
return value.get('elevation', '')
def json_wr_page(self, value: dict) -> str:
return str(Wikilink(value['title'], value.get('text')))
def list_template(self, name: str, value: List[str]) -> str:
return str(wrpylib.mwmarkup.create_template(name, value))
def key_value_template(self, name: str, value: Dict[str, Any], keep_empty: bool = False) -> str:
value = {k: str(v) for k, v in value.items()
if keep_empty or (v is not None and not isinstance(v, jinja2.Undefined) and str(v).strip() != '')}
return str(wrpylib.mwmarkup.create_template(name, [], value))
def json_template(self, value) -> str:
args = []
kwargs = {}
for p in value.get('parameter', []):
v = p.get('value', '')
if 'key' in p:
kwargs[p['key']] = v
else:
args.append(v)
return str(wrpylib.mwmarkup.create_template(value['name'], args, kwargs))
def create_sledrun_wiki(sledrun_json: Dict, map_json: Optional[Dict], impressions_title: Optional[str] = None) -> str:
env = jinja2.Environment(
loader=jinja2.PackageLoader("wrpylib"),
autoescape=jinja2.select_autoescape(),
)
env.filters["german_bool"] = german_bool
template = env.get_template("sledrun_wikitext.txt")
def position_to_lon_lat(value: Optional[dict]) -> Optional[LonLat]:
if value is not None:
lon = value.get('longitude')
lat = value.get('latitude')
if lon is not None and lat is not None:
return LonLat(lon, lat)
return None
def position_ele_to_lon_lat(value: Optional[dict]) -> Optional[LonLat]:
if value is not None:
return position_to_lon_lat(value.get("position"))
return None
def position_ele_to_ele(value: Optional[dict]) -> Optional[int]:
if value is not None:
ele = value.get('elevation')
if ele is not None:
return int(ele)
return None
def aufstiegshilfe() -> Optional[List[Tuple[str, Optional[str]]]]:
ws = sledrun_json.get('walkup_supports')
if ws is None:
return None
return [(w['type'], w.get('note')) for w in ws]
def beleuchtungstage(sledrun_json: Dict) -> Tuple[Optional[int], Optional[str]]:
weekdays_count = sledrun_json.get('nightlight_weekdays_count')
note = sledrun_json.get('nightlight_weekdays_note')
weekdays = sledrun_json.get('nightlight_weekdays')
if weekdays is not None:
assert isinstance(weekdays, list)
if weekdays_count is None:
weekdays_count = len(weekdays)
if note is None:
note = ', '.join(w[:2] for w in weekdays)
return weekdays_count, note
def rodelverleih() -> Optional[List[Tuple[str, Optional[str]]]]:
v = sledrun_json.get('sled_rental')
if v is None:
d = sledrun_json.get('sled_rental_direct')
if d is not None:
return [("Ja", None)]
return None
w = []
for x in v:
n = x.get('name')
c = x.get('note')
p = x.get('wr_page')
link = x.get('weblink')
if p is not None:
n = Jinja2Tools().json_wr_page(p)
if n is None and link is not None:
n = link.get('text')
if n is not None:
w.append((n, c))
return w
def cachet() -> Optional[List]:
v = sledrun_json.get('cachet')
if v is not None:
if not v:
return []
return None
def webauskunft() -> Tuple[Optional[bool], Optional[str]]:
info_web = sledrun_json.get('info_web')
if info_web is None:
return None, None
if len(info_web) == 0:
return False, None
return True, info_web[0]['url']
def telefonauskunft() -> Optional[List[Tuple[str, str]]]:
info_phone = sledrun_json.get('info_phone')
if info_phone is None:
return None
return [(pc['phone'], pc['name']) for pc in info_phone]
def betreiber() -> str:
has_operator = sledrun_json.get('has_operator')
if has_operator is None:
return sledrun_json.get('operator')
if has_operator:
return sledrun_json.get('operator')
return 'Nein'
sledrun_rbb_json = collections.OrderedDict([
('Position', position_to_lon_lat(sledrun_json.get('position'))),
('Position oben', position_ele_to_lon_lat(sledrun_json.get('top'))),
('Höhe oben', position_ele_to_ele(sledrun_json.get('top'))),
('Position unten', position_ele_to_lon_lat(sledrun_json.get('bottom'))),
('Höhe unten', position_ele_to_ele(sledrun_json.get('bottom'))),
('Länge', sledrun_json.get('length')),
('Schwierigkeit', opt_difficulty_german_from_str(sledrun_json.get('difficulty', ''))),
('Lawinen', opt_avalanches_german_from_str(sledrun_json.get('avalanches', ''))),
('Betreiber', (sledrun_json.get('has_operator', True if 'operator' in sledrun_json else None), sledrun_json.get('operator'))),
('Öffentliche Anreise', opt_public_transport_german_from_str(sledrun_json.get('public_transport', ''))),
('Aufstieg möglich', sledrun_json.get('walkup_possible')),
('Aufstieg getrennt', (
tristate_german_from_str(sledrun_json['walkup_separate']) if 'walkup_separate' in sledrun_json else None,
sledrun_json.get('walkup_note'))),
('Gehzeit', sledrun_json.get('walkup_time')),
('Aufstiegshilfe', aufstiegshilfe()),
('Beleuchtungsanlage', (opt_tristate_german_from_str(sledrun_json.get('nightlight_possible', '')),
sledrun_json.get('nightlight_possible_note'))),
('Beleuchtungstage', beleuchtungstage(sledrun_json)),
('Rodelverleih', rodelverleih()),
('Gütesiegel', cachet()),
('Webauskunft', webauskunft()),
('Telefonauskunft', telefonauskunft()),
('Bild', sledrun_json.get('image')),
('In Übersichtskarte', sledrun_json.get('show_in_overview')),
('Forumid', sledrun_json.get('forum_id'))
])
rodelbahnbox = rodelbahnbox_to_str(sledrun_rbb_json)
text = template.render(sledrun_json=sledrun_json,
rodelbahnbox=rodelbahnbox,
map_json=map_json, impressions_title=impressions_title,
h=Jinja2Tools(), **sledrun_json)
return strip_eol(text)