Added test to GasthausboxDictValidator.
[philipp/winterrodeln/wrpylib.git] / wrpylib / wrmwmarkup.py
1 #!/usr/bin/python2.7
2 # -*- coding: iso-8859-15 -*-
3 # $Id$
4 # $HeadURL$
5 """This module contains winterrodeln specific functions that are prcocessing the MediaWiki markup.
6 """
7 import re
8 import xml.etree.ElementTree
9 import collections
10 import formencode
11 import wrpylib.wrvalidators
12 import wrpylib.mwmarkup
13
14 WRMAP_POINT_TYPES = ['gasthaus', 'haltestelle', 'parkplatz', 'achtung', 'foto', 'verleih', 'punkt']
15 WRMAP_LINE_TYPES = ['rodelbahn', 'gehweg', 'alternative', 'lift', 'anfahrt', 'linie']
16
17
18 class ParseError(RuntimeError):
19     """Exception used by some of the functions"""
20     pass
21
22
23 def _conv(fnct, value, fieldname):
24     """Internal function.
25     Like one of the to_xxx functions (e.g. to_bool), but adds the field name to the error message"""
26     try: return fnct(value)
27     except formencode.Invalid as e: raise formencode.Invalid(u"Conversion error in field '%s': %s" % (fieldname, unicode(e)), e.value, e.state)
28
29
30 class RodelbahnboxDictConverter(formencode.Validator):
31     """Converts a dict with Rodelbahnbox properties to a Sledrun class. Does no validation."""
32
33     def to_python(self, value, state=None):
34         """value is a dict of properties. If state is an object with the attribute sledrun, this sledrun class will be populated or updated."""
35         props = value
36         if isinstance(state, object) and hasattr(state, 'sledrun'):
37             sledrun = state.sledrun
38         else:
39             class Sledrun(object):
40                 pass
41             sledrun = Sledrun()
42         for k, v in props.iteritems():
43             if   k == u'Position': sledrun.position_latitude, sledrun.position_longitude = v
44             elif k == u'Position oben': sledrun.top_latitude, sledrun.top_longitude = v
45             elif k == u'Höhe oben': sledrun.top_elevation = v
46             elif k == u'Position unten': sledrun.bottom_latitude, sledrun.bottom_longitude = v
47             elif k == u'Höhe unten': sledrun.bottom_elevation = v
48             elif k == u'Länge': sledrun.length = v
49             elif k == u'Schwierigkeit': sledrun.difficulty = v
50             elif k == u'Lawinen': sledrun.avalanches = v
51             elif k == u'Betreiber': sledrun.operator = v
52             elif k == u'Öffentliche Anreise': sledrun.public_transport = v
53             elif k == u'Aufstieg möglich': sledrun.walkup_possible = v
54             elif k == u'Aufstieg getrennt': sledrun.walkup_separate, sledrun.walkup_separate_comment = v
55             elif k == u'Gehzeit': sledrun.walkup_time = v
56             elif k == u'Aufstiegshilfe': sledrun.lift, sledrun.lift_details = v
57             elif k == u'Beleuchtungsanlage': sledrun.night_light, sledrun.night_light_comment = v
58             elif k == u'Beleuchtungstage': sledrun.night_light_days, sledrun.night_light_days_comment = v
59             elif k == u'Rodelverleih': sledrun.sled_rental, sledrun.sled_rental_comment = v
60             elif k == u'Gütesiegel': sledrun.cachet = v
61             elif k == u'Webauskunft': sledrun.information_web = v
62             elif k == u'Telefonauskunft': sledrun.information_phone = v
63             elif k == u'Bild': sledrun.image = v
64             elif k == u'In Übersichtskarte': sledrun.show_in_overview = v
65             elif k == u'Forumid': sledrun.forum_id = v
66         return sledrun
67
68     def from_python(self, value, state=None):
69         """Converts a sledrun class to a dict of Rodelbahnbox properties. value is a sledrun instance."""
70         sledrun = value
71         r = collections.OrderedDict()
72         r[u'Position'] = (sledrun.position_latitude, sledrun.position_longitude)
73         r[u'Position oben'] = (sledrun.top_latitude, sledrun.top_longitude)
74         r[u'Höhe oben'] = sledrun.top_elevation
75         r[u'Position unten'] = (sledrun.bottom_latitude, sledrun.bottom_longitude)
76         r[u'Höhe unten'] = sledrun.bottom_elevation
77         r[u'Länge'] = sledrun.length
78         r[u'Schwierigkeit'] = sledrun.difficulty
79         r[u'Lawinen'] = sledrun.avalanches
80         r[u'Betreiber'] = sledrun.operator
81         r[u'Öffentliche Anreise'] = sledrun.public_transport
82         r[u'Aufstieg möglich'] = sledrun.walkup_possible
83         r[u'Aufstieg getrennt'] = (sledrun.walkup_separate, sledrun.walkup_separate_comment)
84         r[u'Gehzeit'] = sledrun.walkup_time
85         r[u'Aufstiegshilfe'] = (sledrun.lift, sledrun.lift_details)
86         r[u'Beleuchtungsanlage'] = (sledrun.night_light, sledrun.night_light_comment)
87         r[u'Beleuchtungstage'] = (sledrun.night_light_days, sledrun.night_light_days_comment)
88         r[u'Rodelverleih'] = (sledrun.sled_rental, sledrun.sled_rental_comment)
89         r[u'Gütesiegel'] = sledrun.cachet
90         r[u'Webauskunft'] = sledrun.information_web
91         r[u'Telefonauskunft'] = sledrun.information_phone
92         r[u'Bild'] = sledrun.image
93         r[u'In Übersichtskarte'] = sledrun.show_in_overview
94         r[u'Forumid'] = sledrun.forum_id
95         return r
96
97
98 class WinterrodelnTemplateDict(formencode.Validator):
99     """Private helper class for RodelbahnboxValidator or GasthausboxValidator"""
100     def __init__(self, template_title):
101         self.template_title = template_title
102
103     def to_python(self, value, state):
104         title, anonym_params, named_params = value
105         if title != self.template_title:
106             raise Invalud(u'Template title has to be "{}".'.format(self.template_title), value, state)
107         if len(anonym_params) > 0:
108             raise Invalid(u'No anonymous parameters are allowed in "{}".'.format(self.template_title), value, state)
109         return named_params
110
111     def from_python(self, value, state):
112         return self.template_title, [], value
113
114
115 class RodelbahnboxValidator(wrpylib.wrvalidators.RodelbahnboxDictValidator):
116     def __init__(self):
117         wrpylib.wrvalidators.RodelbahnboxDictValidator.__init__(self)
118         self.pre_validators=[wrpylib.mwmarkup.TemplateValidator(as_table=True, as_table_keylen=20), WinterrodelnTemplateDict(u'Rodelbahnbox')]
119         self.chained_validators = [RodelbahnboxDictConverter()]
120
121
122 def rodelbahnbox_to_sledrun(wikitext, sledrun=None):
123     """Converts a sledrun wiki page containing the {{Rodelbahnbox}}
124     to a sledrun. sledrun may be an instance of WrSledrunCache or an "empty" class (object()) (default).
125     Raises a formencode.Invalid exception if the format is not OK or the Rodelbahnbox is not found.
126     :return: (start, end, sledrun) tuple of the Rodelbahnbox."""
127     # find Rodelbahnbox
128     start, end = wrpylib.mwmarkup.find_template(wikitext, u'Rodelbahnbox')
129     if start is None: raise formencode.Invalid(u"Rodelbahnbox nicht gefunden", wikitext, None)
130
131     # convert to sledrun
132     if sledrun is None:
133         state = None
134     else:
135         class State(object):
136             pass
137         state = State()
138         state.sledrun = sledrun
139     return start, end, RodelbahnboxValidator().to_python(wikitext[start:end], state)
140
141
142 def sledrun_to_rodelbahnbox(sledrun, version=None):
143     """Converts a sledrun class to the {{Rodelbahnbox}} representation.
144     The sledrun class has to have properties like position_latitude, ...
145     See the table sledruncache for field (column) values.
146     :param sledrun: an arbitrary class that contains the right properties
147     :param version: a string specifying the version of the rodelbahnbox zu produce.
148         Version '1.4' is supported."""
149     assert version in [None, '1.4']
150     return RodelbahnboxValidator().from_python(sledrun)
151
152
153 class GasthausboxDictConverter(formencode.Validator):
154     """Converts a dict with Gasthausbox properties to a Inn class. Does no validation."""
155
156     def to_python(self, value, state=None):
157         """value is a dict of properties. If state is an object with the attribute inn, this inn class will be populated or updated."""
158         props = value
159         if isinstance(state, object) and hasattr(state, 'inn'):
160             sledrun = state.sledrun
161         else:
162             class Inn(object):
163                 pass
164             inn = Inn()
165         for k, v in props.iteritems():
166             if   k == u'Position': inn.position_latitude, inn.position_longitude = v
167             elif k == u'Höhe': inn.position_elevation = v
168             elif k == u'Betreiber': inn.operator = v
169             elif k == u'Sitzplätze': inn.seats = v
170             elif k == u'Übernachtung': inn.overnight, inn.overnight_comment = v
171             elif k == u'Rauchfrei': inn.nonsmoker_area, inn.smoker_area = v
172             elif k == u'Rodelverleih': inn.sled_rental, inn.sled_rental_comment = v
173             elif k == u'Handyempfang': inn.mobile_provider = v
174             elif k == u'Homepage': inn.homepage = v
175             elif k == u'E-Mail': inn.email_list = v
176             elif k == u'Telefon': inn.phone_list = v
177             elif k == u'Bild': inn.image = v
178             elif k == u'Rodelbahnen': inn.sledding_list = v
179         return inn
180
181     def from_python(self, value, state=None):
182         """Converts an inn class to a dict of Gasthausbox properties. value is an Inn instance."""
183         inn = value
184         r = collections.OrderedDict()
185         r[u'Position'] = (inn.position_latitude, inn.position_longitude)
186         r[u'Höhe'] = inn.position_elevation
187         r[u'Betreiber'] = inn.operator
188         r[u'Sitzplätze'] = inn.seats
189         r[u'Übernachtung'] = (inn.overnight, inn.overnight_comment)
190         r[u'Rauchfrei'] = (inn.nonsmoker_area, inn.smoker_area)
191         r[u'Rodelverleih'] = (inn.sled_rental, inn.sled_rental_comment)
192         r[u'Handyempfang'] = inn.mobile_provider
193         r[u'Homepage'] = inn.homepage
194         r[u'E-Mail'] = inn.email_list
195         r[u'Telefon'] = inn.phone_list
196         r[u'Bild'] = inn.image
197         r[u'Rodelbahnen'] = inn.sledding_list
198         return r
199
200
201 def gasthausbox_to_inn(wikitext, inn=None):
202     """Converts a inn wiki page containing a {{Gasthausbox}} to an inn.
203     raises a formencode.Invalid exception if an error occurs.
204     :return: (start, end, inn) tuple."""
205     if inn is None:
206         class Inn(object): pass
207         inn = Inn()
208
209     # Match Gasthausbox
210     start, end = wrpylib.mwmarkup.find_template(wikitext, u'Gasthausbox')
211     if start is None: raise formencode.Invalid(u"No 'Gasthausbox' found", wikitext, None)
212     template_title, properties = wrpylib.mwmarkup.split_template(wikitext[start:end])
213
214     # Process properties
215     for key, value in properties.iteritems():
216         if   key == u'Position': inn.position_latitude, inn.position_longitude = _conv(wrpylib.wrvalidators.GeoNone().to_python, value, key) # '47.583333 N 15.75 E'
217         elif key == u'Höhe': inn.position_elevation = _conv(wrpylib.wrvalidators.UnsignedNone().to_python, value, key)
218         elif key == u'Betreiber': inn.operator = _conv(wrpylib.wrvalidators.UnicodeNone().to_python, value, key)
219         elif key == u'Sitzplätze': inn.seats = _conv(wrpylib.wrvalidators.UnsignedNone().to_python, value, key)
220         elif key == u'Übernachtung': inn.overnight, inn.overnight_comment = _conv(wrpylib.wrvalidators.BoolUnicodeTupleValidator().to_python, value, key)
221         elif key == u'Rauchfrei': inn.nonsmoker_area, inn.smoker_area = _conv(wrpylib.wrvalidators.GermanTristateTuple().to_python, value, key)
222         elif key == u'Rodelverleih': inn.sled_rental, inn.sled_rental_comment = _conv(wrpylib.wrvalidators.BoolUnicodeTupleValidator().to_python, value, key)
223         elif key == u'Handyempfang': inn.mobile_provider = _conv(wrpylib.wrvalidators.ValueCommentListNeinLoopNone().to_python, value, key)
224         elif key == u'Homepage': inn.homepage = _conv(wrpylib.wrvalidators.UrlNeinNone().to_python, value, key)
225         elif key == u'E-Mail': inn.email_list = _conv(wrpylib.wrvalidators.EmailCommentListNeinLoopNone(allow_masked_email=True).to_python, value, key)
226         elif key == u'Telefon': inn.phone_list = _conv(wrpylib.wrvalidators.PhoneCommentListNeinLoopNone(comments_are_optional=True).to_python, value, key)
227         elif key == u'Bild': inn.image = _conv(wrpylib.wrvalidators.UnicodeNone().to_python, value, key)
228         elif key == u'Rodelbahnen': inn.sledding_list = _conv(wrpylib.wrvalidators.WikiPageListLoopNone().to_python, value, key)
229         else: raise formencode.Invalid(u"Unbekannte Eigenschaft der Gasthausbox: '%s' (mit Wert '%s')" % (key, value), value, None)
230     return start, end, inn
231
232
233 def inn_to_gasthausbox(inn):
234     """Converts the inn class to the {{Gasthausbox}} representation."""
235     keys = []
236     values = []
237     keys.append(u'Position')
238     values.append(wrpylib.wrvalidators.GeoNone().from_python((inn.position_latitude, inn.position_longitude)))
239     keys.append(u'Höhe')
240     values.append(wrpylib.wrvalidators.UnsignedNone().from_python(inn.position_elevation))
241     keys.append(u'Betreiber')
242     values.append(wrpylib.wrvalidators.UnicodeNone().from_python(inn.operator))
243     keys.append(u'Sitzplätze')
244     values.append(wrpylib.wrvalidators.UnsignedNone().from_python(inn.seats))
245     keys.append(u'Übernachtung')
246     values.append(wrpylib.wrvalidators.BoolUnicodeTupleValidator().from_python((inn.overnight, inn.overnight_comment)))
247     keys.append(u'Rauchfrei')
248     values.append(wrpylib.wrvalidators.GermanTristateTuple().from_python((inn.nonsmoker_area, inn.smoker_area)))
249     keys.append(u'Rodelverleih')
250     values.append(wrpylib.wrvalidators.BoolUnicodeTupleValidator().from_python((inn.sled_rental, inn.sled_rental_comment)))
251     keys.append(u'Handyempfang')
252     values.append(wrpylib.wrvalidators.ValueCommentListNeinLoopNone().from_python(inn.mobile_provider))
253     keys.append(u'Homepage')
254     values.append(wrpylib.wrvalidators.UrlNeinNone().from_python(inn.homepage))
255     keys.append(u'E-Mail')
256     values.append(wrpylib.wrvalidators.EmailCommentListNeinLoopNone(allow_masked_email=True).from_python(inn.email_list))
257     keys.append(u'Telefon')
258     values.append(wrpylib.wrvalidators.PhoneCommentListNeinLoopNone(comments_are_optional=True).from_python(inn.phone_list))
259     keys.append(u'Bild')
260     values.append(wrpylib.wrvalidators.UnicodeNone().from_python(inn.image))
261     keys.append(u'Rodelbahnen')
262     values.append(wrpylib.wrvalidators.WikiPageListLoopNone().from_python(inn.sledding_list))
263     result = [u'{{Gasthausbox']
264     return wrpylib.mwmarkup.create_template(u'Gasthausbox', [], keys, values, True)
265
266
267 def find_template_latlon_ele(wikitext, template_title):
268     """Finds the first occurance of the '{{template_title|47.076207 N 11.453553 E|1890}}' template
269     and returns the tuple (start, end, lat, lon, ele) or (None, None, None, None, None) if the
270     template was not found. If the template has no valid format, an exception is thrown."""
271     start, end = wrpylib.mwmarkup.find_template(wikitext, template_title)
272     if start is None: return (None,) * 5
273     title, params = wrpylib.mwmarkup.split_template(wikitext[start:end])
274     lat, lon = wrpylib.wrvalidators.GeoNone().to_python(params[u'1'].strip())
275     ele = wrpylib.wrvalidators.UnsignedNone().to_python(params[u'2'].strip())
276     return start, end, lat, lon, ele
277
278
279 def create_template_latlon_ele(template_title, lat, lon, ele):
280     geo = wrpylib.wrvalidators.GeoNone().from_python((lat, lon))
281     if len(geo) == 0: geo = u' '
282     ele = wrpylib.wrvalidators.UnsignedNone().from_python(ele)
283     if len(ele) == 0: ele = u' '
284     return wrpylib.mwmarkup.create_template(template_title, [geo, ele])
285
286
287 def find_template_PositionOben(wikitext):
288     """Same as find_template_latlon_ele with template '{{Position oben|47.076207 N 11.453553 E|1890}}'"""
289     return find_template_latlon_ele(wikitext, u'Position oben')
290
291
292 def create_template_PositionOben(lat, lon, ele):
293     return create_template_latlon_ele(u'Position, oben', lat, lon, ele)
294
295
296 def find_template_PositionUnten(wikitext):
297     """Same as find_template_latlon_ele with template '{{Position unten|47.076207 N 11.453553 E|1890}}'"""
298     return find_template_latlon_ele(wikitext, u'Position unten')
299
300
301 def find_template_unsigned(wikitext, template_title):
302     """Finds the first occurance of the '{{template_title|1890}}' template
303     and returns the tuple (start, end, unsigned_value) or (None, None, None) if the
304     template was not found. If the template has no valid format, an exception is thrown."""
305     start, end = wrpylib.mwmarkup.find_template(wikitext, template_title)
306     if start is None: return (None,) * 3
307     title, params = wrpylib.mwmarkup.split_template(wikitext[start:end])
308     unsigned_value = wrpylib.wrvalidators.UnsignedNone().to_python(params[u'1'].strip())
309     return start, end, unsigned_value
310
311
312 def create_template_unsigned(template_title, unsigned):
313     unsigned = wrpylib.wrvalidators.UnsignedNone().from_python(unsigned)
314     if len(unsigned) == 0: unsigned = u' '
315     return wrpylib.mwmarkup.create_template(template_title, [unsigned])
316
317
318 def find_template_Hoehenunterschied(wikitext):
319     """Same as find_template_unsigned with template '{{Höhenunterschied|350}}'"""
320     return find_template_unsigned(wikitext, u'Höhenunterschied')
321
322
323 def create_template_Hoehenunterschied(ele_diff):
324     return create_template_unsigned(u'Höhenunterschied', ele_diff)
325
326
327 def find_template_Bahnlaenge(wikitext):
328     """Same as find_template_unsigned with template '{{Bahnlänge|4500}}'"""
329     return find_template_unsigned(wikitext, u'Bahnlänge')
330
331
332 def create_template_Bahnlaenge(length):
333     return create_template_unsigned(u'Bahnlänge', length)
334
335
336 def find_template_Gehzeit(wikitext):
337     """Same as find_template_unsigned with template '{{Gehzeit|60}}'"""
338     return find_template_unsigned(wikitext, u'Gehzeit')
339
340
341 def create_template_Gehzeit(walkup_time):
342     return create_template_unsigned(u'Gehzeit', walkup_time)
343
344
345 def find_template_Forumlink(wikitext):
346     """Same as find_template_unsigned with template '{{Forumlink|26}}'"""
347     start, end = wrpylib.mwmarkup.find_template(wikitext, u'Forumlink')
348     if start is None: return (None,) * 3
349     title, params = wrpylib.mwmarkup.split_template(wikitext[start:end])
350     forumid = params[u'1'].strip()
351     if forumid == u'<nummer einfügen>': unsigned_value = None
352     else: unsigned_value = wrpylib.wrvalidators.UnsignedNone().to_python(forumid)
353     return start, end, unsigned_value
354     # return find_template_unsigned(wikitext, u'Forumlink')
355
356
357 def find_template_Parkplatz(wikitext):
358     """Same as find_template_latlon_ele with template '{{Parkplatz|47.076207 N 11.453553 E|1890}}'"""
359     return find_template_latlon_ele(wikitext, u'Parkplatz')
360
361
362 def find_template_Haltestelle(wikitext):
363     """Finds the first occurance of the '{{Haltestelle|Ortsname|Haltestellenname|47.076207 N 11.453553 E|1890}}' template
364     and returns the tuple (start, end, city, stop, lat, lon, ele) or (None, None, None, None, None, None, None) if the
365     template was not found. If the template has no valid format, an exception is thrown."""
366     start, end = wrpylib.mwmarkup.find_template(wikitext, u'Haltestelle')
367     if start is None: return (None,) * 7
368     title, params = wrpylib.mwmarkup.split_template(wikitext[start:end])
369     city = wrpylib.wrvalidators.UnicodeNone().to_python(params[u'1'].strip())
370     stop = wrpylib.wrvalidators.UnicodeNone().to_python(params[u'2'].strip())
371     lat, lon = wrpylib.wrvalidators.GeoNone().to_python(params[u'3'].strip())
372     ele = wrpylib.wrvalidators.UnsignedNone().to_python(params[u'4'].strip())
373     return start, end, city, stop, lat, lon, ele
374
375
376 def find_all_templates(wikitext, find_func):
377     """Returns a list of return values of find_func that searches for a template.
378     Example:
379     >>> find_all_tempaltes(wikitext, find_template_Haltestelle)
380     Returns an empty list if the template was not found at all.
381     """
382     results = []
383     result = find_func(wikitext)
384     start, end = result[:2]
385     while start is not None:
386         results.append(result)
387         result = find_func(wikitext[end:])
388         if result[0] is None:
389             start = None
390         else:
391             start = result[0] + end
392             end  += result[1]
393             result = (start, end) + result[2:]
394     return results
395
396
397 def googlemap_to_wrmap(attributes, coords, paths):
398     """Converts the output of parse_googlemap to the GeoJSON format wrmap uses.
399     :returns: (GeoJSON as nested Python datatypes)
400     """
401     json_features = []
402
403     # point
404     for point in coords:
405         lon, lat, symbol, title = point
406         properties = {'type': 'punkt' if symbol is None else symbol.lower()}
407         if title is not None: properties['name'] = title
408         json_features.append({
409             'type': 'Feature',
410             'geometry': {'type': 'Point', 'coordinates': [lon, lat]},
411             'properties': properties})
412         
413     # path
414     for path in paths:
415         style, entries = path
416         style = style.lower()
417         PATH_TYPES = {u'6#ff014e9a': u'rodelbahn', u'6#ffe98401': u'gehweg', u'6#ff7f7fff': u'alternative', u'3#ff000000': u'lift', u'3#ffe1e100': u'anfahrt'}
418         if PATH_TYPES.has_key(style):
419             properties = {'type': PATH_TYPES[style]}
420         else:
421             properties = {'type': 'line'}
422             properties['dicke'] = style[0]
423             properties['farbe'] = style[4:]
424         json_features.append({
425             'type': 'Feature',
426             'geometry': {
427                 'type': 'LineString',
428                 'coordinates': [[lon, lat] for lon, lat, symbol, title in entries]},
429             'properties': properties})
430
431     geojson = {
432             'type': 'FeatureCollection',
433             'features': json_features,
434             'properties': attributes}
435     return geojson
436
437
438 def parse_wrmap_coordinates(coords):
439     '''gets a string coordinates and returns an array of lon/lat coordinate pairs, e.g.
440     47.12 N 11.87 E
441     47.13 N 11.70 E
442     ->
443     [[11.87, 47.12], [11.70, 47.13]]'''
444     result = []
445     pos = 0
446     for match in re.finditer(r'\s*(\d+\.?\d*)\s*N?\s+(\d+\.?\d*)\s*E?\s*', coords):
447         if match.start() != pos:
448             break
449         result.append([float(match.groups()[1]), float(match.groups()[0])])
450         pos = match.end()
451     else:
452         if pos == len(coords):
453             return result
454     raise RuntimeError('Wrong coordinate format: {}'.format(coords))
455
456
457 def parse_wrmap(wikitext):
458     """Parses the (unicode) u'<wrmap ...>content</wrmap>' of the Winterrodeln wrmap extension.
459     If wikitext does not contain the <wrmap> tag or if the <wrmap> tag contains 
460     invalid formatted lines, a ParseError is raised.
461     Use wrpylib.mwmarkup.find_tag(wikitext, 'wrmap') to find the wrmap tag within an arbitrary
462     wikitext before using this function.
463
464     :param wikitext: wikitext containing only the template. Example:
465
466     wikitext = u'''
467     <wrmap lat="47.2417134" lon="11.21408895" zoom="14" width="700" height="400">
468     <gasthaus name="Rosskogelhütte" wiki="Rosskogelhütte">47.240689 11.190454</gasthaus>
469     <parkplatz>47.245789 11.238971</parkplatz>
470     <haltestelle name="Oberperfuss Rangger Köpfl Lift">47.245711 11.238283</haltestelle>
471     <rodelbahn>
472         47.238587 11.203360
473         47.244951 11.230868
474         47.245470 11.237853
475     </rodelbahn>
476     </wrmap>
477     '''
478     :returns: GeoJSON as nested Python datatype
479     """
480     # parse XML
481     try:
482         wrmap_xml = xml.etree.ElementTree.fromstring(wikitext.encode('utf-8'))
483     except xml.etree.ElementTree.ParseError as e:
484         row, column = e.position
485         raise ParseError("XML parse error on row {}, column {}: {}".format(row, column, e))
486     if wrmap_xml.tag not in ['wrmap', 'wrgmap']:
487         raise ParseError('No valid tag name')
488
489     # convert XML to geojson (http://www.geojson.org/geojson-spec.html)
490     json_features = []
491     for feature in wrmap_xml:
492         # determine feature type
493         is_point = feature.tag in WRMAP_POINT_TYPES
494         is_line = feature.tag in WRMAP_LINE_TYPES
495         if (not is_point and not is_line):
496             raise ParseError('Unknown element <{}>.'.format(feature.tag))
497
498         # point
499         if is_point:
500             properties = {'type': feature.tag}
501             allowed_properties = set(['name', 'wiki'])
502             wrong_properties = set(feature.attrib.keys()) - allowed_properties
503             if len(wrong_properties) > 0:
504                 raise ParseError("The attribute '{}' is not allowed at <{}>.".format(list(wrong_properties)[0], feature.tag))
505             properties.update(feature.attrib)
506             coordinates = parse_wrmap_coordinates(feature.text)
507             if len(coordinates) != 1:
508                 raise ParseError('The element <{}> has to have exactly one coordinate pair.'.format(feature.tag))
509             json_features.append({
510                 'type': 'Feature',
511                 'geometry': {'type': 'Point', 'coordinates': coordinates[0]},
512                 'properties': properties})
513
514         # line
515         if is_line:
516             properties = {'type': feature.tag}
517             allowed_properties = set(['farbe', 'dicke'])
518             wrong_properties = set(feature.attrib.keys()) - allowed_properties
519             if len(wrong_properties) > 0:
520                 raise ParseError("The attribute '{}' is not allowed at <{}>.".format(list(wrong_properties)[0], feature.tag))
521             if feature.attrib.has_key('farbe'): 
522                 if not re.match('#[0-9a-fA-F]{6}$', feature.attrib['farbe']):
523                     raise ParseError('The attribute "farbe" has to have a format like "#a0bb43".')
524                 properties['strokeColor'] = feature.attrib['farbe'] # e.g. #a200b7
525             if feature.attrib.has_key('dicke'):
526                 try:
527                     properties['strokeWidth'] = int(feature.attrib['dicke']) # e.g. 6
528                 except ValueError:
529                     raise ParseError('The attribute "dicke" has to be an integer.')
530             json_features.append({
531                 'type': 'Feature',
532                 'geometry': {'type': 'LineString', 'coordinates': parse_wrmap_coordinates(feature.text)},
533                 'properties': properties})
534
535     # attributes
536     properties = {}
537     for k, v in wrmap_xml.attrib.iteritems():
538         if k in ['lat', 'lon']:
539             try:
540                 properties[k] = float(v)
541             except ValueError:
542                 raise ParseError('Attribute "{}" has to be a float value.'.format(k))
543         elif k in ['zoom', 'width', 'height']:
544             try:
545                 properties[k] = int(v)
546             except ValueError:
547                 raise ParseError('Attribute "{}" has to be an integer value.'.format(k))
548         else:
549             raise ParseError('Unknown attribute "{}".'.format(k))
550
551     geojson = {
552         'type': 'FeatureCollection',
553         'features': json_features,
554         'properties': properties}
555
556     return geojson
557
558
559 def create_wrmap_coordinates(coords):
560     result = []
561     for coord in coords:
562         result.append('{:.6f} N {:.6f} E'.format(coord[1], coord[0]))
563     return '\n'.join(result)
564  
565
566 def create_wrmap(geojson):
567     """Creates a <wrmap> wikitext from geojson (as python types)."""
568     wrmap_xml = xml.etree.ElementTree.Element('wrmap')
569     wrmap_xml.text = '\n\n'
570     for k, v in geojson['properties'].iteritems():
571         if k in ['lon', 'lat']:
572             wrmap_xml.attrib[k] = '{:.6f}'.format(v)
573         else:
574             wrmap_xml.attrib[k] = str(v)
575
576     assert geojson['type'] == 'FeatureCollection'
577     json_features = geojson['features']
578     last_json_feature = None
579     for json_feature in json_features:
580         feature_xml = xml.etree.ElementTree.SubElement(wrmap_xml, json_feature['properties']['type'])
581         geo = json_feature['geometry']
582         if geo['type'] == 'Point':
583             feature_xml.text = create_wrmap_coordinates([geo['coordinates']])
584             if last_json_feature is not None:
585                 last_json_feature.tail = '\n'
586         else:
587             if last_json_feature is not None:
588                 last_json_feature.tail = '\n\n'
589             feature_xml.text = '\n' + create_wrmap_coordinates(geo['coordinates']) + '\n'
590         last_json_feature = feature_xml
591         feature_xml.attrib = json_feature['properties']
592         del feature_xml.attrib['type']
593
594     if last_json_feature is not None:
595         last_json_feature.tail = '\n\n'
596     return xml.etree.ElementTree.tostring(wrmap_xml, encoding='utf-8').decode('utf-8')
597