Implemented TestInn.test_inn_to_gasthausbox.
[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, GASTHAUSBOX_DICT
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     # page_id = None # this field is not updated because it is not present in the Gasthausbox
88     # page_title = None # this field is not updated because it is not present in the Gasthausbox
89     def convtodb(value, key):
90         v = value[key]
91         if v is not None:
92             v = GASTHAUSBOX_DICT[key].to_str(v)
93         return v
94     inn.position_longitude, inn.position_latitude = value['Position']
95     inn.position_elevation = value['Höhe']
96     inn.operator = value['Betreiber']
97     inn.seats = value['Sitzplätze']
98     inn.overnight, inn.overnight_comment = value['Übernachtung']
99     inn.smoker_area = value['Rauchfrei'] < 0.9
100     inn.nonsmoker_area = value['Rauchfrei'] > 0.1
101     inn.sled_rental, inn.sled_rental_comment = value['Rodelverleih']
102     inn.mobile_provider = convtodb(value, 'Handyempfang')
103     inn.homepage = convtodb(value, 'Homepage')
104     inn.email_list = convtodb(value, 'E-Mail')
105     inn.phone_list = convtodb(value, 'Telefon')
106     inn.image = value['Bild']
107     inn.sledding_list = convtodb(value, 'Rodelbahnen')
108     # under_construction = None # this field is not updated because it is not present in the GasthausBox
109     return inn
110
111
112 def inn_to_gasthausbox(inn):
113     """Converts an inn class to a dict of Gasthausbox properties. value is an Inn instance."""
114     value = collections.OrderedDict()
115     value['Position'] = LonLat(inn.position_longitude, inn.position_latitude)
116     value['Höhe'] = inn.position_elevation
117     value['Betreiber'] = inn.operator
118     value['Sitzplätze'] = inn.seats
119     value['Übernachtung'] = (inn.overnight, inn.overnight_comment)
120     value['Rauchfrei'] = {(False, True): 0.0, (True, True): 0.5, (True, False): 1.0}.get((inn.nonsmoker_area, inn.smoker_area), None)
121     value['Rodelverleih'] = (inn.sled_rental, inn.sled_rental_comment)
122     value['Handyempfang'] = inn.mobile_provider
123     value['Homepage'] = inn.homepage
124     value['E-Mail'] = inn.email_list
125     value['Telefon'] = inn.phone_list
126     value['Bild'] = inn.image
127     value['Rodelbahnen'] = inn.sledding_list
128     return value
129
130
131 def lonlat_ele_from_template(template):
132     """Template is a mwparserfromhell.nodes.template.Template instance. Returns (lonlat, ele)."""
133     lonlat = opt_lonlat_from_str(template.params[0].strip())
134     ele = opt_uint_from_str(template.params[1].strip())
135     return lonlat, ele
136
137
138 def latlon_ele_to_template(lonlat_ele, name):
139     lonlat, ele = lonlat_ele
140     template = mwparserfromhell.nodes.template.Template(name)
141     template.add(1, opt_lonlat_to_str(lonlat))
142     template.add(2, opt_uint_to_str(ele))
143     wrpylib.mwmarkup.format_template_oneline(template)
144     return template
145
146
147 class ParseError(RuntimeError):
148     """Exception used by some of the functions"""
149     pass
150
151
152
153 def find_template_PositionOben(wikitext):
154     """Same as find_template_latlon_ele with template '{{Position oben|47.076207 N 11.453553 E|1890}}'"""
155     return find_template_latlon_ele(wikitext, 'Position oben')
156
157
158 def create_template_PositionOben(lat, lon, ele):
159     return create_template_latlon_ele('Position, oben', lat, lon, ele)
160
161
162 def find_template_PositionUnten(wikitext):
163     """Same as find_template_latlon_ele with template '{{Position unten|47.076207 N 11.453553 E|1890}}'"""
164     return find_template_latlon_ele(wikitext, 'Position unten')
165
166
167 def find_template_unsigned(wikitext, template_title):
168     """Finds the first occurance of the '{{template_title|1890}}' template
169     and returns the tuple (start, end, unsigned_value) or (None, None, None) if the
170     template was not found. If the template has no valid format, an exception is thrown."""
171     start, end = wrpylib.mwmarkup.find_template(wikitext, template_title)
172     if start is None: return (None,) * 3
173     title, params = wrpylib.mwmarkup.split_template(wikitext[start:end])
174     unsigned_value = wrpylib.wrvalidators.UnsignedNone().to_python(params['1'].strip())
175     return start, end, unsigned_value
176
177
178 def create_template_unsigned(template_title, unsigned):
179     unsigned = wrpylib.wrvalidators.UnsignedNone().from_python(unsigned)
180     if len(unsigned) == 0: unsigned = ' '
181     return wrpylib.mwmarkup.create_template(template_title, [unsigned])
182
183
184 def find_template_Hoehenunterschied(wikitext):
185     """Same as find_template_unsigned with template '{{Höhenunterschied|350}}'"""
186     return find_template_unsigned(wikitext, 'Höhenunterschied')
187
188
189 def create_template_Hoehenunterschied(ele_diff):
190     return create_template_unsigned('Höhenunterschied', ele_diff)
191
192
193 def find_template_Bahnlaenge(wikitext):
194     """Same as find_template_unsigned with template '{{Bahnlänge|4500}}'"""
195     return find_template_unsigned(wikitext, 'Bahnlänge')
196
197
198 def create_template_Bahnlaenge(length):
199     return create_template_unsigned('Bahnlänge', length)
200
201
202 def find_template_Gehzeit(wikitext):
203     """Same as find_template_unsigned with template '{{Gehzeit|60}}'"""
204     return find_template_unsigned(wikitext, 'Gehzeit')
205
206
207 def create_template_Gehzeit(walkup_time):
208     return create_template_unsigned('Gehzeit', walkup_time)
209
210
211 def find_template_Forumlink(wikitext):
212     """Same as find_template_unsigned with template '{{Forumlink|26}}'"""
213     start, end = wrpylib.mwmarkup.find_template(wikitext, 'Forumlink')
214     if start is None: return (None,) * 3
215     title, params = wrpylib.mwmarkup.split_template(wikitext[start:end])
216     forumid = params['1'].strip()
217     if forumid == '<nummer einfügen>': unsigned_value = None
218     else: unsigned_value = wrpylib.wrvalidators.UnsignedNone().to_python(forumid)
219     return start, end, unsigned_value
220     # return find_template_unsigned(wikitext, u'Forumlink')
221
222
223 def find_template_Parkplatz(wikitext):
224     """Same as find_template_latlon_ele with template '{{Parkplatz|47.076207 N 11.453553 E|1890}}'"""
225     return find_template_latlon_ele(wikitext, 'Parkplatz')
226
227
228 def find_template_Haltestelle(wikitext):
229     """Finds the first occurance of the '{{Haltestelle|Ortsname|Haltestellenname|47.076207 N 11.453553 E|1890}}' template
230     and returns the tuple (start, end, city, stop, lat, lon, ele) or (None, None, None, None, None, None, None) if the
231     template was not found. If the template has no valid format, an exception is thrown."""
232     start, end = wrpylib.mwmarkup.find_template(wikitext, 'Haltestelle')
233     if start is None: return (None,) * 7
234     title, params = wrpylib.mwmarkup.split_template(wikitext[start:end])
235     city = wrpylib.wrvalidators.UnicodeNone().to_python(params['1'].strip())
236     stop = wrpylib.wrvalidators.UnicodeNone().to_python(params['2'].strip())
237     lat, lon = wrpylib.wrvalidators.GeoNone().to_python(params['3'].strip())
238     ele = wrpylib.wrvalidators.UnsignedNone().to_python(params['4'].strip())
239     return start, end, city, stop, lat, lon, ele
240
241
242 def parse_wrmap_coordinates(coords):
243     """gets a string coordinates and returns an array of lon/lat coordinate pairs, e.g.
244     47.12 N 11.87 E
245     47.13 N 11.70 E
246     ->
247     [[11.87, 47.12], [11.70, 47.13]]"""
248     result = []
249     pos = 0
250     for match in re.finditer(r'\s*(\d+\.?\d*)\s*N?\s+(\d+\.?\d*)\s*E?\s*', coords):
251         if match.start() != pos:
252             break
253         result.append([float(match.groups()[1]), float(match.groups()[0])])
254         pos = match.end()
255     else:
256         if pos == len(coords):
257             return result
258     raise RuntimeError('Wrong coordinate format: {}'.format(coords))
259
260
261 WRMAP_POINT_TYPES = ['gasthaus', 'haltestelle', 'parkplatz', 'achtung', 'foto', 'verleih', 'punkt']
262 WRMAP_LINE_TYPES = ['rodelbahn', 'gehweg', 'alternative', 'lift', 'anfahrt', 'linie']
263
264
265 def parse_wrmap(wikitext):
266     """Parses the (unicode) u'<wrmap ...>content</wrmap>' of the Winterrodeln wrmap extension.
267     If wikitext does not contain the <wrmap> tag or if the <wrmap> tag contains 
268     invalid formatted lines, a ParseError is raised.
269     Use wrpylib.mwmarkup.find_tag(wikitext, 'wrmap') to find the wrmap tag within an arbitrary
270     wikitext before using this function.
271
272     :param wikitext: wikitext containing only the template. Example:
273
274     wikitext = u'''
275     <wrmap lat="47.2417134" lon="11.21408895" zoom="14" width="700" height="400">
276     <gasthaus name="Rosskogelhütte" wiki="Rosskogelhütte">47.240689 11.190454</gasthaus>
277     <parkplatz>47.245789 11.238971</parkplatz>
278     <haltestelle name="Oberperfuss Rangger Köpfl Lift">47.245711 11.238283</haltestelle>
279     <rodelbahn>
280         47.238587 11.203360
281         47.244951 11.230868
282         47.245470 11.237853
283     </rodelbahn>
284     </wrmap>
285     '''
286     :returns: GeoJSON as nested Python datatype
287     """
288     # parse XML
289     try:
290         wrmap_xml = xml.etree.ElementTree.fromstring(wikitext.encode('utf-8'))
291     except xml.etree.ElementTree.ParseError as e:
292         row, column = e.position
293         raise ParseError("XML parse error on row {}, column {}: {}".format(row, column, e))
294     if wrmap_xml.tag not in ['wrmap', 'wrgmap']:
295         raise ParseError('No valid tag name')
296
297     # convert XML to geojson (http://www.geojson.org/geojson-spec.html)
298     json_features = []
299     for feature in wrmap_xml:
300         # determine feature type
301         is_point = feature.tag in WRMAP_POINT_TYPES
302         is_line = feature.tag in WRMAP_LINE_TYPES
303         if (not is_point and not is_line):
304             raise ParseError('Unknown element <{}>.'.format(feature.tag))
305
306         # point
307         if is_point:
308             properties = {'type': feature.tag}
309             allowed_properties = {'name', 'wiki'}
310             wrong_properties = set(feature.attrib.keys()) - allowed_properties
311             if len(wrong_properties) > 0:
312                 raise ParseError("The attribute '{}' is not allowed at <{}>.".format(list(wrong_properties)[0], feature.tag))
313             properties.update(feature.attrib)
314             coordinates = parse_wrmap_coordinates(feature.text)
315             if len(coordinates) != 1:
316                 raise ParseError('The element <{}> has to have exactly one coordinate pair.'.format(feature.tag))
317             json_features.append({
318                 'type': 'Feature',
319                 'geometry': {'type': 'Point', 'coordinates': coordinates[0]},
320                 'properties': properties})
321
322         # line
323         if is_line:
324             properties = {'type': feature.tag}
325             allowed_properties = {'farbe', 'dicke'}
326             wrong_properties = set(feature.attrib.keys()) - allowed_properties
327             if len(wrong_properties) > 0:
328                 raise ParseError("The attribute '{}' is not allowed at <{}>.".format(list(wrong_properties)[0], feature.tag))
329             if 'farbe' in feature.attrib: 
330                 if not re.match('#[0-9a-fA-F]{6}$', feature.attrib['farbe']):
331                     raise ParseError('The attribute "farbe" has to have a format like "#a0bb43".')
332                 properties['strokeColor'] = feature.attrib['farbe'] # e.g. #a200b7
333             if 'dicke' in feature.attrib:
334                 try:
335                     properties['strokeWidth'] = int(feature.attrib['dicke']) # e.g. 6
336                 except ValueError:
337                     raise ParseError('The attribute "dicke" has to be an integer.')
338             json_features.append({
339                 'type': 'Feature',
340                 'geometry': {'type': 'LineString', 'coordinates': parse_wrmap_coordinates(feature.text)},
341                 'properties': properties})
342
343     # attributes
344     properties = {}
345     for k, v in wrmap_xml.attrib.items():
346         if k in ['lat', 'lon']:
347             try:
348                 properties[k] = float(v)
349             except ValueError:
350                 raise ParseError('Attribute "{}" has to be a float value.'.format(k))
351         elif k in ['zoom', 'width', 'height']:
352             try:
353                 properties[k] = int(v)
354             except ValueError:
355                 raise ParseError('Attribute "{}" has to be an integer value.'.format(k))
356         else:
357             raise ParseError('Unknown attribute "{}".'.format(k))
358
359     geojson = {
360         'type': 'FeatureCollection',
361         'features': json_features,
362         'properties': properties}
363
364     return geojson
365
366
367 def create_wrmap_coordinates(coords):
368     result = []
369     for coord in coords:
370         result.append('{:.6f} N {:.6f} E'.format(coord[1], coord[0]))
371     return '\n'.join(result)
372  
373
374 def create_wrmap(geojson):
375     """Creates a <wrmap> wikitext from geojson (as python types)."""
376     wrmap_xml = xml.etree.ElementTree.Element('wrmap')
377     wrmap_xml.text = '\n\n'
378     for k, v in geojson['properties'].items():
379         if k in ['lon', 'lat']:
380             wrmap_xml.attrib[k] = '{:.6f}'.format(v)
381         else:
382             wrmap_xml.attrib[k] = str(v)
383
384     assert geojson['type'] == 'FeatureCollection'
385     json_features = geojson['features']
386     last_json_feature = None
387     for json_feature in json_features:
388         feature_xml = xml.etree.ElementTree.SubElement(wrmap_xml, json_feature['properties']['type'])
389         geo = json_feature['geometry']
390         if geo['type'] == 'Point':
391             feature_xml.text = create_wrmap_coordinates([geo['coordinates']])
392             if last_json_feature is not None:
393                 last_json_feature.tail = '\n'
394         else:
395             if last_json_feature is not None:
396                 last_json_feature.tail = '\n\n'
397             feature_xml.text = '\n' + create_wrmap_coordinates(geo['coordinates']) + '\n'
398         last_json_feature = feature_xml
399         feature_xml.attrib = json_feature['properties']
400         del feature_xml.attrib['type']
401
402     if last_json_feature is not None:
403         last_json_feature.tail = '\n\n'
404     return xml.etree.ElementTree.tostring(wrmap_xml, encoding='utf-8').decode('utf-8')