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