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