4 """This module contains winterrodeln specific functions that are processing the MediaWiki markup.
7 import xml.etree.ElementTree
9 import mwparserfromhell
10 import wrpylib.wrvalidators
11 import wrpylib.mwmarkup
13 from wrpylib.wrvalidators import LonLat, opt_lonlat_from_str, opt_lonlat_to_str, opt_uint_from_str, opt_uint_to_str, \
14 opt_str_opt_comment_enum_to_str, lift_german_to_str, webauskunft_to_str, cachet_german_to_str, \
15 opt_phone_comment_enum_to_str, lift_german_from_str, GASTHAUSBOX_DICT
18 def sledrun_from_rodelbahnbox(value, sledrun):
19 """Takes a Rodelbahnbox as returned by rodelbahnbox_from_str (that is, an OrderedDict) and
20 updates the sledrun instance with all values present in the Rodelbahnbox. Other values are not
21 updated. Does not validate the arguments."""
22 # sledrun.page_id = None # this field is not updated because it is not present in the RodelbahnBox
23 # sledrun.page_title = None # this field is not updated because it is not present in the RodelbahnBox
24 # sledrun.name_url = None # this field is not updated because it is not present in the RodelbahnBox
25 sledrun.position_longitude, sledrun.position_latitude = value['Position']
26 sledrun.top_longitude, sledrun.top_latitude = value['Position oben']
27 sledrun.top_elevation = value['Höhe oben']
28 sledrun.bottom_longitude, sledrun.bottom_latitude = value['Position unten']
29 sledrun.bottom_elevation = value['Höhe unten']
30 sledrun.length = value['Länge']
31 sledrun.difficulty = value['Schwierigkeit']
32 sledrun.avalanches = value['Lawinen']
33 sledrun.operator = value['Betreiber']
34 sledrun.public_transport = value['Öffentliche Anreise']
35 sledrun.walkup_possible = value['Aufstieg möglich']
36 sledrun.walkup_time = value['Gehzeit']
37 sledrun.walkup_separate, sledrun.walkup_separate_comment = value['Aufstieg getrennt']
38 sledrun.lift = None if value['Aufstiegshilfe'] is None else len(value['Aufstiegshilfe']) > 0
39 sledrun.lift_details = lift_german_to_str(value['Aufstiegshilfe'])
40 sledrun.night_light, sledrun.night_light_comment = value['Beleuchtungsanlage']
41 sledrun.night_light_days, sledrun.night_light_days_comment = value['Beleuchtungstage']
42 sledrun.sled_rental = None if value['Rodelverleih'] is None else len(value['Rodelverleih']) > 0
43 sledrun.sled_rental_comment = opt_str_opt_comment_enum_to_str(value['Rodelverleih'])
44 sledrun.cachet = cachet_german_to_str(value['Gütesiegel'])
45 sledrun.information_web = webauskunft_to_str(value['Webauskunft'])
46 sledrun.information_phone = opt_phone_comment_enum_to_str(value['Telefonauskunft'])
47 sledrun.image = value['Bild']
48 sledrun.show_in_overview = value['In Übersichtskarte']
49 sledrun.forum_id = value['Forumid']
50 # sledrun.under_construction = None # this field is not updated because it is not present in the RodelbahnBox
54 def sledrun_to_rodelbahnbox(sledrun):
55 """Takes a sledrun instance that might come from the database and converts it to a OrderedDict ready
56 to be formatted as RodelbahnBox."""
57 value = collections.OrderedDict()
58 value['Position'] = LonLat(sledrun.position_longitude, sledrun.position_latitude)
59 value['Position oben'] = LonLat(sledrun.top_longitude, sledrun.top_latitude)
60 value['Höhe oben'] = sledrun.top_elevation
61 value['Position unten'] = LonLat(sledrun.bottom_longitude, sledrun.bottom_latitude)
62 value['Höhe unten'] = sledrun.bottom_elevation
63 value['Länge'] = sledrun.length
64 value['Schwierigkeit'] = sledrun.difficulty
65 value['Lawinen'] = sledrun.avalanches
66 value['Betreiber'] = sledrun.operator
67 value['Öffentliche Anreise'] = sledrun.public_transport
68 value['Aufstieg möglich'] = sledrun.walkup_possible
69 value['Gehzeit'] = sledrun.walkup_time
70 value['Aufstieg getrennt'] = sledrun.walkup_separate, sledrun.walkup_separate_comment
71 value['Aufstiegshilfe'] = lift_german_from_str(sledrun.lift_details)
72 value['Beleuchtungsanlage'] = sledrun.night_light, sledrun.night_light_comment
73 value['Beleuchtungstage'] = sledrun.night_light_days, sledrun.night_light_days_comment
74 value['Rodelverleih'] = sledrun.sled_rental, sledrun.sled_rental_comment
75 value['Gütesiegel'] = sledrun.cachet
76 value['Webauskunft'] = sledrun.information_web
77 value['Telefonauskunft'] = sledrun.information_phone
78 value['Bild'] = sledrun.image
79 value['In Übersichtskarte'] = sledrun.show_in_overview
80 value['Forumid'] = sledrun.forum_id
84 def inn_from_gasthausbox(value, inn):
85 """Converts a dict with Gasthausbox properties to a Inn class. Does no validation.
86 value is a dict of properties as returned by gasthausbox_from_str."""
87 # page_id = None # this field is not updated because it is not present in the Gasthausbox
88 # page_title = None # this field is not updated because it is not present in the Gasthausbox
89 def convtodb(value, key):
92 v = GASTHAUSBOX_DICT[key].to_str(v)
94 inn.position_longitude, inn.position_latitude = value['Position']
95 inn.position_elevation = value['Höhe']
96 inn.operator = value['Betreiber']
97 inn.seats = value['Sitzplätze']
98 inn.overnight, inn.overnight_comment = value['Übernachtung']
99 inn.smoker_area = value['Rauchfrei'] < 0.9
100 inn.nonsmoker_area = value['Rauchfrei'] > 0.1
101 inn.sled_rental, inn.sled_rental_comment = value['Rodelverleih']
102 inn.mobile_provider = convtodb(value, 'Handyempfang')
103 inn.homepage = convtodb(value, 'Homepage')
104 inn.email_list = convtodb(value, 'E-Mail')
105 inn.phone_list = convtodb(value, 'Telefon')
106 inn.image = value['Bild']
107 inn.sledding_list = convtodb(value, 'Rodelbahnen')
108 # under_construction = None # this field is not updated because it is not present in the GasthausBox
112 def inn_to_gasthausbox(inn):
113 """Converts an inn class to a dict of Gasthausbox properties. value is an Inn instance."""
114 def convfromdb(value, key):
115 v = '' if value is None else value
116 return GASTHAUSBOX_DICT[key].from_str(v)
117 value = collections.OrderedDict()
118 value['Position'] = LonLat(inn.position_longitude, inn.position_latitude)
119 value['Höhe'] = inn.position_elevation
120 value['Betreiber'] = inn.operator
121 value['Sitzplätze'] = inn.seats
122 value['Übernachtung'] = (inn.overnight, inn.overnight_comment)
123 value['Rauchfrei'] = {(False, True): 0.0, (True, True): 0.5, (True, False): 1.0}.get((inn.nonsmoker_area, inn.smoker_area), None)
124 value['Rodelverleih'] = (inn.sled_rental, inn.sled_rental_comment)
125 value['Handyempfang'] = convfromdb(inn.mobile_provider, 'Handyempfang')
126 value['Homepage'] = convfromdb(inn.homepage, 'Homepage')
127 value['E-Mail'] = convfromdb(inn.email_list, 'E-Mail')
128 value['Telefon'] = convfromdb(inn.phone_list, 'Telefon')
129 value['Bild'] = inn.image
130 value['Rodelbahnen'] = convfromdb(inn.sledding_list, 'Rodelbahnen')
134 def lonlat_ele_from_template(template):
135 """Template is a mwparserfromhell.nodes.template.Template instance. Returns (lonlat, ele)."""
136 lonlat = opt_lonlat_from_str(template.params[0].strip())
137 ele = opt_uint_from_str(template.params[1].strip())
141 def latlon_ele_to_template(lonlat_ele, name):
142 lonlat, ele = lonlat_ele
143 template = mwparserfromhell.nodes.template.Template(name)
144 template.add(1, opt_lonlat_to_str(lonlat))
145 template.add(2, opt_uint_to_str(ele))
146 wrpylib.mwmarkup.format_template_oneline(template)
150 class ParseError(RuntimeError):
151 """Exception used by some of the functions"""
156 def find_template_PositionOben(wikitext):
157 """Same as find_template_latlon_ele with template '{{Position oben|47.076207 N 11.453553 E|1890}}'"""
158 return find_template_latlon_ele(wikitext, 'Position oben')
161 def create_template_PositionOben(lat, lon, ele):
162 return create_template_latlon_ele('Position, oben', lat, lon, ele)
165 def find_template_PositionUnten(wikitext):
166 """Same as find_template_latlon_ele with template '{{Position unten|47.076207 N 11.453553 E|1890}}'"""
167 return find_template_latlon_ele(wikitext, 'Position unten')
170 def find_template_unsigned(wikitext, template_title):
171 """Finds the first occurance of the '{{template_title|1890}}' template
172 and returns the tuple (start, end, unsigned_value) or (None, None, None) if the
173 template was not found. If the template has no valid format, an exception is thrown."""
174 start, end = wrpylib.mwmarkup.find_template(wikitext, template_title)
175 if start is None: return (None,) * 3
176 title, params = wrpylib.mwmarkup.split_template(wikitext[start:end])
177 unsigned_value = wrpylib.wrvalidators.UnsignedNone().to_python(params['1'].strip())
178 return start, end, unsigned_value
181 def create_template_unsigned(template_title, unsigned):
182 unsigned = wrpylib.wrvalidators.UnsignedNone().from_python(unsigned)
183 if len(unsigned) == 0: unsigned = ' '
184 return wrpylib.mwmarkup.create_template(template_title, [unsigned])
187 def find_template_Hoehenunterschied(wikitext):
188 """Same as find_template_unsigned with template '{{Höhenunterschied|350}}'"""
189 return find_template_unsigned(wikitext, 'Höhenunterschied')
192 def create_template_Hoehenunterschied(ele_diff):
193 return create_template_unsigned('Höhenunterschied', ele_diff)
196 def find_template_Bahnlaenge(wikitext):
197 """Same as find_template_unsigned with template '{{Bahnlänge|4500}}'"""
198 return find_template_unsigned(wikitext, 'Bahnlänge')
201 def create_template_Bahnlaenge(length):
202 return create_template_unsigned('Bahnlänge', length)
205 def find_template_Gehzeit(wikitext):
206 """Same as find_template_unsigned with template '{{Gehzeit|60}}'"""
207 return find_template_unsigned(wikitext, 'Gehzeit')
210 def create_template_Gehzeit(walkup_time):
211 return create_template_unsigned('Gehzeit', walkup_time)
214 def find_template_Forumlink(wikitext):
215 """Same as find_template_unsigned with template '{{Forumlink|26}}'"""
216 start, end = wrpylib.mwmarkup.find_template(wikitext, 'Forumlink')
217 if start is None: return (None,) * 3
218 title, params = wrpylib.mwmarkup.split_template(wikitext[start:end])
219 forumid = params['1'].strip()
220 if forumid == '<nummer einfügen>': unsigned_value = None
221 else: unsigned_value = wrpylib.wrvalidators.UnsignedNone().to_python(forumid)
222 return start, end, unsigned_value
223 # return find_template_unsigned(wikitext, u'Forumlink')
226 def find_template_Parkplatz(wikitext):
227 """Same as find_template_latlon_ele with template '{{Parkplatz|47.076207 N 11.453553 E|1890}}'"""
228 return find_template_latlon_ele(wikitext, 'Parkplatz')
231 def find_template_Haltestelle(wikitext):
232 """Finds the first occurance of the '{{Haltestelle|Ortsname|Haltestellenname|47.076207 N 11.453553 E|1890}}' template
233 and returns the tuple (start, end, city, stop, lat, lon, ele) or (None, None, None, None, None, None, None) if the
234 template was not found. If the template has no valid format, an exception is thrown."""
235 start, end = wrpylib.mwmarkup.find_template(wikitext, 'Haltestelle')
236 if start is None: return (None,) * 7
237 title, params = wrpylib.mwmarkup.split_template(wikitext[start:end])
238 city = wrpylib.wrvalidators.UnicodeNone().to_python(params['1'].strip())
239 stop = wrpylib.wrvalidators.UnicodeNone().to_python(params['2'].strip())
240 lat, lon = wrpylib.wrvalidators.GeoNone().to_python(params['3'].strip())
241 ele = wrpylib.wrvalidators.UnsignedNone().to_python(params['4'].strip())
242 return start, end, city, stop, lat, lon, ele
245 def parse_wrmap_coordinates(coords):
246 """gets a string coordinates and returns an array of lon/lat coordinate pairs, e.g.
250 [[11.87, 47.12], [11.70, 47.13]]"""
253 for match in re.finditer(r'\s*(\d+\.?\d*)\s*N?\s+(\d+\.?\d*)\s*E?\s*', coords):
254 if match.start() != pos:
256 result.append([float(match.groups()[1]), float(match.groups()[0])])
259 if pos == len(coords):
261 raise RuntimeError('Wrong coordinate format: {}'.format(coords))
264 WRMAP_POINT_TYPES = ['gasthaus', 'haltestelle', 'parkplatz', 'achtung', 'foto', 'verleih', 'punkt']
265 WRMAP_LINE_TYPES = ['rodelbahn', 'gehweg', 'alternative', 'lift', 'anfahrt', 'linie']
268 def parse_wrmap(wikitext):
269 """Parses the (unicode) u'<wrmap ...>content</wrmap>' of the Winterrodeln wrmap extension.
270 If wikitext does not contain the <wrmap> tag or if the <wrmap> tag contains
271 invalid formatted lines, a ParseError is raised.
272 Use wrpylib.mwmarkup.find_tag(wikitext, 'wrmap') to find the wrmap tag within an arbitrary
273 wikitext before using this function.
275 :param wikitext: wikitext containing only the template. Example:
278 <wrmap lat="47.2417134" lon="11.21408895" zoom="14" width="700" height="400">
279 <gasthaus name="Rosskogelhütte" wiki="Rosskogelhütte">47.240689 11.190454</gasthaus>
280 <parkplatz>47.245789 11.238971</parkplatz>
281 <haltestelle name="Oberperfuss Rangger Köpfl Lift">47.245711 11.238283</haltestelle>
289 :returns: GeoJSON as nested Python datatype
293 wrmap_xml = xml.etree.ElementTree.fromstring(wikitext.encode('utf-8'))
294 except xml.etree.ElementTree.ParseError as e:
295 row, column = e.position
296 raise ParseError("XML parse error on row {}, column {}: {}".format(row, column, e))
297 if wrmap_xml.tag not in ['wrmap', 'wrgmap']:
298 raise ParseError('No valid tag name')
300 # convert XML to geojson (http://www.geojson.org/geojson-spec.html)
302 for feature in wrmap_xml:
303 # determine feature type
304 is_point = feature.tag in WRMAP_POINT_TYPES
305 is_line = feature.tag in WRMAP_LINE_TYPES
306 if (not is_point and not is_line):
307 raise ParseError('Unknown element <{}>.'.format(feature.tag))
311 properties = {'type': feature.tag}
312 allowed_properties = {'name', 'wiki'}
313 wrong_properties = set(feature.attrib.keys()) - allowed_properties
314 if len(wrong_properties) > 0:
315 raise ParseError("The attribute '{}' is not allowed at <{}>.".format(list(wrong_properties)[0], feature.tag))
316 properties.update(feature.attrib)
317 coordinates = parse_wrmap_coordinates(feature.text)
318 if len(coordinates) != 1:
319 raise ParseError('The element <{}> has to have exactly one coordinate pair.'.format(feature.tag))
320 json_features.append({
322 'geometry': {'type': 'Point', 'coordinates': coordinates[0]},
323 'properties': properties})
327 properties = {'type': feature.tag}
328 allowed_properties = {'farbe', 'dicke'}
329 wrong_properties = set(feature.attrib.keys()) - allowed_properties
330 if len(wrong_properties) > 0:
331 raise ParseError("The attribute '{}' is not allowed at <{}>.".format(list(wrong_properties)[0], feature.tag))
332 if 'farbe' in feature.attrib:
333 if not re.match('#[0-9a-fA-F]{6}$', feature.attrib['farbe']):
334 raise ParseError('The attribute "farbe" has to have a format like "#a0bb43".')
335 properties['strokeColor'] = feature.attrib['farbe'] # e.g. #a200b7
336 if 'dicke' in feature.attrib:
338 properties['strokeWidth'] = int(feature.attrib['dicke']) # e.g. 6
340 raise ParseError('The attribute "dicke" has to be an integer.')
341 json_features.append({
343 'geometry': {'type': 'LineString', 'coordinates': parse_wrmap_coordinates(feature.text)},
344 'properties': properties})
348 for k, v in wrmap_xml.attrib.items():
349 if k in ['lat', 'lon']:
351 properties[k] = float(v)
353 raise ParseError('Attribute "{}" has to be a float value.'.format(k))
354 elif k in ['zoom', 'width', 'height']:
356 properties[k] = int(v)
358 raise ParseError('Attribute "{}" has to be an integer value.'.format(k))
360 raise ParseError('Unknown attribute "{}".'.format(k))
363 'type': 'FeatureCollection',
364 'features': json_features,
365 'properties': properties}
370 def create_wrmap_coordinates(coords):
373 result.append('{:.6f} N {:.6f} E'.format(coord[1], coord[0]))
374 return '\n'.join(result)
377 def create_wrmap(geojson):
378 """Creates a <wrmap> wikitext from geojson (as python types)."""
379 wrmap_xml = xml.etree.ElementTree.Element('wrmap')
380 wrmap_xml.text = '\n\n'
381 for k, v in geojson['properties'].items():
382 if k in ['lon', 'lat']:
383 wrmap_xml.attrib[k] = '{:.6f}'.format(v)
385 wrmap_xml.attrib[k] = str(v)
387 assert geojson['type'] == 'FeatureCollection'
388 json_features = geojson['features']
389 last_json_feature = None
390 for json_feature in json_features:
391 feature_xml = xml.etree.ElementTree.SubElement(wrmap_xml, json_feature['properties']['type'])
392 geo = json_feature['geometry']
393 if geo['type'] == 'Point':
394 feature_xml.text = create_wrmap_coordinates([geo['coordinates']])
395 if last_json_feature is not None:
396 last_json_feature.tail = '\n'
398 if last_json_feature is not None:
399 last_json_feature.tail = '\n\n'
400 feature_xml.text = '\n' + create_wrmap_coordinates(geo['coordinates']) + '\n'
401 last_json_feature = feature_xml
402 feature_xml.attrib = json_feature['properties']
403 del feature_xml.attrib['type']
405 if last_json_feature is not None:
406 last_json_feature.tail = '\n\n'
407 return xml.etree.ElementTree.tostring(wrmap_xml, encoding='utf-8').decode('utf-8')