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