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