Fixed typo in docstring of opt_tristate_german_comment_from_str.
[philipp/winterrodeln/wrpylib.git] / wrpylib / wrmwmarkup.py
1 #!/usr/bin/python3.4
2 # $Id$
3 # $HeadURL$
4 """This module contains winterrodeln specific functions that are processing the MediaWiki markup.
5 """
6 import re
7 import xml.etree.ElementTree
8 import collections
9 import mwparserfromhell
10 import wrpylib.wrvalidators
11 import wrpylib.mwmarkup
12 import wrpylib.wrmwdb
13 from wrpylib.wrvalidators import LonLat, opt_lonlat_from_str, opt_lonlat_to_str, opt_uint_from_str, opt_uint_to_str, \
14     opt_str_opt_comment_enum_to_str, lift_german_to_str, webauskunft_to_str, cachet_german_to_str, \
15     opt_phone_comment_enum_to_str, lift_german_from_str
16
17
18 def sledrun_from_rodelbahnbox(value, sledrun):
19     """Takes a Rodelbahnbox as returned by rodelbahnbox_from_str (that is, an OrderedDict) and
20     updates the sledrun instance with all values present in the Rodelbahnbox. Other values are not
21     updated. Does not validate the arguments."""
22     # sledrun.page_id = None # this field is not updated because it is not present in the RodelbahnBox
23     # sledrun.page_title = None # this field is not updated because it is not present in the RodelbahnBox
24     # sledrun.name_url = None # this field is not updated because it is not present in the RodelbahnBox
25     sledrun.position_longitude, sledrun.position_latitude = value['Position']
26     sledrun.top_longitude, sledrun.top_latitude = value['Position oben']
27     sledrun.top_elevation = value['Höhe oben']
28     sledrun.bottom_longitude, sledrun.bottom_latitude = value['Position unten']
29     sledrun.bottom_elevation = value['Höhe unten']
30     sledrun.length = value['Länge']
31     sledrun.difficulty = value['Schwierigkeit']
32     sledrun.avalanches = value['Lawinen']
33     sledrun.operator = value['Betreiber']
34     sledrun.public_transport = value['Öffentliche Anreise']
35     sledrun.walkup_possible = value['Aufstieg möglich']
36     sledrun.walkup_time = value['Gehzeit']
37     sledrun.walkup_separate, sledrun.walkup_separate_comment = value['Aufstieg getrennt']
38     sledrun.lift = None if value['Aufstiegshilfe'] is None else len(value['Aufstiegshilfe']) > 0
39     sledrun.lift_details = lift_german_to_str(value['Aufstiegshilfe'])
40     sledrun.night_light, sledrun.night_light_comment = value['Beleuchtungsanlage']
41     sledrun.night_light_days, sledrun.night_light_days_comment = value['Beleuchtungstage']
42     sledrun.sled_rental = None if value['Rodelverleih'] is None else len(value['Rodelverleih']) > 0
43     sledrun.sled_rental_comment = opt_str_opt_comment_enum_to_str(value['Rodelverleih'])
44     sledrun.cachet = cachet_german_to_str(value['Gütesiegel'])
45     sledrun.information_web = webauskunft_to_str(value['Webauskunft'])
46     sledrun.information_phone = opt_phone_comment_enum_to_str(value['Telefonauskunft'])
47     sledrun.image = value['Bild']
48     sledrun.show_in_overview = value['In Übersichtskarte']
49     sledrun.forum_id = value['Forumid']
50     # sledrun.under_construction = None # this field is not updated because it is not present in the RodelbahnBox
51     return sledrun
52
53
54 def sledrun_to_rodelbahnbox(sledrun):
55     """Takes a sledrun instance that might come from the database and converts it to a OrderedDict ready
56     to be formatted as RodelbahnBox."""
57     value = collections.OrderedDict()
58     value['Position'] = LonLat(sledrun.position_longitude, sledrun.position_latitude)
59     value['Position oben'] = LonLat(sledrun.top_longitude, sledrun.top_latitude)
60     value['Höhe oben'] = sledrun.top_elevation
61     value['Position unten'] = LonLat(sledrun.bottom_longitude, sledrun.bottom_latitude)
62     value['Höhe unten'] = sledrun.bottom_elevation
63     value['Länge'] = sledrun.length
64     value['Schwierigkeit'] = sledrun.difficulty
65     value['Lawinen'] = sledrun.avalanches
66     value['Betreiber'] = sledrun.operator
67     value['Öffentliche Anreise'] = sledrun.public_transport
68     value['Aufstieg möglich'] = sledrun.walkup_possible
69     value['Gehzeit'] = sledrun.walkup_time
70     value['Aufstieg getrennt'] = sledrun.walkup_separate, sledrun.walkup_separate_comment
71     value['Aufstiegshilfe'] = lift_german_from_str(sledrun.lift_details)
72     value['Beleuchtungsanlage'] = sledrun.night_light, sledrun.night_light_comment
73     value['Beleuchtungstage'] = sledrun.night_light_days, sledrun.night_light_days_comment
74     value['Rodelverleih'] = sledrun.sled_rental, sledrun.sled_rental_comment
75     value['Gütesiegel'] = sledrun.cachet
76     value['Webauskunft'] = sledrun.information_web
77     value['Telefonauskunft'] = sledrun.information_phone
78     value['Bild'] = sledrun.image
79     value['In Übersichtskarte'] = sledrun.show_in_overview
80     value['Forumid'] = sledrun.forum_id
81     return value
82
83
84 def inn_from_gasthausbox(value, inn):
85     """Converts a dict with Gasthausbox properties to a Inn class. Does no validation.
86     value is a dict of properties as returned by gasthausbox_from_str."""
87     inn.position_longitude, inn.position_latitude = value['Position']
88     inn.position_elevation = value['Höhe']
89     inn.operator = value['Betreiber']
90     inn.seats = value['Sitzplätze']
91     inn.overnight, inn.overnight_comment = value['Übernachtung']
92     inn.nonsmoker_area, inn.smoker_area = value['Rauchfrei']
93     inn.sled_rental, inn.sled_rental_comment = value['Rodelverleih']
94     inn.mobile_provider = value['Handyempfang']
95     inn.homepage = value['Homepage']
96     inn.email_list = value['E-Mail']
97     inn.phone_list = value['Telefon']
98     inn.image = value['Bild']
99     inn.sledding_list = value['Rodelbahnen']
100     return inn
101
102
103 def inn_to_gasthausbox(inn):
104     """Converts an inn class to a dict of Gasthausbox properties. value is an Inn instance."""
105     value = collections.OrderedDict()
106     value['Position'] = LonLat(inn.position_longitude, inn.position_latitude)
107     value['Höhe'] = inn.position_elevation
108     value['Betreiber'] = inn.operator
109     value['Sitzplätze'] = inn.seats
110     value['Übernachtung'] = (inn.overnight, inn.overnight_comment)
111     value['Rauchfrei'] = (inn.nonsmoker_area, inn.smoker_area)
112     value['Rodelverleih'] = (inn.sled_rental, inn.sled_rental_comment)
113     value['Handyempfang'] = inn.mobile_provider
114     value['Homepage'] = inn.homepage
115     value['E-Mail'] = inn.email_list
116     value['Telefon'] = inn.phone_list
117     value['Bild'] = inn.image
118     value['Rodelbahnen'] = inn.sledding_list
119     return value
120
121
122 def lonlat_ele_from_template(template):
123     """Template is a mwparserfromhell.nodes.template.Template instance. Returns (lonlat, ele)."""
124     lonlat = opt_lonlat_from_str(template.params[0].strip())
125     ele = opt_uint_from_str(template.params[1].strip())
126     return lonlat, ele
127
128
129 def latlon_ele_to_template(lonlat_ele, name):
130     lonlat, ele = lonlat_ele
131     template = mwparserfromhell.nodes.template.Template(name)
132     template.add(1, opt_lonlat_to_str(lonlat))
133     template.add(2, opt_uint_to_str(ele))
134     wrpylib.mwmarkup.format_template_oneline(template)
135     return template
136
137
138 class ParseError(RuntimeError):
139     """Exception used by some of the functions"""
140     pass
141
142
143
144 def find_template_PositionOben(wikitext):
145     """Same as find_template_latlon_ele with template '{{Position oben|47.076207 N 11.453553 E|1890}}'"""
146     return find_template_latlon_ele(wikitext, 'Position oben')
147
148
149 def create_template_PositionOben(lat, lon, ele):
150     return create_template_latlon_ele('Position, oben', lat, lon, ele)
151
152
153 def find_template_PositionUnten(wikitext):
154     """Same as find_template_latlon_ele with template '{{Position unten|47.076207 N 11.453553 E|1890}}'"""
155     return find_template_latlon_ele(wikitext, 'Position unten')
156
157
158 def find_template_unsigned(wikitext, template_title):
159     """Finds the first occurance of the '{{template_title|1890}}' template
160     and returns the tuple (start, end, unsigned_value) or (None, None, None) if the
161     template was not found. If the template has no valid format, an exception is thrown."""
162     start, end = wrpylib.mwmarkup.find_template(wikitext, template_title)
163     if start is None: return (None,) * 3
164     title, params = wrpylib.mwmarkup.split_template(wikitext[start:end])
165     unsigned_value = wrpylib.wrvalidators.UnsignedNone().to_python(params['1'].strip())
166     return start, end, unsigned_value
167
168
169 def create_template_unsigned(template_title, unsigned):
170     unsigned = wrpylib.wrvalidators.UnsignedNone().from_python(unsigned)
171     if len(unsigned) == 0: unsigned = ' '
172     return wrpylib.mwmarkup.create_template(template_title, [unsigned])
173
174
175 def find_template_Hoehenunterschied(wikitext):
176     """Same as find_template_unsigned with template '{{Höhenunterschied|350}}'"""
177     return find_template_unsigned(wikitext, 'Höhenunterschied')
178
179
180 def create_template_Hoehenunterschied(ele_diff):
181     return create_template_unsigned('Höhenunterschied', ele_diff)
182
183
184 def find_template_Bahnlaenge(wikitext):
185     """Same as find_template_unsigned with template '{{Bahnlänge|4500}}'"""
186     return find_template_unsigned(wikitext, 'Bahnlänge')
187
188
189 def create_template_Bahnlaenge(length):
190     return create_template_unsigned('Bahnlänge', length)
191
192
193 def find_template_Gehzeit(wikitext):
194     """Same as find_template_unsigned with template '{{Gehzeit|60}}'"""
195     return find_template_unsigned(wikitext, 'Gehzeit')
196
197
198 def create_template_Gehzeit(walkup_time):
199     return create_template_unsigned('Gehzeit', walkup_time)
200
201
202 def find_template_Forumlink(wikitext):
203     """Same as find_template_unsigned with template '{{Forumlink|26}}'"""
204     start, end = wrpylib.mwmarkup.find_template(wikitext, 'Forumlink')
205     if start is None: return (None,) * 3
206     title, params = wrpylib.mwmarkup.split_template(wikitext[start:end])
207     forumid = params['1'].strip()
208     if forumid == '<nummer einfügen>': unsigned_value = None
209     else: unsigned_value = wrpylib.wrvalidators.UnsignedNone().to_python(forumid)
210     return start, end, unsigned_value
211     # return find_template_unsigned(wikitext, u'Forumlink')
212
213
214 def find_template_Parkplatz(wikitext):
215     """Same as find_template_latlon_ele with template '{{Parkplatz|47.076207 N 11.453553 E|1890}}'"""
216     return find_template_latlon_ele(wikitext, 'Parkplatz')
217
218
219 def find_template_Haltestelle(wikitext):
220     """Finds the first occurance of the '{{Haltestelle|Ortsname|Haltestellenname|47.076207 N 11.453553 E|1890}}' template
221     and returns the tuple (start, end, city, stop, lat, lon, ele) or (None, None, None, None, None, None, None) if the
222     template was not found. If the template has no valid format, an exception is thrown."""
223     start, end = wrpylib.mwmarkup.find_template(wikitext, 'Haltestelle')
224     if start is None: return (None,) * 7
225     title, params = wrpylib.mwmarkup.split_template(wikitext[start:end])
226     city = wrpylib.wrvalidators.UnicodeNone().to_python(params['1'].strip())
227     stop = wrpylib.wrvalidators.UnicodeNone().to_python(params['2'].strip())
228     lat, lon = wrpylib.wrvalidators.GeoNone().to_python(params['3'].strip())
229     ele = wrpylib.wrvalidators.UnsignedNone().to_python(params['4'].strip())
230     return start, end, city, stop, lat, lon, ele
231
232
233 def parse_wrmap_coordinates(coords):
234     """gets a string coordinates and returns an array of lon/lat coordinate pairs, e.g.
235     47.12 N 11.87 E
236     47.13 N 11.70 E
237     ->
238     [[11.87, 47.12], [11.70, 47.13]]"""
239     result = []
240     pos = 0
241     for match in re.finditer(r'\s*(\d+\.?\d*)\s*N?\s+(\d+\.?\d*)\s*E?\s*', coords):
242         if match.start() != pos:
243             break
244         result.append([float(match.groups()[1]), float(match.groups()[0])])
245         pos = match.end()
246     else:
247         if pos == len(coords):
248             return result
249     raise RuntimeError('Wrong coordinate format: {}'.format(coords))
250
251
252 WRMAP_POINT_TYPES = ['gasthaus', 'haltestelle', 'parkplatz', 'achtung', 'foto', 'verleih', 'punkt']
253 WRMAP_LINE_TYPES = ['rodelbahn', 'gehweg', 'alternative', 'lift', 'anfahrt', 'linie']
254
255
256 def parse_wrmap(wikitext):
257     """Parses the (unicode) u'<wrmap ...>content</wrmap>' of the Winterrodeln wrmap extension.
258     If wikitext does not contain the <wrmap> tag or if the <wrmap> tag contains 
259     invalid formatted lines, a ParseError is raised.
260     Use wrpylib.mwmarkup.find_tag(wikitext, 'wrmap') to find the wrmap tag within an arbitrary
261     wikitext before using this function.
262
263     :param wikitext: wikitext containing only the template. Example:
264
265     wikitext = u'''
266     <wrmap lat="47.2417134" lon="11.21408895" zoom="14" width="700" height="400">
267     <gasthaus name="Rosskogelhütte" wiki="Rosskogelhütte">47.240689 11.190454</gasthaus>
268     <parkplatz>47.245789 11.238971</parkplatz>
269     <haltestelle name="Oberperfuss Rangger Köpfl Lift">47.245711 11.238283</haltestelle>
270     <rodelbahn>
271         47.238587 11.203360
272         47.244951 11.230868
273         47.245470 11.237853
274     </rodelbahn>
275     </wrmap>
276     '''
277     :returns: GeoJSON as nested Python datatype
278     """
279     # parse XML
280     try:
281         wrmap_xml = xml.etree.ElementTree.fromstring(wikitext.encode('utf-8'))
282     except xml.etree.ElementTree.ParseError as e:
283         row, column = e.position
284         raise ParseError("XML parse error on row {}, column {}: {}".format(row, column, e))
285     if wrmap_xml.tag not in ['wrmap', 'wrgmap']:
286         raise ParseError('No valid tag name')
287
288     # convert XML to geojson (http://www.geojson.org/geojson-spec.html)
289     json_features = []
290     for feature in wrmap_xml:
291         # determine feature type
292         is_point = feature.tag in WRMAP_POINT_TYPES
293         is_line = feature.tag in WRMAP_LINE_TYPES
294         if (not is_point and not is_line):
295             raise ParseError('Unknown element <{}>.'.format(feature.tag))
296
297         # point
298         if is_point:
299             properties = {'type': feature.tag}
300             allowed_properties = {'name', 'wiki'}
301             wrong_properties = set(feature.attrib.keys()) - allowed_properties
302             if len(wrong_properties) > 0:
303                 raise ParseError("The attribute '{}' is not allowed at <{}>.".format(list(wrong_properties)[0], feature.tag))
304             properties.update(feature.attrib)
305             coordinates = parse_wrmap_coordinates(feature.text)
306             if len(coordinates) != 1:
307                 raise ParseError('The element <{}> has to have exactly one coordinate pair.'.format(feature.tag))
308             json_features.append({
309                 'type': 'Feature',
310                 'geometry': {'type': 'Point', 'coordinates': coordinates[0]},
311                 'properties': properties})
312
313         # line
314         if is_line:
315             properties = {'type': feature.tag}
316             allowed_properties = {'farbe', 'dicke'}
317             wrong_properties = set(feature.attrib.keys()) - allowed_properties
318             if len(wrong_properties) > 0:
319                 raise ParseError("The attribute '{}' is not allowed at <{}>.".format(list(wrong_properties)[0], feature.tag))
320             if 'farbe' in feature.attrib: 
321                 if not re.match('#[0-9a-fA-F]{6}$', feature.attrib['farbe']):
322                     raise ParseError('The attribute "farbe" has to have a format like "#a0bb43".')
323                 properties['strokeColor'] = feature.attrib['farbe'] # e.g. #a200b7
324             if 'dicke' in feature.attrib:
325                 try:
326                     properties['strokeWidth'] = int(feature.attrib['dicke']) # e.g. 6
327                 except ValueError:
328                     raise ParseError('The attribute "dicke" has to be an integer.')
329             json_features.append({
330                 'type': 'Feature',
331                 'geometry': {'type': 'LineString', 'coordinates': parse_wrmap_coordinates(feature.text)},
332                 'properties': properties})
333
334     # attributes
335     properties = {}
336     for k, v in wrmap_xml.attrib.items():
337         if k in ['lat', 'lon']:
338             try:
339                 properties[k] = float(v)
340             except ValueError:
341                 raise ParseError('Attribute "{}" has to be a float value.'.format(k))
342         elif k in ['zoom', 'width', 'height']:
343             try:
344                 properties[k] = int(v)
345             except ValueError:
346                 raise ParseError('Attribute "{}" has to be an integer value.'.format(k))
347         else:
348             raise ParseError('Unknown attribute "{}".'.format(k))
349
350     geojson = {
351         'type': 'FeatureCollection',
352         'features': json_features,
353         'properties': properties}
354
355     return geojson
356
357
358 def create_wrmap_coordinates(coords):
359     result = []
360     for coord in coords:
361         result.append('{:.6f} N {:.6f} E'.format(coord[1], coord[0]))
362     return '\n'.join(result)
363  
364
365 def create_wrmap(geojson):
366     """Creates a <wrmap> wikitext from geojson (as python types)."""
367     wrmap_xml = xml.etree.ElementTree.Element('wrmap')
368     wrmap_xml.text = '\n\n'
369     for k, v in geojson['properties'].items():
370         if k in ['lon', 'lat']:
371             wrmap_xml.attrib[k] = '{:.6f}'.format(v)
372         else:
373             wrmap_xml.attrib[k] = str(v)
374
375     assert geojson['type'] == 'FeatureCollection'
376     json_features = geojson['features']
377     last_json_feature = None
378     for json_feature in json_features:
379         feature_xml = xml.etree.ElementTree.SubElement(wrmap_xml, json_feature['properties']['type'])
380         geo = json_feature['geometry']
381         if geo['type'] == 'Point':
382             feature_xml.text = create_wrmap_coordinates([geo['coordinates']])
383             if last_json_feature is not None:
384                 last_json_feature.tail = '\n'
385         else:
386             if last_json_feature is not None:
387                 last_json_feature.tail = '\n\n'
388             feature_xml.text = '\n' + create_wrmap_coordinates(geo['coordinates']) + '\n'
389         last_json_feature = feature_xml
390         feature_xml.attrib = json_feature['properties']
391         del feature_xml.attrib['type']
392
393     if last_json_feature is not None:
394         last_json_feature.tail = '\n\n'
395     return xml.etree.ElementTree.tostring(wrmap_xml, encoding='utf-8').decode('utf-8')