From cd69a05cda82e480a9294d46e93a8eb1b40e5eed Mon Sep 17 00:00:00 2001 From: Brandon Clayton <bclayton@usgs.gov> Date: Fri, 28 Feb 2025 09:36:05 -0700 Subject: [PATCH 01/24] add gmm and utils-java --- .../nshmp/www/NshmpMicronautServlet.java | 51 ++ .../earthquake/nshmp/www/ResponseBody.java | 213 +++++++ .../nshmp/www/ResponseMetadata.java | 18 + .../earthquake/nshmp/www/SwaggerUtils.java | 157 +++++ .../usgs/earthquake/nshmp/www/WsUtils.java | 100 ++++ .../earthquake/nshmp/www/gmm/GmmCalc.java | 384 ++++++++++++ .../nshmp/www/gmm/GmmController.java | 547 ++++++++++++++++++ .../earthquake/nshmp/www/gmm/GmmService.java | 499 ++++++++++++++++ .../earthquake/nshmp/www/gmm/ServiceUtil.java | 459 +++++++++++++++ .../usgs/earthquake/nshmp/www/gmm/Utils.java | 65 +++ .../earthquake/nshmp/www/gmm/XyDataGroup.java | 65 +++ .../nshmp/www/meta/EnumParameter.java | 22 + .../earthquake/nshmp/www/meta/Status.java | 18 + .../nshmp/www/meta/StringParameter.java | 20 + 14 files changed, 2618 insertions(+) create mode 100644 src/main/java/gov/usgs/earthquake/nshmp/www/NshmpMicronautServlet.java create mode 100644 src/main/java/gov/usgs/earthquake/nshmp/www/ResponseBody.java create mode 100644 src/main/java/gov/usgs/earthquake/nshmp/www/ResponseMetadata.java create mode 100644 src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerUtils.java create mode 100644 src/main/java/gov/usgs/earthquake/nshmp/www/WsUtils.java create mode 100644 src/main/java/gov/usgs/earthquake/nshmp/www/gmm/GmmCalc.java create mode 100644 src/main/java/gov/usgs/earthquake/nshmp/www/gmm/GmmController.java create mode 100644 src/main/java/gov/usgs/earthquake/nshmp/www/gmm/GmmService.java create mode 100644 src/main/java/gov/usgs/earthquake/nshmp/www/gmm/ServiceUtil.java create mode 100644 src/main/java/gov/usgs/earthquake/nshmp/www/gmm/Utils.java create mode 100644 src/main/java/gov/usgs/earthquake/nshmp/www/gmm/XyDataGroup.java create mode 100644 src/main/java/gov/usgs/earthquake/nshmp/www/meta/EnumParameter.java create mode 100644 src/main/java/gov/usgs/earthquake/nshmp/www/meta/Status.java create mode 100644 src/main/java/gov/usgs/earthquake/nshmp/www/meta/StringParameter.java diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/NshmpMicronautServlet.java b/src/main/java/gov/usgs/earthquake/nshmp/www/NshmpMicronautServlet.java new file mode 100644 index 00000000..0336511a --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/NshmpMicronautServlet.java @@ -0,0 +1,51 @@ +package gov.usgs.earthquake.nshmp.www; + +import org.reactivestreams.Publisher; + +import io.micronaut.core.type.MutableHeaders; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.annotation.Filter; +import io.micronaut.http.filter.HttpServerFilter; +import io.micronaut.http.filter.ServerFilterChain; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.schedulers.Schedulers; + +/** + * Custom NSHMP servlet implementation and URL helper class for Micronaut + * services. + * + * <p>This class sets custom response headers and provides a helper class to + * ensure serialized response URLs propagate the correct host and protocol from + * requests on USGS servers and caches that may have been forwarded. + * + * @author U.S. Geological Survey + */ +@Filter("/**") +public class NshmpMicronautServlet implements HttpServerFilter { + + /* + * Set CORS headers and content type. + * + * Because NSHMP services may be called by both the USGS website, other + * websites, and directly by 3rd party applications, responses generated by + * direct requests will not have the necessary header information that would + * be required by security protocols for web requests. This means that any + * initial direct request will pollute intermediate caches with a response + * that a browser will deem invalid. + */ + @Override + public Publisher<MutableHttpResponse<?>> doFilter( + HttpRequest<?> request, + ServerFilterChain chain) { + return Flowable.just(chain).subscribeOn(Schedulers.io()) + .switchMap(bool -> chain.proceed(request)) + .doOnNext(res -> { + MutableHeaders headers = res.getHeaders(); + headers.add("Access-Control-Allow-Origin", "*"); + headers.add("Access-Control-Allow-Methods", "*"); + headers.add("Access-Control-Allow-Headers", "accept,origin,authorization,content-type"); + }); + } + +} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/ResponseBody.java b/src/main/java/gov/usgs/earthquake/nshmp/www/ResponseBody.java new file mode 100644 index 00000000..0e103753 --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/ResponseBody.java @@ -0,0 +1,213 @@ +package gov.usgs.earthquake.nshmp.www; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.time.ZonedDateTime; + +import gov.usgs.earthquake.nshmp.www.meta.Status; + +/** + * Generic wrapper around a web service response object that is typically + * serialized to JSON and sent back to requestor as an HttpResponse 'body'. + * + * <p>To create a response, use one of the three static builder methods: + * {@link Builder#error()}, {@link Builder#success()}, or + * {@link Builder#usage()}. + * + * @author U.S. Geological Survey + * + * @param <T> The request type + * @param <V> The response type + */ +public class ResponseBody<T, V> { + + private final String name; + private final String date; + private final String status; + private final String url; + private final T request; + private final V response; + private final ResponseMetadata metadata; + + private ResponseBody(Builder<T, V> builder) { + name = builder.name; + date = ZonedDateTime.now().format(WsUtils.DATE_FMT); + status = builder.status; + url = builder.url; + request = builder.request; + response = builder.response; + metadata = builder.metadata; + } + + protected ResponseBody() { + date = null; + metadata = null; + name = null; + request = null; + response = null; + status = null; + url = null; + } + + /** + * The date and time this request/response. + */ + public String getDate() { + return date; + } + + /** + * The metadata. + */ + public ResponseMetadata getMetadata() { + return metadata; + } + + /** + * The name of the service. + */ + public String getName() { + return name; + } + + /** + * The request object. + */ + public T getRequest() { + return request; + } + + /** + * The response object. + */ + public V getResponse() { + return response; + } + + /** + * The response status. + */ + public String getStatus() { + return status; + } + + /** + * The URL used to call the service + */ + public String getUrl() { + return url; + } + + /** + * Create a new builder initialized to an error response. + * + * @param <T> The request type + * @param <V> The response type + */ + public static <T, V> Builder<T, V> error() { + return new Builder<T, V>(Status.ERROR); + } + + /** + * Create a new builder initialized to a success response. + * + * @param <T> The request type + * @param <V> The response type + */ + public static <T, V> Builder<T, V> success() { + return new Builder<T, V>(Status.SUCCESS); + } + + /** + * Create a new builder initialized to a usage response. + * + * @param <T> The request type + * @param <V> The response type + */ + public static <T, V> Builder<T, V> usage() { + return new Builder<T, V>(Status.USAGE); + } + + /** + * A {@code ResponseBody} builder. + * + * @param <T> The request type + * @param <V> The response type + */ + public static class Builder<T, V> { + + private String name; + private String status; + private String url; + private ResponseMetadata metadata; + private T request; + private V response; + + private Builder(String status) { + this.status = status; + } + + /** + * Set the metadata. + * + * @param metadata The servie metadata + */ + public Builder<T, V> metadata(ResponseMetadata metadata) { + this.metadata = metadata; + return this; + } + + /** + * Set the service name. + * + * @param name of the service being called + */ + public Builder<T, V> name(String name) { + this.name = name; + return this; + } + + /** + * Set the request object. + * + * @param request object + */ + public Builder<T, V> request(T request) { + this.request = request; + return this; + } + + /** + * Set the response object. + * + * @param response object + */ + public Builder<T, V> response(V response) { + this.response = response; + return this; + } + + /** + * Set the url used to call the service. + * + * @param url used to generate this response + */ + public Builder<T, V> url(String url) { + this.url = url; + return this; + } + + /** + * Returns a new Response + */ + public ResponseBody<T, V> build() { + checkNotNull(metadata); + checkNotNull(name); + checkNotNull(request); + checkNotNull(response); + checkNotNull(url); + checkNotNull(status); + return new ResponseBody<T, V>(this); + } + } +} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/ResponseMetadata.java b/src/main/java/gov/usgs/earthquake/nshmp/www/ResponseMetadata.java new file mode 100644 index 00000000..9439810f --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/ResponseMetadata.java @@ -0,0 +1,18 @@ +package gov.usgs.earthquake.nshmp.www; + +import gov.usgs.earthquake.nshmp.internal.AppVersion.VersionInfo; + +/** + * The response metadata with version info. + */ +public class ResponseMetadata { + public final VersionInfo[] repositories; + + public ResponseMetadata(VersionInfo... repositories) { + this.repositories = repositories; + } + + public VersionInfo[] getRepositories() { + return repositories; + } +} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerUtils.java b/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerUtils.java new file mode 100644 index 00000000..92d460b4 --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerUtils.java @@ -0,0 +1,157 @@ +package gov.usgs.earthquake.nshmp.www; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import gov.usgs.earthquake.nshmp.geo.Location; +import gov.usgs.earthquake.nshmp.gmm.Imt; +import gov.usgs.earthquake.nshmp.gmm.NehrpSiteClass; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.parameters.Parameter; +import io.swagger.v3.parser.util.SchemaTypeUtil; + +/** + * General Swagger utilities + * + * @author U.S. Geological Survey + */ +public class SwaggerUtils { + + /** + * Update "longitude" and "latitude" parameters with min and max bounds and + * add bounds to description. + * + * @param parameters The Swagger parameters + * @param min The minimum bounds + * @param max The maximum bounds + * @return + */ + public static List<Parameter> addLocationBounds( + List<Parameter> parameters, + Location min, + Location max) { + var latitudeDescription = String.format(" [%s, %s]", min.latitude, max.latitude); + var longtudeDescription = String.format(" [%s, %s]", min.longitude, max.longitude); + + parameters.forEach(parameter -> { + if (parameter.getName().equals("latitude")) { + parameter.setDescription(parameter.getDescription() + latitudeDescription); + parameter.getSchema().setMinimum(BigDecimal.valueOf(min.latitude)); + parameter.getSchema().setMaximum(BigDecimal.valueOf(max.latitude)); + } else if (parameter.getName().equals("longitude")) { + parameter.setDescription(parameter.getDescription() + longtudeDescription); + parameter.getSchema().setMinimum(BigDecimal.valueOf(min.longitude)); + parameter.getSchema().setMaximum(BigDecimal.valueOf(max.longitude)); + } + }); + + return parameters; + } + + /** + * Update any "longitude" and "latitude" parameters with min and max bounds + * and add bounds to description. + * + * @param openApi The Open API + * @param min The minimum location bounds + * @param max The maximum location bounds + */ + public static OpenAPI addLocationBounds(OpenAPI openApi, Location min, Location max) { + openApi.getPaths().values().stream() + .flatMap(path -> path.readOperations().stream()) + .forEach(operation -> addLocationBounds(operation.getParameters(), min, max)); + + return openApi; + } + + /** + * Returns markdown string listing the IMTs. + * + * @param siteClasses The IMTs + * @param heading The Markdown heading value. Default: ### + */ + public static String imtInfo(List<Imt> imts, Optional<String> heading) { + return new StringBuilder() + .append(heading.orElse("###") + " Intensity Measure Types \n") + .append(imts.stream().sorted() + .map(imt -> "- " + imt.toString()) + .collect(Collectors.joining("\n"))) + .append("\n") + .toString(); + } + + /** + * Returns updated Swagger schemas with provided IMTs. + * + * @param imts The IMTs + */ + public static Map<String, Schema> imtSchema( + Map<String, Schema> schemas, + List<Imt> imts) { + var schema = new Schema<String>(); + schema.setType(SchemaTypeUtil.STRING_TYPE); + imts.stream() + .sorted() + .forEach(imt -> schema.addEnumItemObject(imt.name())); + + schemas.put(Imt.class.getSimpleName(), schema); + return schemas; + } + + /** + * Returns markdown string listing the min and max location bounds. + * + * @param min The minimum bounds + * @param max The maximum bounds + * @param heading The Markdown heading value. Default: ### + */ + public static String locationBoundsInfo(Location min, Location max, Optional<String> heading) { + return new StringBuilder() + .append(heading.orElse("###") + " Latitude Bounds\n") + .append(String.format("- Minimum Latitude: %s°\n", min.latitude)) + .append(String.format("- Maximum Latitude: %s°\n", max.latitude)) + .append(heading.orElse("###") + " Longitude Bounds\n") + .append(String.format("- Minimum Longitude: %s°\n", min.longitude)) + .append(String.format("- Maximum Longitude: %s°\n", max.longitude)) + .toString(); + } + + /** + * Returns markdown string listing the site classes. + * + * @param siteClasses The NEHRP site classes + * @param heading The Markdown heading value. Default: ### + */ + public static String siteClassInfo(List<NehrpSiteClass> siteClasses, Optional<String> heading) { + return new StringBuilder() + .append(heading.orElse("###") + " Site Classes\n") + .append(siteClasses.stream().sorted() + .map(siteClass -> "- " + siteClass.toString()) + .collect(Collectors.joining("\n"))) + .append("\n") + .toString(); + } + + /** + * Returns updated Swagger schemas with provided site classes. + * + * @param siteClasses The NERHP site classes + */ + public static Map<String, Schema> siteClassSchema( + Map<String, Schema> schemas, + List<NehrpSiteClass> siteClasses) { + var schema = new Schema<NehrpSiteClass>(); + schema.setType(SchemaTypeUtil.STRING_TYPE); + siteClasses.stream() + .sorted() + .forEach(siteClass -> schema.addEnumItemObject(siteClass)); + + schemas.put(NehrpSiteClass.class.getSimpleName(), schema); + return schemas; + } +} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/WsUtils.java b/src/main/java/gov/usgs/earthquake/nshmp/www/WsUtils.java new file mode 100644 index 00000000..1e0d6939 --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/WsUtils.java @@ -0,0 +1,100 @@ +package gov.usgs.earthquake.nshmp.www; + +import java.lang.reflect.Type; +import java.time.format.DateTimeFormatter; +import java.util.Optional; + +import com.google.common.collect.Range; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +import gov.usgs.earthquake.nshmp.gmm.GmmInput; +import gov.usgs.earthquake.nshmp.gmm.GmmInput.Field; + +/** + * Web service utilities. + * + * @author U.S. Geological Survey + */ +public class WsUtils { + + public static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern( + "yyyy-MM-dd'T'HH:mm:ssXXX"); + + public static <T, E extends Enum<E>> T checkValue(E key, T value) { + if (value == null) { + throw new IllegalStateException("Missing [" + key.toString() + "]"); + } + + return value; + } + + /* Constrain all doubles to 8 decimal places */ + public static final class DoubleSerializer implements JsonSerializer<Double> { + @Override + public JsonElement serialize(Double d, Type type, JsonSerializationContext context) { + double dOut = Double.valueOf(String.format("%.8g", d)); + return new JsonPrimitive(dOut); + } + } + + /* Convert NaN to null */ + public static final class NaNSerializer implements JsonSerializer<Double> { + @Override + public JsonElement serialize(Double d, Type type, JsonSerializationContext context) { + return Double.isNaN(d) ? null : new JsonPrimitive(d); + } + } + + public static final class ConstraintsSerializer implements JsonSerializer<GmmInput.Constraints> { + @Override + public JsonElement serialize( + GmmInput.Constraints constraints, + Type type, + JsonSerializationContext context) { + JsonArray json = new JsonArray(); + + for (Field field : Field.values()) { + Optional<?> opt = constraints.get(field); + if (opt.isPresent()) { + Range<?> value = (Range<?>) opt.orElseThrow(); + Constraint constraint = new Constraint( + field.id, + value.lowerEndpoint(), + value.upperEndpoint()); + json.add(context.serialize(constraint)); + } + } + + return json; + } + } + + public static final class EnumSerializer<E extends Enum<E>> implements JsonSerializer<E> { + @Override + public JsonElement serialize(E src, Type type, JsonSerializationContext context) { + JsonObject jObj = new JsonObject(); + jObj.addProperty("value", src.name()); + jObj.addProperty("display", src.toString()); + + return jObj; + } + } + + @SuppressWarnings("unused") + private static class Constraint { + final String id; + final Object min; + final Object max; + + Constraint(String id, Object min, Object max) { + this.id = id; + this.min = min; + this.max = max; + } + } +} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/GmmCalc.java b/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/GmmCalc.java new file mode 100644 index 00000000..31c51dbe --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/GmmCalc.java @@ -0,0 +1,384 @@ +package gov.usgs.earthquake.nshmp.www.gmm; + +import static java.lang.Math.cos; +import static java.lang.Math.hypot; +import static java.lang.Math.sin; +import static java.lang.Math.tan; +import static java.lang.Math.toRadians; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import com.google.common.primitives.Doubles; + +import gov.usgs.earthquake.nshmp.Maths; +import gov.usgs.earthquake.nshmp.gmm.Gmm; +import gov.usgs.earthquake.nshmp.gmm.GmmInput; +import gov.usgs.earthquake.nshmp.gmm.GroundMotion; +import gov.usgs.earthquake.nshmp.gmm.GroundMotionModel; +import gov.usgs.earthquake.nshmp.gmm.GroundMotions; +import gov.usgs.earthquake.nshmp.gmm.Imt; +import gov.usgs.earthquake.nshmp.tree.Branch; +import gov.usgs.earthquake.nshmp.tree.LogicTree; +import gov.usgs.earthquake.nshmp.www.gmm.GmmService.Distance; +import gov.usgs.earthquake.nshmp.www.gmm.GmmService.Magnitude; +import gov.usgs.earthquake.nshmp.www.gmm.GmmService.Request; +import gov.usgs.earthquake.nshmp.www.gmm.XyDataGroup.EpiSeries; + +import jakarta.inject.Singleton; + +/* + * GMM service calculators. + * + * @author U.S. Geological Survey + */ +@Singleton +class GmmCalc { + + /* Compute ground motion response spectra. */ + static Map<Gmm, GmmSpectraData> spectra(Request request) { + + Map<Gmm, GmmSpectraData> gmmSpectra = new EnumMap<>(Gmm.class); + + for (Gmm gmm : request.gmms) { + Set<Imt> saImts = gmm.responseSpectrumImts(); + + List<LogicTree<GroundMotion>> saImtTrees = saImts.stream() + .map(imt -> gmm.instance(imt).calc(request.input)) + .collect(Collectors.toList()); + + List<Double> saPeriods = saImts.stream() + .map(imt -> imt.period()) + .collect(Collectors.toList()); + + gmmSpectra.put( + gmm, + new GmmSpectraData( + treeToDataGroup(gmm, Imt.PGA, request.input), + treeToDataGroup(gmm, Imt.PGV, request.input), + treesToDataGroup(saPeriods, saImtTrees))); + } + + return gmmSpectra; + } + + private static Optional<GmmData<Double>> treeToDataGroup(Gmm gmm, Imt imt, GmmInput input) { + Optional<GmmData<Double>> dataGroup = Optional.empty(); + + if (gmm.supportedImts().contains(imt)) { + LogicTree<GroundMotion> imtTrees = gmm.instance(imt).calc(input); + dataGroup = Optional.of(treeToDataGroup(imtTrees)); + } + + return dataGroup; + } + + private static GmmData<Double> treeToDataGroup(LogicTree<GroundMotion> tree) { + List<String> branchIds = tree.stream() + .map(Branch::id) + .collect(Collectors.toList()); + + GroundMotion combined = GroundMotions.combine(tree); + double μs = combined.mean(); + double σs = combined.sigma(); + + List<EpiBranch<Double>> epiBranches = new ArrayList<>(); + + // short circuit if tree is single branch + if (tree.size() > 1) { + // branch index, imt index + double[] μBranches = tree.stream().mapToDouble(branch -> branch.value().mean()).toArray(); + double[] σBranches = tree.stream().mapToDouble(branch -> branch.value().sigma()).toArray(); + double[] weights = tree.stream().mapToDouble(branch -> branch.weight()).toArray(); + + for (int i = 0; i < tree.size(); i++) { + EpiBranch<Double> epiBranch = new EpiBranch<>( + branchIds.get(i), + μBranches[i], + σBranches[i], + weights[i]); + + epiBranches.add(epiBranch); + } + } + + return new GmmData<>(μs, σs, epiBranches); + } + + private static GmmDataXs<double[]> treesToDataGroup( + List<Double> xValues, + List<LogicTree<GroundMotion>> trees) { + + // Can't use Trees.transpose() because some + // GMMs have period dependent weights + + List<GmmData<Double>> imtData = trees.stream() + .map(tree -> treeToDataGroup(tree)) + .collect(Collectors.toList()); + + LogicTree<GroundMotion> modelTree = trees.get(0); + List<EpiBranch<double[]>> epiBranches = new ArrayList<>(); + + List<String> branchIds = modelTree.stream() + .map(Branch::id) + .collect(Collectors.toList()); + + if (modelTree.size() > 1) { + List<double[]> μBranches = new ArrayList<>(); + List<double[]> σBranches = new ArrayList<>(); + List<double[]> weights = new ArrayList<>(); + for (int i = 0; i < modelTree.size(); i++) { + μBranches.add(new double[xValues.size()]); + σBranches.add(new double[xValues.size()]); + weights.add(new double[xValues.size()]); + } + + // imt indexing + for (int i = 0; i < imtData.size(); i++) { + GmmData<Double> data = imtData.get(i); + + // branch indexing + for (int j = 0; j < data.tree.size(); j++) { + EpiBranch<Double> branch = data.tree.get(j); + μBranches.get(j)[i] = branch.μs; + σBranches.get(j)[i] = branch.σs; + weights.get(j)[i] = branch.weights; + } + } + + for (int i = 0; i < modelTree.size(); i++) { + EpiBranch<double[]> epiBranch = new EpiBranch<>( + branchIds.get(i), + μBranches.get(i), + σBranches.get(i), + weights.get(i)); + epiBranches.add(epiBranch); + } + } + + double[] xs = xValues.stream().mapToDouble(Double::doubleValue).toArray(); + double[] μs = imtData.stream().mapToDouble(data -> data.μs).toArray(); + double[] σs = imtData.stream().mapToDouble(data -> data.σs).toArray(); + + return new GmmDataXs<>(xs, μs, σs, epiBranches); + } + + /* Compute ground motions over a range of distances. */ + static Map<Gmm, GmmDataXs<double[]>> distance(Distance.Request request, double[] distances) { + var inputList = hangingWallDistances(request.input, distances); + return calculateGroundMotions(request.gmms, inputList, request.imt, distances); + } + + /* Compute ground motions over a range of magnitudes. */ + static Map<Gmm, GmmDataXs<double[]>> magnitude( + Magnitude.Request request, + double[] magnitudes, + double distance) { + + var gmmInputs = Arrays.stream(magnitudes) + .mapToObj(Mw -> GmmInput.builder().fromCopy(request.input).mag(Mw).build()) + .map(gmmInput -> hangingWallDistance(gmmInput, distance)) + .collect(Collectors.toList()); + return calculateGroundMotions(request.gmms, gmmInputs, request.imt, magnitudes); + } + + private static List<GmmInput> hangingWallDistances( + GmmInput inputModel, + double[] rValues) { + + return Arrays.stream(rValues) + .mapToObj(r -> hangingWallDistance(inputModel, r)) + .collect(Collectors.toList()); + } + + /* Compute distance metrics for a fault. */ + private static GmmInput hangingWallDistance(GmmInput in, double r) { + /* Dip in radians */ + double δ = toRadians(in.dip); + + /* Horizontal and vertical widths of fault */ + double h = cos(δ) * in.width; + double v = sin(δ) * in.width; + + /* Depth to bottom of rupture */ + double zBot = in.zTor + v; + + /* Distance range over which site is normal to fault plane */ + double rCutLo = tan(δ) * in.zTor; + double rCutHi = tan(δ) * zBot + h; + + /* rRup values corresponding to cutoffs above */ + double rRupLo = Maths.hypot(in.zTor, rCutLo); + double rRupHi = Maths.hypot(zBot, rCutHi - h); + + double rJB = (r < 0) ? -r : (r < h) ? 0.0 : r - h; + double rRup = (r < rCutLo) + ? hypot(r, in.zTor) + : (r > rCutHi) + ? hypot(r - h, zBot) + : rRupScaled( + r, rCutLo, rCutHi, rRupLo, rRupHi); + + return GmmInput.builder() + .fromCopy(in) + .distances(rJB, rRup, r) + .build(); + } + + private static Map<Gmm, GmmDataXs<double[]>> calculateGroundMotions( + Set<Gmm> gmms, + List<GmmInput> gmmInputs, + Imt imt, + double[] distances) { + + Map<Gmm, GmmDataXs<double[]>> gmValues = new EnumMap<>(Gmm.class); + + for (Gmm gmm : gmms) { + List<LogicTree<GroundMotion>> trees = new ArrayList<>(); + GroundMotionModel model = gmm.instance(imt); + for (GmmInput gmmInput : gmmInputs) { + trees.add(model.calc(gmmInput)); + } + GmmDataXs<double[]> dataGroup = GmmCalc.treesToDataGroup(Doubles.asList(distances), trees); + gmValues.put(gmm, dataGroup); + } + return gmValues; + } + + /* + * Computes rRup for a surface distance r. The range [rCutLo, rCutHi] must + * contain r; rRupLo and rRupHi are rRup at rCutLo and rCutHi, respectively. + */ + private static double rRupScaled( + double r, + double rCutLo, + double rCutHi, + double rRupLo, + double rRupHi) { + + double rRupΔ = rRupHi - rRupLo; + double rCutΔ = rCutHi - rCutLo; + return rRupLo + (r - rCutLo) / rCutΔ * rRupΔ; + } + + static class GmmSpectraData { + final Optional<GmmData<Double>> pga; + final Optional<GmmData<Double>> pgv; + final GmmDataXs<double[]> sa; + + GmmSpectraData( + Optional<GmmData<Double>> pga, + Optional<GmmData<Double>> pgv, + GmmDataXs<double[]> sa) { + this.pga = pga; + this.pgv = pgv; + this.sa = sa; + } + } + + static class GmmData<T> { + final T μs; + final T σs; + final List<EpiBranch<T>> tree; + + GmmData( + T μs, + T σs, + List<EpiBranch<T>> tree) { + + this.μs = μs; + this.σs = σs; + this.tree = tree; + } + } + + static class GmmDataXs<T> extends GmmData<T> { + + final T xs; + + GmmDataXs( + T xs, + T μs, + T σs, + List<EpiBranch<T>> tree) { + super(μs, σs, tree); + this.xs = xs; + } + } + + static class EpiBranch<T> { + + final String id; + final T μs; + final T σs; + final T weights; + + EpiBranch(String id, T μs, T σs, T weights) { + this.id = id; + this.μs = μs; + this.σs = σs; + this.weights = weights; + } + } + + static class SpectraTree { + final String id; + final TreeValues values; + final TreeValues weights; + + SpectraTree(String id, TreeValues values, TreeValues weights) { + this.id = id; + this.values = values; + this.weights = weights; + } + + static List<SpectraTree> toList( + List<EpiSeries<Double>> pgaBranches, + List<EpiSeries<Double>> pgvBranches, + List<EpiSeries<double[]>> saBranches) { + List<SpectraTree> trees = new ArrayList<>(); + + for (int i = 0; i < saBranches.size(); i++) { + Optional<EpiSeries<Double>> pga = + pgaBranches.isEmpty() ? Optional.empty() : Optional.of(pgaBranches.get(i)); + Optional<EpiSeries<Double>> pgv = + pgvBranches.isEmpty() ? Optional.empty() : Optional.of(pgvBranches.get(i)); + + EpiSeries<double[]> sa = saBranches.get(i); + + TreeValues values = new TreeValues( + pga.isPresent() ? pga.get().values : null, + pgv.isPresent() ? pgv.get().values : null, + sa.values); + + TreeValues weights = new TreeValues( + pga.isPresent() ? pga.get().weights : null, + pgv.isPresent() ? pgv.get().weights : null, + sa.weights); + + trees.add(new SpectraTree(sa.id, values, weights)); + } + + return trees; + } + } + + static class TreeValues { + final Double pga; + final Double pgv; + final double[] sa; + + TreeValues(Double pga, Double pgv, double[] sa) { + this.pga = pga; + this.pgv = pgv; + this.sa = sa; + } + } + +} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/GmmController.java b/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/GmmController.java new file mode 100644 index 00000000..4415dd3b --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/GmmController.java @@ -0,0 +1,547 @@ +package gov.usgs.earthquake.nshmp.www.gmm; + +import java.util.Optional; +import java.util.Set; + +import gov.usgs.earthquake.nshmp.gmm.Gmm; +import gov.usgs.earthquake.nshmp.gmm.GmmInput; +import gov.usgs.earthquake.nshmp.gmm.Imt; +import gov.usgs.earthquake.nshmp.www.NshmpMicronautServlet; +import gov.usgs.earthquake.nshmp.www.gmm.GmmService.Distance; +import gov.usgs.earthquake.nshmp.www.gmm.GmmService.Id; +import gov.usgs.earthquake.nshmp.www.gmm.GmmService.Magnitude; +import gov.usgs.earthquake.nshmp.www.gmm.GmmService.Request; +import gov.usgs.earthquake.nshmp.www.gmm.GmmService.Spectra; + +import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.MediaType; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.QueryValue; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.inject.Inject; + +@Tag(name = "Ground Motion Models") +@Controller("/gmm") +class GmmController { + + private static final String JAVADOC_URL = + "https://earthquake.usgs.gov/nshmp/docs/nshmp-lib/gov/usgs/earthquake/nshmp"; + + private static final String GMM_URL = JAVADOC_URL + "/gmm/Gmm.html"; + private static final String GMM_INPUT_URL = JAVADOC_URL + "/gmm/GmmInput.html"; + private static final String DEFAULTS_URL = + JAVADOC_URL + "/gmm/GmmInput.Builder.html#withDefaults()"; + + private static final String GMM_INPUT_PARAMS = + "<pre>[Mw, rJB, rRup, rX, dip, width, zTor, rake, vs30, z1p0, z2p5, zSed]</pre>"; + + private static final String SPECTRA_QUERY_1 = "/gmm/spectra?gmm=ASK_14"; + private static final String SPECTRA_QUERY_2 = + "/gmm/spectra?gmm=ASK_14,BSSA_14,CB_14,CY_14&Mw=7.2&vs30=530"; + private static final String SPECTRA_EX_1 = + "<a href=\"" + SPECTRA_QUERY_1 + "\">" + SPECTRA_QUERY_1 + "</a>"; + private static final String SPECTRA_EX_2 = + "<a href=\"" + SPECTRA_QUERY_2 + "\">" + SPECTRA_QUERY_2 + "</a>"; + + private static final String SPECTRA_DESCRIPTION = + "Returns the response spectra of one or more GMMs for a given scenario defined " + + "by the following source and site parameters: " + GMM_INPUT_PARAMS + "<br><br>" + + "At a minimum, one GMM argument is required (default source and site parameters are used):" + + "<br><br>" + SPECTRA_EX_1 + "<br><br>" + + "Alternatively, multiple GMMs may be requested with custom source and site parameters:" + + "<br><br>" + SPECTRA_EX_2 + "<br><br>" + + "See the <i>nshmp-haz</i> documentation for the list of supported " + + "<a href=\"" + GMM_URL + "\">GMM IDs</a> and <a href=\"" + GMM_INPUT_URL + "\"> " + + "GMM input parameters</a> and built in <a href=\"" + DEFAULTS_URL + + "\"> default values</a>." + + "<br><br>For supported GMMs and *GmmInput* parameters with default values " + + "see the usage information." + + "<br><br>Given no query parameters the usage information is returned."; + + private static final String SPECTRA_SUMMARY = + "Ground motion model (GMM) deterministic response spectra"; + + @Inject + private NshmpMicronautServlet servlet; + + /** + * GET method for response spectra. + * + * <p> Web service path: /nshmp/data/spectra + * + * <p> A {@code GmmInput} is constructed off the remaining HTTP parameters + * that are not defined in the @Get + * + * @param request The HTTP request + * @param gmm The ground motion models + * @param Mw The moment magnitude of an earthquake ([-2, 9.7]) + * @param rJB The shortest distance from a site to the surface projection of a + * rupture, in kilometers ([0, 1000]) + * @param rRup The shortest distance from a site to a rupture, in kilometers + * ([0, 1000]) + * @param rX The shortest distance from a site to the extended trace a fault, + * in kilometers ([0, 1000]) + * @param dip The dip of a rupture surface, in degrees ([0, 90]) + * @param width The width of a rupture surface, in kilometers ([0, 60]) + * @param zTop The depth to the top of a rupture surface, in kilometers and + * positive-down ([0, 700]) + * @param zHyp The depth to the hypocenter on a rupture surface, in kilometers + * and positive-down ([0, 700]) + * @param rake The rake (or sense of slip) of a rupture surface, in degrees + * ([-180, 1080]) + * @param vs30 The average shear-wave velocity down to 30 meters, in + * kilometers per second ([150, 3000]) + * @param z1p0 Depth to a shear-wave velocity of 1.0 kilometers per second, in + * kilometers ([0, 5]) + * @param z2p5 Depth to a shear-wave velocity of 2.5 kilometers per second, in + * kilometers ([0, 10]) + * @param zSed Sediment thickness, in kilometers ([0, 20]) + */ + @Operation( + summary = SPECTRA_SUMMARY, + description = SPECTRA_DESCRIPTION, + operationId = "gmm_spectra_doGetSpectra") + @ApiResponse( + description = "Response Spectra Service", + responseCode = "200", + content = @Content(schema = @Schema(type = "string"))) + @Get(uri = "/spectra", produces = MediaType.APPLICATION_JSON) + public String doGetSpectra( + HttpRequest<?> http, + @Schema(required = true) @QueryValue @Nullable Set<Gmm> gmm, + @Schema( + defaultValue = "7.5", + minimum = "-2", + maximum = "9.7") @QueryValue @Nullable Double Mw, + @Schema( + defaultValue = "10", + minimum = "0", + maximum = "1000") @QueryValue @Nullable Double rJB, + @Schema( + defaultValue = "10.3", + minimum = "0", + maximum = "1000") @QueryValue @Nullable Double rRup, + @Schema( + defaultValue = "10", + minimum = "0", + maximum = "1000") @QueryValue @Nullable Double rX, + @Schema( + defaultValue = "90", + minimum = "0", + maximum = "90") @QueryValue @Nullable Double dip, + @Schema( + defaultValue = "14", + minimum = "0", + maximum = "60") @QueryValue @Nullable Double width, + @Schema( + defaultValue = "0.5", + minimum = "0", + maximum = "700") @QueryValue @Nullable Double zTop, + @Schema( + defaultValue = "7.5", + minimum = "0", + maximum = "700") @QueryValue @Nullable Double zHyp, + @Schema( + defaultValue = "0", + minimum = "-180", + maximum = "180") @QueryValue @Nullable Double rake, + @Schema( + defaultValue = "760", + minimum = "150", + maximum = "3000") @QueryValue @Nullable Double vs30, + @Schema( + defaultValue = "", + minimum = "0", + maximum = "5") @QueryValue @Nullable Double z1p0, + @Schema( + defaultValue = "", + minimum = "0", + maximum = "10") @QueryValue @Nullable Double z2p5, + @Schema( + defaultValue = "", + minimum = "0", + maximum = "20") @QueryValue @Nullable Double zSed) { + Id id = Id.SPECTRA; + try { + Set<Gmm> gmms = ServiceUtil.readGmms(http); + if (gmms.isEmpty()) { + return ServiceUtil.metadata(http, id); + } + GmmInput in = ServiceUtil.readGmmInput(http); + Request gmmRequest = new Request(http, id, gmms, in); + return Spectra.process(gmmRequest); + } catch (Exception e) { + return Utils.handleError(e, id.name, http.getUri().getPath()); + } + } + + /** + * GET method for ground motion vs. distance, with distance in log space. + * + * <p> Web service path: /nshmp/data/gmm/distance + * + * <p> A {@code GmmInput} is constructed off the remaining HTTP parameters + * that are not defined in the @Get + * + * @param request The HTTP request + * @param gmm The ground motion models + * @param imt The IMT + * @param rMin The minimum distance to compute ((0, 1000]) + * @param rMax The maximum distance to compute ((0, 1000]) + * @param Mw The moment magnitude of an earthquake ([-2, 9.7]) + * @param rJB The shortest distance from a site to the surface projection of a + * rupture, in kilometers ([0, 1000]) + * @param rRup The shortest distance from a site to a rupture, in kilometers + * ([0, 1000]) + * @param rX The shortest distance from a site to the extended trace a fault, + * in kilometers ([0, 1000]) + * @param dip The dip of a rupture surface, in degrees ([0, 90]) + * @param width The width of a rupture surface, in kilometers ([0, 60]) + * @param zTop The depth to the top of a rupture surface, in kilometers and + * positive-down ([0, 700]) + * @param zHyp The depth to the hypocenter on a rupture surface, in kilometers + * and positive-down ([0, 700]) + * @param rake The rake (or sense of slip) of a rupture surface, in degrees + * ([-180, 1080]) + * @param vs30 The average shear-wave velocity down to 30 meters, in + * kilometers per second ([150, 3000]) + * @param z1p0 Depth to a shear-wave velocity of 1.0 kilometers per second, in + * kilometers ([0, 5]) + * @param z2p5 Depth to a shear-wave velocity of 2.5 kilometers per second, in + * kilometers ([0, 10]) + * @param zSed Sediment thickness, in kilometers ([0, 20]) + */ + @Operation( + summary = "Return ground motion Vs. distance in log space given a ground motion model", + description = "Returns ground motion Vs. distance in log space " + + "given a GMM and a *GmmInput*.\n\n" + + "For supported GMMs and *GmmInput* parameters with default values " + + "see the usage information.\n\n" + + "Given no query parameters the usage information is returned.", + operationId = "gmm_distance_doGetDistance") + @ApiResponse( + description = "Ground motion Vs. distance in log space", + responseCode = "200", + content = @Content(schema = @Schema(type = "string"))) + @Get(uri = "/distance{?gmm,imt,rMin,rMax}", produces = MediaType.APPLICATION_JSON) + public String doGetDistance( + HttpRequest<?> http, + @Schema(required = true) @QueryValue @Nullable Set<Gmm> gmm, + @Schema(required = true) @QueryValue Optional<Imt> imt, + @Schema( + defaultValue = "0.001", + minimum = "0", + maximum = "1000", + exclusiveMinimum = true) @QueryValue @Nullable Double rMin, + @Schema( + defaultValue = "100", + minimum = "0", + maximum = "1000", + exclusiveMinimum = true) @QueryValue @Nullable Double rMax, + @Schema( + defaultValue = "6.5", + minimum = "-2", + maximum = "9.7") @QueryValue @Nullable Double Mw, + @Schema( + defaultValue = "10", + minimum = "0", + maximum = "1000") @QueryValue @Nullable Double rJB, + @Schema( + defaultValue = "10.3", + minimum = "0", + maximum = "1000") @QueryValue @Nullable Double rRup, + @Schema( + defaultValue = "10", + minimum = "0", + maximum = "1000") @QueryValue @Nullable Double rX, + @Schema( + defaultValue = "90", + minimum = "0", + maximum = "90") @QueryValue @Nullable Double dip, + @Schema( + defaultValue = "14", + minimum = "0", + maximum = "60") @QueryValue @Nullable Double width, + @Schema( + defaultValue = "0.5", + minimum = "0", + maximum = "700") @QueryValue @Nullable Double zTop, + @Schema( + defaultValue = "7.5", + minimum = "0", + maximum = "700") @QueryValue @Nullable Double zHyp, + @Schema( + defaultValue = "0", + minimum = "-180", + maximum = "180") @QueryValue @Nullable Double rake, + @Schema( + defaultValue = "760", + minimum = "150", + maximum = "3000") @QueryValue @Nullable Double vs30, + @Schema( + defaultValue = "", + minimum = "0", + maximum = "5") @QueryValue @Nullable Double z1p0, + @Schema( + defaultValue = "", + minimum = "0", + maximum = "10") @QueryValue @Nullable Double z2p5, + @Schema( + defaultValue = "", + minimum = "0", + maximum = "20") @QueryValue @Nullable Double zSed) { + Id id = Id.DISTANCE; + try { + Set<Gmm> gmms = ServiceUtil.readGmms(http); + if (gmms.isEmpty()) { + return ServiceUtil.metadata(http, id); + } + GmmInput in = ServiceUtil.readGmmInput(http); + Distance.Request request = new Distance.Request( + http, id, gmms, in, imt, Optional.ofNullable(rMin), Optional.ofNullable(rMax)); + return Distance.process(request); + } catch (Exception e) { + return Utils.handleError(e, id.name, http.getUri().getPath()); + } + } + + /** + * GET method for ground motion vs. distance. + * + * <p> Web service path: /nshmp/data/gmm/distance + * + * <p> A {@code GmmInput} is constructed off the remaining HTTP parameters + * that are not defined in the @Get + * + * @param request The HTTP request + * @param gmm The ground motion models + * @param imt The IMT + * @param rMin The minimum distance to compute ([-1000, 1000]) + * @param rMax The maximum distance to compute ([-1000, 1000]) + * @param Mw The moment magnitude of an earthquake ([-2, 9.7]) + * @param dip The dip of a rupture surface, in degrees ([0, 90]) + * @param width The width of a rupture surface, in kilometers ([0, 60]) + * @param zTop The depth to the top of a rupture surface, in kilometers and + * positive-down ([0, 700]) + * @param zHyp The depth to the hypocenter on a rupture surface, in kilometers + * and positive-down ([0, 700]) + * @param rake The rake (or sense of slip) of a rupture surface, in degrees + * ([-180, 1080]) + * @param vs30 The average shear-wave velocity down to 30 meters, in + * kilometers per second ([150, 3000]) + * @param z1p0 Depth to a shear-wave velocity of 1.0 kilometers per second, in + * kilometers ([0, 5]) + * @param z2p5 Depth to a shear-wave velocity of 2.5 kilometers per second, in + * kilometers ([0, 10]) + * @param zSed Sediment thickness, in kilometers ([0, 20]) + */ + @Operation( + summary = "Return ground motion Vs. distance given a ground motion model", + description = "Returns ground motion Vs. distance given a GMM and a *GmmInput*.\n\n" + + "For supported GMMs and *GmmInput* parameters with default values " + + "see the usage information.\n\n" + + "Given no query parameters the usage information is returned.", + operationId = "gmm_hwfw_doGetHwFw") + @ApiResponse( + description = "Ground motion Vs. distance", + responseCode = "200", + content = @Content(schema = @Schema(type = "string"))) + @Get(uri = "/hw-fw{?gmm,imt,rMin,rMax}", produces = MediaType.APPLICATION_JSON) + public String doGetHwFw( + HttpRequest<?> http, + @Schema(required = true) @QueryValue @Nullable Set<Gmm> gmm, + @Schema(required = true) @QueryValue Optional<Imt> imt, + @Schema( + defaultValue = "-100", + minimum = "-1000", + maximum = "1000", + exclusiveMinimum = true) @QueryValue @Nullable Double rMin, + @Schema( + defaultValue = "100", + minimum = "-1000", + maximum = "1000", + exclusiveMinimum = true) @QueryValue @Nullable Double rMax, + @Schema( + defaultValue = "6.5", + minimum = "-2", + maximum = "9.7") @QueryValue @Nullable Double Mw, + @Schema( + defaultValue = "90", + minimum = "0", + maximum = "90") @QueryValue @Nullable Double dip, + @Schema( + defaultValue = "14", + minimum = "0", + maximum = "60") @QueryValue @Nullable Double width, + @Schema( + defaultValue = "0.5", + minimum = "0", + maximum = "700") @QueryValue @Nullable Double zTop, + @Schema( + defaultValue = "7.5", + minimum = "0", + maximum = "700") @QueryValue @Nullable Double zHyp, + @Schema( + defaultValue = "0", + minimum = "-180", + maximum = "180") @QueryValue @Nullable Double rake, + @Schema( + defaultValue = "760", + minimum = "150", + maximum = "3000") @QueryValue @Nullable Double vs30, + @Schema( + defaultValue = "", + minimum = "0", + maximum = "5") @QueryValue @Nullable Double z1p0, + @Schema( + defaultValue = "", + minimum = "0", + maximum = "10") @QueryValue @Nullable Double z2p5, + @Schema( + defaultValue = "", + minimum = "0", + maximum = "20") @QueryValue @Nullable Double zSed) { + + Id id = Id.HW_FW; + try { + Set<Gmm> gmms = ServiceUtil.readGmms(http); + if (gmms.isEmpty()) { + return ServiceUtil.metadata(http, id); + } + GmmInput in = ServiceUtil.readGmmInput(http); + Distance.Request request = new Distance.Request( + http, id, gmms, in, imt, Optional.ofNullable(rMin), Optional.ofNullable(rMax)); + return Distance.process(request); + } catch (Exception e) { + return Utils.handleError(e, id.name, http.getUri().getPath()); + } + } + + /** + * GET method for ground motion vs. distance. + * + * <p> Web service path: /nshmp/data/gmm/distance + * + * <p> A {@code GmmInput} is constructed off the remaining HTTP parameters + * that are not defined in the @Get + * + * @param request The HTTP request + * @param gmm The ground motion models + * @param imt The IMT + * @param rMin The minimum distance to compute ([-1000, 1000]) + * @param rMax The maximum distance to compute ([-1000, 1000]) + * @param Mw The moment magnitude of an earthquake ([-2, 9.7]) + * @param dip The dip of a rupture surface, in degrees ([0, 90]) + * @param width The width of a rupture surface, in kilometers ([0, 60]) + * @param zTop The depth to the top of a rupture surface, in kilometers and + * positive-down ([0, 700]) + * @param zHyp The depth to the hypocenter on a rupture surface, in kilometers + * and positive-down ([0, 700]) + * @param rake The rake (or sense of slip) of a rupture surface, in degrees + * ([-180, 1080]) + * @param vs30 The average shear-wave velocity down to 30 meters, in + * kilometers per second ([150, 3000]) + * @param z1p0 Depth to a shear-wave velocity of 1.0 kilometers per second, in + * kilometers ([0, 5]) + * @param z2p5 Depth to a shear-wave velocity of 2.5 kilometers per second, in + * kilometers ([0, 10]) + * @param zSed Sediment thickness, in kilometers ([0, 20]) + */ + @Operation( + summary = "Return ground motion Vs. magnitude given a ground motion model", + description = "Returns ground motion Vs. magnitude given a GMM and a *GmmInput*.\n\n" + + "For supported GMMs and *GmmInput* parameters with default values " + + "see the usage information.\n\n" + + "Given no query parameters the usage information is returned.", + operationId = "gmm_magnitude_doGetMagnitude") + @ApiResponse( + description = "Ground motion Vs. magnitude", + responseCode = "200", + content = @Content(schema = @Schema(type = "string"))) + @Get(uri = "/magnitude{?gmm,imt,mMin,mMax,step}", produces = MediaType.APPLICATION_JSON) + public String doGetMagnitude( + HttpRequest<?> http, + @Schema(required = true) @QueryValue @Nullable Set<Gmm> gmm, + @Schema(required = true) @QueryValue Optional<Imt> imt, + @Schema( + defaultValue = "5", + minimum = "-2", + maximum = "9.7", + exclusiveMinimum = true) @QueryValue @Nullable Double mMin, + @Schema( + defaultValue = "8", + minimum = "-2", + maximum = "9.7", + exclusiveMinimum = true) @QueryValue @Nullable Double mMax, + @Schema( + defaultValue = "0.1", + minimum = "0", + maximum = "1", + exclusiveMinimum = true) @QueryValue @Nullable Double step, + @Schema( + defaultValue = "10", + minimum = "0", + maximum = "1000", + exclusiveMinimum = true) @QueryValue @Nullable Double distance, + @Schema( + defaultValue = "6.5", + minimum = "-2", + maximum = "9.7") @QueryValue @Nullable Double Mw, + @Schema( + defaultValue = "90", + minimum = "0", + maximum = "90") @QueryValue @Nullable Double dip, + @Schema( + defaultValue = "14", + minimum = "0", + maximum = "60") @QueryValue @Nullable Double width, + @Schema( + defaultValue = "0.5", + minimum = "0", + maximum = "700") @QueryValue @Nullable Double zTop, + @Schema( + defaultValue = "7.5", + minimum = "0", + maximum = "700") @QueryValue @Nullable Double zHyp, + @Schema( + defaultValue = "0", + minimum = "-180", + maximum = "180") @QueryValue @Nullable Double rake, + @Schema( + defaultValue = "760", + minimum = "150", + maximum = "3000") @QueryValue @Nullable Double vs30, + @Schema( + defaultValue = "", + minimum = "0", + maximum = "5") @QueryValue @Nullable Double z1p0, + @Schema( + defaultValue = "", + minimum = "0", + maximum = "10") @QueryValue @Nullable Double z2p5, + @Schema( + defaultValue = "", + minimum = "0", + maximum = "20") @QueryValue @Nullable Double zSed) { + Id id = Id.MAGNITUDE; + try { + Set<Gmm> gmms = ServiceUtil.readGmms(http); + if (gmms.isEmpty()) { + return ServiceUtil.metadata(http, id); + } + GmmInput in = ServiceUtil.readGmmInput(http); + Magnitude.Request gmmRequest = new Magnitude.Request( + http, id, gmms, in, imt, Optional.ofNullable(mMin), Optional.ofNullable(mMax), + Optional.ofNullable(step), Optional.ofNullable(distance)); + return Magnitude.process(gmmRequest); + } catch (Exception e) { + return Utils.handleError(e, id.name, http.getUri().getPath()); + } + } +} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/GmmService.java b/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/GmmService.java new file mode 100644 index 00000000..b4b5587f --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/GmmService.java @@ -0,0 +1,499 @@ +package gov.usgs.earthquake.nshmp.www.gmm; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import gov.usgs.earthquake.nshmp.Maths; +import gov.usgs.earthquake.nshmp.data.DoubleData; +import gov.usgs.earthquake.nshmp.data.XySequence; +import gov.usgs.earthquake.nshmp.gmm.Gmm; +import gov.usgs.earthquake.nshmp.gmm.GmmInput; +import gov.usgs.earthquake.nshmp.gmm.Imt; +import gov.usgs.earthquake.nshmp.www.HazVersion; +import gov.usgs.earthquake.nshmp.www.ResponseBody; +import gov.usgs.earthquake.nshmp.www.ResponseMetadata; +import gov.usgs.earthquake.nshmp.www.gmm.GmmCalc.EpiBranch; +import gov.usgs.earthquake.nshmp.www.gmm.GmmCalc.GmmDataXs; +import gov.usgs.earthquake.nshmp.www.gmm.GmmCalc.GmmSpectraData; +import gov.usgs.earthquake.nshmp.www.gmm.GmmCalc.SpectraTree; +import gov.usgs.earthquake.nshmp.www.gmm.XyDataGroup.EpiSeries; + +import io.micronaut.http.HttpRequest; +import jakarta.inject.Singleton; + +/* + * GMM service implementations. + * + * @author U.S. Geological Survey + */ +@Singleton +class GmmService { + + /* Base request object for all GMM services. */ + static class Request { + + transient final HttpRequest<?> http; + transient final Id serviceId; + + final Set<Gmm> gmms; + final GmmInput input; + + public Request( + HttpRequest<?> http, Id id, + Set<Gmm> gmms, GmmInput input) { + this.http = http; + this.serviceId = id; + this.input = input; + this.gmms = gmms; + } + } + + /* Response object for all GMM services. */ + static class Response<T, U> { + + XyDataGroup<T, U> means; + XyDataGroup<T, U> sigmas; + + private Response(Id service, Optional<Imt> imt) { + String yLabelMedian = imt.isPresent() + ? String.format("%s (%s)", service.yLabelMedian, imt.get().units()) + : service.yLabelMedian; + + means = XyDataGroup.create( + service.groupNameMean, + service.xLabel, + yLabelMedian); + + sigmas = XyDataGroup.create( + service.groupNameSigma, + service.xLabel, + service.yLabelSigma); + } + + static Response<XySequence, EpiSeries<double[]>> create( + Id service, + Map<Gmm, GmmDataXs<double[]>> result, + Optional<Imt> imt) { + + Response<XySequence, EpiSeries<double[]>> response = new Response<>(service, imt); + + for (Entry<Gmm, GmmDataXs<double[]>> entry : result.entrySet()) { + Gmm gmm = entry.getKey(); + GmmDataXs<double[]> data = entry.getValue(); + + XySequence μTotal = XySequence.create(data.xs, formatExp(data.μs)); + XySequence σTotal = XySequence.create(data.xs, format(data.σs)); + + EpiGroup<double[]> group = treeToEpiGroup(data.tree); + response.means.add(gmm.name(), gmm.toString(), μTotal, group.μ); + response.sigmas.add(gmm.name(), gmm.toString(), σTotal, group.sigma); + } + + return response; + } + + private static EpiGroup<Double> treeToEpiGroupSingle(List<EpiBranch<Double>> tree) { + List<EpiSeries<Double>> μEpi = List.of(); + List<EpiSeries<Double>> sigmaEpi = List.of(); + + // If we have a GMM with a single GroundMotion branch, + // then the DataGroup branch list is empty + if (!tree.isEmpty()) { + boolean hasSigmaBranches = tree.get(0).id.contains(" : "); + + μEpi = tree.stream() + .map(b -> new EpiSeries<>(b.id, formatExp(b.μs), b.weights)) + .collect(Collectors.toList()); + + if (hasSigmaBranches) { + μEpi = collapseGmTreeSingle(μEpi, 0); + + sigmaEpi = tree.stream() + .map(b -> new EpiSeries<>(b.id, format(b.σs), b.weights)) + .collect(Collectors.toList()); + sigmaEpi = collapseGmTreeSingle(sigmaEpi, 1); + } + } + + return new EpiGroup<>(μEpi, sigmaEpi); + } + + private static EpiGroup<double[]> treeToEpiGroup(List<EpiBranch<double[]>> tree) { + List<EpiSeries<double[]>> μEpi = List.of(); + List<EpiSeries<double[]>> sigmaEpi = List.of(); + + // If we have a GMM with a single GroundMotion branch, + // then the DataGroup branch list is empty + if (!tree.isEmpty()) { + boolean hasSigmaBranches = tree.get(0).id.contains(" : "); + + μEpi = tree.stream() + .map(b -> new EpiSeries<>(b.id, formatExp(b.μs), b.weights)) + .collect(Collectors.toList()); + + if (hasSigmaBranches) { + μEpi = collapseGmTree(μEpi, 0); + + sigmaEpi = tree.stream() + .map(b -> new EpiSeries<>(b.id, format(b.σs), b.weights)) + .collect(Collectors.toList()); + sigmaEpi = collapseGmTree(sigmaEpi, 1); + } + } + + return new EpiGroup<>(μEpi, sigmaEpi); + } + + static Response<SpectraData, SpectraTree> spectraCreate( + Id service, + Map<Gmm, GmmSpectraData> result, + Optional<Imt> imt) { + + Response<SpectraData, SpectraTree> response = new Response<>(service, imt); + + for (Entry<Gmm, GmmSpectraData> entry : result.entrySet()) { + Gmm gmm = entry.getKey(); + GmmSpectraData data = entry.getValue(); + + XySequence saμTotal = XySequence.create(data.sa.xs, formatExp(data.sa.μs)); + XySequence saσTotal = XySequence.create(data.sa.xs, format(data.sa.σs)); + + SpectraData μTotal = new SpectraData( + data.pga.isPresent() ? formatExp(data.pga.get().μs) : null, + data.pgv.isPresent() ? formatExp(data.pgv.get().μs) : null, + saμTotal); + + SpectraData σTotal = new SpectraData( + data.pga.isPresent() ? format(data.pga.get().σs) : null, + data.pgv.isPresent() ? format(data.pgv.get().σs) : null, + saσTotal); + + EpiGroup<double[]> saGroup = treeToEpiGroup(data.sa.tree); + Optional<EpiGroup<Double>> pgaGroup = Optional.empty(); + Optional<EpiGroup<Double>> pgvGroup = Optional.empty(); + + if (data.pga.isPresent()) { + pgaGroup = Optional.of(treeToEpiGroupSingle(data.pga.get().tree)); + } + + if (data.pgv.isPresent()) { + pgvGroup = Optional.of(treeToEpiGroupSingle(data.pgv.get().tree)); + } + + List<SpectraTree> μTrees = SpectraTree.toList( + pgaGroup.isPresent() ? pgaGroup.get().μ : List.of(), + pgvGroup.isPresent() ? pgvGroup.get().μ : List.of(), + saGroup.μ); + + List<SpectraTree> sigmaTrees = SpectraTree.toList( + pgaGroup.isPresent() ? pgaGroup.get().sigma : List.of(), + pgvGroup.isPresent() ? pgvGroup.get().sigma : List.of(), + saGroup.sigma); + + response.means.add(gmm.name(), gmm.toString(), μTotal, μTrees); + response.sigmas.add(gmm.name(), gmm.toString(), σTotal, sigmaTrees); + } + + return response; + } + } + + static class Spectra { + + static String process(Request request) { + Map<Gmm, GmmSpectraData> spectra = GmmCalc.spectra(request); + Response<SpectraData, SpectraTree> response = + Response.spectraCreate(request.serviceId, spectra, Optional.empty()); + var body = ResponseBody.success() + .name(request.serviceId.name) + .url(request.http.getUri().getPath()) + .metadata(new ResponseMetadata(HazVersion.appVersions())) + .request(request) + .response(response) + .build(); + return ServiceUtil.GSON.toJson(body); + } + + } + + static class Distance { + + private static final double R_MIN = -100.0; + private static final double R_MAX = 100.0; + private final static int R_POINTS = 100; + + static String process(Distance.Request request) { + + double[] rArray = distanceArray(request); + + Map<Gmm, GmmDataXs<double[]>> gmvr = GmmCalc.distance(request, rArray); + Response<XySequence, EpiSeries<double[]>> response = + Response.create(request.serviceId, gmvr, Optional.of(request.imt)); + var body = ResponseBody.success() + .name(request.serviceId.name) + .url(request.http.getUri().getPath()) + .metadata(new ResponseMetadata(HazVersion.appVersions())) + .request(request) + .response(response) + .build(); + return ServiceUtil.GSON.toJson(body); + } + + private static double[] distanceArray( + GmmService.Distance.Request request) { + boolean isLog = request.serviceId.equals(Id.DISTANCE) ? true : false; + double rStep = isLog + ? (Math.log10(request.rMax / request.rMin)) / (R_POINTS - 1) + : 1.0; + return isLog + ? ServiceUtil.sequenceLog(request.rMin, request.rMax, rStep) + : ServiceUtil.sequenceLinear(request.rMin, request.rMax, rStep); + } + + static class Request extends GmmService.Request { + + Imt imt; + double rMin; + double rMax; + + Request( + HttpRequest<?> http, Id serviceId, + Set<Gmm> gmms, GmmInput in, + Optional<Imt> imt, + Optional<Double> rMin, + Optional<Double> rMax) { + + super(http, serviceId, gmms, in); + this.imt = imt.orElse(Imt.PGA); + this.rMin = rMin.orElse(R_MIN); + this.rMax = rMax.orElse(R_MAX); + } + } + } + + static class Magnitude { + + public static final double M_MIN = 5.0; + public static final double M_MAX = 8.0; + public static final double M_STEP = 0.1; + public static final double M_DISTANCE = 10.0; + + static String process(Magnitude.Request request) { + + double[] mArray = ServiceUtil.sequenceLinear( + request.mMin, request.mMax, request.step); + + Map<Gmm, GmmDataXs<double[]>> gmvm = GmmCalc.magnitude(request, mArray, request.distance); + Response<XySequence, EpiSeries<double[]>> response = + Response.create(request.serviceId, gmvm, Optional.of(request.imt)); + var body = ResponseBody.success() + .name(request.serviceId.name) + .url(request.http.getUri().getPath()) + .metadata(new ResponseMetadata(HazVersion.appVersions())) + .request(request) + .response(response) + .build(); + return ServiceUtil.GSON.toJson(body); + } + + static class Request extends GmmService.Request { + + Imt imt; + double mMin; + double mMax; + double step; + double distance; + + Request( + HttpRequest<?> http, Id serviceId, + Set<Gmm> gmms, GmmInput in, + Optional<Imt> imt, + Optional<Double> mMin, + Optional<Double> mMax, + Optional<Double> step, + Optional<Double> distance) { + + super(http, serviceId, gmms, in); + this.imt = imt.orElse(Imt.PGA); + this.mMin = mMin.orElse(M_MIN); + this.mMax = mMax.orElse(M_MAX); + this.step = step.orElse(M_STEP); + this.distance = distance.orElse(M_DISTANCE); + } + } + } + + /* + * Consolidators of epi branches. GMMs return logic trees that represent all + * combinations of means and sigmas. For the response spectra service we want + * those branches with either common means or common sigmas recombined. + * + * We do know a bit about the branch IDs in ground motion logic trees and we + * can leverage that when combining branches. They always take the form + * "μ_id : σ_id" so and index of 0 will consolidate using the IDs of the mean + * branches and an index of 1 will consolidate using the IDs of the sigma + * branches. + */ + + static List<EpiSeries<Double>> collapseGmTreeSingle(List<EpiSeries<Double>> epiList, int index) { + List<String> ids = new ArrayList<>(); + Map<String, Double> valueMap = new HashMap<>(); + Map<String, Double> wtMap = new HashMap<>(); + + for (EpiSeries<Double> epi : epiList) { + String id = epi.id.split(" : ")[index]; + + if (!ids.contains(id)) { + ids.add(id); + valueMap.put(id, epi.values); + wtMap.put(id, epi.weights); + } else { + wtMap.merge(id, epi.weights, (a, b) -> a + b); + } + } + + return ids.stream() + .map(id -> new EpiSeries<>(id, valueMap.get(id), format(wtMap.get(id)))) + .collect(Collectors.toList()); + } + + static List<EpiSeries<double[]>> collapseGmTree(List<EpiSeries<double[]>> epiList, int index) { + + List<String> ids = new ArrayList<>(); + Map<String, double[]> valueMap = new HashMap<>(); + Map<String, double[]> wtMap = new HashMap<>(); + + for (EpiSeries<double[]> epi : epiList) { + String id = epi.id.split(" : ")[index]; + if (!ids.contains(id)) { + ids.add(id); + valueMap.put(id, epi.values); + wtMap.put(id, Arrays.copyOf(epi.weights, epi.weights.length)); + } else { + wtMap.merge(id, epi.weights, DoubleData::add); + } + } + return ids.stream() + .map(id -> new EpiSeries<>(id, valueMap.get(id), format(wtMap.get(id)))) + .collect(Collectors.toList()); + } + + private static double formatExp(double value) { + return Maths.roundToDigits(Math.exp(value), ServiceUtil.ROUND); + } + + private static double[] formatExp(double[] values) { + return Arrays.stream(values) + .map(d -> formatExp(d)) + .toArray(); + } + + private static double format(double value) { + return Maths.roundToDigits(value, ServiceUtil.ROUND); + } + + private static double[] format(double[] values) { + return Arrays.stream(values) + .map(d -> format(d)) + .toArray(); + } + + static class SpectraData { + final Double pga; + final Double pgv; + final XySequence sa; + + SpectraData(Double pga, Double pgv, XySequence sa) { + this.pga = pga; + this.pgv = pgv; + this.sa = sa; + } + } + + private static class EpiGroup<T> { + final List<EpiSeries<T>> μ; + final List<EpiSeries<T>> sigma; + + EpiGroup(List<EpiSeries<T>> μ, List<EpiSeries<T>> sigma) { + this.μ = μ; + this.sigma = sigma; + } + } + + public static enum Id { + + DISTANCE( + "Ground Motion Vs. Distance", + "Compute ground motion Vs. distance", + "/distance", + "Means", + "Sigmas", + "Distance (km)", + "Median ground motion", + "Standard deviation"), + + HW_FW( + "Hanging Wall Effect", + "Compute hanging wall effect on ground motion Vs. distance", + "/hw-fw", + "Means", + "Sigmas", + "Distance (km)", + "Median ground motion", + "Standard deviation"), + + MAGNITUDE( + "Ground Motion Vs. Magnitude", + "Compute ground motion Vs. magnitude", + "/magnitude", + "Means", + "Sigmas", + "Magnitude", + "Median ground motion", + "Standard deviation"), + + SPECTRA( + "Deterministic Response Spectra", + "Compute deterministic response spectra", + "/spectra", + "Means", + "Sigmas", + "Period (s)", + "Median ground motion (g)", + "Standard deviation"); + + public final String name; + public final String description; + public final String pathInfo; + public final String resultName; + public final String groupNameMean; + public final String groupNameSigma; + public final String xLabel; + public final String yLabelMedian; + public final String yLabelSigma; + + private Id( + String name, String description, + String pathInfo, String groupNameMean, + String groupNameSigma, String xLabel, + String yLabelMedian, String yLabelSigma) { + this.name = name; + this.description = description; + this.resultName = name + " Results"; + this.pathInfo = pathInfo; + this.groupNameMean = groupNameMean; + this.groupNameSigma = groupNameSigma; + this.xLabel = xLabel; + this.yLabelMedian = yLabelMedian; + this.yLabelSigma = yLabelSigma; + } + } + +} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/ServiceUtil.java b/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/ServiceUtil.java new file mode 100644 index 00000000..d4ad5af4 --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/ServiceUtil.java @@ -0,0 +1,459 @@ +package gov.usgs.earthquake.nshmp.www.gmm; + +import static gov.usgs.earthquake.nshmp.gmm.GmmInput.Field.DIP; +import static gov.usgs.earthquake.nshmp.gmm.GmmInput.Field.MW; +import static gov.usgs.earthquake.nshmp.gmm.GmmInput.Field.RAKE; +import static gov.usgs.earthquake.nshmp.gmm.GmmInput.Field.RJB; +import static gov.usgs.earthquake.nshmp.gmm.GmmInput.Field.RRUP; +import static gov.usgs.earthquake.nshmp.gmm.GmmInput.Field.RX; +import static gov.usgs.earthquake.nshmp.gmm.GmmInput.Field.VS30; +import static gov.usgs.earthquake.nshmp.gmm.GmmInput.Field.WIDTH; +import static gov.usgs.earthquake.nshmp.gmm.GmmInput.Field.Z1P0; +import static gov.usgs.earthquake.nshmp.gmm.GmmInput.Field.Z2P5; +import static gov.usgs.earthquake.nshmp.gmm.GmmInput.Field.ZHYP; +import static gov.usgs.earthquake.nshmp.gmm.GmmInput.Field.ZSED; +import static gov.usgs.earthquake.nshmp.gmm.GmmInput.Field.ZTOR; +import static gov.usgs.earthquake.nshmp.gmm.Imt.AI; +import static gov.usgs.earthquake.nshmp.gmm.Imt.PGD; +import static io.micronaut.core.type.Argument.DOUBLE; +import static java.util.stream.Collectors.toCollection; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.EnumSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import com.google.common.collect.Range; +import com.google.common.primitives.Doubles; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +import gov.usgs.earthquake.nshmp.data.DoubleData; +import gov.usgs.earthquake.nshmp.data.Sequences; +import gov.usgs.earthquake.nshmp.gmm.Gmm; +import gov.usgs.earthquake.nshmp.gmm.GmmInput; +import gov.usgs.earthquake.nshmp.gmm.GmmInput.Constraints; +import gov.usgs.earthquake.nshmp.gmm.GmmInput.Field; +import gov.usgs.earthquake.nshmp.gmm.Imt; +import gov.usgs.earthquake.nshmp.model.TectonicSetting; +import gov.usgs.earthquake.nshmp.www.HazVersion; +import gov.usgs.earthquake.nshmp.www.ResponseBody; +import gov.usgs.earthquake.nshmp.www.ResponseMetadata; +import gov.usgs.earthquake.nshmp.www.WsUtils; +import gov.usgs.earthquake.nshmp.www.gmm.GmmService.Id; +import gov.usgs.earthquake.nshmp.www.meta.EnumParameter; + +import io.micronaut.http.HttpParameters; +import io.micronaut.http.HttpRequest; + +class ServiceUtil { + + final static int ROUND = 5; + static final Range<Double> MAGNITUDE_DEFAULT_VALUES = Range.closed(5.0, 8.0); + static final double MAGNITUDE_DEFAULT_STEP = 0.1; + static final double MAGNITUDE_DEFAULT_DISTANCE = 10.0; + static final Range<Double> DISTANCE_DEFAULT_VALUES = Range.closed(-100.0, 100.0); + + static final Gson GSON; + + static { + GSON = new GsonBuilder() + .setPrettyPrinting() + .serializeNulls() + .disableHtmlEscaping() + .registerTypeAdapter(Double.class, new WsUtils.NaNSerializer()) + .registerTypeAdapter(ServiceUtil.Parameters.class, + new ServiceUtil.Parameters.Serializer()) + .registerTypeAdapter(Imt.class, new WsUtils.EnumSerializer<Imt>()) + .registerTypeAdapter(Constraints.class, new WsUtils.ConstraintsSerializer()) + .create(); + } + + public static String metadata(HttpRequest<?> request, Id service) { + return GSON.toJson(ServiceUtil.getMetadata(request, service)); + } + + /** Query and JSON reqest/response keys. */ + static final class Key { + public static final String DISTANCE = "distance"; + public static final String IMT = "imt"; + public static final String GMM = "gmm"; + public static final String M_MIN = "mMin"; + public static final String M_MAX = "mMax"; + public static final String R_MAX = "rMax"; + public static final String R_MIN = "rMin"; + public static final String STEP = "step"; + } + + /* Read the GMM input query values. */ + static GmmInput readGmmInput(HttpRequest<?> request) { + HttpParameters params = request.getParameters(); + GmmInput.Builder b = GmmInput.builder().withDefaults(); + params.getFirst(MW.id, DOUBLE).ifPresent(b::mag); + params.getFirst(RJB.id, DOUBLE).ifPresent(b::rJB); + params.getFirst(RRUP.id, DOUBLE).ifPresent(b::rRup); + params.getFirst(RX.id, DOUBLE).ifPresent(b::rX); + params.getFirst(DIP.id, DOUBLE).ifPresent(b::dip); + params.getFirst(WIDTH.id, DOUBLE).ifPresent(b::width); + params.getFirst(ZTOR.id, DOUBLE).ifPresent(b::zTor); + params.getFirst(ZHYP.id, DOUBLE).ifPresent(b::zHyp); + params.getFirst(RAKE.id, DOUBLE).ifPresent(b::rake); + params.getFirst(VS30.id, DOUBLE).ifPresent(b::vs30); + params.getFirst(Z1P0.id, DOUBLE).ifPresent(b::z1p0); + params.getFirst(Z2P5.id, DOUBLE).ifPresent(b::z2p5); + params.getFirst(ZSED.id, DOUBLE).ifPresent(b::zSed); + return b.build(); + } + + /* Read the 'gmm' query values. */ + static Set<Gmm> readGmms(HttpRequest<?> request) { + return request.getParameters() + .getAll(Key.GMM) + .stream() + .map(s -> s.split(",")) + .flatMap(Arrays::stream) + .map(Gmm::valueOf) + .collect(toCollection(() -> EnumSet.noneOf(Gmm.class))); + } + + static ResponseBody<String, MetadataResponse> getMetadata( + HttpRequest<?> request, + Id service) { + + String url = request.getUri().getPath(); + return ResponseBody.<String, MetadataResponse> usage() + .name(service.name) + .url(url) + .metadata(new ResponseMetadata(HazVersion.appVersions())) + .request(url) + .response(new MetadataResponse(request, service)) + .build(); + } + + static double[] sequenceLog(double min, double max, double step) { + return DoubleData.round( + ROUND, + DoubleData.pow10( + Sequences.arrayBuilder(Math.log10(min), Math.log10(max), step) + .centered() + .build())); + } + + static double[] sequenceLinear(double min, double max, double step) { + return Sequences.arrayBuilder(min, max, step) + .scale(ROUND) + .centered() + .build(); + } + + static final class MetadataResponse { + String description; + String syntax; + Parameters parameters; + + public MetadataResponse(HttpRequest<?> request, Id service) { + this.syntax = request.getUri().getPath(); + this.description = service.description; + this.parameters = new Parameters(service); + } + } + + @SuppressWarnings("unchecked") + private static Param createGmmInputParam( + Field field, + Optional<?> constraint) { + + // if (field == VSINF) return new BooleanParam(field); + return new NumberParam(field, (Range<Double>) constraint.orElseThrow()); + } + + /* + * Placeholder class; all parameter serialization is done via the custom + * Serializer. Service reference needed serialize(). + */ + static final class Parameters { + + private final Id service; + + Parameters(Id service) { + this.service = service; + } + + static final class Serializer implements JsonSerializer<Parameters> { + + @Override + public JsonElement serialize( + Parameters meta, + Type type, + JsonSerializationContext context) { + + JsonObject root = new JsonObject(); + + if (!meta.service.equals(Id.SPECTRA)) { + Set<Imt> imtSet = EnumSet.complementOf(EnumSet.range(PGD, AI)); + final EnumParameter<Imt> imts = new EnumParameter<>( + "Intensity measure type", + imtSet); + root.add(Key.IMT, context.serialize(imts)); + } + + /* Serialize input fields. */ + Constraints defaults = Constraints.defaults(); + for (Field field : Field.values()) { + if (!meta.service.equals(Id.SPECTRA) && + (field.equals(Field.RX) || field.equals(Field.RRUP) || field.equals(Field.RJB))) { + continue; + } + + Param param = createGmmInputParam(field, defaults.get(field)); + JsonElement fieldElem = context.serialize(param); + root.add(field.id, fieldElem); + } + + if (meta.service.equals(Id.DISTANCE) || meta.service.equals(Id.HW_FW)) { + double min = meta.service.equals(Id.HW_FW) + ? Range.openClosed(0.0, 1.0).lowerEndpoint() + : DISTANCE_DEFAULT_VALUES.lowerEndpoint(); + NumberParam rMin = new NumberParam( + "Minimum distance", + "The minimum distance to use for calculation", + Field.RX.units.orElse(null), + Range.openClosed(0.0, 1000.0), + min); + root.add(Key.R_MIN, context.serialize(rMin)); + + NumberParam rMax = new NumberParam( + "Maximum distance", + "The maximum distance to use for calculation", + Field.RX.units.orElse(null), + Range.closed(-1000.0, 1000.0), + DISTANCE_DEFAULT_VALUES.upperEndpoint()); + root.add(Key.R_MAX, context.serialize(rMax)); + } + + if (meta.service.equals(Id.MAGNITUDE)) { + @SuppressWarnings("unchecked") + NumberParam mMin = new NumberParam( + "Minimum distance", + "The minimum distance to use for calculation", + Field.MW.units.orElse(null), + (Range<Double>) defaults.get(Field.MW).orElseThrow(), + MAGNITUDE_DEFAULT_VALUES.lowerEndpoint()); + root.add(Key.M_MIN, context.serialize(mMin)); + + @SuppressWarnings("unchecked") + NumberParam mMax = new NumberParam( + "Maximum magnitude", + "The maximum magnitude to use for calculation", + Field.MW.units.orElse(null), + (Range<Double>) defaults.get(Field.MW).orElseThrow(), + MAGNITUDE_DEFAULT_VALUES.upperEndpoint()); + root.add(Key.M_MAX, context.serialize(mMax)); + + @SuppressWarnings("unchecked") + NumberParam distance = new NumberParam( + "Distance", + "The distance between site and trace ", + Field.RX.units.orElse(null), + (Range<Double>) defaults.get(Field.RX).orElseThrow(), + MAGNITUDE_DEFAULT_DISTANCE); + root.add(Key.DISTANCE, context.serialize(distance)); + + NumberParam step = new NumberParam( + "Magnitude step", + "The step between each magnitude ", + null, + Range.closed(0.0001, 1.0), + MAGNITUDE_DEFAULT_STEP); + root.add(Key.STEP, context.serialize(step)); + } + + /* Add only add those Gmms that belong to a Group. */ + List<Gmm> gmms = Arrays.stream(Gmm.Group.values()) + .flatMap(group -> group.gmms().stream()) + .sorted(Comparator.comparing(Object::toString)) + .distinct() + .collect(Collectors.toList()); + + GmmParam gmmParam = new GmmParam( + GMM_NAME, + GMM_INFO, + gmms); + root.add(Key.GMM, context.serialize(gmmParam)); + + /* Add gmm groups. */ + GroupParam groups = new GroupParam( + GROUP_NAME, + GROUP_INFO, + EnumSet.allOf(Gmm.Group.class)); + root.add(GROUP_KEY, context.serialize(groups)); + + return root; + } + } + } + + /* + * Marker interface for spectra parameters. This was previously implemented as + * an abstract class for label, info, and units, but Gson serialized subclass + * fields before parent fields. To maintain a preferred order, one can write + * custom serializers or repeat these four fields in each implementation. + */ + private static interface Param {} + + @SuppressWarnings("unused") + private static final class NumberParam implements Param { + + final String label; + final String info; + final String units; + final Double min; + final Double max; + final Double value; + + NumberParam(GmmInput.Field field, Range<Double> constraint) { + this(field, constraint, field.defaultValue); + } + + NumberParam(GmmInput.Field field, Range<Double> constraint, Double value) { + this.label = field.label; + this.info = field.info; + this.units = field.units.orElse(null); + this.min = constraint.lowerEndpoint(); + this.max = constraint.upperEndpoint(); + this.value = Doubles.isFinite(value) ? value : null; + } + + NumberParam(String label, String info, String units, Range<Double> constraint, Double value) { + this.label = label; + this.info = info; + this.units = units; + this.min = constraint.lowerEndpoint(); + this.max = constraint.upperEndpoint(); + this.value = Doubles.isFinite(value) ? value : null; + } + } + + @SuppressWarnings("unused") + @Deprecated + private static final class BooleanParam implements Param { + + // used to support vsInf + // will likely be used for other flags in future (e.g. vertical GM) + + final String label; + final String info; + final boolean value; + + BooleanParam(GmmInput.Field field) { + this(field, field.defaultValue == 1.0); + } + + BooleanParam(GmmInput.Field field, boolean value) { + this.label = field.label; + this.info = field.info; + this.value = value; + } + } + + private static final String GMM_NAME = "Ground Motion Models"; + private static final String GMM_INFO = "Empirical models of ground motion"; + + @SuppressWarnings("unused") + private static class GmmParam implements Param { + + final String label; + final String info; + final List<Value> values; + + GmmParam(String label, String info, List<Gmm> gmms) { + this.label = label; + this.info = info; + this.values = gmms.stream() + .map(gmm -> new Value(gmm)) + .collect(Collectors.toList()); + } + + private static class Value { + + final String id; + final String label; + final Gmm.Type type; + final ArrayList<String> supportedImts; + final Constraints constraints; + + Value(Gmm gmm) { + this.id = gmm.name(); + this.label = gmm.toString(); + this.type = gmm.type(); + this.supportedImts = supportedImts(gmm.supportedImts()); + this.constraints = gmm.constraints(); + } + } + + private static ArrayList<String> supportedImts(Set<Imt> imts) { + ArrayList<String> supportedImts = new ArrayList<>(); + + for (Imt imt : imts) { + supportedImts.add(imt.name()); + } + + return supportedImts; + } + + } + + private static final String GROUP_KEY = "group"; + private static final String GROUP_NAME = "Ground Motion Model Groups"; + private static final String GROUP_INFO = "Groups of related ground motion models "; + + @SuppressWarnings("unused") + private static final class GroupParam implements Param { + + final String label; + final String info; + final List<Value> values; + + GroupParam(String label, String info, Set<Gmm.Group> groups) { + this.label = label; + this.info = info; + this.values = new ArrayList<>(); + for (Gmm.Group group : groups) { + this.values.add(new Value(group)); + } + } + + private static class Value { + + final String id; + final String label; + final List<Gmm> data; + final String type; + + Value(Gmm.Group group) { + this.id = group.name(); + this.label = group.toString(); + this.data = group.gmms(); + + /* TODO get group gmm type directly from group */ + if (group.toString().contains("Active Volcanic (HI)")) { + this.type = TectonicSetting.VOLCANIC.name(); + } else { + this.type = group.gmms().stream() + .map(gmm -> gmm.type().name()) + .findFirst() + .orElseThrow(); + } + } + } + } +} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/Utils.java b/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/Utils.java new file mode 100644 index 00000000..66668f22 --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/Utils.java @@ -0,0 +1,65 @@ +package gov.usgs.earthquake.nshmp.www.gmm; + +import static com.google.common.base.CaseFormat.LOWER_CAMEL; +import static com.google.common.base.CaseFormat.UPPER_UNDERSCORE; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import gov.usgs.earthquake.nshmp.www.HazVersion; +import gov.usgs.earthquake.nshmp.www.ResponseBody; +import gov.usgs.earthquake.nshmp.www.ResponseMetadata; + +public class Utils { + public static final Gson GSON; + + static { + GSON = new GsonBuilder() + .disableHtmlEscaping() + .serializeNulls() + .setPrettyPrinting() + .create(); + } + + public enum Key { + DISTANCE, + ID, + IMT, + GMM, + GPSDATASET, + GROUP, + LABEL, + LATITUDE, + LONGITUDE, + M_MIN, + M_MAX, + MODEL, + NAME, + R_MAX, + R_MIN, + STEP, + Z1P0, + Z2P5; + + @Override + public String toString() { + return UPPER_UNDERSCORE.to(LOWER_CAMEL, name()); + } + } + + public static String handleError( + Throwable e, + String name, + String url) { + var msg = e.getMessage() + " (see logs)"; + var svcResponse = ResponseBody.error() + .name(name) + .url(url) + .metadata(new ResponseMetadata(HazVersion.appVersions())) + .request(msg) + .response(url) + .build(); + e.printStackTrace(); + return GSON.toJson(svcResponse); + } +} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/XyDataGroup.java b/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/XyDataGroup.java new file mode 100644 index 00000000..c7a233ce --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/XyDataGroup.java @@ -0,0 +1,65 @@ +package gov.usgs.earthquake.nshmp.www.gmm; + +import java.util.ArrayList; +import java.util.List; + +/* + * XY data sequences serialization object. + * + * @author U.S. Geological Survey + */ +@SuppressWarnings("unused") +class XyDataGroup<T, U> { + + private final String label; + private final String xLabel; + private final String yLabel; + private final List<Series<T, U>> data; + + private XyDataGroup(String label, String xLabel, String yLabel) { + this.label = label; + this.xLabel = xLabel; + this.yLabel = yLabel; + this.data = new ArrayList<>(); + } + + /* Create a data group. */ + static <T, U> XyDataGroup<T, U> create(String name, String xLabel, String yLabel) { + return new XyDataGroup<T, U>(name, xLabel, yLabel); + } + + /* Add a data sequence */ + XyDataGroup<T, U> add(String id, String name, T data, List<U> tree) { + this.data.add(new Series<>(id, name, data, tree)); + return this; + } + + static class Series<T, U> { + + private final String id; + private final String label; + private final T data; + private final List<U> tree; + + Series(String id, String label, T data, List<U> tree) { + this.id = id; + this.label = label; + this.data = data; + this.tree = tree; + } + } + + static class EpiSeries<T> { + + final String id; + final T values; + final T weights; + + EpiSeries(String id, T values, T weights) { + this.id = id; + this.values = values; + this.weights = weights; + } + } + +} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/meta/EnumParameter.java b/src/main/java/gov/usgs/earthquake/nshmp/www/meta/EnumParameter.java new file mode 100644 index 00000000..154bedc4 --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/meta/EnumParameter.java @@ -0,0 +1,22 @@ +package gov.usgs.earthquake.nshmp.www.meta; + +import java.util.Set; + +/** + * An enum parameter. + * + * @author U.S. Geological Survey + * + * @param <E> The enum type + */ +public final class EnumParameter<E extends Enum<E>> { + + private final String label; + private final Set<E> values; + + public EnumParameter(String label, Set<E> values) { + this.label = label; + this.values = values; + } + +} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/meta/Status.java b/src/main/java/gov/usgs/earthquake/nshmp/www/meta/Status.java new file mode 100644 index 00000000..9511b464 --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/meta/Status.java @@ -0,0 +1,18 @@ +package gov.usgs.earthquake.nshmp.www.meta; + +/** + * Service request status identifier. + * + * @author U.S. Geological Survey + */ +public class Status { + + /** Error reponse status. */ + public static final String ERROR = "error"; + + /** Success reponse status. */ + public static final String SUCCESS = "success"; + + /** Usage reponse status. */ + public static final String USAGE = "usage"; +} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/meta/StringParameter.java b/src/main/java/gov/usgs/earthquake/nshmp/www/meta/StringParameter.java new file mode 100644 index 00000000..d8262749 --- /dev/null +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/meta/StringParameter.java @@ -0,0 +1,20 @@ +package gov.usgs.earthquake.nshmp.www.meta; + +import java.util.Set; + +/** + * A string parameter. + * + * @author U.S. Geological Survey + */ +public class StringParameter { + + public final String label; + public final Set<String> values; + + public StringParameter(String label, Set<String> values) { + this.label = label; + this.values = values; + } + +} -- GitLab From 37ba79155a20b9d8bbea768a86e2a918a3c0dd6e Mon Sep 17 00:00:00 2001 From: Brandon Clayton <bclayton@usgs.gov> Date: Fri, 28 Feb 2025 09:36:14 -0700 Subject: [PATCH 02/24] add rx --- gradle.properties | 2 +- gradle/dependencies.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index e4fda753..40887399 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,11 +6,11 @@ jacksonVersion = 2.9.0 jgitVersion = 6.7.0.202309050840-r junitVersion = 5.8.2 micronautAppVersion = 3.7.10 +micronautRxVersion = 2.1.1 micronautVersion = 3.10.1 nodePluginVersion = 3.0.1 nodeVersion = 16.3.0 nshmpLibVersion = 1.5.9 -nshmpUtilsJavaVersion = 0.4.0 openApiVersion = 4.0.0 shadowVersion = 7.1.2 spotbugsVersion = 4.7.3 diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index 8440bb3a..9aa5de3c 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -6,7 +6,6 @@ dependencies { } else { implementation "ghsc:nshmp-lib:${nshmpLibVersion}" } - implementation "ghsc:nshmp-utils-java:${nshmpUtilsJavaVersion}" // Git implementation "org.eclipse.jgit:org.eclipse.jgit:${jgitVersion}" @@ -21,6 +20,7 @@ dependencies { implementation("jakarta.annotation:jakarta.annotation-api") implementation("io.micronaut.aws:micronaut-function-aws-api-proxy") implementation("io.micronaut:micronaut-http-server-netty") + implementation "io.micronaut.rxjava3:micronaut-rxjava3:${micronautRxVersion}" runtimeOnly("ch.qos.logback:logback-classic") // Swagger -- GitLab From a8d423b1bb66c3328b858405d38c8e322756f2ae Mon Sep 17 00:00:00 2001 From: Brandon Clayton <bclayton@usgs.gov> Date: Fri, 28 Feb 2025 09:36:21 -0700 Subject: [PATCH 03/24] remove ws --- .../java/gov/usgs/earthquake/nshmp/www/HazVersion.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/HazVersion.java b/src/main/java/gov/usgs/earthquake/nshmp/www/HazVersion.java index 538964aa..28e1f365 100644 --- a/src/main/java/gov/usgs/earthquake/nshmp/www/HazVersion.java +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/HazVersion.java @@ -18,7 +18,6 @@ public class HazVersion implements AppVersion { var versions = new ArrayList<VersionInfo>(); versions.add(new HazVersion().getVersionInfo()); versions.add(new LibVersion().getVersionInfo()); - versions.add(new WsUtilsVersion().getVersionInfo()); var nshmVersion = getNshmVersion(modelPath); @@ -29,6 +28,14 @@ public class HazVersion implements AppVersion { return versions.toArray(new VersionInfo[0]); } + public static VersionInfo[] appVersions() { + var versions = new ArrayList<VersionInfo>(); + versions.add(new HazVersion().getVersionInfo()); + versions.add(new LibVersion().getVersionInfo()); + + return versions.toArray(new VersionInfo[0]); + } + /** * Returns the version info from resources file. */ -- GitLab From 032c9a569e2f11584f7c35419a7e791796e01c4a Mon Sep 17 00:00:00 2001 From: Brandon Clayton <bclayton@usgs.gov> Date: Fri, 28 Feb 2025 09:36:29 -0700 Subject: [PATCH 04/24] handle gmm priming --- .../earthquake/nshmp/www/PrimingResource.java | 46 ++++++++++--------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/PrimingResource.java b/src/main/java/gov/usgs/earthquake/nshmp/www/PrimingResource.java index 6e1d3619..5a5c3bbd 100644 --- a/src/main/java/gov/usgs/earthquake/nshmp/www/PrimingResource.java +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/PrimingResource.java @@ -57,36 +57,40 @@ public class PrimingResource implements OrderedResource { @Value("${nshmp-haz.model-path}") private Path modelPath; + @Value("${nshmp-haz.gmm}") + private boolean gmmDeploy; + @Override public void beforeCheckpoint(Context<? extends Resource> context) throws Exception { var model = ServletUtil.loadModel(modelPath); ServletUtil.model(model); updateParameter(); + if (!gmmDeploy) { + var region = Regions.createRectangular("Bounds", model.bounds().min, model.bounds().max); - var region = Regions.createRectangular("Bounds", model.bounds().min, model.bounds().max); - - try (MicronautLambdaHandler handler = new MicronautLambdaHandler()) { - var paths = List.of("/hazard", "/disagg"); + try (MicronautLambdaHandler handler = new MicronautLambdaHandler()) { + var paths = List.of("/hazard", "/disagg"); - paths.forEach(path -> { - handler.handleRequest( - getAwsProxyRequest(path, Optional.empty()), - new MockLambdaContext()); - }); - - paths.forEach(path -> { - LOCATIONS.forEach(namedLocation -> { - var location = namedLocation.location(); + paths.forEach(path -> { + handler.handleRequest( + getAwsProxyRequest(path, Optional.empty()), + new MockLambdaContext()); + }); - if (region.contains(location)) { - handler.handleRequest( - getAwsProxyRequest( - String.format("%s/%f/%f/760", path, location.longitude, location.latitude), - Optional.of(Imt.PGA)), - new MockLambdaContext()); - } + paths.forEach(path -> { + LOCATIONS.forEach(namedLocation -> { + var location = namedLocation.location(); + + if (region.contains(location)) { + handler.handleRequest( + getAwsProxyRequest( + String.format("%s/%f/%f/760", path, location.longitude, location.latitude), + Optional.of(Imt.PGA)), + new MockLambdaContext()); + } + }); }); - }); + } } } -- GitLab From 0c87b6705ccc01f01865306770a6b57d6c83eaea Mon Sep 17 00:00:00 2001 From: Brandon Clayton <bclayton@usgs.gov> Date: Fri, 28 Feb 2025 09:36:38 -0700 Subject: [PATCH 05/24] make package public --- src/main/java/gov/usgs/earthquake/nshmp/www/ServletUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/ServletUtil.java b/src/main/java/gov/usgs/earthquake/nshmp/www/ServletUtil.java index b44aaaa6..448a30b0 100644 --- a/src/main/java/gov/usgs/earthquake/nshmp/www/ServletUtil.java +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/ServletUtil.java @@ -62,7 +62,7 @@ public class ServletUtil { private static HazardModel HAZARD_MODEL; - private static Optional<String> awsRuntime = + static Optional<String> awsRuntime = Optional.ofNullable(System.getenv("AWS_LAMBDA_RUNTIME_API")); static { -- GitLab From cfad99b3447049d83e68b5128139dcb32995f43a Mon Sep 17 00:00:00 2001 From: Brandon Clayton <bclayton@usgs.gov> Date: Fri, 28 Feb 2025 09:36:49 -0700 Subject: [PATCH 06/24] handle gmm swagger --- .../nshmp/www/SwaggerController.java | 62 ++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerController.java b/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerController.java index 2068cc2e..0171f875 100644 --- a/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerController.java +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerController.java @@ -2,12 +2,14 @@ package gov.usgs.earthquake.nshmp.www; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; import org.slf4j.LoggerFactory; import gov.usgs.earthquake.nshmp.model.HazardModel; import gov.usgs.earthquake.nshmp.www.source.FeaturesService; +import io.micronaut.context.annotation.Value; import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpResponse; import io.micronaut.http.MediaType; @@ -17,6 +19,7 @@ import io.swagger.v3.core.util.Yaml; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Paths; import io.swagger.v3.parser.OpenAPIV3Parser; import jakarta.inject.Inject; @@ -35,6 +38,9 @@ public class SwaggerController { @Inject private NshmpMicronautServlet servlet; + @Value("${nshmp-haz.gmm}") + private boolean gmmDeploy; + @Get(produces = MediaType.TEXT_EVENT_STREAM) public HttpResponse<String> doGet(HttpRequest<?> request) { try { @@ -51,7 +57,62 @@ public class SwaggerController { private OpenAPI getOpenAPI( HttpRequest<?> request, HazardModel model) { + System.out.println("Is GMM deploy: " + gmmDeploy); var openApi = new OpenAPIV3Parser().read("META-INF/swagger/nshmp-haz.yml"); + + if (gmmDeploy && ServletUtil.awsRuntime.isPresent()) { + openApi = gmmSwagger(openApi); + } else if (ServletUtil.awsRuntime.isPresent()) { + openApi = hazardSwagger(openApi, model); + } + + openApi.servers(null); + + return openApi; + } + + /** + * Filter all services except GMM. + * + * @param openApi The OpenAPI docs. + */ + private OpenAPI gmmSwagger(OpenAPI openApi) { + Paths paths = new Paths(); + openApi.getPaths().forEach((path, pathItem) -> { + if (path.contains("/gmm")) { + paths.put(path, pathItem); + } + }); + openApi.paths(paths); + + openApi.setTags(openApi.getTags().stream() + .filter(tag -> tag.getName().contains("Ground Motion Models")) + .collect(Collectors.toList())); + + openApi.getInfo().setTitle("NSHMP Ground Motion Models"); + openApi.getInfo().setDescription("Ground motion model web services."); + + return openApi; + } + + /** + * Fillter GMM services. + * + * @param openApi OpenAPI docs. + * @param model The current model. + */ + private OpenAPI hazardSwagger(OpenAPI openApi, HazardModel model) { + Paths paths = new Paths(); + openApi.getPaths().forEach((path, pathItem) -> { + if (!path.contains("/gmm")) { + paths.put(path, pathItem); + } + }); + openApi.paths(paths); + + openApi.setTags(openApi.getTags().stream() + .filter(tag -> !tag.getName().contains("Ground Motion Models")) + .collect(Collectors.toList())); var bounds = model.bounds(); var components = openApi.getComponents(); var schemas = components.getSchemas(); @@ -59,7 +120,6 @@ public class SwaggerController { SwaggerUtils.imtSchema(schemas, List.copyOf(model.config().hazard.imts)); var boundsInfo = SwaggerUtils.locationBoundsInfo(bounds.min, bounds.max, Optional.empty()); FeaturesService.featureTypeSchema(schemas); - openApi.servers(null); openApi.getInfo().setTitle(model.name() + " Web Services"); openApi.getInfo().setDescription( -- GitLab From 09b5676a4e25a0eedd159d6170975f4308df5cdc Mon Sep 17 00:00:00 2001 From: Brandon Clayton <bclayton@usgs.gov> Date: Fri, 28 Feb 2025 09:37:12 -0700 Subject: [PATCH 07/24] add gmm --- src/main/resources/application.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 37590421..285168b3 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -21,3 +21,4 @@ nshmp-haz: # java -jar build/libs/nshmp-haz.jar --model=<path/to/model> # model-path: ${MODEL:nshms/nshm-conus-2018} + gmm: ${GMM_DEPLOY:true} -- GitLab From a7644092d4afcd43345134456bb7f73a5d155634 Mon Sep 17 00:00:00 2001 From: Brandon Clayton <bclayton@usgs.gov> Date: Tue, 4 Mar 2025 12:02:57 -0700 Subject: [PATCH 08/24] remove --- .../earthquake/nshmp/www/ResponseBody.java | 213 ------------------ .../usgs/earthquake/nshmp/www/WsUtils.java | 100 -------- .../nshmp/www/meta/EnumParameter.java | 22 -- .../earthquake/nshmp/www/meta/Status.java | 18 -- .../nshmp/www/meta/StringParameter.java | 20 -- 5 files changed, 373 deletions(-) delete mode 100644 src/main/java/gov/usgs/earthquake/nshmp/www/ResponseBody.java delete mode 100644 src/main/java/gov/usgs/earthquake/nshmp/www/WsUtils.java delete mode 100644 src/main/java/gov/usgs/earthquake/nshmp/www/meta/EnumParameter.java delete mode 100644 src/main/java/gov/usgs/earthquake/nshmp/www/meta/Status.java delete mode 100644 src/main/java/gov/usgs/earthquake/nshmp/www/meta/StringParameter.java diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/ResponseBody.java b/src/main/java/gov/usgs/earthquake/nshmp/www/ResponseBody.java deleted file mode 100644 index 0e103753..00000000 --- a/src/main/java/gov/usgs/earthquake/nshmp/www/ResponseBody.java +++ /dev/null @@ -1,213 +0,0 @@ -package gov.usgs.earthquake.nshmp.www; - -import static com.google.common.base.Preconditions.checkNotNull; - -import java.time.ZonedDateTime; - -import gov.usgs.earthquake.nshmp.www.meta.Status; - -/** - * Generic wrapper around a web service response object that is typically - * serialized to JSON and sent back to requestor as an HttpResponse 'body'. - * - * <p>To create a response, use one of the three static builder methods: - * {@link Builder#error()}, {@link Builder#success()}, or - * {@link Builder#usage()}. - * - * @author U.S. Geological Survey - * - * @param <T> The request type - * @param <V> The response type - */ -public class ResponseBody<T, V> { - - private final String name; - private final String date; - private final String status; - private final String url; - private final T request; - private final V response; - private final ResponseMetadata metadata; - - private ResponseBody(Builder<T, V> builder) { - name = builder.name; - date = ZonedDateTime.now().format(WsUtils.DATE_FMT); - status = builder.status; - url = builder.url; - request = builder.request; - response = builder.response; - metadata = builder.metadata; - } - - protected ResponseBody() { - date = null; - metadata = null; - name = null; - request = null; - response = null; - status = null; - url = null; - } - - /** - * The date and time this request/response. - */ - public String getDate() { - return date; - } - - /** - * The metadata. - */ - public ResponseMetadata getMetadata() { - return metadata; - } - - /** - * The name of the service. - */ - public String getName() { - return name; - } - - /** - * The request object. - */ - public T getRequest() { - return request; - } - - /** - * The response object. - */ - public V getResponse() { - return response; - } - - /** - * The response status. - */ - public String getStatus() { - return status; - } - - /** - * The URL used to call the service - */ - public String getUrl() { - return url; - } - - /** - * Create a new builder initialized to an error response. - * - * @param <T> The request type - * @param <V> The response type - */ - public static <T, V> Builder<T, V> error() { - return new Builder<T, V>(Status.ERROR); - } - - /** - * Create a new builder initialized to a success response. - * - * @param <T> The request type - * @param <V> The response type - */ - public static <T, V> Builder<T, V> success() { - return new Builder<T, V>(Status.SUCCESS); - } - - /** - * Create a new builder initialized to a usage response. - * - * @param <T> The request type - * @param <V> The response type - */ - public static <T, V> Builder<T, V> usage() { - return new Builder<T, V>(Status.USAGE); - } - - /** - * A {@code ResponseBody} builder. - * - * @param <T> The request type - * @param <V> The response type - */ - public static class Builder<T, V> { - - private String name; - private String status; - private String url; - private ResponseMetadata metadata; - private T request; - private V response; - - private Builder(String status) { - this.status = status; - } - - /** - * Set the metadata. - * - * @param metadata The servie metadata - */ - public Builder<T, V> metadata(ResponseMetadata metadata) { - this.metadata = metadata; - return this; - } - - /** - * Set the service name. - * - * @param name of the service being called - */ - public Builder<T, V> name(String name) { - this.name = name; - return this; - } - - /** - * Set the request object. - * - * @param request object - */ - public Builder<T, V> request(T request) { - this.request = request; - return this; - } - - /** - * Set the response object. - * - * @param response object - */ - public Builder<T, V> response(V response) { - this.response = response; - return this; - } - - /** - * Set the url used to call the service. - * - * @param url used to generate this response - */ - public Builder<T, V> url(String url) { - this.url = url; - return this; - } - - /** - * Returns a new Response - */ - public ResponseBody<T, V> build() { - checkNotNull(metadata); - checkNotNull(name); - checkNotNull(request); - checkNotNull(response); - checkNotNull(url); - checkNotNull(status); - return new ResponseBody<T, V>(this); - } - } -} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/WsUtils.java b/src/main/java/gov/usgs/earthquake/nshmp/www/WsUtils.java deleted file mode 100644 index 1e0d6939..00000000 --- a/src/main/java/gov/usgs/earthquake/nshmp/www/WsUtils.java +++ /dev/null @@ -1,100 +0,0 @@ -package gov.usgs.earthquake.nshmp.www; - -import java.lang.reflect.Type; -import java.time.format.DateTimeFormatter; -import java.util.Optional; - -import com.google.common.collect.Range; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonPrimitive; -import com.google.gson.JsonSerializationContext; -import com.google.gson.JsonSerializer; - -import gov.usgs.earthquake.nshmp.gmm.GmmInput; -import gov.usgs.earthquake.nshmp.gmm.GmmInput.Field; - -/** - * Web service utilities. - * - * @author U.S. Geological Survey - */ -public class WsUtils { - - public static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern( - "yyyy-MM-dd'T'HH:mm:ssXXX"); - - public static <T, E extends Enum<E>> T checkValue(E key, T value) { - if (value == null) { - throw new IllegalStateException("Missing [" + key.toString() + "]"); - } - - return value; - } - - /* Constrain all doubles to 8 decimal places */ - public static final class DoubleSerializer implements JsonSerializer<Double> { - @Override - public JsonElement serialize(Double d, Type type, JsonSerializationContext context) { - double dOut = Double.valueOf(String.format("%.8g", d)); - return new JsonPrimitive(dOut); - } - } - - /* Convert NaN to null */ - public static final class NaNSerializer implements JsonSerializer<Double> { - @Override - public JsonElement serialize(Double d, Type type, JsonSerializationContext context) { - return Double.isNaN(d) ? null : new JsonPrimitive(d); - } - } - - public static final class ConstraintsSerializer implements JsonSerializer<GmmInput.Constraints> { - @Override - public JsonElement serialize( - GmmInput.Constraints constraints, - Type type, - JsonSerializationContext context) { - JsonArray json = new JsonArray(); - - for (Field field : Field.values()) { - Optional<?> opt = constraints.get(field); - if (opt.isPresent()) { - Range<?> value = (Range<?>) opt.orElseThrow(); - Constraint constraint = new Constraint( - field.id, - value.lowerEndpoint(), - value.upperEndpoint()); - json.add(context.serialize(constraint)); - } - } - - return json; - } - } - - public static final class EnumSerializer<E extends Enum<E>> implements JsonSerializer<E> { - @Override - public JsonElement serialize(E src, Type type, JsonSerializationContext context) { - JsonObject jObj = new JsonObject(); - jObj.addProperty("value", src.name()); - jObj.addProperty("display", src.toString()); - - return jObj; - } - } - - @SuppressWarnings("unused") - private static class Constraint { - final String id; - final Object min; - final Object max; - - Constraint(String id, Object min, Object max) { - this.id = id; - this.min = min; - this.max = max; - } - } -} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/meta/EnumParameter.java b/src/main/java/gov/usgs/earthquake/nshmp/www/meta/EnumParameter.java deleted file mode 100644 index 154bedc4..00000000 --- a/src/main/java/gov/usgs/earthquake/nshmp/www/meta/EnumParameter.java +++ /dev/null @@ -1,22 +0,0 @@ -package gov.usgs.earthquake.nshmp.www.meta; - -import java.util.Set; - -/** - * An enum parameter. - * - * @author U.S. Geological Survey - * - * @param <E> The enum type - */ -public final class EnumParameter<E extends Enum<E>> { - - private final String label; - private final Set<E> values; - - public EnumParameter(String label, Set<E> values) { - this.label = label; - this.values = values; - } - -} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/meta/Status.java b/src/main/java/gov/usgs/earthquake/nshmp/www/meta/Status.java deleted file mode 100644 index 9511b464..00000000 --- a/src/main/java/gov/usgs/earthquake/nshmp/www/meta/Status.java +++ /dev/null @@ -1,18 +0,0 @@ -package gov.usgs.earthquake.nshmp.www.meta; - -/** - * Service request status identifier. - * - * @author U.S. Geological Survey - */ -public class Status { - - /** Error reponse status. */ - public static final String ERROR = "error"; - - /** Success reponse status. */ - public static final String SUCCESS = "success"; - - /** Usage reponse status. */ - public static final String USAGE = "usage"; -} diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/meta/StringParameter.java b/src/main/java/gov/usgs/earthquake/nshmp/www/meta/StringParameter.java deleted file mode 100644 index d8262749..00000000 --- a/src/main/java/gov/usgs/earthquake/nshmp/www/meta/StringParameter.java +++ /dev/null @@ -1,20 +0,0 @@ -package gov.usgs.earthquake.nshmp.www.meta; - -import java.util.Set; - -/** - * A string parameter. - * - * @author U.S. Geological Survey - */ -public class StringParameter { - - public final String label; - public final Set<String> values; - - public StringParameter(String label, Set<String> values) { - this.label = label; - this.values = values; - } - -} -- GitLab From 80ab6a2ff0dfe752de6ea8eb79f64585fe02042d Mon Sep 17 00:00:00 2001 From: Brandon Clayton <bclayton@usgs.gov> Date: Tue, 4 Mar 2025 12:07:13 -0700 Subject: [PATCH 09/24] add dep --- gradle.properties | 1 + gradle/dependencies.gradle | 1 + 2 files changed, 2 insertions(+) diff --git a/gradle.properties b/gradle.properties index 40887399..0f0e3997 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,6 +11,7 @@ micronautVersion = 3.10.1 nodePluginVersion = 3.0.1 nodeVersion = 16.3.0 nshmpLibVersion = 1.5.9 +nshmpUtilsJavaVersion = 0.4.0 openApiVersion = 4.0.0 shadowVersion = 7.1.2 spotbugsVersion = 4.7.3 diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index 9aa5de3c..d6169667 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -6,6 +6,7 @@ dependencies { } else { implementation "ghsc:nshmp-lib:${nshmpLibVersion}" } + implementation "ghsc:nshmp-utils-java:${nshmpUtilsJavaVersion}" // Git implementation "org.eclipse.jgit:org.eclipse.jgit:${jgitVersion}" -- GitLab From fdf5daff06970765f8ab63b8bc72fe81a19e1a85 Mon Sep 17 00:00:00 2001 From: Brandon Clayton <bclayton@usgs.gov> Date: Tue, 4 Mar 2025 12:08:01 -0700 Subject: [PATCH 10/24] handle path --- .../java/gov/usgs/earthquake/nshmp/www/SwaggerController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerController.java b/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerController.java index 0171f875..57d4de6b 100644 --- a/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerController.java +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerController.java @@ -57,7 +57,6 @@ public class SwaggerController { private OpenAPI getOpenAPI( HttpRequest<?> request, HazardModel model) { - System.out.println("Is GMM deploy: " + gmmDeploy); var openApi = new OpenAPIV3Parser().read("META-INF/swagger/nshmp-haz.yml"); if (gmmDeploy && ServletUtil.awsRuntime.isPresent()) { @@ -80,7 +79,8 @@ public class SwaggerController { Paths paths = new Paths(); openApi.getPaths().forEach((path, pathItem) -> { if (path.contains("/gmm")) { - paths.put(path, pathItem); + // Remove "/gmm" for AWS deployments + paths.put(path.substring(4), pathItem); } }); openApi.paths(paths); -- GitLab From 6249022c7f640db1ef598c2c57f06e22c043ee01 Mon Sep 17 00:00:00 2001 From: Brandon Clayton <bclayton@usgs.gov> Date: Tue, 4 Mar 2025 12:08:08 -0700 Subject: [PATCH 11/24] add path --- .../java/gov/usgs/earthquake/nshmp/www/gmm/GmmController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/GmmController.java b/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/GmmController.java index 4415dd3b..c9ff40c6 100644 --- a/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/GmmController.java +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/gmm/GmmController.java @@ -27,7 +27,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.inject.Inject; @Tag(name = "Ground Motion Models") -@Controller("/gmm") +@Controller("${nshmp-haz.gmm-path}") class GmmController { private static final String JAVADOC_URL = -- GitLab From cd33f7926b214669df2901e83254f7ebd18be779 Mon Sep 17 00:00:00 2001 From: Brandon Clayton <bclayton@usgs.gov> Date: Tue, 4 Mar 2025 12:08:23 -0700 Subject: [PATCH 12/24] add parameters --- src/main/resources/application.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 285168b3..442887eb 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -21,4 +21,11 @@ nshmp-haz: # java -jar build/libs/nshmp-haz.jar --model=<path/to/model> # model-path: ${MODEL:nshms/nshm-conus-2018} + + ## + # Whether this is a GMM service deploy gmm: ${GMM_DEPLOY:true} + + ## + # The base path for GMM services + gmm-path: ${GMM_PATH:/gmm} -- GitLab From 1bb0bf5f861159ab47eae0bd3a34046e1ec1a936 Mon Sep 17 00:00:00 2001 From: Brandon Clayton <bclayton@usgs.gov> Date: Tue, 4 Mar 2025 12:21:56 -0700 Subject: [PATCH 13/24] remove dep --- gradle.properties | 1 - gradle/dependencies.gradle | 1 - 2 files changed, 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 0f0e3997..e4fda753 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,6 @@ jacksonVersion = 2.9.0 jgitVersion = 6.7.0.202309050840-r junitVersion = 5.8.2 micronautAppVersion = 3.7.10 -micronautRxVersion = 2.1.1 micronautVersion = 3.10.1 nodePluginVersion = 3.0.1 nodeVersion = 16.3.0 diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index d6169667..8440bb3a 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -21,7 +21,6 @@ dependencies { implementation("jakarta.annotation:jakarta.annotation-api") implementation("io.micronaut.aws:micronaut-function-aws-api-proxy") implementation("io.micronaut:micronaut-http-server-netty") - implementation "io.micronaut.rxjava3:micronaut-rxjava3:${micronautRxVersion}" runtimeOnly("ch.qos.logback:logback-classic") // Swagger -- GitLab From 1b843bb5230eb28593935e965dfc8704c0322750 Mon Sep 17 00:00:00 2001 From: Brandon Clayton <bclayton@usgs.gov> Date: Tue, 4 Mar 2025 12:22:35 -0700 Subject: [PATCH 14/24] remove file --- .../nshmp/www/NshmpMicronautServlet.java | 51 ------------------- 1 file changed, 51 deletions(-) delete mode 100644 src/main/java/gov/usgs/earthquake/nshmp/www/NshmpMicronautServlet.java diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/NshmpMicronautServlet.java b/src/main/java/gov/usgs/earthquake/nshmp/www/NshmpMicronautServlet.java deleted file mode 100644 index 0336511a..00000000 --- a/src/main/java/gov/usgs/earthquake/nshmp/www/NshmpMicronautServlet.java +++ /dev/null @@ -1,51 +0,0 @@ -package gov.usgs.earthquake.nshmp.www; - -import org.reactivestreams.Publisher; - -import io.micronaut.core.type.MutableHeaders; -import io.micronaut.http.HttpRequest; -import io.micronaut.http.MutableHttpResponse; -import io.micronaut.http.annotation.Filter; -import io.micronaut.http.filter.HttpServerFilter; -import io.micronaut.http.filter.ServerFilterChain; -import io.reactivex.rxjava3.core.Flowable; -import io.reactivex.rxjava3.schedulers.Schedulers; - -/** - * Custom NSHMP servlet implementation and URL helper class for Micronaut - * services. - * - * <p>This class sets custom response headers and provides a helper class to - * ensure serialized response URLs propagate the correct host and protocol from - * requests on USGS servers and caches that may have been forwarded. - * - * @author U.S. Geological Survey - */ -@Filter("/**") -public class NshmpMicronautServlet implements HttpServerFilter { - - /* - * Set CORS headers and content type. - * - * Because NSHMP services may be called by both the USGS website, other - * websites, and directly by 3rd party applications, responses generated by - * direct requests will not have the necessary header information that would - * be required by security protocols for web requests. This means that any - * initial direct request will pollute intermediate caches with a response - * that a browser will deem invalid. - */ - @Override - public Publisher<MutableHttpResponse<?>> doFilter( - HttpRequest<?> request, - ServerFilterChain chain) { - return Flowable.just(chain).subscribeOn(Schedulers.io()) - .switchMap(bool -> chain.proceed(request)) - .doOnNext(res -> { - MutableHeaders headers = res.getHeaders(); - headers.add("Access-Control-Allow-Origin", "*"); - headers.add("Access-Control-Allow-Methods", "*"); - headers.add("Access-Control-Allow-Headers", "accept,origin,authorization,content-type"); - }); - } - -} -- GitLab From 7330532c47be510fbe6946d4962fe67651aa6567 Mon Sep 17 00:00:00 2001 From: Brandon Clayton <bclayton@usgs.gov> Date: Tue, 4 Mar 2025 12:23:24 -0700 Subject: [PATCH 15/24] remove file --- .../earthquake/nshmp/www/ResponseMetadata.java | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 src/main/java/gov/usgs/earthquake/nshmp/www/ResponseMetadata.java diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/ResponseMetadata.java b/src/main/java/gov/usgs/earthquake/nshmp/www/ResponseMetadata.java deleted file mode 100644 index 9439810f..00000000 --- a/src/main/java/gov/usgs/earthquake/nshmp/www/ResponseMetadata.java +++ /dev/null @@ -1,18 +0,0 @@ -package gov.usgs.earthquake.nshmp.www; - -import gov.usgs.earthquake.nshmp.internal.AppVersion.VersionInfo; - -/** - * The response metadata with version info. - */ -public class ResponseMetadata { - public final VersionInfo[] repositories; - - public ResponseMetadata(VersionInfo... repositories) { - this.repositories = repositories; - } - - public VersionInfo[] getRepositories() { - return repositories; - } -} -- GitLab From 6ab29a6faa743b1f6e443a152365ce7c0e4ed13a Mon Sep 17 00:00:00 2001 From: Brandon Clayton <bclayton@usgs.gov> Date: Tue, 4 Mar 2025 12:23:56 -0700 Subject: [PATCH 16/24] remove --- .../earthquake/nshmp/www/SwaggerUtils.java | 157 ------------------ 1 file changed, 157 deletions(-) delete mode 100644 src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerUtils.java diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerUtils.java b/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerUtils.java deleted file mode 100644 index 92d460b4..00000000 --- a/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerUtils.java +++ /dev/null @@ -1,157 +0,0 @@ -package gov.usgs.earthquake.nshmp.www; - -import java.math.BigDecimal; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; - -import gov.usgs.earthquake.nshmp.geo.Location; -import gov.usgs.earthquake.nshmp.gmm.Imt; -import gov.usgs.earthquake.nshmp.gmm.NehrpSiteClass; - -import io.swagger.v3.oas.models.OpenAPI; -import io.swagger.v3.oas.models.media.Schema; -import io.swagger.v3.oas.models.parameters.Parameter; -import io.swagger.v3.parser.util.SchemaTypeUtil; - -/** - * General Swagger utilities - * - * @author U.S. Geological Survey - */ -public class SwaggerUtils { - - /** - * Update "longitude" and "latitude" parameters with min and max bounds and - * add bounds to description. - * - * @param parameters The Swagger parameters - * @param min The minimum bounds - * @param max The maximum bounds - * @return - */ - public static List<Parameter> addLocationBounds( - List<Parameter> parameters, - Location min, - Location max) { - var latitudeDescription = String.format(" [%s, %s]", min.latitude, max.latitude); - var longtudeDescription = String.format(" [%s, %s]", min.longitude, max.longitude); - - parameters.forEach(parameter -> { - if (parameter.getName().equals("latitude")) { - parameter.setDescription(parameter.getDescription() + latitudeDescription); - parameter.getSchema().setMinimum(BigDecimal.valueOf(min.latitude)); - parameter.getSchema().setMaximum(BigDecimal.valueOf(max.latitude)); - } else if (parameter.getName().equals("longitude")) { - parameter.setDescription(parameter.getDescription() + longtudeDescription); - parameter.getSchema().setMinimum(BigDecimal.valueOf(min.longitude)); - parameter.getSchema().setMaximum(BigDecimal.valueOf(max.longitude)); - } - }); - - return parameters; - } - - /** - * Update any "longitude" and "latitude" parameters with min and max bounds - * and add bounds to description. - * - * @param openApi The Open API - * @param min The minimum location bounds - * @param max The maximum location bounds - */ - public static OpenAPI addLocationBounds(OpenAPI openApi, Location min, Location max) { - openApi.getPaths().values().stream() - .flatMap(path -> path.readOperations().stream()) - .forEach(operation -> addLocationBounds(operation.getParameters(), min, max)); - - return openApi; - } - - /** - * Returns markdown string listing the IMTs. - * - * @param siteClasses The IMTs - * @param heading The Markdown heading value. Default: ### - */ - public static String imtInfo(List<Imt> imts, Optional<String> heading) { - return new StringBuilder() - .append(heading.orElse("###") + " Intensity Measure Types \n") - .append(imts.stream().sorted() - .map(imt -> "- " + imt.toString()) - .collect(Collectors.joining("\n"))) - .append("\n") - .toString(); - } - - /** - * Returns updated Swagger schemas with provided IMTs. - * - * @param imts The IMTs - */ - public static Map<String, Schema> imtSchema( - Map<String, Schema> schemas, - List<Imt> imts) { - var schema = new Schema<String>(); - schema.setType(SchemaTypeUtil.STRING_TYPE); - imts.stream() - .sorted() - .forEach(imt -> schema.addEnumItemObject(imt.name())); - - schemas.put(Imt.class.getSimpleName(), schema); - return schemas; - } - - /** - * Returns markdown string listing the min and max location bounds. - * - * @param min The minimum bounds - * @param max The maximum bounds - * @param heading The Markdown heading value. Default: ### - */ - public static String locationBoundsInfo(Location min, Location max, Optional<String> heading) { - return new StringBuilder() - .append(heading.orElse("###") + " Latitude Bounds\n") - .append(String.format("- Minimum Latitude: %s°\n", min.latitude)) - .append(String.format("- Maximum Latitude: %s°\n", max.latitude)) - .append(heading.orElse("###") + " Longitude Bounds\n") - .append(String.format("- Minimum Longitude: %s°\n", min.longitude)) - .append(String.format("- Maximum Longitude: %s°\n", max.longitude)) - .toString(); - } - - /** - * Returns markdown string listing the site classes. - * - * @param siteClasses The NEHRP site classes - * @param heading The Markdown heading value. Default: ### - */ - public static String siteClassInfo(List<NehrpSiteClass> siteClasses, Optional<String> heading) { - return new StringBuilder() - .append(heading.orElse("###") + " Site Classes\n") - .append(siteClasses.stream().sorted() - .map(siteClass -> "- " + siteClass.toString()) - .collect(Collectors.joining("\n"))) - .append("\n") - .toString(); - } - - /** - * Returns updated Swagger schemas with provided site classes. - * - * @param siteClasses The NERHP site classes - */ - public static Map<String, Schema> siteClassSchema( - Map<String, Schema> schemas, - List<NehrpSiteClass> siteClasses) { - var schema = new Schema<NehrpSiteClass>(); - schema.setType(SchemaTypeUtil.STRING_TYPE); - siteClasses.stream() - .sorted() - .forEach(siteClass -> schema.addEnumItemObject(siteClass)); - - schemas.put(NehrpSiteClass.class.getSimpleName(), schema); - return schemas; - } -} -- GitLab From b38082150c3ae30d4cd3f0b566ebace92348efbe Mon Sep 17 00:00:00 2001 From: Brandon Clayton <bclayton@usgs.gov> Date: Tue, 4 Mar 2025 12:27:35 -0700 Subject: [PATCH 17/24] clean --- .../java/gov/usgs/earthquake/nshmp/www/SwaggerController.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerController.java b/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerController.java index 57d4de6b..02c9a10c 100644 --- a/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerController.java +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerController.java @@ -113,6 +113,7 @@ public class SwaggerController { openApi.setTags(openApi.getTags().stream() .filter(tag -> !tag.getName().contains("Ground Motion Models")) .collect(Collectors.toList())); + var bounds = model.bounds(); var components = openApi.getComponents(); var schemas = components.getSchemas(); -- GitLab From a7210649f506c22beb342c1ba84a2b6b7aa8af0b Mon Sep 17 00:00:00 2001 From: Brandon Clayton <bclayton@usgs.gov> Date: Tue, 4 Mar 2025 12:27:50 -0700 Subject: [PATCH 18/24] clean --- .../java/gov/usgs/earthquake/nshmp/www/SwaggerController.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerController.java b/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerController.java index 02c9a10c..57d4de6b 100644 --- a/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerController.java +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerController.java @@ -113,7 +113,6 @@ public class SwaggerController { openApi.setTags(openApi.getTags().stream() .filter(tag -> !tag.getName().contains("Ground Motion Models")) .collect(Collectors.toList())); - var bounds = model.bounds(); var components = openApi.getComponents(); var schemas = components.getSchemas(); -- GitLab From 1aa465f2961059f0bcaf6ca67bfbdbd6a4c5638b Mon Sep 17 00:00:00 2001 From: Brandon Clayton <bclayton@usgs.gov> Date: Tue, 4 Mar 2025 12:45:17 -0700 Subject: [PATCH 19/24] simplify --- src/main/java/gov/usgs/earthquake/nshmp/www/HazVersion.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/HazVersion.java b/src/main/java/gov/usgs/earthquake/nshmp/www/HazVersion.java index 28e1f365..05221edc 100644 --- a/src/main/java/gov/usgs/earthquake/nshmp/www/HazVersion.java +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/HazVersion.java @@ -2,6 +2,7 @@ package gov.usgs.earthquake.nshmp.www; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; import org.eclipse.jgit.api.Git; @@ -15,10 +16,7 @@ public class HazVersion implements AppVersion { private static final String MODEL_FILE = "model-version.json"; public static VersionInfo[] appVersions(Path modelPath) { - var versions = new ArrayList<VersionInfo>(); - versions.add(new HazVersion().getVersionInfo()); - versions.add(new LibVersion().getVersionInfo()); - + var versions = Arrays.asList(versions()); var nshmVersion = getNshmVersion(modelPath); if (nshmVersion != null) { -- GitLab From 999b43307a52509cb70788a98f0ccf0efce4d1c7 Mon Sep 17 00:00:00 2001 From: Brandon Clayton <bclayton@usgs.gov> Date: Tue, 4 Mar 2025 12:47:37 -0700 Subject: [PATCH 20/24] fix --- src/main/java/gov/usgs/earthquake/nshmp/www/HazVersion.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/HazVersion.java b/src/main/java/gov/usgs/earthquake/nshmp/www/HazVersion.java index 05221edc..9bf189f4 100644 --- a/src/main/java/gov/usgs/earthquake/nshmp/www/HazVersion.java +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/HazVersion.java @@ -16,7 +16,7 @@ public class HazVersion implements AppVersion { private static final String MODEL_FILE = "model-version.json"; public static VersionInfo[] appVersions(Path modelPath) { - var versions = Arrays.asList(versions()); + var versions = Arrays.asList(appVersions()); var nshmVersion = getNshmVersion(modelPath); if (nshmVersion != null) { -- GitLab From 50dd8f9fb0d99a9ca00816d21780b618b32a6c09 Mon Sep 17 00:00:00 2001 From: Brandon Clayton <bclayton@usgs.gov> Date: Tue, 4 Mar 2025 13:04:41 -0700 Subject: [PATCH 21/24] handle local swagger --- .../nshmp/www/SwaggerController.java | 41 ++++++++++++------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerController.java b/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerController.java index 57d4de6b..63b1b348 100644 --- a/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerController.java +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerController.java @@ -59,9 +59,14 @@ public class SwaggerController { HazardModel model) { var openApi = new OpenAPIV3Parser().read("META-INF/swagger/nshmp-haz.yml"); - if (gmmDeploy && ServletUtil.awsRuntime.isPresent()) { - openApi = gmmSwagger(openApi); - } else if (ServletUtil.awsRuntime.isPresent()) { + if (ServletUtil.awsRuntime.isPresent()) { + if (gmmDeploy) { + openApi = gmmSwagger(openApi); + } else { + openApi = hazardSwagger(openApi, model); + filterGmms(openApi); + } + } else { openApi = hazardSwagger(openApi, model); } @@ -70,6 +75,25 @@ public class SwaggerController { return openApi; } + /** + * Filter GMM services. + * + * @param openApi OpenAPI docs. + */ + private void filterGmms(OpenAPI openApi) { + Paths paths = new Paths(); + openApi.getPaths().forEach((path, pathItem) -> { + if (!path.contains("/gmm")) { + paths.put(path, pathItem); + } + }); + openApi.paths(paths); + + openApi.setTags(openApi.getTags().stream() + .filter(tag -> !tag.getName().contains("Ground Motion Models")) + .collect(Collectors.toList())); + } + /** * Filter all services except GMM. * @@ -102,17 +126,6 @@ public class SwaggerController { * @param model The current model. */ private OpenAPI hazardSwagger(OpenAPI openApi, HazardModel model) { - Paths paths = new Paths(); - openApi.getPaths().forEach((path, pathItem) -> { - if (!path.contains("/gmm")) { - paths.put(path, pathItem); - } - }); - openApi.paths(paths); - - openApi.setTags(openApi.getTags().stream() - .filter(tag -> !tag.getName().contains("Ground Motion Models")) - .collect(Collectors.toList())); var bounds = model.bounds(); var components = openApi.getComponents(); var schemas = components.getSchemas(); -- GitLab From 46caa1d3431d8b698407bb38fc96d903f05ba58b Mon Sep 17 00:00:00 2001 From: Brandon Clayton <bclayton@usgs.gov> Date: Tue, 4 Mar 2025 13:55:48 -0700 Subject: [PATCH 22/24] change value --- .../gov/usgs/earthquake/nshmp/www/SwaggerController.java | 7 ++++--- src/main/resources/application.yml | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerController.java b/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerController.java index 63b1b348..ee6affd7 100644 --- a/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerController.java +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/SwaggerController.java @@ -63,8 +63,7 @@ public class SwaggerController { if (gmmDeploy) { openApi = gmmSwagger(openApi); } else { - openApi = hazardSwagger(openApi, model); - filterGmms(openApi); + openApi = hazardSwagger(filterGmms(openApi), model); } } else { openApi = hazardSwagger(openApi, model); @@ -80,7 +79,7 @@ public class SwaggerController { * * @param openApi OpenAPI docs. */ - private void filterGmms(OpenAPI openApi) { + private OpenAPI filterGmms(OpenAPI openApi) { Paths paths = new Paths(); openApi.getPaths().forEach((path, pathItem) -> { if (!path.contains("/gmm")) { @@ -92,6 +91,8 @@ public class SwaggerController { openApi.setTags(openApi.getTags().stream() .filter(tag -> !tag.getName().contains("Ground Motion Models")) .collect(Collectors.toList())); + + return openApi; } /** diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 442887eb..ecab27f1 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -24,7 +24,7 @@ nshmp-haz: ## # Whether this is a GMM service deploy - gmm: ${GMM_DEPLOY:true} + gmm: ${GMM_DEPLOY:false} ## # The base path for GMM services -- GitLab From 1df84ba3104c38bbe4ca2f6f793a323dbcbf9c69 Mon Sep 17 00:00:00 2001 From: Brandon Clayton <bclayton@usgs.gov> Date: Tue, 4 Mar 2025 14:38:31 -0700 Subject: [PATCH 23/24] change to stream --- src/main/java/gov/usgs/earthquake/nshmp/www/HazVersion.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/HazVersion.java b/src/main/java/gov/usgs/earthquake/nshmp/www/HazVersion.java index 9bf189f4..e3e3435f 100644 --- a/src/main/java/gov/usgs/earthquake/nshmp/www/HazVersion.java +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/HazVersion.java @@ -3,6 +3,7 @@ package gov.usgs.earthquake.nshmp.www; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; +import java.util.stream.Collectors; import org.eclipse.jgit.api.Git; @@ -16,7 +17,7 @@ public class HazVersion implements AppVersion { private static final String MODEL_FILE = "model-version.json"; public static VersionInfo[] appVersions(Path modelPath) { - var versions = Arrays.asList(appVersions()); + var versions = Arrays.stream(appVersions()).collect(Collectors.toList()); var nshmVersion = getNshmVersion(modelPath); if (nshmVersion != null) { @@ -30,6 +31,7 @@ public class HazVersion implements AppVersion { var versions = new ArrayList<VersionInfo>(); versions.add(new HazVersion().getVersionInfo()); versions.add(new LibVersion().getVersionInfo()); + versions.add(new WsUtilsVersion().getVersionInfo()); return versions.toArray(new VersionInfo[0]); } -- GitLab From b682a29f8a904b74495d3557c9798fc38fa66fca Mon Sep 17 00:00:00 2001 From: Brandon Clayton <bclayton@usgs.gov> Date: Tue, 4 Mar 2025 14:40:57 -0700 Subject: [PATCH 24/24] use lists --- src/main/java/gov/usgs/earthquake/nshmp/www/HazVersion.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/HazVersion.java b/src/main/java/gov/usgs/earthquake/nshmp/www/HazVersion.java index e3e3435f..b3c072cb 100644 --- a/src/main/java/gov/usgs/earthquake/nshmp/www/HazVersion.java +++ b/src/main/java/gov/usgs/earthquake/nshmp/www/HazVersion.java @@ -2,11 +2,10 @@ package gov.usgs.earthquake.nshmp.www; import java.nio.file.Path; import java.util.ArrayList; -import java.util.Arrays; -import java.util.stream.Collectors; import org.eclipse.jgit.api.Git; +import com.google.common.collect.Lists; import com.google.common.io.Resources; import gov.usgs.earthquake.nshmp.internal.AppVersion; @@ -17,7 +16,7 @@ public class HazVersion implements AppVersion { private static final String MODEL_FILE = "model-version.json"; public static VersionInfo[] appVersions(Path modelPath) { - var versions = Arrays.stream(appVersions()).collect(Collectors.toList()); + var versions = Lists.newArrayList(appVersions()); var nshmVersion = getNshmVersion(modelPath); if (nshmVersion != null) { -- GitLab