def check_wms_endpoint(source, info_msgs, warning_msgs, error_msgs): """ Check WMS Endpoint source Currently it is only tested if a GetCapabilities request can be parsed. Parameters ---------- source : dict Source dictionary info_msgs : list Good messages warning_msgs: list Warning messages error_msgs: list: Error Messages """ wms_url = source["properties"]["url"] source_headers = get_http_headers(source) if not validators.url(wms_url): error_msgs.append(f"URL validation error: {wms_url}") wms_args = {} u = urlparse(wms_url) for k, v in parse_qsl(u.query, keep_blank_values=True): wms_args[k.lower()] = v for wmsversion in [None, "1.3.0", "1.1.1", "1.1.0", "1.0.0"]: try: url = wmshelper.get_getcapabilities_url(wms_url, wms_version=wmsversion) r = requests.get(url, headers=source_headers) xml = r.text wms = wmshelper.parse_wms(xml) for access_constraint in wms["AccessConstraints"]: info_msgs.append(f"AccessConstraints: {access_constraint}") for fee in wms["Fees"]: info_msgs.append(f"Fee: {fee}") break except Exception as e: error_msgs.append(f"WMS: {wmsversion} Exception: {e}")
def check_wms(source, info_msgs, warning_msgs, error_msgs): """ Check WMS source Parameters ---------- source : dict Source dictionary info_msgs : list Good messages warning_msgs: list Warning messages error_msgs: list: Error Messages """ wms_url = source["properties"]["url"] source_headers = get_http_headers(source) params = ["{proj}", "{bbox}", "{width}", "{height}"] missingparams = [p for p in params if p not in wms_url] if len(missingparams) > 0: error_msgs.append( f"The following values are missing in the URL: {','.join(missingparams)}" ) wms_args = {} u = urlparse(wms_url) url_parts = list(u) for k, v in parse_qsl(u.query, keep_blank_values=True): wms_args[k.lower()] = v def validate_wms_getmap_url(): """ Layers and styles can contain whitespaces. Ignore them here. They are checked against GetCapabilities later. """ url_parts_without_layers = "&".join([ f"{key}={value}" for key, value in wms_args.items() if key not in {"layers", "styles"} ]) parts = url_parts.copy() parts[4] = url_parts_without_layers url = urlunparse(parts).replace("{", "").replace("}", "") return validators.url(url) if not validate_wms_getmap_url(): error_msgs.append(f"URL validation error: {wms_url}") # Check mandatory WMS GetMap parameters (Table 8, Section 7.3.2, WMS 1.3.0 specification) missing_request_parameters = set() is_esri = "request" not in wms_args if is_esri: required_parameters = [ "f", "bbox", "size", "imageSR", "bboxSR", "format" ] else: required_parameters = [ "version", "request", "layers", "bbox", "width", "height", "format", ] for request_parameter in required_parameters: if request_parameter.lower() not in wms_args: missing_request_parameters.add(request_parameter) # Nothing more to do for esri rest api if is_esri: return if "version" in wms_args and wms_args["version"] == "1.3.0": if "crs" not in wms_args: missing_request_parameters.add("crs") if "srs" in wms_args: error_msgs.append( f"WMS {wms_args['version']} urls should not contain SRS parameter." ) elif "version" in wms_args and not wms_args["version"] == "1.3.0": if "srs" not in wms_args: missing_request_parameters.add("srs") if "crs" in wms_args: error_msgs.append( f"WMS {wms_args['version']} urls should not contain CRS parameter." ) if len(missing_request_parameters) > 0: missing_request_parameters_str = ",".join(missing_request_parameters) error_msgs.append( f"Parameter '{missing_request_parameters_str}' is missing in url.") return # Styles is mandatory according to the WMS specification, but some WMS servers seems not to care if "styles" not in wms_args: warning_msgs.append( "Parameter 'styles' is missing in url. 'STYLES=' can be used to request default style." ) # We first send a service=WMS&request=GetCapabilities request to server # According to the WMS Specification Section 6.2 Version numbering and negotiation, the server should return # the GetCapabilities XML with the highest version the server supports. # If this fails, it is tried to explicitly specify a WMS version exceptions = [] wms = None for wmsversion in [None, "1.3.0", "1.1.1", "1.1.0", "1.0.0"]: if wmsversion is None: wmsversion_str = "-" else: wmsversion_str = wmsversion try: wms_getcapabilites_url = wmshelper.get_getcapabilities_url( wms_url, wmsversion) r = requests.get(wms_getcapabilites_url, headers=source_headers) xml = r.text wms = wmshelper.parse_wms(xml) if wms is not None: break except Exception as e: exceptions.append(f"WMS {wmsversion_str}: Error: {e}") continue if wms is None: for msg in exceptions: error_msgs.append(msg) return for access_constraint in wms["AccessConstraints"]: info_msgs.append(f"AccessConstraints: {access_constraint}") for fee in wms["Fees"]: info_msgs.append(f"Fee: {fee}") if source["geometry"] is None: geom = None else: geom = eliutils.parse_eli_geometry(source["geometry"]) # Check layers if "layers" in wms_args: layer_arg = wms_args["layers"] layers = layer_arg.split(",") not_found_layers = [] for layer_name in layer_arg.split(","): if layer_name not in wms["layers"]: not_found_layers.append(layer_name) if len(not_found_layers) > 0: error_msgs.append( f"Layers '{','.join(not_found_layers)}' not advertised by WMS GetCapabilities " "request.") # Check source geometry against layer bounding box # Regardless of its projection, each layer should advertise an approximated bounding box in lon/lat. # See WMS 1.3.0 Specification Section 7.2.4.6.6 EX_GeographicBoundingBox if geom is not None and geom.is_valid: max_outside = 0.0 for layer_name in layers: if layer_name in wms["layers"]: bbox = wms["layers"][layer_name]["BBOX"] geom_bbox = box(*bbox) geom_outside_bbox = geom.difference(geom_bbox) area_outside_bbox = geom_outside_bbox.area / geom.area * 100.0 max_outside = max(max_outside, area_outside_bbox) # 5% is an arbitrary chosen value and should be adapted as needed if max_outside > 5.0: error_msgs.append( f"{round(area_outside_bbox, 2)}% of geometry is outside of the layers bounding box. " "Geometry should be checked") # Check styles if "styles" in wms_args: style = wms_args["styles"] # default style needs not to be advertised by the server if not (style == "default" or style == "" or style == "," * len(layers)): styles = wms_args["styles"].split(",") if not len(styles) == len(layers): error_msgs.append( "Not the same number of styles and layers.") else: for layer_name, style in zip(layers, styles): if (len(style) > 0 and not style == "default" and layer_name in wms["layers"] and style not in wms["layers"][layer_name]["Styles"]): error_msgs.append( f"Layer '{layer_name}' does not support style '{style}'" ) # Check CRS crs_should_included_if_available = {"EPSG:4326", "EPSG:3857", "CRS:84"} if "available_projections" not in source["properties"]: error_msgs.append( "source is missing 'available_projections' element.") else: for layer_name in layers: if layer_name in wms["layers"]: not_supported_crs = set() for crs in source["properties"]["available_projections"]: if crs.upper() not in wms["layers"][layer_name]["CRS"]: not_supported_crs.add(crs) if len(not_supported_crs) > 0: supported_crs_str = ",".join( wms["layers"][layer_name]["CRS"]) not_supported_crs_str = ",".join(not_supported_crs) warning_msgs.append( f"Layer '{layer_name}': CRS '{not_supported_crs_str}' not in: {supported_crs_str}. Some server support CRS which are not advertised." ) supported_but_not_included = set() for crs in crs_should_included_if_available: if (crs not in source["properties"] ["available_projections"] and crs in wms["layers"][layer_name]["CRS"]): supported_but_not_included.add(crs) if len(supported_but_not_included) > 0: supported_but_not_included_str = ",".join( supported_but_not_included) warning_msgs.append( f"Layer '{layer_name}': CRS '{supported_but_not_included_str}' not included in available_projections but " "supported by server.") if wms_args["version"] < wms["version"]: warning_msgs.append( f"Query requests WMS version '{wms_args['version']}', server supports '{wms['version']}'" ) # Check formats imagery_format = wms_args["format"] imagery_formats_str = "', '".join(wms["formats"]) if imagery_format not in wms["formats"]: error_msgs.append( f"Format '{imagery_format}' not in '{imagery_formats_str}'.") if ("category" in source["properties"] and "photo" in source["properties"]["category"]): if "jpeg" not in imagery_format and "jpeg" in imagery_formats_str: warning_msgs.append( f"Server supports JPEG, but '{imagery_format}' is used. " "JPEG is typically preferred for photo sources, but might not be always " "the best choice. " f"(Server supports: '{imagery_formats_str}')")
async def update_wms(wms_url, session: ClientSession, messages): """Update wms parameters using WMS GetCapabilities request Parameters ---------- wms_url : str session : ClientSession messages : list Returns ------- dict Dict with new url and available projections """ wms_args = {} u = urlparse(wms_url) for k, v in parse_qsl(u.query, keep_blank_values=True): wms_args[k.lower()] = v layers = wms_args["layers"].split(",") # Fetch highest supported WMS GetCapabilities wms = None for wmsversion in supported_wms_versions: # Do not try versions older than in the url if wmsversion < wms_args["version"]: continue try: wms_getcapabilites_url = wmshelper.get_getcapabilities_url( wms_url, wmsversion) messages.append(f"WMS url: {wms_getcapabilites_url}") resp = await get_url(wms_getcapabilites_url, session, with_text=True) if resp.exception is not None: messages.append( f"Could not download GetCapabilites URL for {wms_getcapabilites_url}: {resp.exception}" ) continue xml = resp.text if isinstance(xml, bytes): # Parse xml encoding to decode try: xml_ignored = xml.decode(errors="ignore") str_encoding = re.search('encoding="(.*?)"', xml_ignored).group(1) xml = xml.decode(encoding=str_encoding) except Exception as e: raise RuntimeError("Could not parse encoding: {}".format( str(e))) wms = wmshelper.parse_wms(xml) if wms is not None: break except Exception as e: messages.append( f"Could not get GetCapabilites URL for {wmsversion}: {e}") if wms is None: # Could not request GetCapabilities messages.append("Could not contact WMS server") return None # Abort if a layer is not advertised by WMS server # If layer name exists but case does not match update to layer name advertised by server layers_advertised = wms["layers"] layers_advertised_lower_case = { layer.lower(): layer for layer in layers_advertised } updated_layers = [] for layer in layers: layer_lower = layer.lower() if layer_lower not in layers_advertised_lower_case: messages.append( f"Layer {layer} not advertised by server {wms_getcapabilites_url}" ) return None else: updated_layers.append(layers_advertised_lower_case[layer_lower]) layers = updated_layers wms_args["version"] = wms["version"] if wms_args["version"] == "1.3.0": wms_args.pop("srs", None) wms_args["crs"] = "{proj}" else: wms_args.pop("crs", None) wms_args["srs"] = "{proj}" if "styles" not in wms_args: wms_args["styles"] = "" def test_proj(proj): if proj == "CRS:84": return True if "AUTO" in proj: return False # 'EPSG:102067' is not valid, should be ESRI:102067: https://epsg.io/102067 if proj == "EPSG:102067": return False # 'EPSG:102066' is not valid, should be ESRI:102066: https://epsg.io/102066 if proj == "EPSG:102066": return False if "EPSG" in proj: try: CRS.from_string(proj) return True except: return False return False # For multilayer WMS queries only EPSG codes are valid that are available for all layers new_projections = wms["layers"][layers[0]]["CRS"] for layer in layers[1:]: new_projections.intersection_update(wms["layers"][layer]["CRS"]) new_projections = set( [proj.upper() for proj in new_projections if test_proj(proj)]) if len(new_projections) == 0: if len(layers) > 1: messages.append("No common valid projections among layers.") else: messages.append("No valid projections for layer.") return None new_wms_parameters = OrderedDict() for key in [ "map", "layers", "styles", "format", "transparent", "crs", "srs", "width", "height", "bbox", "version", "service", "request", ]: if key in wms_args: new_wms_parameters[key] = wms_args[key] for key in wms_args: if key not in new_wms_parameters: new_wms_parameters[key] = wms_args[key] baseurl = wms_url.split("?")[0] url_parameters = "&".join([ "{}={}".format(key.upper(), value) for key, value in new_wms_parameters.items() ]) new_url = f"{baseurl}?{url_parameters}" return {"url": new_url, "available_projections": new_projections}