From eae110428d6200b109e9535147ca191bd11dbaa5 Mon Sep 17 00:00:00 2001 From: amber Date: Tue, 18 Nov 2025 16:42:23 +1100 Subject: [PATCH 1/3] add ncwms map query url logic --- .../server/core/service/wms/WmsServer.java | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) 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..3ade8974 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 @@ -170,10 +170,47 @@ protected List createMapQueryUrl(String url, String uuid, FeatureRequest if (!pathSegments.isEmpty()) { Map param = new HashMap<>(); + // Detect if this is ncWMS or regular WMS + // https://geoserver-123.aodn.org.au/geoserver/ncwms?LAYERS=srs_ghrsst_l3s_M_1d_ngt_url/sea_surface_temperature&TRANSPARENT=TRUE&VERSION=1.3.0&FORMAT=image/png&EXCEPTIONS=application/vnd.ogc.se_xml&TILED=true&SERVICE=ncwms&REQUEST=GetMap&STYLES=&QUERYABLE=true&CRS=EPSG:4326&NUMCOLORBANDS=253&TIME=2018-06-02T15:20:00.000Z&BBOX=-45,110,-20,145&WIDTH=256&HEIGHT=256 + 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()); + + // ncWMS requires specific parameters not needed by regular WMS + param.put("NUMCOLORBANDS", "253"); + + // ncWMS uses TIME parameter instead of CQL_FILTER for temporal filtering + // It requires exact timestamps, not date ranges + String timeValue; + + if (request.getDatetime() != null) { + String datetime = request.getDatetime(); + + if (datetime.contains("/")) { + // User selected a date range (e.g., "2015-01-01T00:00:00Z/2020-12-31T00:00:00Z") + // ncWMS only accepts single timestamps, so we use the END date + String[] parts = datetime.split("/"); + String endDate = parts[1]; // Take the end of the range + String datePart = endDate.substring(0, 10); // Extract YYYY-MM-DD + // This layer uses 15:20:00.000Z as the standard time of day + 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); + timeValue = datePart + "T15:20:00.000Z"; + log.debug("ncWMS: Using timestamp {}", timeValue); + } + } else { + // No date selected - use the most recent available date as default + timeValue = "2025-11-01T15:20:00.000Z"; + log.debug("ncWMS: No datetime provided, using default {}", timeValue); + } + + param.put("TIME", timeValue); } // Now we add the missing argument from the request From 703f69e76cd26dab9f5506f053f29de8ebce2ee2 Mon Sep 17 00:00:00 2001 From: amber Date: Thu, 20 Nov 2025 12:59:15 +1100 Subject: [PATCH 2/3] fix and add test case --- .../server/core/service/wms/WmsServer.java | 85 +++--- .../core/service/wms/WmsServerTest.java | 245 +++++++++++++++++- 2 files changed, 298 insertions(+), 32 deletions(-) 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 3ade8974..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,60 +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<>(); - // Detect if this is ncWMS or regular WMS - // https://geoserver-123.aodn.org.au/geoserver/ncwms?LAYERS=srs_ghrsst_l3s_M_1d_ngt_url/sea_surface_temperature&TRANSPARENT=TRUE&VERSION=1.3.0&FORMAT=image/png&EXCEPTIONS=application/vnd.ogc.se_xml&TILED=true&SERVICE=ncwms&REQUEST=GetMap&STYLES=&QUERYABLE=true&CRS=EPSG:4326&NUMCOLORBANDS=253&TIME=2018-06-02T15:20:00.000Z&BBOX=-45,110,-20,145&WIDTH=256&HEIGHT=256 + // 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 (isNcwms) { param.putAll(wmsDefaultParam.getNcwms()); - - // ncWMS requires specific parameters not needed by regular WMS param.put("NUMCOLORBANDS", "253"); - // ncWMS uses TIME parameter instead of CQL_FILTER for temporal filtering - // It requires exact timestamps, not date ranges + // 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) { + if (request.getDatetime() != null && !request.getDatetime().isEmpty()) { String datetime = request.getDatetime(); if (datetime.contains("/")) { - // User selected a date range (e.g., "2015-01-01T00:00:00Z/2020-12-31T00:00:00Z") - // ncWMS only accepts single timestamps, so we use the END date + // Date range provided - use end date to show displaying snapshot for current solution String[] parts = datetime.split("/"); - String endDate = parts[1]; // Take the end of the range - String datePart = endDate.substring(0, 10); // Extract YYYY-MM-DD - // This layer uses 15:20:00.000Z as the standard time of day - timeValue = datePart + "T15:20:00.000Z"; - log.debug("ncWMS: Converted date range {} to timestamp {}", datetime, timeValue); + 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); - timeValue = datePart + "T15:20:00.000Z"; - log.debug("ncWMS: Using timestamp {}", timeValue); + + // 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 date selected - use the most recent available date as default - timeValue = "2025-11-01T15:20:00.000Z"; - log.debug("ncWMS: No datetime provided, using default {}", timeValue); + // 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") @@ -232,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") @@ -252,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/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")); + } } From 3a808991d19e793f9099de6a8f838f4b570e7fc3 Mon Sep 17 00:00:00 2001 From: amber Date: Thu, 20 Nov 2025 15:15:11 +1100 Subject: [PATCH 3/3] fix typo --- server/src/main/resources/job-started-email.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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