Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ public record DasJsonResult(JsonNode body, String cacheControl) {
public DasTileResult getVisualTile(String productId, String date, int zoom, int tileX, int tileY,
String ext, String colormap, String rescale) {
UriComponentsBuilder builder = UriComponentsBuilder
.fromUriString(dasProperties.host() + VISUAL_TILES_BASE + "/{product}/{date}/{z}/{x}/{y}.{ext}");
.fromUriString(dasProperties.host() + VISUAL_TILES_BASE + "/{product}/{z}/{x}/{y}.{ext}")
.queryParam("date", "{date}");
Map<String, Object> params = new HashMap<>();
params.put("product", productId);
params.put("date", date);
Expand Down Expand Up @@ -111,7 +112,8 @@ public DasTileResult getVisualTile(String productId, String date, int zoom, int
*/
public DasTileResult getDataTile(String productId, String date, int lod, int x, int y) {
UriComponentsBuilder builder = UriComponentsBuilder
.fromUriString(dasProperties.host() + DATA_TILES_BASE + "/{product}/{date}/{z}/{x}/{y}.png");
.fromUriString(dasProperties.host() + DATA_TILES_BASE + "/{product}/{z}/{x}/{y}.png")
.queryParam("date", "{date}");
Map<String, Object> params = new HashMap<>();
params.put("product", productId);
params.put("date", date);
Expand All @@ -128,7 +130,8 @@ public DasTileResult getDataTile(String productId, String date, int lod, int x,
*/
public DasJsonResult getPoint(String productId, String date, double lat, double lon) {
UriComponentsBuilder builder = UriComponentsBuilder
.fromUriString(dasProperties.host() + DATA_TILES_BASE + "/{product}/{date}/point")
.fromUriString(dasProperties.host() + DATA_TILES_BASE + "/{product}/point")
.queryParam("date", "{date}")
.queryParam("lat", "{lat}")
.queryParam("lon", "{lon}");
Map<String, Object> params = new HashMap<>();
Expand All @@ -148,7 +151,8 @@ public DasJsonResult getPoint(String productId, String date, double lat, double
*/
public DasJsonResult getDataManifest(String productId, String date) {
UriComponentsBuilder builder = UriComponentsBuilder
.fromUriString(dasProperties.host() + DATA_TILES_BASE + "/{product}/{date}/manifest.json");
.fromUriString(dasProperties.host() + DATA_TILES_BASE + "/{product}/manifest.json")
.queryParam("date", "{date}");
Map<String, Object> params = new HashMap<>();
params.put("product", productId);
params.put("date", date);
Expand Down Expand Up @@ -176,10 +180,17 @@ public List<JsonNode> getProducts() {
* Fetches the product/date manifest, forwarding its {@code Cache-Control} rather than this
* service inventing a freshness of its own.
*/
public DasJsonResult getManifest() {
public DasJsonResult getManifest(String collectionId) {
UriComponentsBuilder builder = UriComponentsBuilder
.fromUriString(dasProperties.host() + VISUAL_TILES_BASE + "/manifest");
return exchangeForJson(builder, Map.of());
Map<String, Object> params = new HashMap<>();

if (collectionId != null) {
builder.queryParam("metadata_uuid", "{metadata_uuid}");
params.put("metadata_uuid", collectionId);
}

return exchangeForJson(builder, params);
}

public JsonNode getColormaps() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,12 @@ public static String validateAndFormatDate(String dateInput, boolean isStartDate
}

/**
* Parse an ISO-8601 date-time and verify it is in UTC (offset 'Z').
* Parse an ISO-8601 date-time and verify it is at zero offset from UTC — accepts both the 'Z'
* designator and an explicit "+00:00"/"-00:00" offset, since they are equivalent instants.
*
* @param dateTime ISO-8601 date-time string, e.g. "2026-06-16T00:00:00Z"
* @param dateTime ISO-8601 date-time string, e.g. "2026-06-16T00:00:00Z" or "2026-06-16T00:00:00+00:00"
* @return the parsed date-time
* @throws IllegalArgumentException if the value is not a valid ISO-8601 date-time or is not in UTC
* @throws IllegalArgumentException if the value is not a valid ISO-8601 date-time or is not at zero UTC offset
*/
public static java.time.OffsetDateTime parseUtcDateTime(String dateTime) {
java.time.OffsetDateTime parsed;
Expand All @@ -127,7 +128,7 @@ public static java.time.OffsetDateTime parseUtcDateTime(String dateTime) {
}

if (!parsed.getOffset().equals(java.time.ZoneOffset.UTC)) {
throw new IllegalArgumentException("Date-time must be in UTC (offset 'Z'): " + dateTime);
throw new IllegalArgumentException("Date-time must be at zero UTC offset (e.g. 'Z' or '+00:00'): " + dateTime);
}
return parsed;
}
Expand Down
16 changes: 11 additions & 5 deletions server/src/main/java/au/org/aodn/ogcapi/server/tile/RestApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import au.org.aodn.ogcapi.server.core.exception.ResourceNotFoundException;
import au.org.aodn.ogcapi.server.core.model.ErrorResponse;
import au.org.aodn.ogcapi.server.core.service.das.DasTilerService;
import au.org.aodn.ogcapi.server.core.util.DatetimeUtils;
import au.org.aodn.ogcapi.tile.api.*;
import au.org.aodn.ogcapi.tile.model.*;
import io.swagger.v3.oas.annotations.Hidden;
Expand Down Expand Up @@ -100,7 +101,7 @@ public ResponseEntity<String> collectionMapGetTile(String tileMatrix, Integer ti
@Content(mediaType = "image/png", schema = @Schema(type = "string", format = "binary")),
@Content(mediaType = "image/webp", schema = @Schema(type = "string", format = "binary"))}),
@ApiResponse(responseCode = "400", description = "`dataset`, `variable` or `datetime` missing, " +
"`datetime` not `YYYY-MM-DD`, `f` neither `png` nor `webp`, or z/x/y out of range.",
"`datetime` not a full UTC ISO-8601 timestamp, `f` neither `png` nor `webp`, or z/x/y out of range.",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(responseCode = "404", description = "`tileMatrixSetId` is not `WebMercatorQuad`, " +
Expand Down Expand Up @@ -165,9 +166,9 @@ public ResponseEntity<byte[]> getCollectionVisualMapTile(
@RequestParam(required = false) String variable,

@Parameter(in = ParameterIn.QUERY, required = true,
description = "Date to render, strict `YYYY-MM-DD` — not an RFC 3339 date-time. Must be " +
description = "Date to render, a full UTC ISO-8601 timestamp (offset `Z`). Must be " +
"one of the product's `available_dates`.",
example = "2024-01-01")
example = "2024-01-01T00:00:00Z")
@RequestParam(required = false) String datetime,

@Parameter(in = ParameterIn.QUERY,
Expand Down Expand Up @@ -205,8 +206,13 @@ public ResponseEntity<byte[]> getCollectionVisualMapTile(
if (variable == null || variable.isBlank()) {
throw new InvalidParameterException("variable is required");
}
if (datetime == null || !datetime.matches("^\\d{4}-\\d{2}-\\d{2}$")) {
throw new InvalidParameterException("datetime is required and must be YYYY-MM-DD");
if (datetime == null) {
throw new InvalidParameterException("datetime is required");
}
try {
DatetimeUtils.parseUtcDateTime(datetime);
} catch (IllegalArgumentException e) {
throw new InvalidParameterException(e.getMessage());
}
if (!"png".equals(f) && !"webp".equals(f)) {
throw new InvalidParameterException("f must be 'png' or 'webp'");
Expand Down
34 changes: 20 additions & 14 deletions server/src/main/java/au/org/aodn/ogcapi/server/tile/RestExtApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import au.org.aodn.ogcapi.server.core.exception.InvalidParameterException;
import au.org.aodn.ogcapi.server.core.model.ErrorResponse;
import au.org.aodn.ogcapi.server.core.service.das.DasTilerService;
import au.org.aodn.ogcapi.server.core.util.DatetimeUtils;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
Expand Down Expand Up @@ -111,7 +112,7 @@ public ResponseEntity<JsonNode> getCollectionProducts(
example = "0c9eb39c-9cbe-4c6a-8a10-5867087e703a")
@PathVariable String collectionId) {
List<JsonNode> products = dasTilerService.productsForCollection(collectionId);
DasTilerService.DasJsonResult manifest = dasTilerService.getManifest();
DasTilerService.DasJsonResult manifest = dasTilerService.getManifest(collectionId);
JsonNode manifestProducts = manifest.body() != null ? manifest.body().path("products") : null;

ArrayNode result = mapper.createArrayNode();
Expand Down Expand Up @@ -220,7 +221,7 @@ public ResponseEntity<JsonNode> getCollectionProducts(
content = @Content(mediaType = "image/png",
schema = @Schema(type = "string", format = "binary"))),
@ApiResponse(responseCode = "400", description = "`dataset` or `variable` missing, `variable` " +
"containing a space (an unencoded `+`), `datetime` not `YYYY-MM-DD`, `lod` below 1, or " +
"containing a space (an unencoded `+`), `datetime` not a full UTC ISO-8601 timestamp, `lod` below 1, or " +
"negative `x`/`y`.",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class))),
Expand Down Expand Up @@ -283,9 +284,9 @@ public ResponseEntity<byte[]> getCollectionDataTile(
@RequestParam(required = false) String variable,

@Parameter(in = ParameterIn.QUERY, required = true,
description = "Date to decode, strict `YYYY-MM-DD` — not an RFC 3339 date-time. Must be " +
description = "Date to decode, a full UTC ISO-8601 timestamp (offset `Z`). Must be " +
"one of the product's `available_dates`.",
example = "2024-01-01")
example = "2024-01-01T00:00:00Z")
@RequestParam(required = false) String datetime) {

// Unlike the visual route's z (bounded 0..24 by WebMercatorQuad), the LOD grid is computed
Expand Down Expand Up @@ -336,7 +337,7 @@ public ResponseEntity<byte[]> getCollectionDataTile(
}
}"""))),
@ApiResponse(responseCode = "400", description = "`dataset` or `variable` missing, `variable` " +
"containing a space (an unencoded `+`), `datetime` not `YYYY-MM-DD`, or `lat`/`lon` " +
"containing a space (an unencoded `+`), `datetime` not a full UTC ISO-8601 timestamp, or `lat`/`lon` " +
"missing or out of range.",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class))),
Expand Down Expand Up @@ -382,9 +383,9 @@ public ResponseEntity<JsonNode> getCollectionDataPoint(
@RequestParam(required = false) String variable,

@Parameter(in = ParameterIn.QUERY, required = true,
description = "Date to decode, strict `YYYY-MM-DD`. Must be one of the product's " +
"`available_dates`.",
example = "2024-01-01")
description = "Date to decode, a full UTC ISO-8601 timestamp (offset `Z`). Must be one of " +
"the product's `available_dates`.",
example = "2024-01-01T00:00:00Z")
@RequestParam(required = false) String datetime,

@Parameter(in = ParameterIn.QUERY, required = true,
Expand Down Expand Up @@ -426,7 +427,7 @@ public ResponseEntity<JsonNode> getCollectionDataPoint(
@ApiResponse(responseCode = "200", description = "The data-tile decode manifest.",
content = @Content(mediaType = "application/json")),
@ApiResponse(responseCode = "400", description = "`dataset` or `variable` missing, `variable` " +
"containing a space (an unencoded `+`), or `datetime` not `YYYY-MM-DD`.",
"containing a space (an unencoded `+`), or `datetime` not a full UTC ISO-8601 timestamp.",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(responseCode = "404", description = "DAS reported an unknown product " +
Expand Down Expand Up @@ -471,9 +472,9 @@ public ResponseEntity<JsonNode> getCollectionDataManifest(
@RequestParam(required = false) String variable,

@Parameter(in = ParameterIn.QUERY, required = true,
description = "Date to decode, strict `YYYY-MM-DD`. Must be one of the product's " +
"`available_dates`.",
example = "2024-01-01")
description = "Date to decode, a full UTC ISO-8601 timestamp (offset `Z`). Must be one of " +
"the product's `available_dates`.",
example = "2024-01-01T00:00:00Z")
@RequestParam(required = false) String datetime) {

validateProductParams(dataset, variable, datetime);
Expand Down Expand Up @@ -502,8 +503,13 @@ private void validateProductParams(String dataset, String variable, String datet
"variable must not contain spaces; percent-encode the '+' of a two-variable " +
"product as %2B (e.g. variable=ucur%2Bvcur)");
}
if (datetime == null || !datetime.matches("^\\d{4}-\\d{2}-\\d{2}$")) {
throw new InvalidParameterException("datetime is required and must be YYYY-MM-DD");
if (datetime == null) {
throw new InvalidParameterException("datetime is required");
}
try {
DatetimeUtils.parseUtcDateTime(datetime);
} catch (IllegalArgumentException e) {
throw new InvalidParameterException(e.getMessage());
}
}

Expand Down
Loading
Loading