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