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