#!/usr/bin/python3.4 # $Id$ # $HeadURL$ """This module contains winterrodeln specific functions that are processing the MediaWiki markup. """ import re import xml.etree.ElementTree import collections import mwparserfromhell import wrpylib.wrvalidators import wrpylib.mwmarkup import wrpylib.wrmwdb from wrpylib.wrvalidators import LonLat, opt_lonlat_from_str, opt_lonlat_to_str, opt_uint_from_str, opt_uint_to_str, \ opt_str_opt_comment_enum_to_str, lift_german_to_str, webauskunft_to_str, cachet_german_to_str, \ opt_phone_comment_enum_to_str, lift_german_from_str, GASTHAUSBOX_DICT def sledrun_from_rodelbahnbox(value, sledrun): """Takes a Rodelbahnbox as returned by rodelbahnbox_from_str (that is, an OrderedDict) and updates the sledrun instance with all values present in the Rodelbahnbox. Other values are not updated. Does not validate the arguments.""" # sledrun.page_id = None # this field is not updated because it is not present in the RodelbahnBox # sledrun.page_title = None # this field is not updated because it is not present in the RodelbahnBox # sledrun.name_url = None # this field is not updated because it is not present in the RodelbahnBox sledrun.position_longitude, sledrun.position_latitude = value['Position'] sledrun.top_longitude, sledrun.top_latitude = value['Position oben'] sledrun.top_elevation = value['Höhe oben'] sledrun.bottom_longitude, sledrun.bottom_latitude = value['Position unten'] sledrun.bottom_elevation = value['Höhe unten'] sledrun.length = value['Länge'] sledrun.difficulty = value['Schwierigkeit'] sledrun.avalanches = value['Lawinen'] sledrun.operator = value['Betreiber'] sledrun.public_transport = value['Öffentliche Anreise'] sledrun.walkup_possible = value['Aufstieg möglich'] sledrun.walkup_time = value['Gehzeit'] sledrun.walkup_separate, sledrun.walkup_separate_comment = value['Aufstieg getrennt'] sledrun.lift = None if value['Aufstiegshilfe'] is None else len(value['Aufstiegshilfe']) > 0 sledrun.lift_details = lift_german_to_str(value['Aufstiegshilfe']) sledrun.night_light, sledrun.night_light_comment = value['Beleuchtungsanlage'] sledrun.night_light_days, sledrun.night_light_days_comment = value['Beleuchtungstage'] sledrun.sled_rental = None if value['Rodelverleih'] is None else len(value['Rodelverleih']) > 0 sledrun.sled_rental_comment = opt_str_opt_comment_enum_to_str(value['Rodelverleih']) sledrun.cachet = cachet_german_to_str(value['Gütesiegel']) sledrun.information_web = webauskunft_to_str(value['Webauskunft']) sledrun.information_phone = opt_phone_comment_enum_to_str(value['Telefonauskunft']) sledrun.image = value['Bild'] sledrun.show_in_overview = value['In Übersichtskarte'] sledrun.forum_id = value['Forumid'] # sledrun.under_construction = None # this field is not updated because it is not present in the RodelbahnBox return sledrun def sledrun_to_rodelbahnbox(sledrun): """Takes a sledrun instance that might come from the database and converts it to a OrderedDict ready to be formatted as RodelbahnBox.""" value = collections.OrderedDict() value['Position'] = LonLat(sledrun.position_longitude, sledrun.position_latitude) value['Position oben'] = LonLat(sledrun.top_longitude, sledrun.top_latitude) value['Höhe oben'] = sledrun.top_elevation value['Position unten'] = LonLat(sledrun.bottom_longitude, sledrun.bottom_latitude) value['Höhe unten'] = sledrun.bottom_elevation value['Länge'] = sledrun.length value['Schwierigkeit'] = sledrun.difficulty value['Lawinen'] = sledrun.avalanches value['Betreiber'] = sledrun.operator value['Öffentliche Anreise'] = sledrun.public_transport value['Aufstieg möglich'] = sledrun.walkup_possible value['Gehzeit'] = sledrun.walkup_time value['Aufstieg getrennt'] = sledrun.walkup_separate, sledrun.walkup_separate_comment value['Aufstiegshilfe'] = lift_german_from_str(sledrun.lift_details) value['Beleuchtungsanlage'] = sledrun.night_light, sledrun.night_light_comment value['Beleuchtungstage'] = sledrun.night_light_days, sledrun.night_light_days_comment value['Rodelverleih'] = sledrun.sled_rental, sledrun.sled_rental_comment value['Gütesiegel'] = sledrun.cachet value['Webauskunft'] = sledrun.information_web value['Telefonauskunft'] = sledrun.information_phone value['Bild'] = sledrun.image value['In Übersichtskarte'] = sledrun.show_in_overview value['Forumid'] = sledrun.forum_id return value def inn_from_gasthausbox(value, inn): """Converts a dict with Gasthausbox properties to a Inn class. Does no validation. value is a dict of properties as returned by gasthausbox_from_str.""" # page_id = None # this field is not updated because it is not present in the Gasthausbox # page_title = None # this field is not updated because it is not present in the Gasthausbox def convtodb(value, key): v = value[key] if v is not None: v = GASTHAUSBOX_DICT[key].to_str(v) return v inn.position_longitude, inn.position_latitude = value['Position'] inn.position_elevation = value['Höhe'] inn.operator = value['Betreiber'] inn.seats = value['Sitzplätze'] inn.overnight, inn.overnight_comment = value['Übernachtung'] inn.smoker_area = value['Rauchfrei'] < 0.9 inn.nonsmoker_area = value['Rauchfrei'] > 0.1 inn.sled_rental, inn.sled_rental_comment = value['Rodelverleih'] inn.mobile_provider = convtodb(value, 'Handyempfang') inn.homepage = convtodb(value, 'Homepage') inn.email_list = convtodb(value, 'E-Mail') inn.phone_list = convtodb(value, 'Telefon') inn.image = value['Bild'] inn.sledding_list = convtodb(value, 'Rodelbahnen') # under_construction = None # this field is not updated because it is not present in the GasthausBox return inn def inn_to_gasthausbox(inn): """Converts an inn class to a dict of Gasthausbox properties. value is an Inn instance.""" def convfromdb(value, key): v = '' if value is None else value return GASTHAUSBOX_DICT[key].from_str(v) value = collections.OrderedDict() value['Position'] = LonLat(inn.position_longitude, inn.position_latitude) value['Höhe'] = inn.position_elevation value['Betreiber'] = inn.operator value['Sitzplätze'] = inn.seats value['Übernachtung'] = (inn.overnight, inn.overnight_comment) value['Rauchfrei'] = {(False, True): 0.0, (True, True): 0.5, (True, False): 1.0}.get((inn.nonsmoker_area, inn.smoker_area), None) value['Rodelverleih'] = (inn.sled_rental, inn.sled_rental_comment) value['Handyempfang'] = convfromdb(inn.mobile_provider, 'Handyempfang') value['Homepage'] = convfromdb(inn.homepage, 'Homepage') value['E-Mail'] = convfromdb(inn.email_list, 'E-Mail') value['Telefon'] = convfromdb(inn.phone_list, 'Telefon') value['Bild'] = inn.image value['Rodelbahnen'] = convfromdb(inn.sledding_list, 'Rodelbahnen') return value def lonlat_ele_from_template(template): """Template is a mwparserfromhell.nodes.template.Template instance. Returns (lonlat, ele).""" lonlat = opt_lonlat_from_str(template.params[0].strip()) ele = opt_uint_from_str(template.params[1].strip()) return lonlat, ele def latlon_ele_to_template(lonlat_ele, name): lonlat, ele = lonlat_ele template = mwparserfromhell.nodes.template.Template(name) template.add(1, opt_lonlat_to_str(lonlat)) template.add(2, opt_uint_to_str(ele)) wrpylib.mwmarkup.format_template_oneline(template) return template class ParseError(RuntimeError): """Exception used by some of the functions""" pass def find_template_PositionOben(wikitext): """Same as find_template_latlon_ele with template '{{Position oben|47.076207 N 11.453553 E|1890}}'""" return find_template_latlon_ele(wikitext, 'Position oben') def create_template_PositionOben(lat, lon, ele): return create_template_latlon_ele('Position, oben', lat, lon, ele) def find_template_PositionUnten(wikitext): """Same as find_template_latlon_ele with template '{{Position unten|47.076207 N 11.453553 E|1890}}'""" return find_template_latlon_ele(wikitext, 'Position unten') def find_template_unsigned(wikitext, template_title): """Finds the first occurance of the '{{template_title|1890}}' template and returns the tuple (start, end, unsigned_value) or (None, None, None) if the template was not found. If the template has no valid format, an exception is thrown.""" start, end = wrpylib.mwmarkup.find_template(wikitext, template_title) if start is None: return (None,) * 3 title, params = wrpylib.mwmarkup.split_template(wikitext[start:end]) unsigned_value = wrpylib.wrvalidators.UnsignedNone().to_python(params['1'].strip()) return start, end, unsigned_value def create_template_unsigned(template_title, unsigned): unsigned = wrpylib.wrvalidators.UnsignedNone().from_python(unsigned) if len(unsigned) == 0: unsigned = ' ' return wrpylib.mwmarkup.create_template(template_title, [unsigned]) def find_template_Hoehenunterschied(wikitext): """Same as find_template_unsigned with template '{{Höhenunterschied|350}}'""" return find_template_unsigned(wikitext, 'Höhenunterschied') def create_template_Hoehenunterschied(ele_diff): return create_template_unsigned('Höhenunterschied', ele_diff) def find_template_Bahnlaenge(wikitext): """Same as find_template_unsigned with template '{{Bahnlänge|4500}}'""" return find_template_unsigned(wikitext, 'Bahnlänge') def create_template_Bahnlaenge(length): return create_template_unsigned('Bahnlänge', length) def find_template_Gehzeit(wikitext): """Same as find_template_unsigned with template '{{Gehzeit|60}}'""" return find_template_unsigned(wikitext, 'Gehzeit') def create_template_Gehzeit(walkup_time): return create_template_unsigned('Gehzeit', walkup_time) def find_template_Forumlink(wikitext): """Same as find_template_unsigned with template '{{Forumlink|26}}'""" start, end = wrpylib.mwmarkup.find_template(wikitext, 'Forumlink') if start is None: return (None,) * 3 title, params = wrpylib.mwmarkup.split_template(wikitext[start:end]) forumid = params['1'].strip() if forumid == '': unsigned_value = None else: unsigned_value = wrpylib.wrvalidators.UnsignedNone().to_python(forumid) return start, end, unsigned_value # return find_template_unsigned(wikitext, u'Forumlink') def find_template_Parkplatz(wikitext): """Same as find_template_latlon_ele with template '{{Parkplatz|47.076207 N 11.453553 E|1890}}'""" return find_template_latlon_ele(wikitext, 'Parkplatz') def find_template_Haltestelle(wikitext): """Finds the first occurance of the '{{Haltestelle|Ortsname|Haltestellenname|47.076207 N 11.453553 E|1890}}' template and returns the tuple (start, end, city, stop, lat, lon, ele) or (None, None, None, None, None, None, None) if the template was not found. If the template has no valid format, an exception is thrown.""" start, end = wrpylib.mwmarkup.find_template(wikitext, 'Haltestelle') if start is None: return (None,) * 7 title, params = wrpylib.mwmarkup.split_template(wikitext[start:end]) city = wrpylib.wrvalidators.UnicodeNone().to_python(params['1'].strip()) stop = wrpylib.wrvalidators.UnicodeNone().to_python(params['2'].strip()) lat, lon = wrpylib.wrvalidators.GeoNone().to_python(params['3'].strip()) ele = wrpylib.wrvalidators.UnsignedNone().to_python(params['4'].strip()) return start, end, city, stop, lat, lon, ele def parse_wrmap_coordinates(coords): """gets a string coordinates and returns an array of lon/lat coordinate pairs, e.g. 47.12 N 11.87 E 47.13 N 11.70 E -> [[11.87, 47.12], [11.70, 47.13]]""" result = [] pos = 0 for match in re.finditer(r'\s*(\d+\.?\d*)\s*N?\s+(\d+\.?\d*)\s*E?\s*', coords): if match.start() != pos: break result.append([float(match.groups()[1]), float(match.groups()[0])]) pos = match.end() else: if pos == len(coords): return result raise RuntimeError('Wrong coordinate format: {}'.format(coords)) WRMAP_POINT_TYPES = ['gasthaus', 'haltestelle', 'parkplatz', 'achtung', 'foto', 'verleih', 'punkt'] WRMAP_LINE_TYPES = ['rodelbahn', 'gehweg', 'alternative', 'lift', 'anfahrt', 'linie'] def parse_wrmap(wikitext): """Parses the (unicode) u'content' of the Winterrodeln wrmap extension. If wikitext does not contain the tag or if the tag contains invalid formatted lines, a ParseError is raised. Use wrpylib.mwmarkup.find_tag(wikitext, 'wrmap') to find the wrmap tag within an arbitrary wikitext before using this function. :param wikitext: wikitext containing only the template. Example: wikitext = u''' 47.240689 11.190454 47.245789 11.238971 47.245711 11.238283 47.238587 11.203360 47.244951 11.230868 47.245470 11.237853 ''' :returns: GeoJSON as nested Python datatype """ # parse XML try: wrmap_xml = xml.etree.ElementTree.fromstring(wikitext.encode('utf-8')) except xml.etree.ElementTree.ParseError as e: row, column = e.position raise ParseError("XML parse error on row {}, column {}: {}".format(row, column, e)) if wrmap_xml.tag not in ['wrmap', 'wrgmap']: raise ParseError('No valid tag name') # convert XML to geojson (http://www.geojson.org/geojson-spec.html) json_features = [] for feature in wrmap_xml: # determine feature type is_point = feature.tag in WRMAP_POINT_TYPES is_line = feature.tag in WRMAP_LINE_TYPES if (not is_point and not is_line): raise ParseError('Unknown element <{}>.'.format(feature.tag)) # point if is_point: properties = {'type': feature.tag} allowed_properties = {'name', 'wiki'} wrong_properties = set(feature.attrib.keys()) - allowed_properties if len(wrong_properties) > 0: raise ParseError("The attribute '{}' is not allowed at <{}>.".format(list(wrong_properties)[0], feature.tag)) properties.update(feature.attrib) coordinates = parse_wrmap_coordinates(feature.text) if len(coordinates) != 1: raise ParseError('The element <{}> has to have exactly one coordinate pair.'.format(feature.tag)) json_features.append({ 'type': 'Feature', 'geometry': {'type': 'Point', 'coordinates': coordinates[0]}, 'properties': properties}) # line if is_line: properties = {'type': feature.tag} allowed_properties = {'farbe', 'dicke'} wrong_properties = set(feature.attrib.keys()) - allowed_properties if len(wrong_properties) > 0: raise ParseError("The attribute '{}' is not allowed at <{}>.".format(list(wrong_properties)[0], feature.tag)) if 'farbe' in feature.attrib: if not re.match('#[0-9a-fA-F]{6}$', feature.attrib['farbe']): raise ParseError('The attribute "farbe" has to have a format like "#a0bb43".') properties['strokeColor'] = feature.attrib['farbe'] # e.g. #a200b7 if 'dicke' in feature.attrib: try: properties['strokeWidth'] = int(feature.attrib['dicke']) # e.g. 6 except ValueError: raise ParseError('The attribute "dicke" has to be an integer.') json_features.append({ 'type': 'Feature', 'geometry': {'type': 'LineString', 'coordinates': parse_wrmap_coordinates(feature.text)}, 'properties': properties}) # attributes properties = {} for k, v in wrmap_xml.attrib.items(): if k in ['lat', 'lon']: try: properties[k] = float(v) except ValueError: raise ParseError('Attribute "{}" has to be a float value.'.format(k)) elif k in ['zoom', 'width', 'height']: try: properties[k] = int(v) except ValueError: raise ParseError('Attribute "{}" has to be an integer value.'.format(k)) else: raise ParseError('Unknown attribute "{}".'.format(k)) geojson = { 'type': 'FeatureCollection', 'features': json_features, 'properties': properties} return geojson def create_wrmap_coordinates(coords): result = [] for coord in coords: result.append('{:.6f} N {:.6f} E'.format(coord[1], coord[0])) return '\n'.join(result) def create_wrmap(geojson): """Creates a wikitext from geojson (as python types).""" wrmap_xml = xml.etree.ElementTree.Element('wrmap') wrmap_xml.text = '\n\n' for k, v in geojson['properties'].items(): if k in ['lon', 'lat']: wrmap_xml.attrib[k] = '{:.6f}'.format(v) else: wrmap_xml.attrib[k] = str(v) assert geojson['type'] == 'FeatureCollection' json_features = geojson['features'] last_json_feature = None for json_feature in json_features: feature_xml = xml.etree.ElementTree.SubElement(wrmap_xml, json_feature['properties']['type']) geo = json_feature['geometry'] if geo['type'] == 'Point': feature_xml.text = create_wrmap_coordinates([geo['coordinates']]) if last_json_feature is not None: last_json_feature.tail = '\n' else: if last_json_feature is not None: last_json_feature.tail = '\n\n' feature_xml.text = '\n' + create_wrmap_coordinates(geo['coordinates']) + '\n' last_json_feature = feature_xml feature_xml.attrib = json_feature['properties'] del feature_xml.attrib['type'] if last_json_feature is not None: last_json_feature.tail = '\n\n' return xml.etree.ElementTree.tostring(wrmap_xml, encoding='utf-8').decode('utf-8')