a27525b292ad21ee3b4e03c53520beff36d34a04
[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 RuntimeError 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: (attributes, GeoJSON as 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}
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 = {'type': 'FeatureCollection', 'features': json_features}
375     return {'lon': center[0], 'lat': center[1], 'zoom': zoom}, geojson
376
377
378 def parse_wrmap_coordinates(coords):
379     '''gets a string coordinates and returns an array of lon/lat coordinate pairs, e.g.
380     47.12 N 11.87 E
381     47.13 N 11.70 E
382     ->
383     [[11.87, 47.12], [11.70, 47.13]]'''
384     result = []
385     pos = 0
386     for match in re.finditer(r'\s*(\d+\.?\d*)\s*N?\s+(\d+\.?\d*)\s*E?\s*', coords):
387         if match.start() != pos:
388             break
389         result.append([float(match.groups()[1]), float(match.groups()[0])])
390         pos = match.end()
391     else:
392         if pos == len(coords):
393             return result
394     raise RuntimeError('Wrong coordinate format: {}'.format(coords))
395
396
397 def parse_wrmap(wikitext):
398     """Parses the (unicode) u'<wrmap ...>content</wrmap>' of the Winterrodeln wrmap extension
399     out of a page. If wikitext does not contain the wrmap extension text None is returned.
400     If the wrmap contains invalid formatted lines, a ParseError is raised.
401
402     :param wikitext: wikitext containing the template. Example:
403
404     wikitext = u'''
405     <wrmap lat="47.2417134" lon="11.21408895" zoom="14" width="700" height="400">
406     <gasthaus name="Rosskogelhütte" wiki="Rosskogelhütte">47.240689 11.190454</gasthaus>
407     <parkplatz>47.245789 11.238971</parkplatz>
408     <haltestelle name="Oberperfuss Rangger Köpfl Lift">47.245711 11.238283</haltestelle>
409     <rodelbahn>
410         47.238587 11.203360
411         47.244951 11.230868
412         47.245470 11.237853
413     </rodelbahn>
414     </wrmap>
415     '''
416     :returns: GeoJSON as nested Python datatype
417     """
418     # parse XML
419     try:
420         wrmap_xml = xml.etree.ElementTree.fromstring(wikitext.encode('utf-8'))
421     except xml.etree.ElementTree.ParseError as e:
422         row, column = e.position
423         raise ParseError("XML parse error on row {}, column {}: {}".format(row, column, e))
424     if wrmap_xml.tag not in ['wrmap', 'wrgmap']:
425         raise ParseError('No valid tag name')
426
427     # convert XML to geojson (http://www.geojson.org/geojson-spec.html)
428     json_features = []
429     for feature in wrmap_xml:
430         # determine feature type
431         is_point = feature.tag in WRMAP_POINT_TYPE
432         is_line = feature.tag in WRMAP_LINE_TYPE
433         if (not is_point and not is_line):
434             raise ParseError('Unknown element <{}>.'.format(feature.tag))
435
436         # point
437         if is_point:
438             properties = {'type': feature.tag}
439             allowed_properties = set(['name', 'wiki'])
440             wrong_properties = set(feature.attrib.keys()) - allowed_properties
441             if len(wrong_properties) > 0:
442                 raise ParseError("The attribute '{}' is not allowed at <{}>.".format(list(wrong_properties)[0], feature.tag))
443             properties.update(feature.attrib)
444             coordinates = parse_wrmap_coordinates(feature.text)
445             if len(coordinates) != 1:
446                 raise ParseError('The element <{}> has to have exactly one coordinate pair.'.format(feature.tag))
447             json_features.append({
448                 'type': 'Feature',
449                 'geometry': {'type': 'Point', 'coordinates': coordinates[0]},
450                 'properties': properties})
451
452         # line
453         if is_line:
454             properties = {'type': feature.tag}
455             allowed_properties = set(['farbe', 'dicke'])
456             wrong_properties = set(feature.attrib.keys()) - allowed_properties
457             if len(wrong_properties) > 0:
458                 raise ParseError("The attribute '{}' is not allowed at <{}>.".format(list(wrong_properties)[0], feature.tag))
459             if feature.attrib.has_key('farbe'): 
460                 if not re.match('#[0-9a-fA-F]{6}$', feature.attrib['farbe']):
461                     raise ParseError('The attribute "farbe" has to have a format like "#a0bb43".')
462                 properties['strokeColor'] = feature.attrib['farbe'] # e.g. #a200b7
463             if feature.attrib.has_key('dicke'):
464                 try:
465                     properties['strokeWidth'] = int(feature.attrib['dicke']) # e.g. 6
466                 except ValueError:
467                     raise ParseError('The attribute "dicke" has to be an integer.')
468             json_features.append({
469                 'type': 'Feature',
470                 'geometry': {'type': 'LineString', 'coordinates': parse_wrmap_coordinates(feature.text)},
471                 'properties': properties})
472
473     # attributes
474     properties = {}
475     for k, v in wrmap_xml.attrib.iteritems():
476         if k in ['lat', 'lon']:
477             try:
478                 properties[k] = float(v)
479             except ValueError:
480                 raise ParseError('Attribute "{}" has to be a float value.'.format(k))
481         elif k in ['zoom', 'width', 'height']:
482             try:
483                 properties[k] = int(v)
484             except ValueError:
485                 raise ParseError('Attribute "{}" has to be an integer value.'.format(k))
486         else:
487             raise ParseError('Unknown attribute "{}".'.format(k))
488
489     geojson = {
490         'type': 'FeatureCollection',
491         'features': json_features,
492         'properties': properties}
493
494     return geojson
495
496
497 def create_wrmap_coordinates(coords):
498     result = []
499     for coord in coords:
500         result.append('{:.6f} N {:.6f} E'.format(coord[1], coord[0]))
501     return '\n'.join(result)
502  
503
504 def create_wrmap(attributes, geojson):
505     """Creates a <wrmap> wikitext from geojson (as python types)."""
506     wrmap_xml = xml.etree.ElementTree.Element('wrmap')
507     for k, v in attributes.iteritems():
508         wrmap_xml.attrib[k] = str(v)
509
510     assert geojson['type'] == 'FeatureCollection'
511     json_features = geojson['features']
512     for json_feature in json_features:
513         geo = json_feature['geometry']
514         if geo['type'] == 'Point':
515             wrmap_type = dict(zip(WRMAP_POINT_TYPE.values(), WRMAP_POINT_TYPE.keys()))
516             feature_xml = xml.etree.ElementTree.SubElement(wrmap_xml, wrmap_type[json_feature['properties']['type']])
517             feature_xml.text = create_wrmap_coordinates([geo['coordinates']])
518             feature_xml.attrib = json_feature['properties']
519         if geo['type'] == 'LineString':
520             wrmap_type = dict(zip(WRMAP_LINE_TYPE.values(), WRMAP_LINE_TYPE.keys()))
521             feature_xml = xml.etree.ElementTree.SubElement(wrmap_xml, wrmap_type[json_feature['properties']['type']])
522             feature_xml.text = create_wrmap_coordinates(geo['coordinates'])
523             feature_xml.attrib = json_feature['properties']
524
525     return xml.etree.ElementTree.tostring(wrmap_xml)
526