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