import glob
import os.path
from functools import partial
import logging
# third party imports
import pyproj
from shapely.geometry import Point, Polygon, MultiPolygon
from shapely.ops import transform
import shapely.wkt
import numpy as np
from openquake.hazardlib.geo.geodetic import min_distance_to_segment
from validate import ValidateError
import shakemap.utils.config as config
from shakemap.utils.config import path_macro_sub
# ##########################################################################
# We can't use normal ConfigObj validation because there are
# inconsistent sub-section structures (i.e., acr, scr, and volcanic
# vs. subduction. There are also optional sections with variable
# structure (i.e., the layers). So we do our validation and variable
# conversion here.
# ##########################################################################
[docs]def validate_config(mydict, install_path, data_path, global_data_path):
"""Recursively validate select.conf.
Args:
mydict (dict): Full or partial config dictionary.
install_path (str):
"""
for key in mydict:
if isinstance(mydict[key], dict):
validate_config(mydict[key], install_path, data_path, global_data_path)
continue
if key == "horizontal_buffer" or key == "vertical_buffer":
mydict[key] = config.cfg_float(mydict[key])
elif key == "use_slab":
mydict[key] = config.cfg_bool(mydict[key])
elif key == "gmpe":
mydict[key] = config.gmpe_list(mydict[key], 1)
elif key == "min_depth" or key == "max_depth":
mydict[key] = config.cfg_float_list(mydict[key])
elif key == "layer_dir":
mydict[key] = path_macro_sub(
mydict[key], ip=install_path, dp=data_path, gp=global_data_path
)
elif key in ("x1", "x2", "p1", "p2", "p_kagan_default", "default_slab_depth"):
mydict[key] = float(mydict[key])
elif key in ("ipe", "gmice", "ccf"):
pass
else:
raise ValidateError(f'Invalid entry in config: "{key}"')
return
[docs]def nearest_edge(elon, elat, poly):
"""
Return the distance from a point to the nearest edge of a
polygon.
Args:
elon (float): The longitude of the reference point.
elat (float): The latitude of the reference point.
poly (Polygon): An instance of a shapely Polygon.
Returns:
float: The distance (in km) from the reference point to the
nearest edge (or vertex) of the polygon.
"""
elon_arr = np.array([elon])
elat_arr = np.array([elat])
x, y = poly.exterior.xy
nearest = 99999.0
for ix in range(1, len(x) - 1):
dd = min_distance_to_segment(
np.array(x[ix - 1 : ix + 1]),
np.array(y[ix - 1 : ix + 1]),
elon_arr,
elat_arr,
)
if np.abs(dd[0]) < nearest:
nearest = np.abs(dd[0])
return nearest
[docs]def dist_to_layer(elon, elat, geom):
"""
Return the distance from a point to the polygon(s) in a layer; zero if
the point is inside the polygon. If the nearest edge of the polygon is
greater than 5000 km from the point, the point cannot be inside the
polygon and the distance reported will be the distance to the nearest
edge. So don't make polygons too big.
Args:
elon (float): The longitude of the reference point.
elat (float): The latitude of the reference point.
geom (Polygon or MultiPolygon): An instance of a shapely Polygon
or MultiPolygon.
Returns:
float: The distance (in km) from the reference point to the
nearest polygon in the layer. The distance will be zero if the
point lies inside the polygon.
"""
if isinstance(geom, Polygon):
plist = [geom]
elif isinstance(geom, MultiPolygon):
plist = list(geom.geoms)
else:
raise TypeError(f"Invalid geometry type in layer: {type(geom)}")
project = partial(
pyproj.transform,
pyproj.Proj(proj="latlong", datum="WGS84"),
pyproj.Proj(proj=f"aeqd +lat_0={elat:f} +lon_0={elon:f} +datum=WGS84"),
)
ep = Point(0.0, 0.0)
min_dist = 99999.0
for poly in plist:
nearest = nearest_edge(elon, elat, poly)
if nearest < 5000:
nearest = ep.distance(transform(project, poly)) / 1000.0
if nearest < min_dist:
min_dist = nearest
if min_dist == 0:
break
return min_dist
[docs]def get_layer_distances(elon, elat, layer_dir):
"""
Return the distances from a point to the nearest polygon in each
layer file found in 'layer_dir'. The distance will be zero if
the point is inside a polygon. If the nearest edge of a polygon is
greater than 5000 km from the point, the point cannot be inside the
polygon and the distance reported will be the distance to the
nearest edge. So don't make polygons too big.
The layer files should be written in Well-Known Text (with .wkt
extensions), and should contain either a single POLYGON or
MULTIPOLYGON object. The layer name will be the file's basename.
Args:
elon (float): The longitude of the reference point.
elat (float): The latitude of the reference point.
layer_dir (str): The path to the directory containg the layer
files.
Returns:
dict: A dictionary where the keys are the layer names, and the
values are the distance (in km) from the reference point to the
nearest polygon in the layer. The distance will be zero if the
point lies inside the polygon.
"""
layer_files = glob.glob(os.path.join(layer_dir, "*.wkt"))
dist_dict = {}
for file in layer_files:
layer_name = os.path.splitext(os.path.basename(file))[0]
with open(file, "r") as fd:
data = fd.read()
geom = shapely.wkt.loads(data)
dist_dict[layer_name] = dist_to_layer(elon, elat, geom)
return dist_dict
[docs]def update_config_regions(lat, lon, config):
min_dist_to_layer = 999999.9
inside_layer_name = None
if "layers" in config and "layer_dir" in config["layers"]:
layer_dir = config["layers"]["layer_dir"]
if layer_dir:
geo_layers = get_layer_distances(lon, lat, layer_dir)
else:
geo_layers = {}
for layer in config["layers"]:
if layer == "layer_dir":
continue
if layer not in geo_layers:
logging.warn(f"Error: cannot find layer {layer} in {layer_dir}")
continue
ldist = geo_layers[layer]
if ldist < min_dist_to_layer:
min_dist_to_layer = ldist
if min_dist_to_layer == 0:
inside_layer_name = layer
break
if inside_layer_name is None:
return config
else:
layer_config = config["layers"][inside_layer_name]
for region, rdict in layer_config.items():
if region == "horizontal_buffer":
continue
config["tectonic_regions"][region].update(rdict)
return config