From 1c0e9cb90ceea169b5649e87f3def275b194e072 Mon Sep 17 00:00:00 2001 From: Brandon Clayton <bclayton@usgs.gov> Date: Fri, 29 Jul 2022 09:51:56 -0600 Subject: [PATCH] Handle single netcdf file --- .../netcdf/NetcdfDataFilesHazardCurves.java | 52 ++++--------- .../netcdf/www/NetcdfServiceHazardCurves.java | 72 +++++++++--------- .../nshmp/netcdf/www/SwaggerHazardCurves.java | 65 ++++++++-------- src/hazard/src/main/resources/application.yml | 4 +- .../src/main/resources/hazard-example.nc | Bin 10332874 -> 10332981 bytes 5 files changed, 81 insertions(+), 112 deletions(-) diff --git a/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/NetcdfDataFilesHazardCurves.java b/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/NetcdfDataFilesHazardCurves.java index 6078488..c429b83 100644 --- a/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/NetcdfDataFilesHazardCurves.java +++ b/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/NetcdfDataFilesHazardCurves.java @@ -1,16 +1,13 @@ package gov.usgs.earthquake.nshmp.netcdf; -import java.io.FileNotFoundException; -import java.io.IOException; import java.nio.file.Path; import java.util.List; import java.util.Set; -import java.util.stream.Collectors; import gov.usgs.earthquake.nshmp.gmm.Imt; /** - * Read in and parse all hazard NetCDF data files. + * Read in and parse a single hazard NetCDF data file. * * @author U.S. Geological Survey */ @@ -22,54 +19,31 @@ public class NetcdfDataFilesHazardCurves extends NetcdfDataFiles<NetcdfHazardCur } /** - * Returns the set of NEHRP site classes for all data files. + * Returns the set of IMTs. */ public Set<Imt> imts() { - return stream() - .flatMap(netcdf -> netcdf.netcdfData().imts().stream()) - .sorted() - .collect(Collectors.toSet()); + return Set.copyOf(netcdf().netcdfData().imts()); } /** - * Returns the Netcdf object associated with a location. - * - * @param location The location to find the NetCDF object - * @throws IllegalArgumentException If no location is contained in a Netcdf - * object. + * Returns the Netcdf object. */ - public NetcdfHazardCurves netcdf(Nshm nshm) { + public NetcdfHazardCurves netcdf() { return stream() - .filter(netcdf -> netcdf.nshm() == nshm) .findFirst() - .orElseThrow(() -> new IllegalArgumentException( - String.format("NSHM %s not found in data sets.", nshm))); - } - - /** - * Returns the {@link Nshm NSHMs} associated with the data sets read in. - */ - public Set<Nshm> nshms() { - return stream() - .map(netcdf -> netcdf.nshm()) - .sorted() - .collect(Collectors.toSet()); + .orElseThrow(() -> new IllegalArgumentException("Data set not found")); } @Override protected List<NetcdfHazardCurves> readFiles(Path netcdfPath, NetcdfDataType dataType) { - try { - List<NetcdfHazardCurves> data = walkFiles(netcdfPath, dataType) - .map(NetcdfHazardCurves::new) - .collect(Collectors.toList()); + NetcdfDataType fileDataType = NetcdfDataType.getDataType((netcdfPath)); - if (data.isEmpty()) { - throw new FileNotFoundException("Failed to find hazard NetCDF files"); - } + if (!fileDataType.equals(dataType)) { + throw new RuntimeException( + String.format("Data type %s of file must be of type %s", fileDataType, dataType)); + } ; - return List.copyOf(data); - } catch (IOException e) { - throw new RuntimeException("Failed to read NetCDF directory " + netcdfPath, e); - } + NetcdfHazardCurves data = new NetcdfHazardCurves(netcdfPath); + return List.of(data); } } diff --git a/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfServiceHazardCurves.java b/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfServiceHazardCurves.java index 74d1f52..6984e67 100644 --- a/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfServiceHazardCurves.java +++ b/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfServiceHazardCurves.java @@ -9,15 +9,15 @@ import gov.usgs.earthquake.nshmp.gmm.Imt; import gov.usgs.earthquake.nshmp.netcdf.NetcdfDataFilesHazardCurves; import gov.usgs.earthquake.nshmp.netcdf.NetcdfHazardCurves; import gov.usgs.earthquake.nshmp.netcdf.NetcdfVersion; -import gov.usgs.earthquake.nshmp.netcdf.Nshm; import gov.usgs.earthquake.nshmp.netcdf.data.StaticData; import gov.usgs.earthquake.nshmp.netcdf.data.StaticDataHazardCurves; import gov.usgs.earthquake.nshmp.netcdf.www.HazardMetadata.HazardResponseMetadata; +import gov.usgs.earthquake.nshmp.netcdf.www.NetcdfService.SourceModel; import gov.usgs.earthquake.nshmp.netcdf.www.NetcdfWsUtils.Key; import gov.usgs.earthquake.nshmp.netcdf.www.Query.Service; -import gov.usgs.earthquake.nshmp.netcdf.www.RequestHazardCurves.HazardRequestData; +import gov.usgs.earthquake.nshmp.netcdf.www.Request.RequestData; +import gov.usgs.earthquake.nshmp.netcdf.www.Request.RequestDataSiteClass; import gov.usgs.earthquake.nshmp.netcdf.www.RequestHazardCurves.HazardRequestDataImt; -import gov.usgs.earthquake.nshmp.netcdf.www.RequestHazardCurves.HazardRequestDataSiteClass; import gov.usgs.earthquake.nshmp.www.ResponseBody; import gov.usgs.earthquake.nshmp.www.ResponseMetadata; import gov.usgs.earthquake.nshmp.www.WsUtils; @@ -31,7 +31,8 @@ import io.micronaut.http.HttpRequest; * * @author U.S. Geological Survey */ -public class NetcdfServiceHazardCurves extends NetcdfService<HazardQuery> { +public class NetcdfServiceHazardCurves extends + NetcdfService<NetcdfServiceHazardCurves.HazardSourceModel, HazardQuery> { static final String SERVICE_DESCRIPTION = "Get static hazard curves from a NetCDF file"; static final String X_LABEL = "Ground Motion (g)"; @@ -42,9 +43,10 @@ public class NetcdfServiceHazardCurves extends NetcdfService<HazardQuery> { } @Override - ResponseBody<String, Metadata<HazardQuery>> getMetadataResponse(HttpRequest<?> request) { - var metadata = new Metadata<HazardQuery>(request, this, SERVICE_DESCRIPTION); - return ResponseBody.<String, Metadata<HazardQuery>> usage() + ResponseBody<String, Metadata<HazardSourceModel, HazardQuery>> getMetadataResponse( + HttpRequest<?> request) { + var metadata = new Metadata<HazardSourceModel, HazardQuery>(request, this, SERVICE_DESCRIPTION); + return ResponseBody.<String, Metadata<HazardSourceModel, HazardQuery>> usage() .metadata(new ResponseMetadata(NetcdfVersion.appVersions())) .name(getServiceName()) .request(NetcdfWsUtils.getRequestUrl(request)) @@ -66,14 +68,12 @@ public class NetcdfServiceHazardCurves extends NetcdfService<HazardQuery> { @Override String getServiceName() { - return "Static Hazard Curves for NSHMs"; + return netcdfDataFiles().netcdf().netcdfData().scienceBaseMetadata().label; } @Override - List<SourceModel> getSourceModels() { - return netcdfDataFiles().stream() - .map(HazardSourceModel::new) - .collect(Collectors.toList()); + HazardSourceModel getSourceModels() { + return new HazardSourceModel(netcdfDataFiles().netcdf()); } /** @@ -82,8 +82,8 @@ public class NetcdfServiceHazardCurves extends NetcdfService<HazardQuery> { * * @param nshm The NSHM to get the NetCDF data */ - NetcdfHazardCurves netcdf(Nshm nshm) { - return netcdfDataFiles().netcdf(nshm); + NetcdfHazardCurves netcdf() { + return netcdfDataFiles().netcdf(); } @Override @@ -91,16 +91,16 @@ public class NetcdfServiceHazardCurves extends NetcdfService<HazardQuery> { return (NetcdfDataFilesHazardCurves) netcdfDataFiles; } - ResponseBody<HazardRequestData, List<List<ResponseData<HazardResponseMetadata>>>> processCurves( - HazardRequestData request, + ResponseBody<RequestData, List<List<ResponseData<HazardResponseMetadata>>>> processCurves( + RequestData request, String url) { WsUtils.checkValue(Key.LATITUDE, request.latitude); WsUtils.checkValue(Key.LONGITUDE, request.longitude); - var curves = netcdf(request.nshm).staticData(request.site); + var curves = netcdf().staticData(request.site); var curvesAsList = toList(request, curves); return ResponseBody - .<HazardRequestData, List<List<ResponseData<HazardResponseMetadata>>>> success() + .<RequestData, List<List<ResponseData<HazardResponseMetadata>>>> success() .metadata(new ResponseMetadata(NetcdfVersion.appVersions())) .name(getServiceName()) .request(request) @@ -109,17 +109,17 @@ public class NetcdfServiceHazardCurves extends NetcdfService<HazardQuery> { .build(); } - ResponseBody<HazardRequestDataSiteClass, List<ResponseData<HazardResponseMetadata>>> processCurvesSiteClass( - HazardRequestDataSiteClass request, + ResponseBody<RequestDataSiteClass, List<ResponseData<HazardResponseMetadata>>> processCurvesSiteClass( + RequestDataSiteClass request, String url) { WsUtils.checkValue(Key.LATITUDE, request.latitude); WsUtils.checkValue(Key.LONGITUDE, request.longitude); WsUtils.checkValue(Key.SITE_CLASS, request.siteClass); - var curves = netcdf(request.nshm).staticData(request.site, request.siteClass); + var curves = netcdf().staticData(request.site, request.siteClass); var curvesAsList = toList(request, curves); return ResponseBody - .<HazardRequestDataSiteClass, List<ResponseData<HazardResponseMetadata>>> success() + .<RequestDataSiteClass, List<ResponseData<HazardResponseMetadata>>> success() .metadata(new ResponseMetadata(NetcdfVersion.appVersions())) .name(getServiceName()) .request(request) @@ -128,16 +128,16 @@ public class NetcdfServiceHazardCurves extends NetcdfService<HazardQuery> { .build(); } - ResponseBody<HazardRequestDataSiteClass, ResponseData<HazardResponseMetadata>> processCurvesImt( + ResponseBody<RequestDataSiteClass, ResponseData<HazardResponseMetadata>> processCurvesImt( HazardRequestDataImt request, String url) { WsUtils.checkValue(Key.LATITUDE, request.latitude); WsUtils.checkValue(Key.LONGITUDE, request.longitude); WsUtils.checkValue(Key.SITE_CLASS, request.siteClass); WsUtils.checkValue(Key.IMT, request.imt); - var curves = netcdf(request.nshm).staticData(request.site, request.siteClass); + var curves = netcdf().staticData(request.site, request.siteClass); - return ResponseBody.<HazardRequestDataSiteClass, ResponseData<HazardResponseMetadata>> success() + return ResponseBody.<RequestDataSiteClass, ResponseData<HazardResponseMetadata>> success() .metadata(new ResponseMetadata(NetcdfVersion.appVersions())) .name(getServiceName()) .request(request) @@ -154,15 +154,15 @@ public class NetcdfServiceHazardCurves extends NetcdfService<HazardQuery> { switch (service) { case CURVES: - var requestData = new HazardRequestData(query.nshm, site, query.format); + var requestData = new RequestData(site, query.format); return toResponseFromListOfList(processCurves(requestData, url)); case CURVES_BY_SITE_CLASS: var requestDataSiteClass = - new HazardRequestDataSiteClass(query.nshm, site, query.siteClass, query.format); + new RequestDataSiteClass(site, query.siteClass, query.format); return toResponseFromList(processCurvesSiteClass(requestDataSiteClass, url)); case CURVES_BY_IMT: var requestDataImt = - new HazardRequestDataImt(query.nshm, site, query.siteClass, query.imt, query.format); + new HazardRequestDataImt(site, query.siteClass, query.imt, query.format); return toResponse(processCurvesImt(requestDataImt, url)); default: throw new RuntimeException("Netcdf service [" + service + "] not found"); @@ -170,7 +170,7 @@ public class NetcdfServiceHazardCurves extends NetcdfService<HazardQuery> { } private String toResponseFromListOfList( - ResponseBody<HazardRequestData, List<List<ResponseData<HazardResponseMetadata>>>> serviceResponse) { + ResponseBody<RequestData, List<List<ResponseData<HazardResponseMetadata>>>> serviceResponse) { if (serviceResponse.getRequest().format == ResponseFormat.CSV) { var csvResponse = toCsvFromListOfList(serviceResponse.getRequest(), serviceResponse.getResponse()); @@ -180,14 +180,14 @@ public class NetcdfServiceHazardCurves extends NetcdfService<HazardQuery> { } } - private String toCsvFromListOfList(HazardRequestData requestData, + private String toCsvFromListOfList(RequestData requestData, List<List<ResponseData<HazardResponseMetadata>>> responseData) { return responseData.stream().map(responses -> toCsvResponseFromList(requestData, responses)) .collect(Collectors.joining("\n\n")); } List<ResponseData<HazardResponseMetadata>> toList( - HazardRequestDataSiteClass request, + RequestDataSiteClass request, StaticDataHazardCurves curves) { return curves.entrySet().stream() .map((entry) -> toResponseData(request, entry.getKey(), entry.getValue())) @@ -195,12 +195,12 @@ public class NetcdfServiceHazardCurves extends NetcdfService<HazardQuery> { } List<List<ResponseData<HazardResponseMetadata>>> toList( - HazardRequestData requestData, + RequestData requestData, StaticData<StaticDataHazardCurves> curves) { return curves.entrySet().stream() .map(entry -> { var request = - new HazardRequestDataSiteClass(requestData.nshm, requestData.site, entry.getKey(), + new RequestDataSiteClass(requestData.site, entry.getKey(), requestData.format); return toList(request, entry.getValue()); }) @@ -208,21 +208,19 @@ public class NetcdfServiceHazardCurves extends NetcdfService<HazardQuery> { } ResponseData<HazardResponseMetadata> toResponseData( - HazardRequestDataSiteClass request, + RequestDataSiteClass request, Imt imt, XySequence curves) { var metadata = - new HazardResponseMetadata(request.nshm, request.siteClass, imt, X_LABEL, Y_LABEL); + new HazardResponseMetadata(request.siteClass, imt, X_LABEL, Y_LABEL); return new ResponseData<>(metadata, curves); } static class HazardSourceModel extends SourceModel { - public final Nshm nshm; public final List<Imt> imts; HazardSourceModel(NetcdfHazardCurves netcdf) { super(netcdf); - nshm = netcdf.nshm(); imts = netcdf.netcdfData().imts().stream().sorted().collect(Collectors.toList()); } } diff --git a/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/SwaggerHazardCurves.java b/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/SwaggerHazardCurves.java index 2e2d592..64e880c 100644 --- a/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/SwaggerHazardCurves.java +++ b/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/SwaggerHazardCurves.java @@ -2,16 +2,12 @@ package gov.usgs.earthquake.nshmp.netcdf.www; import java.io.IOException; import java.util.List; -import java.util.Map; -import java.util.Set; -import gov.usgs.earthquake.nshmp.netcdf.Nshm; +import gov.usgs.earthquake.nshmp.netcdf.data.ScienceBaseMetadata; import gov.usgs.earthquake.nshmp.www.SwaggerUtils; import io.micronaut.http.HttpRequest; import io.swagger.v3.oas.models.OpenAPI; -import io.swagger.v3.oas.models.media.Schema; -import io.swagger.v3.parser.util.SchemaTypeUtil; /** * Swagger page updates for hazard services. @@ -24,16 +20,30 @@ public class SwaggerHazardCurves extends Swagger<NetcdfServiceHazardCurves> { super(request, service); } + @Override + String description() { + StringBuilder builder = new StringBuilder(); + + service.netcdfDataFiles() + .forEach(netcdf -> { + ScienceBaseMetadata metadata = netcdf.netcdfData().scienceBaseMetadata(); + builder + .append(metadata.description + "\n") + .append(parameterSection(netcdf.netcdfData()) + "\n") + .append(scienceBaseSection(metadata) + "\n"); + }); + + return builder.toString(); + } + @Override String descriptionHeader() { - return "NSHM Data Sets"; + return ""; } @Override String serviceInfo() { - return String.join("", - "Get hazard curves for user-specified NSHM, latitudes, ", - "longitudes, and/or site classes of the National Seismic Hazard Models data releases."); + return ""; } @Override @@ -51,18 +61,17 @@ public class SwaggerHazardCurves extends Swagger<NetcdfServiceHazardCurves> { "The query based service call is in the form of:\n" + "```text\n" + - url + "/hazard?nshm={nshm}&longitude={number}&latitude={number}\n" + - url + "/hazard" + - "?nshm={nshm}&longitude={number}&latitude={number}&siteClass={string}\n" + + url + "/hazard?longitude={number}&latitude={number}\n" + + url + "/hazard?longitude={number}&latitude={number}&siteClass={string}\n" + url + "/hazard" + - "?nshm={nshm}&longitude={number}&latitude={number}&siteClass={string}&imt={string}\n" + + "?longitude={number}&latitude={number}&siteClass={string}&imt={string}\n" + "````\n" + "Example:\n" + "```text\n" + - url + "/hazard?nshm=CONUS_2018&longitude=-118&latitude=34\n" + - url + "/hazard?nshm=CONUS_2018&longitude=-118&latitude=34&siteClass=BC\n" + - url + "/hazard?nshm=CONUS_2018&longitude=-118&latitude=34&siteClass=BC&imt=PGA\n" + + url + "/hazard?longitude=-118&latitude=34\n" + + url + "/hazard?longitude=-118&latitude=34&siteClass=BC\n" + + url + "/hazard?longitude=-118&latitude=34&siteClass=BC&imt=PGA\n" + "```\n") .append( "### Slash Pattern\n" + @@ -70,17 +79,17 @@ public class SwaggerHazardCurves extends Swagger<NetcdfServiceHazardCurves> { "The slash based service call is in the form of:\n" + "```text\n" + - url + "/hazard/{nshm}/{longitude}/{latitude}\n" + - url + "/hazard/{nshm}/{longitude}/{latitude}/{siteClass}\n" + - url + "/hazard/{nshm}/{longitude}/{latitude}/{siteClass}/{imt}\n" + + url + "/hazard/{longitude}/{latitude}\n" + + url + "/hazard/{longitude}/{latitude}/{siteClass}\n" + + url + "/hazard/{longitude}/{latitude}/{siteClass}/{imt}\n" + "```\n" + "Example:\n" + "```text\n" + - url + "/hazard/CONUS_2018/-118/34\n" + - url + "/hazard/CONUS_2018/-118/34/BC\n" + - url + "/hazard/CONUS_2018/-118/34/BC/PGA\n" + + url + "/hazard/-118/34\n" + + url + "/hazard/-118/34/BC\n" + + url + "/hazard/-118/34/BC/PGA\n" + "```\n") .append("</details>") .toString(); @@ -94,18 +103,6 @@ public class SwaggerHazardCurves extends Swagger<NetcdfServiceHazardCurves> { openApi.getComponents().getSchemas(), List.copyOf(service.netcdfDataFiles().imts())); - nshmSchema(openApi.getComponents().getSchemas(), service.netcdfDataFiles().nshms()); - return openApi; } - - private void nshmSchema(Map<String, Schema> schemas, Set<Nshm> nshms) { - Schema<Nshm> schema = new Schema<>(); - schema.setType(SchemaTypeUtil.STRING_TYPE); - nshms.stream() - .sorted() - .forEach(nshm -> schema.addEnumItemObject(nshm)); - - schemas.put(Nshm.class.getSimpleName(), schema); - } } diff --git a/src/hazard/src/main/resources/application.yml b/src/hazard/src/main/resources/application.yml index 1455ba2..eb7e65e 100644 --- a/src/hazard/src/main/resources/application.yml +++ b/src/hazard/src/main/resources/application.yml @@ -16,5 +16,5 @@ micronaut: logger-name: http nshmp-ws-static: - # Path to directory holding NetCDF files - netcdf-path: ${netcdf:src/main/resources} + # Path to hazard NetCDF file + netcdf-path: ${netcdf:src/main/resources/hazard-example.nc} diff --git a/src/hazard/src/main/resources/hazard-example.nc b/src/hazard/src/main/resources/hazard-example.nc index 884b1262c63574f58ac7399fdeb3d2c43606fb28..1969150bcb1f553bd4e701ed35ea61c4fd8a96f9 100644 GIT binary patch delta 3445 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 WfS-bs;Ah}v@CtbK+;sn`%Krgqn*c)q delta 3129 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 KJXtY(toDBojfb58 -- GitLab