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