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 find_template_latlon_ele(wikitext, template_title):
227 """Finds the first occurance of the '{{template_title|47.076207 N 11.453553 E|1890}}' template
228 and returns the tuple (start, end, lat, lon, ele) or (None, None, None, None, None) if the
229 template was not found. If the template has no valid format, an exception is thrown."""
230 start, end = wrpylib.mwmarkup.find_template(wikitext, template_title)
231 if start is None: return (None,) * 5
232 title, params = wrpylib.mwmarkup.split_template(wikitext[start:end])
233 lat, lon = wrpylib.wrvalidators.GeoNone().to_python(params['1'].strip())
234 ele = wrpylib.wrvalidators.UnsignedNone().to_python(params['2'].strip())
235 return start, end, lat, lon, ele
238 def create_template_latlon_ele(template_title, lat, lon, ele):
239 geo = wrpylib.wrvalidators.GeoNone().from_python((lat, lon))
240 if len(geo) == 0: geo = ' '
241 ele = wrpylib.wrvalidators.UnsignedNone().from_python(ele)
242 if len(ele) == 0: ele = ' '
243 return wrpylib.mwmarkup.create_template(template_title, [geo, ele])
246 def find_template_PositionOben(wikitext):
247 """Same as find_template_latlon_ele with template '{{Position oben|47.076207 N 11.453553 E|1890}}'"""
248 return find_template_latlon_ele(wikitext, 'Position oben')
251 def create_template_PositionOben(lat, lon, ele):
252 return create_template_latlon_ele('Position, oben', lat, lon, ele)
255 def find_template_PositionUnten(wikitext):
256 """Same as find_template_latlon_ele with template '{{Position unten|47.076207 N 11.453553 E|1890}}'"""
257 return find_template_latlon_ele(wikitext, 'Position unten')
260 def find_template_unsigned(wikitext, template_title):
261 """Finds the first occurance of the '{{template_title|1890}}' template
262 and returns the tuple (start, end, unsigned_value) or (None, None, None) if the
263 template was not found. If the template has no valid format, an exception is thrown."""
264 start, end = wrpylib.mwmarkup.find_template(wikitext, template_title)
265 if start is None: return (None,) * 3
266 title, params = wrpylib.mwmarkup.split_template(wikitext[start:end])
267 unsigned_value = wrpylib.wrvalidators.UnsignedNone().to_python(params['1'].strip())
268 return start, end, unsigned_value
271 def create_template_unsigned(template_title, unsigned):
272 unsigned = wrpylib.wrvalidators.UnsignedNone().from_python(unsigned)
273 if len(unsigned) == 0: unsigned = ' '
274 return wrpylib.mwmarkup.create_template(template_title, [unsigned])
277 def find_template_Hoehenunterschied(wikitext):
278 """Same as find_template_unsigned with template '{{Höhenunterschied|350}}'"""
279 return find_template_unsigned(wikitext, 'Höhenunterschied')
282 def create_template_Hoehenunterschied(ele_diff):
283 return create_template_unsigned('Höhenunterschied', ele_diff)
286 def find_template_Bahnlaenge(wikitext):
287 """Same as find_template_unsigned with template '{{Bahnlänge|4500}}'"""
288 return find_template_unsigned(wikitext, 'Bahnlänge')
291 def create_template_Bahnlaenge(length):
292 return create_template_unsigned('Bahnlänge', length)
295 def find_template_Gehzeit(wikitext):
296 """Same as find_template_unsigned with template '{{Gehzeit|60}}'"""
297 return find_template_unsigned(wikitext, 'Gehzeit')
300 def create_template_Gehzeit(walkup_time):
301 return create_template_unsigned('Gehzeit', walkup_time)
304 def find_template_Forumlink(wikitext):
305 """Same as find_template_unsigned with template '{{Forumlink|26}}'"""
306 start, end = wrpylib.mwmarkup.find_template(wikitext, 'Forumlink')
307 if start is None: return (None,) * 3
308 title, params = wrpylib.mwmarkup.split_template(wikitext[start:end])
309 forumid = params['1'].strip()
310 if forumid == '<nummer einfügen>': unsigned_value = None
311 else: unsigned_value = wrpylib.wrvalidators.UnsignedNone().to_python(forumid)
312 return start, end, unsigned_value
313 # return find_template_unsigned(wikitext, u'Forumlink')
316 def find_template_Parkplatz(wikitext):
317 """Same as find_template_latlon_ele with template '{{Parkplatz|47.076207 N 11.453553 E|1890}}'"""
318 return find_template_latlon_ele(wikitext, 'Parkplatz')
321 def find_template_Haltestelle(wikitext):
322 """Finds the first occurance of the '{{Haltestelle|Ortsname|Haltestellenname|47.076207 N 11.453553 E|1890}}' template
323 and returns the tuple (start, end, city, stop, lat, lon, ele) or (None, None, None, None, None, None, None) if the
324 template was not found. If the template has no valid format, an exception is thrown."""
325 start, end = wrpylib.mwmarkup.find_template(wikitext, 'Haltestelle')
326 if start is None: return (None,) * 7
327 title, params = wrpylib.mwmarkup.split_template(wikitext[start:end])
328 city = wrpylib.wrvalidators.UnicodeNone().to_python(params['1'].strip())
329 stop = wrpylib.wrvalidators.UnicodeNone().to_python(params['2'].strip())
330 lat, lon = wrpylib.wrvalidators.GeoNone().to_python(params['3'].strip())
331 ele = wrpylib.wrvalidators.UnsignedNone().to_python(params['4'].strip())
332 return start, end, city, stop, lat, lon, ele
335 def find_all_templates(wikitext, find_func):
336 """Returns a list of return values of find_func that searches for a template.
338 >>> find_all_templates(wikitext, find_template_Haltestelle)
339 Returns an empty list if the template was not found at all.
342 result = find_func(wikitext)
343 start, end = result[:2]
344 while start is not None:
345 results.append(result)
346 result = find_func(wikitext[end:])
347 if result[0] is None:
350 start = result[0] + end
352 result = (start, end) + result[2:]
356 def googlemap_to_wrmap(attributes, coords, paths):
357 """Converts the output of parse_googlemap to the GeoJSON format wrmap uses.
358 :returns: (GeoJSON as nested Python datatypes)
364 lon, lat, symbol, title = point
365 properties = {'type': 'punkt' if symbol is None else symbol.lower()}
366 if title is not None: properties['name'] = title
367 json_features.append({
369 'geometry': {'type': 'Point', 'coordinates': [lon, lat]},
370 'properties': properties})
374 style, entries = path
375 style = style.lower()
376 PATH_TYPES = {'6#ff014e9a': 'rodelbahn', '6#ffe98401': 'gehweg', '6#ff7f7fff': 'alternative', '3#ff000000': 'lift', '3#ffe1e100': 'anfahrt'}
377 if style in PATH_TYPES:
378 properties = {'type': PATH_TYPES[style]}
380 properties = {'type': 'line'}
381 properties['dicke'] = style[0]
382 properties['farbe'] = style[4:]
383 json_features.append({
386 'type': 'LineString',
387 'coordinates': [[lon, lat] for lon, lat, symbol, title in entries]},
388 'properties': properties})
391 'type': 'FeatureCollection',
392 'features': json_features,
393 'properties': attributes}
397 def parse_wrmap_coordinates(coords):
398 '''gets a string coordinates and returns an array of lon/lat coordinate pairs, e.g.
402 [[11.87, 47.12], [11.70, 47.13]]'''
405 for match in re.finditer(r'\s*(\d+\.?\d*)\s*N?\s+(\d+\.?\d*)\s*E?\s*', coords):
406 if match.start() != pos:
408 result.append([float(match.groups()[1]), float(match.groups()[0])])
411 if pos == len(coords):
413 raise RuntimeError('Wrong coordinate format: {}'.format(coords))
416 def parse_wrmap(wikitext):
417 """Parses the (unicode) u'<wrmap ...>content</wrmap>' of the Winterrodeln wrmap extension.
418 If wikitext does not contain the <wrmap> tag or if the <wrmap> tag contains
419 invalid formatted lines, a ParseError is raised.
420 Use wrpylib.mwmarkup.find_tag(wikitext, 'wrmap') to find the wrmap tag within an arbitrary
421 wikitext before using this function.
423 :param wikitext: wikitext containing only the template. Example:
426 <wrmap lat="47.2417134" lon="11.21408895" zoom="14" width="700" height="400">
427 <gasthaus name="Rosskogelhütte" wiki="Rosskogelhütte">47.240689 11.190454</gasthaus>
428 <parkplatz>47.245789 11.238971</parkplatz>
429 <haltestelle name="Oberperfuss Rangger Köpfl Lift">47.245711 11.238283</haltestelle>
437 :returns: GeoJSON as nested Python datatype
441 wrmap_xml = xml.etree.ElementTree.fromstring(wikitext.encode('utf-8'))
442 except xml.etree.ElementTree.ParseError as e:
443 row, column = e.position
444 raise ParseError("XML parse error on row {}, column {}: {}".format(row, column, e))
445 if wrmap_xml.tag not in ['wrmap', 'wrgmap']:
446 raise ParseError('No valid tag name')
448 # convert XML to geojson (http://www.geojson.org/geojson-spec.html)
450 for feature in wrmap_xml:
451 # determine feature type
452 is_point = feature.tag in WRMAP_POINT_TYPES
453 is_line = feature.tag in WRMAP_LINE_TYPES
454 if (not is_point and not is_line):
455 raise ParseError('Unknown element <{}>.'.format(feature.tag))
459 properties = {'type': feature.tag}
460 allowed_properties = set(['name', 'wiki'])
461 wrong_properties = set(feature.attrib.keys()) - allowed_properties
462 if len(wrong_properties) > 0:
463 raise ParseError("The attribute '{}' is not allowed at <{}>.".format(list(wrong_properties)[0], feature.tag))
464 properties.update(feature.attrib)
465 coordinates = parse_wrmap_coordinates(feature.text)
466 if len(coordinates) != 1:
467 raise ParseError('The element <{}> has to have exactly one coordinate pair.'.format(feature.tag))
468 json_features.append({
470 'geometry': {'type': 'Point', 'coordinates': coordinates[0]},
471 'properties': properties})
475 properties = {'type': feature.tag}
476 allowed_properties = set(['farbe', 'dicke'])
477 wrong_properties = set(feature.attrib.keys()) - allowed_properties
478 if len(wrong_properties) > 0:
479 raise ParseError("The attribute '{}' is not allowed at <{}>.".format(list(wrong_properties)[0], feature.tag))
480 if 'farbe' in feature.attrib:
481 if not re.match('#[0-9a-fA-F]{6}$', feature.attrib['farbe']):
482 raise ParseError('The attribute "farbe" has to have a format like "#a0bb43".')
483 properties['strokeColor'] = feature.attrib['farbe'] # e.g. #a200b7
484 if 'dicke' in feature.attrib:
486 properties['strokeWidth'] = int(feature.attrib['dicke']) # e.g. 6
488 raise ParseError('The attribute "dicke" has to be an integer.')
489 json_features.append({
491 'geometry': {'type': 'LineString', 'coordinates': parse_wrmap_coordinates(feature.text)},
492 'properties': properties})
496 for k, v in wrmap_xml.attrib.items():
497 if k in ['lat', 'lon']:
499 properties[k] = float(v)
501 raise ParseError('Attribute "{}" has to be a float value.'.format(k))
502 elif k in ['zoom', 'width', 'height']:
504 properties[k] = int(v)
506 raise ParseError('Attribute "{}" has to be an integer value.'.format(k))
508 raise ParseError('Unknown attribute "{}".'.format(k))
511 'type': 'FeatureCollection',
512 'features': json_features,
513 'properties': properties}
518 def create_wrmap_coordinates(coords):
521 result.append('{:.6f} N {:.6f} E'.format(coord[1], coord[0]))
522 return '\n'.join(result)
525 def create_wrmap(geojson):
526 """Creates a <wrmap> wikitext from geojson (as python types)."""
527 wrmap_xml = xml.etree.ElementTree.Element('wrmap')
528 wrmap_xml.text = '\n\n'
529 for k, v in geojson['properties'].items():
530 if k in ['lon', 'lat']:
531 wrmap_xml.attrib[k] = '{:.6f}'.format(v)
533 wrmap_xml.attrib[k] = str(v)
535 assert geojson['type'] == 'FeatureCollection'
536 json_features = geojson['features']
537 last_json_feature = None
538 for json_feature in json_features:
539 feature_xml = xml.etree.ElementTree.SubElement(wrmap_xml, json_feature['properties']['type'])
540 geo = json_feature['geometry']
541 if geo['type'] == 'Point':
542 feature_xml.text = create_wrmap_coordinates([geo['coordinates']])
543 if last_json_feature is not None:
544 last_json_feature.tail = '\n'
546 if last_json_feature is not None:
547 last_json_feature.tail = '\n\n'
548 feature_xml.text = '\n' + create_wrmap_coordinates(geo['coordinates']) + '\n'
549 last_json_feature = feature_xml
550 feature_xml.attrib = json_feature['properties']
551 del feature_xml.attrib['type']
553 if last_json_feature is not None:
554 last_json_feature.tail = '\n\n'
555 return xml.etree.ElementTree.tostring(wrmap_xml, encoding='utf-8').decode('utf-8')