# stdlib imports
import os.path
import glob
import json
import shutil
# third party imports
import fiona
from fiona.crs import from_epsg
from openquake.hazardlib import imt
from configobj import ConfigObj
# local imports
from .base import CoreModule, Contents
from shakelib.plotting.contour import contour
from shakemap.utils.utils import get_object_from_config
from impactutils.io.smcontainers import ShakeMapOutputContainer
from shakelib.utils.imt_string import oq_to_file
from shakemap.utils.config import (
get_config_paths,
get_configspec,
get_custom_validator,
config_error,
)
FORMATS = {"geojson": ("GeoJSON", "json")}
DEFAULT_FILTER_SIZE = 10
[docs]class ContourModule(CoreModule):
"""
contour -- Generate contours of all IMT values from the
shake_result.hdf output file.
"""
command_name = "contour"
targets = [r"products/cont_.*\.json"]
dependencies = [("products/shake_result.hdf", True)]
def __init__(self, eventid, filter=None):
super(ContourModule, self).__init__(eventid)
if filter is not None:
self.filter_size = filter
else:
self.filter_size = DEFAULT_FILTER_SIZE
self.contents = Contents("Ground Motion Contours", "contours", eventid)
[docs] def execute(self):
"""
Create contour files for all configured IMT values.
Raises:
NotADirectoryError: When the event data directory does not exist.
FileNotFoundError: When the the shake_result HDF file does not
exist.
"""
install_path, data_path = get_config_paths()
datadir = os.path.join(data_path, self._eventid, "current", "products")
if not os.path.isdir(datadir):
raise NotADirectoryError(f"{datadir} is not a valid directory.")
datafile = os.path.join(datadir, "shake_result.hdf")
if not os.path.isfile(datafile):
raise FileNotFoundError(f"{datafile} does not exist.")
# Open the ShakeMapOutputContainer and extract the data
container = ShakeMapOutputContainer.load(datafile)
# get the path to the products.conf file, load the config
config_file = os.path.join(install_path, "config", "products.conf")
spec_file = get_configspec("products")
validator = get_custom_validator()
config = ConfigObj(config_file, configspec=spec_file)
results = config.validate(validator)
if not isinstance(results, bool) or not results:
config_error(config, results)
if container.getDataType() != "grid":
raise NotImplementedError(
"contour module can only contour " "gridded data, not sets of points"
)
# get the filter size from the products.conf
filter_size = config["products"]["contour"]["filter_size"]
# create contour files
self.logger.debug("Contouring to files...")
contour_to_files(container, datadir, self.logger, self.contents, filter_size)
container.close()
[docs]def contour_to_files(
container, output_dir, logger, contents, filter_size=DEFAULT_FILTER_SIZE
):
"""
Generate contours of all IMT values.
Args:
container (ShakeMapOutputContainer): ShakeMapOutputContainer with
ShakeMap output data.
output_dir (str): Path to directory where output files will be written.
logger (logging.Logger): Python logging Logger instance.
Raises:
LookupError: When configured file format is not supported
"""
# Right now geojson is all we support; if that changes, we'll have
# to add a configuration or command-line option
file_format = "geojson"
# open a file for writing
driver, extension = FORMATS[file_format]
sa_schema = {
"geometry": "MultiLineString",
"properties": {
"value": "float",
"units": "str",
"color": "str",
"weight": "int",
},
}
mmi_schema = {
"geometry": "MultiLineString",
"properties": {
"value": "float",
"units": "str",
"color": "str",
"weight": "int",
},
}
crs = from_epsg(4326)
config = container.getConfig()
gmice = get_object_from_config("gmice", "modeling", config)
gmice_imts = [imt.__name__ for imt in gmice.DEFINED_FOR_INTENSITY_MEASURE_TYPES]
gmice_pers = gmice.DEFINED_FOR_SA_PERIODS
imtlist = container.getIMTs()
for imtype in imtlist:
component, imtype = imtype.split("/")
fileimt = oq_to_file(imtype)
oqimt = imt.from_string(imtype)
if component == "GREATER_OF_TWO_HORIZONTAL":
fname = f"cont_{fileimt}.{extension}"
else:
fname = f"cont_{fileimt}_{component}.{extension}"
if imtype == "MMI":
contents.addFile(
"mmiContour",
"Intensity Contours",
"Contours of macroseismic intensity.",
fname,
"application/json",
)
contents.addFile(
"miContour",
"Intensity Contours (Legacy Naming)",
"Contours of macroseismic intensity.",
"cont_mi.json",
"application/json",
)
elif imtype == "PGA":
contents.addFile(
"pgaContour",
"PGA Contours",
"Contours of " + component + " peak " "ground acceleration (%g).",
fname,
"application/json",
)
elif imtype == "PGV":
contents.addFile(
"pgvContour",
"PGV Contours",
"Contours of " + component + " peak " "ground velocity (cm/s).",
fname,
"application/json",
)
else:
contents.addFile(
imtype + "Contour",
imtype.upper() + " Contours",
"Contours of "
+ component
+ " 5% damped "
+ str(oqimt.period)
+ " sec spectral acceleration (%g).",
fname,
"application/json",
)
filename = os.path.join(output_dir, fname)
if os.path.isfile(filename):
fpath, fext = os.path.splitext(filename)
flist = glob.glob(fpath + ".*")
for fname in flist:
os.remove(fname)
if imtype == "MMI" or (
imtype not in gmice_imts
and ("SA" not in gmice_imts or oqimt.period not in gmice_pers)
):
my_gmice = None
else:
my_gmice = gmice
# fiona spews a warning here when driver is geojson
# this warning appears to be un-catchable using
# with warnings.catch_warnings()
# or
# logging.captureWarning()
# or
# even redirecting stderr/stdout to IO streams
# not sure where the warning is coming from,
# but there appears to be no way to stop it...
with fiona.Env():
if imtype == "MMI":
selected_schema = mmi_schema
else:
selected_schema = sa_schema
vector_file = fiona.open(
filename, "w", driver=driver, schema=selected_schema, crs=crs
)
line_strings = contour(
container.getIMTGrids(imtype, component), imtype, filter_size, my_gmice
)
for feature in line_strings:
vector_file.write(feature)
# Grab some metadata
meta = container.getMetadata()
event_info = meta["input"]["event_information"]
mdict = {
"eventid": event_info["event_id"],
"longitude": float(event_info["longitude"]),
"latitude": float(event_info["latitude"]),
}
logger.debug(f"Writing contour file {filename}")
vector_file.close()
# Get bounds
tmp = fiona.open(filename)
try:
bounds = tmp.bounds
except Exception:
# If there were no contours, there won't be any bounds, so
# we just make the bbox the epicenter...
bounds = "[%s, %s, %s, %s]" % (
event_info["longitude"],
event_info["latitude"],
event_info["longitude"],
event_info["latitude"],
)
tmp.close()
# Read back in to add metadata/bounds
data = json.load(open(filename))
data["metadata"] = mdict
data["bbox"] = bounds
with open(filename, "w") as outfile:
json.dump(data, outfile)
#####################################
# Make an extra version of the MMI contour file
# so that the current web rendering code can find it.
# Delete this file once everyone has moved to new version
# of ComCat code.
if imtype == "MMI":
old_file = os.path.join(output_dir, "cont_mi.json")
shutil.copy(filename, old_file)
#####################################