2 # -*- coding: iso-8859-15 -*-
5 """This module contains winterrodeln specific functions that are processing the MediaWiki markup.
8 import xml.etree.ElementTree
11 import wrpylib.wrvalidators
12 import wrpylib.mwmarkup
14 WRMAP_POINT_TYPES = ['gasthaus', 'haltestelle', 'parkplatz', 'achtung', 'foto', 'verleih', 'punkt']
15 WRMAP_LINE_TYPES = ['rodelbahn', 'gehweg', 'alternative', 'lift', 'anfahrt', 'linie']
18 class ParseError(RuntimeError):
19 """Exception used by some of the functions"""
23 class RodelbahnboxDictConverter(formencode.Validator):
24 """Converts a dict with Rodelbahnbox properties to a Sledrun class. Does no validation."""
26 def to_python(self, value, state=None):
27 """value is a dict of properties. If state is an object with the attribute sledrun, this sledrun class will be populated or updated."""
29 if isinstance(state, object) and hasattr(state, 'sledrun'):
30 sledrun = state.sledrun
32 class Sledrun(object):
35 for k, v in props.items():
36 if k == 'Position': sledrun.position_latitude, sledrun.position_longitude = v
37 elif k == 'Position oben': sledrun.top_latitude, sledrun.top_longitude = v
38 elif k == 'Höhe oben': sledrun.top_elevation = v
39 elif k == 'Position unten': sledrun.bottom_latitude, sledrun.bottom_longitude = v
40 elif k == 'Höhe unten': sledrun.bottom_elevation = v
41 elif k == 'Länge': sledrun.length = v
42 elif k == 'Schwierigkeit': sledrun.difficulty = v
43 elif k == 'Lawinen': sledrun.avalanches = v
44 elif k == 'Betreiber': sledrun.operator = v
45 elif k == 'Öffentliche Anreise': sledrun.public_transport = v
46 elif k == 'Aufstieg möglich': sledrun.walkup_possible = v
47 elif k == 'Aufstieg getrennt': sledrun.walkup_separate, sledrun.walkup_separate_comment = v
48 elif k == 'Gehzeit': sledrun.walkup_time = v
49 elif k == 'Aufstiegshilfe': sledrun.lift, sledrun.lift_details = v
50 elif k == 'Beleuchtungsanlage': sledrun.night_light, sledrun.night_light_comment = v
51 elif k == 'Beleuchtungstage': sledrun.night_light_days, sledrun.night_light_days_comment = v
52 elif k == 'Rodelverleih': sledrun.sled_rental, sledrun.sled_rental_comment = v
53 elif k == 'Gütesiegel': sledrun.cachet = v
54 elif k == 'Webauskunft': sledrun.information_web = v
55 elif k == 'Telefonauskunft': sledrun.information_phone = v
56 elif k == 'Bild': sledrun.image = v
57 elif k == 'In Übersichtskarte': sledrun.show_in_overview = v
58 elif k == 'Forumid': sledrun.forum_id = v
61 def from_python(self, value, state=None):
62 """Converts a sledrun class to a dict of Rodelbahnbox properties. value is a sledrun instance."""
64 r = collections.OrderedDict()
65 r['Position'] = (sledrun.position_latitude, sledrun.position_longitude)
66 r['Position oben'] = (sledrun.top_latitude, sledrun.top_longitude)
67 r['Höhe oben'] = sledrun.top_elevation
68 r['Position unten'] = (sledrun.bottom_latitude, sledrun.bottom_longitude)
69 r['Höhe unten'] = sledrun.bottom_elevation
70 r['Länge'] = sledrun.length
71 r['Schwierigkeit'] = sledrun.difficulty
72 r['Lawinen'] = sledrun.avalanches
73 r['Betreiber'] = sledrun.operator
74 r['Öffentliche Anreise'] = sledrun.public_transport
75 r['Aufstieg möglich'] = sledrun.walkup_possible
76 r['Aufstieg getrennt'] = (sledrun.walkup_separate, sledrun.walkup_separate_comment)
77 r['Gehzeit'] = sledrun.walkup_time
78 r['Aufstiegshilfe'] = (sledrun.lift, sledrun.lift_details)
79 r['Beleuchtungsanlage'] = (sledrun.night_light, sledrun.night_light_comment)
80 r['Beleuchtungstage'] = (sledrun.night_light_days, sledrun.night_light_days_comment)
81 r['Rodelverleih'] = (sledrun.sled_rental, sledrun.sled_rental_comment)
82 r['Gütesiegel'] = sledrun.cachet
83 r['Webauskunft'] = sledrun.information_web
84 r['Telefonauskunft'] = sledrun.information_phone
85 r['Bild'] = sledrun.image
86 r['In Übersichtskarte'] = sledrun.show_in_overview
87 r['Forumid'] = sledrun.forum_id
91 class WinterrodelnTemplateDict(formencode.Validator):
92 """Private helper class for RodelbahnboxValidator or GasthausboxValidator"""
93 def __init__(self, template_title):
94 self.template_title = template_title
96 def to_python(self, value, state):
97 title, anonym_params, named_params = value
98 if title != self.template_title:
99 raise formencode.Invalid('Template title has to be "{}".'.format(self.template_title), value, state)
100 if len(anonym_params) > 0:
101 raise formencode.Invalid('No anonymous parameters are allowed in "{}".'.format(self.template_title), value, state)
104 def from_python(self, value, state):
105 return self.template_title, [], value
108 class RodelbahnboxValidator(wrpylib.wrvalidators.RodelbahnboxDictValidator):
110 wrpylib.wrvalidators.RodelbahnboxDictValidator.__init__(self)
111 self.pre_validators=[wrpylib.mwmarkup.TemplateValidator(as_table=True, as_table_keylen=20), WinterrodelnTemplateDict('Rodelbahnbox')]
112 self.chained_validators = [RodelbahnboxDictConverter()]
115 def rodelbahnbox_to_sledrun(wikitext, sledrun=None):
116 """Converts a sledrun wiki page containing the {{Rodelbahnbox}}
117 to a sledrun. sledrun may be an instance of WrSledrunCache or an "empty" class (object()) (default).
118 Raises a formencode.Invalid exception if the format is not OK or the Rodelbahnbox is not found.
119 :return: (start, end, sledrun) tuple of the Rodelbahnbox."""
121 start, end = wrpylib.mwmarkup.find_template(wikitext, 'Rodelbahnbox')
122 if start is None: raise formencode.Invalid("Rodelbahnbox nicht gefunden", wikitext, None)
131 state.sledrun = sledrun
132 return start, end, RodelbahnboxValidator().to_python(wikitext[start:end], state)
135 def sledrun_to_rodelbahnbox(sledrun, version=None):
136 """Converts a sledrun class to the {{Rodelbahnbox}} representation.
137 The sledrun class has to have properties like position_latitude, ...
138 See the table sledruncache for field (column) values.
139 :param sledrun: an arbitrary class that contains the right properties
140 :param version: a string specifying the version of the rodelbahnbox zu produce.
141 Version '1.4' is supported."""
142 assert version in [None, '1.4']
143 return RodelbahnboxValidator().from_python(sledrun)
146 class GasthausboxDictConverter(formencode.Validator):
147 """Converts a dict with Gasthausbox properties to a Inn class. Does no validation."""
149 def to_python(self, value, state=None):
150 """value is a dict of properties. If state is an object with the attribute inn, this inn class will be populated or updated."""
152 if isinstance(state, object) and hasattr(state, 'inn'):
158 for k, v in props.items():
159 if k == 'Position': inn.position_latitude, inn.position_longitude = v
160 elif k == 'Höhe': inn.position_elevation = v
161 elif k == 'Betreiber': inn.operator = v
162 elif k == 'Sitzplätze': inn.seats = v
163 elif k == 'Übernachtung': inn.overnight, inn.overnight_comment = v
164 elif k == 'Rauchfrei': inn.nonsmoker_area, inn.smoker_area = v
165 elif k == 'Rodelverleih': inn.sled_rental, inn.sled_rental_comment = v
166 elif k == 'Handyempfang': inn.mobile_provider = v
167 elif k == 'Homepage': inn.homepage = v
168 elif k == 'E-Mail': inn.email_list = v
169 elif k == 'Telefon': inn.phone_list = v
170 elif k == 'Bild': inn.image = v
171 elif k == 'Rodelbahnen': inn.sledding_list = v
174 def from_python(self, value, state=None):
175 """Converts an inn class to a dict of Gasthausbox properties. value is an Inn instance."""
177 r = collections.OrderedDict()
178 r['Position'] = (inn.position_latitude, inn.position_longitude)
179 r['Höhe'] = inn.position_elevation
180 r['Betreiber'] = inn.operator
181 r['Sitzplätze'] = inn.seats
182 r['Übernachtung'] = (inn.overnight, inn.overnight_comment)
183 r['Rauchfrei'] = (inn.nonsmoker_area, inn.smoker_area)
184 r['Rodelverleih'] = (inn.sled_rental, inn.sled_rental_comment)
185 r['Handyempfang'] = inn.mobile_provider
186 r['Homepage'] = inn.homepage
187 r['E-Mail'] = inn.email_list
188 r['Telefon'] = inn.phone_list
189 r['Bild'] = inn.image
190 r['Rodelbahnen'] = inn.sledding_list
194 class GasthausboxValidator(wrpylib.wrvalidators.GasthausboxDictValidator):
196 wrpylib.wrvalidators.GasthausboxDictValidator.__init__(self)
197 self.pre_validators=[wrpylib.mwmarkup.TemplateValidator(as_table=True, as_table_keylen=17), WinterrodelnTemplateDict('Gasthausbox')]
198 self.chained_validators = [GasthausboxDictConverter()]
201 def gasthausbox_to_inn(wikitext, inn=None):
202 """Converts a inn wiki page containing a {{Gasthausbox}} to an inn.
203 inn may be an instance of WrInnCache or an "empty" class (default).
204 raises a formencode.Invalid exception if the format is not OK or the Gasthausbox is not found.
205 :return: (start, end, inn) tuple."""
207 start, end = wrpylib.mwmarkup.find_template(wikitext, 'Gasthausbox')
208 if start is None: raise formencode.Invalid("No 'Gasthausbox' found", wikitext, None)
218 return start, end, GasthausboxValidator().to_python(wikitext[start:end], state)
221 def inn_to_gasthausbox(inn):
222 """Converts the inn class to the {{Gasthausbox}} representation."""
223 return GasthausboxValidator().from_python(inn)
226 def split_template_latlon_ele(template):
227 """Template is a mwparserfromhell.nodes.template.Template instance. Returns (latlon, ele)."""
228 latlon = opt_geostr_to_lat_lon(template.params[1].strip())
229 ele = opt_intstr_to_int(template.params[2].strip())
233 def create_template_latlon_ele(template_name, latlon, ele):
234 geo = wrpylib.wrvalidators.GeoNone().from_python((latlon))
235 if len(geo) == 0: geo = ' '
236 ele = wrpylib.wrvalidators.UnsignedNone().from_python(ele)
237 if len(ele) == 0: ele = ' '
238 return wrpylib.mwmarkup.create_template(template_name, [geo, ele])
241 def find_template_PositionOben(wikitext):
242 """Same as find_template_latlon_ele with template '{{Position oben|47.076207 N 11.453553 E|1890}}'"""
243 return find_template_latlon_ele(wikitext, 'Position oben')
246 def create_template_PositionOben(lat, lon, ele):
247 return create_template_latlon_ele('Position, oben', lat, lon, ele)
250 def find_template_PositionUnten(wikitext):
251 """Same as find_template_latlon_ele with template '{{Position unten|47.076207 N 11.453553 E|1890}}'"""
252 return find_template_latlon_ele(wikitext, 'Position unten')
255 def find_template_unsigned(wikitext, template_title):
256 """Finds the first occurance of the '{{template_title|1890}}' template
257 and returns the tuple (start, end, unsigned_value) or (None, None, None) if the
258 template was not found. If the template has no valid format, an exception is thrown."""
259 start, end = wrpylib.mwmarkup.find_template(wikitext, template_title)
260 if start is None: return (None,) * 3
261 title, params = wrpylib.mwmarkup.split_template(wikitext[start:end])
262 unsigned_value = wrpylib.wrvalidators.UnsignedNone().to_python(params['1'].strip())
263 return start, end, unsigned_value
266 def create_template_unsigned(template_title, unsigned):
267 unsigned = wrpylib.wrvalidators.UnsignedNone().from_python(unsigned)
268 if len(unsigned) == 0: unsigned = ' '
269 return wrpylib.mwmarkup.create_template(template_title, [unsigned])
272 def find_template_Hoehenunterschied(wikitext):
273 """Same as find_template_unsigned with template '{{Höhenunterschied|350}}'"""
274 return find_template_unsigned(wikitext, 'Höhenunterschied')
277 def create_template_Hoehenunterschied(ele_diff):
278 return create_template_unsigned('Höhenunterschied', ele_diff)
281 def find_template_Bahnlaenge(wikitext):
282 """Same as find_template_unsigned with template '{{Bahnlänge|4500}}'"""
283 return find_template_unsigned(wikitext, 'Bahnlänge')
286 def create_template_Bahnlaenge(length):
287 return create_template_unsigned('Bahnlänge', length)
290 def find_template_Gehzeit(wikitext):
291 """Same as find_template_unsigned with template '{{Gehzeit|60}}'"""
292 return find_template_unsigned(wikitext, 'Gehzeit')
295 def create_template_Gehzeit(walkup_time):
296 return create_template_unsigned('Gehzeit', walkup_time)
299 def find_template_Forumlink(wikitext):
300 """Same as find_template_unsigned with template '{{Forumlink|26}}'"""
301 start, end = wrpylib.mwmarkup.find_template(wikitext, 'Forumlink')
302 if start is None: return (None,) * 3
303 title, params = wrpylib.mwmarkup.split_template(wikitext[start:end])
304 forumid = params['1'].strip()
305 if forumid == '<nummer einfügen>': unsigned_value = None
306 else: unsigned_value = wrpylib.wrvalidators.UnsignedNone().to_python(forumid)
307 return start, end, unsigned_value
308 # return find_template_unsigned(wikitext, u'Forumlink')
311 def find_template_Parkplatz(wikitext):
312 """Same as find_template_latlon_ele with template '{{Parkplatz|47.076207 N 11.453553 E|1890}}'"""
313 return find_template_latlon_ele(wikitext, 'Parkplatz')
316 def find_template_Haltestelle(wikitext):
317 """Finds the first occurance of the '{{Haltestelle|Ortsname|Haltestellenname|47.076207 N 11.453553 E|1890}}' template
318 and returns the tuple (start, end, city, stop, lat, lon, ele) or (None, None, None, None, None, None, None) if the
319 template was not found. If the template has no valid format, an exception is thrown."""
320 start, end = wrpylib.mwmarkup.find_template(wikitext, 'Haltestelle')
321 if start is None: return (None,) * 7
322 title, params = wrpylib.mwmarkup.split_template(wikitext[start:end])
323 city = wrpylib.wrvalidators.UnicodeNone().to_python(params['1'].strip())
324 stop = wrpylib.wrvalidators.UnicodeNone().to_python(params['2'].strip())
325 lat, lon = wrpylib.wrvalidators.GeoNone().to_python(params['3'].strip())
326 ele = wrpylib.wrvalidators.UnsignedNone().to_python(params['4'].strip())
327 return start, end, city, stop, lat, lon, ele
330 def parse_wrmap_coordinates(coords):
331 '''gets a string coordinates and returns an array of lon/lat coordinate pairs, e.g.
335 [[11.87, 47.12], [11.70, 47.13]]'''
338 for match in re.finditer(r'\s*(\d+\.?\d*)\s*N?\s+(\d+\.?\d*)\s*E?\s*', coords):
339 if match.start() != pos:
341 result.append([float(match.groups()[1]), float(match.groups()[0])])
344 if pos == len(coords):
346 raise RuntimeError('Wrong coordinate format: {}'.format(coords))
349 def parse_wrmap(wikitext):
350 """Parses the (unicode) u'<wrmap ...>content</wrmap>' of the Winterrodeln wrmap extension.
351 If wikitext does not contain the <wrmap> tag or if the <wrmap> tag contains
352 invalid formatted lines, a ParseError is raised.
353 Use wrpylib.mwmarkup.find_tag(wikitext, 'wrmap') to find the wrmap tag within an arbitrary
354 wikitext before using this function.
356 :param wikitext: wikitext containing only the template. Example:
359 <wrmap lat="47.2417134" lon="11.21408895" zoom="14" width="700" height="400">
360 <gasthaus name="Rosskogelhütte" wiki="Rosskogelhütte">47.240689 11.190454</gasthaus>
361 <parkplatz>47.245789 11.238971</parkplatz>
362 <haltestelle name="Oberperfuss Rangger Köpfl Lift">47.245711 11.238283</haltestelle>
370 :returns: GeoJSON as nested Python datatype
374 wrmap_xml = xml.etree.ElementTree.fromstring(wikitext.encode('utf-8'))
375 except xml.etree.ElementTree.ParseError as e:
376 row, column = e.position
377 raise ParseError("XML parse error on row {}, column {}: {}".format(row, column, e))
378 if wrmap_xml.tag not in ['wrmap', 'wrgmap']:
379 raise ParseError('No valid tag name')
381 # convert XML to geojson (http://www.geojson.org/geojson-spec.html)
383 for feature in wrmap_xml:
384 # determine feature type
385 is_point = feature.tag in WRMAP_POINT_TYPES
386 is_line = feature.tag in WRMAP_LINE_TYPES
387 if (not is_point and not is_line):
388 raise ParseError('Unknown element <{}>.'.format(feature.tag))
392 properties = {'type': feature.tag}
393 allowed_properties = {'name', 'wiki'}
394 wrong_properties = set(feature.attrib.keys()) - allowed_properties
395 if len(wrong_properties) > 0:
396 raise ParseError("The attribute '{}' is not allowed at <{}>.".format(list(wrong_properties)[0], feature.tag))
397 properties.update(feature.attrib)
398 coordinates = parse_wrmap_coordinates(feature.text)
399 if len(coordinates) != 1:
400 raise ParseError('The element <{}> has to have exactly one coordinate pair.'.format(feature.tag))
401 json_features.append({
403 'geometry': {'type': 'Point', 'coordinates': coordinates[0]},
404 'properties': properties})
408 properties = {'type': feature.tag}
409 allowed_properties = {'farbe', 'dicke'}
410 wrong_properties = set(feature.attrib.keys()) - allowed_properties
411 if len(wrong_properties) > 0:
412 raise ParseError("The attribute '{}' is not allowed at <{}>.".format(list(wrong_properties)[0], feature.tag))
413 if 'farbe' in feature.attrib:
414 if not re.match('#[0-9a-fA-F]{6}$', feature.attrib['farbe']):
415 raise ParseError('The attribute "farbe" has to have a format like "#a0bb43".')
416 properties['strokeColor'] = feature.attrib['farbe'] # e.g. #a200b7
417 if 'dicke' in feature.attrib:
419 properties['strokeWidth'] = int(feature.attrib['dicke']) # e.g. 6
421 raise ParseError('The attribute "dicke" has to be an integer.')
422 json_features.append({
424 'geometry': {'type': 'LineString', 'coordinates': parse_wrmap_coordinates(feature.text)},
425 'properties': properties})
429 for k, v in wrmap_xml.attrib.items():
430 if k in ['lat', 'lon']:
432 properties[k] = float(v)
434 raise ParseError('Attribute "{}" has to be a float value.'.format(k))
435 elif k in ['zoom', 'width', 'height']:
437 properties[k] = int(v)
439 raise ParseError('Attribute "{}" has to be an integer value.'.format(k))
441 raise ParseError('Unknown attribute "{}".'.format(k))
444 'type': 'FeatureCollection',
445 'features': json_features,
446 'properties': properties}
451 def create_wrmap_coordinates(coords):
454 result.append('{:.6f} N {:.6f} E'.format(coord[1], coord[0]))
455 return '\n'.join(result)
458 def create_wrmap(geojson):
459 """Creates a <wrmap> wikitext from geojson (as python types)."""
460 wrmap_xml = xml.etree.ElementTree.Element('wrmap')
461 wrmap_xml.text = '\n\n'
462 for k, v in geojson['properties'].items():
463 if k in ['lon', 'lat']:
464 wrmap_xml.attrib[k] = '{:.6f}'.format(v)
466 wrmap_xml.attrib[k] = str(v)
468 assert geojson['type'] == 'FeatureCollection'
469 json_features = geojson['features']
470 last_json_feature = None
471 for json_feature in json_features:
472 feature_xml = xml.etree.ElementTree.SubElement(wrmap_xml, json_feature['properties']['type'])
473 geo = json_feature['geometry']
474 if geo['type'] == 'Point':
475 feature_xml.text = create_wrmap_coordinates([geo['coordinates']])
476 if last_json_feature is not None:
477 last_json_feature.tail = '\n'
479 if last_json_feature is not None:
480 last_json_feature.tail = '\n\n'
481 feature_xml.text = '\n' + create_wrmap_coordinates(geo['coordinates']) + '\n'
482 last_json_feature = feature_xml
483 feature_xml.attrib = json_feature['properties']
484 del feature_xml.attrib['type']
486 if last_json_feature is not None:
487 last_json_feature.tail = '\n\n'
488 return xml.etree.ElementTree.tostring(wrmap_xml, encoding='utf-8').decode('utf-8')