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):
117 return GASTHAUSBOX_DICT[key].from_str(value)
118 value = collections.OrderedDict()
119 value['Position'] = LonLat(inn.position_longitude, inn.position_latitude)
120 value['Höhe'] = inn.position_elevation
121 value['Betreiber'] = inn.operator
122 value['Sitzplätze'] = inn.seats
123 value['Übernachtung'] = (inn.overnight, inn.overnight_comment)
124 value['Rauchfrei'] = {(False, True): 0.0, (True, True): 0.5, (True, False): 1.0}.get((inn.nonsmoker_area, inn.smoker_area), None)
125 value['Rodelverleih'] = (inn.sled_rental, inn.sled_rental_comment)
126 value['Handyempfang'] = convfromdb(inn.mobile_provider, 'Handyempfang')
127 value['Homepage'] = convfromdb(inn.homepage, 'Homepage')
128 value['E-Mail'] = convfromdb(inn.email_list, 'E-Mail')
129 value['Telefon'] = convfromdb(inn.phone_list, 'Telefon')
130 value['Bild'] = inn.image
131 value['Rodelbahnen'] = convfromdb(inn.sledding_list, 'Rodelbahnen')
135 def lonlat_ele_from_template(template):
136 """Template is a mwparserfromhell.nodes.template.Template instance. Returns (lonlat, ele)."""
137 lonlat = opt_lonlat_from_str(template.params[0].strip())
138 ele = opt_uint_from_str(template.params[1].strip())
142 def latlon_ele_to_template(lonlat_ele, name):
143 lonlat, ele = lonlat_ele
144 template = mwparserfromhell.nodes.template.Template(name)
145 template.add(1, opt_lonlat_to_str(lonlat))
146 template.add(2, opt_uint_to_str(ele))
147 wrpylib.mwmarkup.format_template_oneline(template)
151 class ParseError(RuntimeError):
152 """Exception used by some of the functions"""
157 def find_template_PositionOben(wikitext):
158 """Same as find_template_latlon_ele with template '{{Position oben|47.076207 N 11.453553 E|1890}}'"""
159 return find_template_latlon_ele(wikitext, 'Position oben')
162 def create_template_PositionOben(lat, lon, ele):
163 return create_template_latlon_ele('Position, oben', lat, lon, ele)
166 def find_template_PositionUnten(wikitext):
167 """Same as find_template_latlon_ele with template '{{Position unten|47.076207 N 11.453553 E|1890}}'"""
168 return find_template_latlon_ele(wikitext, 'Position unten')
171 def find_template_unsigned(wikitext, template_title):
172 """Finds the first occurance of the '{{template_title|1890}}' template
173 and returns the tuple (start, end, unsigned_value) or (None, None, None) if the
174 template was not found. If the template has no valid format, an exception is thrown."""
175 start, end = wrpylib.mwmarkup.find_template(wikitext, template_title)
176 if start is None: return (None,) * 3
177 title, params = wrpylib.mwmarkup.split_template(wikitext[start:end])
178 unsigned_value = wrpylib.wrvalidators.UnsignedNone().to_python(params['1'].strip())
179 return start, end, unsigned_value
182 def create_template_unsigned(template_title, unsigned):
183 unsigned = wrpylib.wrvalidators.UnsignedNone().from_python(unsigned)
184 if len(unsigned) == 0: unsigned = ' '
185 return wrpylib.mwmarkup.create_template(template_title, [unsigned])
188 def find_template_Hoehenunterschied(wikitext):
189 """Same as find_template_unsigned with template '{{Höhenunterschied|350}}'"""
190 return find_template_unsigned(wikitext, 'Höhenunterschied')
193 def create_template_Hoehenunterschied(ele_diff):
194 return create_template_unsigned('Höhenunterschied', ele_diff)
197 def find_template_Bahnlaenge(wikitext):
198 """Same as find_template_unsigned with template '{{Bahnlänge|4500}}'"""
199 return find_template_unsigned(wikitext, 'Bahnlänge')
202 def create_template_Bahnlaenge(length):
203 return create_template_unsigned('Bahnlänge', length)
206 def find_template_Gehzeit(wikitext):
207 """Same as find_template_unsigned with template '{{Gehzeit|60}}'"""
208 return find_template_unsigned(wikitext, 'Gehzeit')
211 def create_template_Gehzeit(walkup_time):
212 return create_template_unsigned('Gehzeit', walkup_time)
215 def find_template_Forumlink(wikitext):
216 """Same as find_template_unsigned with template '{{Forumlink|26}}'"""
217 start, end = wrpylib.mwmarkup.find_template(wikitext, 'Forumlink')
218 if start is None: return (None,) * 3
219 title, params = wrpylib.mwmarkup.split_template(wikitext[start:end])
220 forumid = params['1'].strip()
221 if forumid == '<nummer einfügen>': unsigned_value = None
222 else: unsigned_value = wrpylib.wrvalidators.UnsignedNone().to_python(forumid)
223 return start, end, unsigned_value
224 # return find_template_unsigned(wikitext, u'Forumlink')
227 def find_template_Parkplatz(wikitext):
228 """Same as find_template_latlon_ele with template '{{Parkplatz|47.076207 N 11.453553 E|1890}}'"""
229 return find_template_latlon_ele(wikitext, 'Parkplatz')
232 def find_template_Haltestelle(wikitext):
233 """Finds the first occurance of the '{{Haltestelle|Ortsname|Haltestellenname|47.076207 N 11.453553 E|1890}}' template
234 and returns the tuple (start, end, city, stop, lat, lon, ele) or (None, None, None, None, None, None, None) if the
235 template was not found. If the template has no valid format, an exception is thrown."""
236 start, end = wrpylib.mwmarkup.find_template(wikitext, 'Haltestelle')
237 if start is None: return (None,) * 7
238 title, params = wrpylib.mwmarkup.split_template(wikitext[start:end])
239 city = wrpylib.wrvalidators.UnicodeNone().to_python(params['1'].strip())
240 stop = wrpylib.wrvalidators.UnicodeNone().to_python(params['2'].strip())
241 lat, lon = wrpylib.wrvalidators.GeoNone().to_python(params['3'].strip())
242 ele = wrpylib.wrvalidators.UnsignedNone().to_python(params['4'].strip())
243 return start, end, city, stop, lat, lon, ele
246 def parse_wrmap_coordinates(coords):
247 """gets a string coordinates and returns an array of lon/lat coordinate pairs, e.g.
251 [[11.87, 47.12], [11.70, 47.13]]"""
254 for match in re.finditer(r'\s*(\d+\.?\d*)\s*N?\s+(\d+\.?\d*)\s*E?\s*', coords):
255 if match.start() != pos:
257 result.append([float(match.groups()[1]), float(match.groups()[0])])
260 if pos == len(coords):
262 raise RuntimeError('Wrong coordinate format: {}'.format(coords))
265 WRMAP_POINT_TYPES = ['gasthaus', 'haltestelle', 'parkplatz', 'achtung', 'foto', 'verleih', 'punkt']
266 WRMAP_LINE_TYPES = ['rodelbahn', 'gehweg', 'alternative', 'lift', 'anfahrt', 'linie']
269 def parse_wrmap(wikitext):
270 """Parses the (unicode) u'<wrmap ...>content</wrmap>' of the Winterrodeln wrmap extension.
271 If wikitext does not contain the <wrmap> tag or if the <wrmap> tag contains
272 invalid formatted lines, a ParseError is raised.
273 Use wrpylib.mwmarkup.find_tag(wikitext, 'wrmap') to find the wrmap tag within an arbitrary
274 wikitext before using this function.
276 :param wikitext: wikitext containing only the template. Example:
279 <wrmap lat="47.2417134" lon="11.21408895" zoom="14" width="700" height="400">
280 <gasthaus name="Rosskogelhütte" wiki="Rosskogelhütte">47.240689 11.190454</gasthaus>
281 <parkplatz>47.245789 11.238971</parkplatz>
282 <haltestelle name="Oberperfuss Rangger Köpfl Lift">47.245711 11.238283</haltestelle>
290 :returns: GeoJSON as nested Python datatype
294 wrmap_xml = xml.etree.ElementTree.fromstring(wikitext.encode('utf-8'))
295 except xml.etree.ElementTree.ParseError as e:
296 row, column = e.position
297 raise ParseError("XML parse error on row {}, column {}: {}".format(row, column, e))
298 if wrmap_xml.tag not in ['wrmap', 'wrgmap']:
299 raise ParseError('No valid tag name')
301 # convert XML to geojson (http://www.geojson.org/geojson-spec.html)
303 for feature in wrmap_xml:
304 # determine feature type
305 is_point = feature.tag in WRMAP_POINT_TYPES
306 is_line = feature.tag in WRMAP_LINE_TYPES
307 if (not is_point and not is_line):
308 raise ParseError('Unknown element <{}>.'.format(feature.tag))
312 properties = {'type': feature.tag}
313 allowed_properties = {'name', 'wiki'}
314 wrong_properties = set(feature.attrib.keys()) - allowed_properties
315 if len(wrong_properties) > 0:
316 raise ParseError("The attribute '{}' is not allowed at <{}>.".format(list(wrong_properties)[0], feature.tag))
317 properties.update(feature.attrib)
318 coordinates = parse_wrmap_coordinates(feature.text)
319 if len(coordinates) != 1:
320 raise ParseError('The element <{}> has to have exactly one coordinate pair.'.format(feature.tag))
321 json_features.append({
323 'geometry': {'type': 'Point', 'coordinates': coordinates[0]},
324 'properties': properties})
328 properties = {'type': feature.tag}
329 allowed_properties = {'farbe', 'dicke'}
330 wrong_properties = set(feature.attrib.keys()) - allowed_properties
331 if len(wrong_properties) > 0:
332 raise ParseError("The attribute '{}' is not allowed at <{}>.".format(list(wrong_properties)[0], feature.tag))
333 if 'farbe' in feature.attrib:
334 if not re.match('#[0-9a-fA-F]{6}$', feature.attrib['farbe']):
335 raise ParseError('The attribute "farbe" has to have a format like "#a0bb43".')
336 properties['strokeColor'] = feature.attrib['farbe'] # e.g. #a200b7
337 if 'dicke' in feature.attrib:
339 properties['strokeWidth'] = int(feature.attrib['dicke']) # e.g. 6
341 raise ParseError('The attribute "dicke" has to be an integer.')
342 json_features.append({
344 'geometry': {'type': 'LineString', 'coordinates': parse_wrmap_coordinates(feature.text)},
345 'properties': properties})
349 for k, v in wrmap_xml.attrib.items():
350 if k in ['lat', 'lon']:
352 properties[k] = float(v)
354 raise ParseError('Attribute "{}" has to be a float value.'.format(k))
355 elif k in ['zoom', 'width', 'height']:
357 properties[k] = int(v)
359 raise ParseError('Attribute "{}" has to be an integer value.'.format(k))
361 raise ParseError('Unknown attribute "{}".'.format(k))
364 'type': 'FeatureCollection',
365 'features': json_features,
366 'properties': properties}
371 def create_wrmap_coordinates(coords):
374 result.append('{:.6f} N {:.6f} E'.format(coord[1], coord[0]))
375 return '\n'.join(result)
378 def create_wrmap(geojson):
379 """Creates a <wrmap> wikitext from geojson (as python types)."""
380 wrmap_xml = xml.etree.ElementTree.Element('wrmap')
381 wrmap_xml.text = '\n\n'
382 for k, v in geojson['properties'].items():
383 if k in ['lon', 'lat']:
384 wrmap_xml.attrib[k] = '{:.6f}'.format(v)
386 wrmap_xml.attrib[k] = str(v)
388 assert geojson['type'] == 'FeatureCollection'
389 json_features = geojson['features']
390 last_json_feature = None
391 for json_feature in json_features:
392 feature_xml = xml.etree.ElementTree.SubElement(wrmap_xml, json_feature['properties']['type'])
393 geo = json_feature['geometry']
394 if geo['type'] == 'Point':
395 feature_xml.text = create_wrmap_coordinates([geo['coordinates']])
396 if last_json_feature is not None:
397 last_json_feature.tail = '\n'
399 if last_json_feature is not None:
400 last_json_feature.tail = '\n\n'
401 feature_xml.text = '\n' + create_wrmap_coordinates(geo['coordinates']) + '\n'
402 last_json_feature = feature_xml
403 feature_xml.attrib = json_feature['properties']
404 del feature_xml.attrib['type']
406 if last_json_feature is not None:
407 last_json_feature.tail = '\n\n'
408 return xml.etree.ElementTree.tostring(wrmap_xml, encoding='utf-8').decode('utf-8')