diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/wms/WmsServer.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/wms/WmsServer.java index b35f1958..cffbf8c4 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/wms/WmsServer.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/wms/WmsServer.java @@ -29,6 +29,7 @@ import java.math.BigDecimal; import java.net.URISyntaxException; +import java.time.LocalDate; import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -165,23 +166,90 @@ protected List createMapQueryUrl(String url, String uuid, FeatureRequest try { UriComponents components = UriComponentsBuilder.fromUriString(url).build(); if (components.getPath() != null) { - // Now depends on the service, we need to have different arguments List pathSegments = components.getPathSegments(); if (!pathSegments.isEmpty()) { Map param = new HashMap<>(); + // ncWMS is a specialized WMS for NetCDF time-series data that only accepts single timestamps + boolean isNcwms = pathSegments.get(pathSegments.size() - 1).equalsIgnoreCase("ncwms"); + if (pathSegments.get(pathSegments.size() - 1).equalsIgnoreCase("wms")) { param.putAll(wmsDefaultParam.getWms()); - } else if (pathSegments.get(pathSegments.size() - 1).equalsIgnoreCase("ncwms")) { + } else if (isNcwms) { param.putAll(wmsDefaultParam.getNcwms()); + param.put("NUMCOLORBANDS", "253"); + + // ncWMS uses TIME parameter (single timestamp only) instead of CQL_FILTER + // Note: This specific ncWMS layer uses T15:20:00.000Z as the standard time for daily snapshots + String timeValue; + + if (request.getDatetime() != null && !request.getDatetime().isEmpty()) { + String datetime = request.getDatetime(); + + if (datetime.contains("/")) { + // Date range provided - use end date to show displaying snapshot for current solution + String[] parts = datetime.split("/"); + String endDate = parts[1].trim(); + String datePart = endDate.substring(0, 10); + + // Validate: check if date is in the future or too far in the past + LocalDate requestedDate = LocalDate.parse(datePart); + LocalDate today = LocalDate.now(); + LocalDate safeMaxDate = today.minusDays(3); // Account for data processing delay + LocalDate minDate = LocalDate.of(2012, 1, 1); // Todo: Layer's earliest data should not be hardcoded + + if (requestedDate.isAfter(safeMaxDate)) { + // Date is too recent - use safe default + log.warn("ncWMS: Requested end date {} is beyond available data (processing delay ~3 days), using {} instead", + requestedDate, safeMaxDate); + timeValue = safeMaxDate.toString() + "T15:20:00.000Z"; + } else if (requestedDate.isBefore(minDate)) { + // Date is before data availability + log.warn("ncWMS: Requested end date {} is before layer start date {}, using {} instead", + requestedDate, minDate, minDate); + timeValue = minDate.toString() + "T15:20:00.000Z"; + } else { + // Date is valid + timeValue = datePart + "T15:20:00.000Z"; + log.debug("ncWMS: Converted date range {} to timestamp {}", datetime, timeValue); + } + } else { + // Single date provided + String datePart = datetime.substring(0, 10); + + // Validate single date + LocalDate requestedDate = LocalDate.parse(datePart); + LocalDate today = LocalDate.now(); + LocalDate safeMaxDate = today.minusDays(3); + LocalDate minDate = LocalDate.of(2015, 1, 1); + + if (requestedDate.isAfter(safeMaxDate)) { + log.warn("ncWMS: Requested date {} is beyond available data, using {} instead", + requestedDate, safeMaxDate); + timeValue = safeMaxDate.toString() + "T15:20:00.000Z"; + } else if (requestedDate.isBefore(minDate)) { + log.warn("ncWMS: Requested date {} is before layer start date, using {} instead", + requestedDate, minDate); + timeValue = minDate.toString() + "T15:20:00.000Z"; + } else { + timeValue = datePart + "T15:20:00.000Z"; + log.debug("ncWMS: Using timestamp {}", timeValue); + } + } + } else { + // No datetime provided - use date 3 days ago as safe default + // (ncWMS data typically has 2-3 day processing delay) + LocalDate safeDate = LocalDate.now().minusDays(3); + timeValue = safeDate.toString() + "T15:20:00.000Z"; + log.debug("ncWMS: No datetime provided, using safe default {} (3 days ago)", timeValue); + } + + param.put("TIME", timeValue); } - // Now we add the missing argument from the request param.put("LAYERS", request.getLayerName()); param.put("BBOX", request.getBbox().stream().map(BigDecimal::toString).collect(Collectors.joining(","))); - // Very specific to IMOS, if we see geoserver-123.aodn.org.au/geoserver/wms, then - // we should try cache server -> https://tilecache.aodn.org.au/geowebcache/service/wms, if not work fall back List urls = new ArrayList<>(); if (components.getHost() != null && components.getHost().equalsIgnoreCase("geoserver-123.aodn.org.au") @@ -195,14 +263,11 @@ protected List createMapQueryUrl(String url, String uuid, FeatureRequest builder.queryParam(key, value); } }); - // Cannot set cql in param as it contains value like "/" which is not allow in UriComponent checks - // but server must use "/" in param and cannot encode it to %2F, so to avoid exception in the - // build() call, we append the cql after the construction. String target = String.join("&", builder.build().toUriString(), createCQLFilter(uuid, request)); log.debug("Cache url to wms geoserver {}", target); urls.add(target); } - // This is the normal route + UriComponentsBuilder builder = UriComponentsBuilder .newInstance() .scheme("https") @@ -215,9 +280,6 @@ protected List createMapQueryUrl(String url, String uuid, FeatureRequest builder.queryParam(key, value); } }); - // Cannot set cql in param as it contains value like "/" which is not allow in UriComponent checks - // but server must use "/" in param and cannot encode it to %2F, so to avoid exception in the - // build() call, we append the cql after the construction. String target = String.join("&", builder.build().toUriString(), createCQLFilter(uuid, request)); log.debug("Url to wms geoserver {}", target); urls.add(target); diff --git a/server/src/main/resources/job-started-email.html b/server/src/main/resources/job-started-email.html index 4686c1c8..0294eb72 100644 --- a/server/src/main/resources/job-started-email.html +++ b/server/src/main/resources/job-started-email.html @@ -804,7 +804,7 @@
-

Usage Constrains

+

Usage Constraints

diff --git a/server/src/test/java/au/org/aodn/ogcapi/server/core/service/wms/WmsServerTest.java b/server/src/test/java/au/org/aodn/ogcapi/server/core/service/wms/WmsServerTest.java index 387cd458..8833e1ea 100644 --- a/server/src/test/java/au/org/aodn/ogcapi/server/core/service/wms/WmsServerTest.java +++ b/server/src/test/java/au/org/aodn/ogcapi/server/core/service/wms/WmsServerTest.java @@ -28,13 +28,13 @@ import org.springframework.web.util.UriComponentsBuilder; import java.math.BigDecimal; +import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import static au.org.aodn.ogcapi.server.core.service.wfs.WfsDefaultParam.WFS_LINK_MARKER; import static au.org.aodn.ogcapi.server.core.service.wms.WmsDefaultParam.WMS_LINK_MARKER; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.when; @@ -597,4 +597,245 @@ public void verifyCreateCQLRangeDateTimeWithCQL() throws JsonProcessingException assertEquals("CQL_FILTER=start_juld >= 2023-01-01 AND end_juld <= 2023-12-31 AND set_code=1234", result); } + + @Test + public void verifyNcwmsValidDateRange() { + // Mock the search service + ElasticSearchBase.SearchResult emptyResult = new ElasticSearchBase.SearchResult<>(); + emptyResult.setCollections(new ArrayList<>()); + when(search.searchCollections(anyString())).thenReturn(emptyResult); + + FeatureRequest featureRequest = FeatureRequest + .builder() + .layerName("srs_ghrsst_l3s_M_1d_ngt_url/sea_surface_temperature") + .datetime("2020-01-01T00:00:00Z/2020-12-31T23:59:59Z") + .bbox(List.of( + BigDecimal.valueOf(-45.0), + BigDecimal.valueOf(110.0), + BigDecimal.valueOf(-20.0), + BigDecimal.valueOf(145.0))) + .build(); + + List urls = wmsServer.createMapQueryUrl( + "https://geoserver-123.aodn.org.au/geoserver/ncwms", + "d3e3bce3-adb4-433a-a192-93abc91899d3", + featureRequest + ); + + // 1. Test URL format result + assertNotNull(urls); + assertEquals(1, urls.size()); + UriComponents result = UriComponentsBuilder.fromUriString(urls.get(0)).build(); + + // 2. Validate date and time - should use end date with standard time + assertEquals("2020-12-31T15:20:00.000Z", result.getQueryParams().getFirst("TIME")); + assertEquals("253", result.getQueryParams().getFirst("NUMCOLORBANDS")); + } + + @Test + public void verifyNcwmsSingleDate() { + // Mock the search service + ElasticSearchBase.SearchResult emptyResult = new ElasticSearchBase.SearchResult<>(); + emptyResult.setCollections(new ArrayList<>()); + when(search.searchCollections(anyString())).thenReturn(emptyResult); + + FeatureRequest featureRequest = FeatureRequest + .builder() + .layerName("srs_ghrsst_l3s_M_1d_ngt_url/sea_surface_temperature") + .datetime("2020-06-15T00:00:00Z") + .bbox(List.of( + BigDecimal.valueOf(-45.0), + BigDecimal.valueOf(110.0), + BigDecimal.valueOf(-20.0), + BigDecimal.valueOf(145.0))) + .build(); + + List urls = wmsServer.createMapQueryUrl( + "https://geoserver-123.aodn.org.au/geoserver/ncwms", + "d3e3bce3-adb4-433a-a192-93abc91899d3", + featureRequest + ); + + // 1. Test URL format result + assertNotNull(urls); + assertEquals(1, urls.size()); + UriComponents result = UriComponentsBuilder.fromUriString(urls.get(0)).build(); + + // 2. Validate date and time - should add standard time to single date + assertEquals("2020-06-15T15:20:00.000Z", result.getQueryParams().getFirst("TIME")); + } + + @Test + public void verifyNcwmsFutureDateCorrection() { + // Mock the search service + ElasticSearchBase.SearchResult emptyResult = new ElasticSearchBase.SearchResult<>(); + emptyResult.setCollections(new ArrayList<>()); + when(search.searchCollections(anyString())).thenReturn(emptyResult); + + LocalDate futureDate = LocalDate.now().plusDays(10); + String futureDateStr = futureDate.toString() + "T00:00:00Z"; + + FeatureRequest featureRequest = FeatureRequest + .builder() + .layerName("srs_ghrsst_l3s_M_1d_ngt_url/sea_surface_temperature") + .datetime("2020-01-01T00:00:00Z/" + futureDateStr) + .bbox(List.of( + BigDecimal.valueOf(-45.0), + BigDecimal.valueOf(110.0), + BigDecimal.valueOf(-20.0), + BigDecimal.valueOf(145.0))) + .build(); + + List urls = wmsServer.createMapQueryUrl( + "https://geoserver-123.aodn.org.au/geoserver/ncwms", + "d3e3bce3-adb4-433a-a192-93abc91899d3", + featureRequest + ); + + // 1. Test URL format result + assertNotNull(urls); + assertEquals(1, urls.size()); + UriComponents result = UriComponentsBuilder.fromUriString(urls.get(0)).build(); + + // 2. Validate date and time - should correct future date to safe default (today - 3 days) + LocalDate expectedDate = LocalDate.now().minusDays(3); + String expectedTime = expectedDate.toString() + "T15:20:00.000Z"; + assertEquals(expectedTime, result.getQueryParams().getFirst("TIME")); + } + + @Test + public void verifyNcwmsOldDateCorrection() { + // Mock the search service + ElasticSearchBase.SearchResult emptyResult = new ElasticSearchBase.SearchResult<>(); + emptyResult.setCollections(new ArrayList<>()); + when(search.searchCollections(anyString())).thenReturn(emptyResult); + + FeatureRequest featureRequest = FeatureRequest + .builder() + .layerName("srs_ghrsst_l3s_M_1d_ngt_url/sea_surface_temperature") + .datetime("2010-01-01T00:00:00Z/2010-12-31T23:59:59Z") + .bbox(List.of( + BigDecimal.valueOf(-45.0), + BigDecimal.valueOf(110.0), + BigDecimal.valueOf(-20.0), + BigDecimal.valueOf(145.0))) + .build(); + + List urls = wmsServer.createMapQueryUrl( + "https://geoserver-123.aodn.org.au/geoserver/ncwms", + "d3e3bce3-adb4-433a-a192-93abc91899d3", + featureRequest + ); + + // 1. Test URL format result + assertNotNull(urls); + assertEquals(1, urls.size()); + UriComponents result = UriComponentsBuilder.fromUriString(urls.get(0)).build(); + + // 2. Validate date and time - should correct old date to minimum date (2012-01-01) + assertEquals("2012-01-01T15:20:00.000Z", result.getQueryParams().getFirst("TIME")); + } + + @Test + public void verifyNcwmsNoDatetime() { + // Mock the search service + ElasticSearchBase.SearchResult emptyResult = new ElasticSearchBase.SearchResult<>(); + emptyResult.setCollections(new ArrayList<>()); + when(search.searchCollections(anyString())).thenReturn(emptyResult); + + FeatureRequest featureRequest = FeatureRequest + .builder() + .layerName("srs_ghrsst_l3s_M_1d_ngt_url/sea_surface_temperature") + .bbox(List.of( + BigDecimal.valueOf(-45.0), + BigDecimal.valueOf(110.0), + BigDecimal.valueOf(-20.0), + BigDecimal.valueOf(145.0))) + .build(); + + List urls = wmsServer.createMapQueryUrl( + "https://geoserver-123.aodn.org.au/geoserver/ncwms", + "d3e3bce3-adb4-433a-a192-93abc91899d3", + featureRequest + ); + + // 1. Test URL format result + assertNotNull(urls); + assertEquals(1, urls.size()); + UriComponents result = UriComponentsBuilder.fromUriString(urls.get(0)).build(); + + // 2. Validate date and time - should use safe default (today - 3 days) + LocalDate expectedDate = LocalDate.now().minusDays(3); + String expectedTime = expectedDate.toString() + "T15:20:00.000Z"; + assertEquals(expectedTime, result.getQueryParams().getFirst("TIME")); + } + + @Test + public void verifyNcwmsEmptyDatetime() { + // Mock the search service + ElasticSearchBase.SearchResult emptyResult = new ElasticSearchBase.SearchResult<>(); + emptyResult.setCollections(new ArrayList<>()); + when(search.searchCollections(anyString())).thenReturn(emptyResult); + + FeatureRequest featureRequest = FeatureRequest + .builder() + .layerName("srs_ghrsst_l3s_M_1d_ngt_url/sea_surface_temperature") + .datetime("") + .bbox(List.of( + BigDecimal.valueOf(-45.0), + BigDecimal.valueOf(110.0), + BigDecimal.valueOf(-20.0), + BigDecimal.valueOf(145.0))) + .build(); + + List urls = wmsServer.createMapQueryUrl( + "https://geoserver-123.aodn.org.au/geoserver/ncwms", + "d3e3bce3-adb4-433a-a192-93abc91899d3", + featureRequest + ); + + // 1. Test URL format result + assertNotNull(urls); + assertEquals(1, urls.size()); + UriComponents result = UriComponentsBuilder.fromUriString(urls.get(0)).build(); + + // 2. Validate date and time - should use safe default (today - 3 days) + LocalDate expectedDate = LocalDate.now().minusDays(3); + String expectedTime = expectedDate.toString() + "T15:20:00.000Z"; + assertEquals(expectedTime, result.getQueryParams().getFirst("TIME")); + } + + @Test + public void verifyRegularWmsNoTimeParameter() { + // Mock the search service + ElasticSearchBase.SearchResult emptyResult = new ElasticSearchBase.SearchResult<>(); + emptyResult.setCollections(new ArrayList<>()); + when(search.searchCollections(anyString())).thenReturn(emptyResult); + + FeatureRequest featureRequest = FeatureRequest + .builder() + .layerName("imos:argo_profile_map") + .datetime("2020-01-01T00:00:00Z/2020-12-31T23:59:59Z") + .bbox(List.of( + BigDecimal.valueOf(-111.86719179153421), + BigDecimal.valueOf(-69.03714171275249), + BigDecimal.valueOf(111.8671917915342), + BigDecimal.valueOf(69.03714171275138))) + .build(); + + List urls = wmsServer.createMapQueryUrl( + "http://geoserver-123.aodn.org.au/geoserver/wms", + "uuid1", + featureRequest + ); + + // 1. Test URL format result + assertNotNull(urls); + assertEquals(2, urls.size()); + UriComponents result = UriComponentsBuilder.fromUriString(urls.get(1)).build(); + + // 2. Validate date and time - regular WMS should NOT have TIME parameter + assertNull(result.getQueryParams().getFirst("TIME")); + assertNull(result.getQueryParams().getFirst("NUMCOLORBANDS")); + } }