diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/mapper/BinaryResponseToBytes.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/mapper/BinaryResponseToBytes.java deleted file mode 100644 index 4fa8c66a..00000000 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/mapper/BinaryResponseToBytes.java +++ /dev/null @@ -1,31 +0,0 @@ -package au.org.aodn.ogcapi.server.core.mapper; - -import co.elastic.clients.transport.endpoints.BinaryResponse; -import org.mapstruct.Mapper; -import org.opengis.filter.Filter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Service; - -import java.io.IOException; -import java.io.InputStream; - -@Service -@Mapper(componentModel = "spring") -public abstract class BinaryResponseToBytes implements Converter { - - protected Logger logger = LoggerFactory.getLogger(BinaryResponseToBytes.class); - - @Override - public byte[] convert(BinaryResponse from, Filter noUse) { - logger.debug("Incoming BinaryResponse type is {}", from.contentType()); - - try (InputStream s = from.content()) { - return s.readAllBytes(); - } - catch (IOException e) { - logger.warn("Fail to read datastream from BinaryResponse"); - } - return null; - } -} diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/ElasticSearch.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/ElasticSearch.java index 0b7ceaff..236ff806 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/ElasticSearch.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/ElasticSearch.java @@ -8,13 +8,10 @@ import co.elastic.clients.elasticsearch.ElasticsearchClient; import co.elastic.clients.elasticsearch._types.*; import co.elastic.clients.elasticsearch._types.query_dsl.*; -import co.elastic.clients.elasticsearch.core.SearchMvtRequest; import co.elastic.clients.elasticsearch.core.SearchRequest; import co.elastic.clients.elasticsearch.core.SearchResponse; import co.elastic.clients.elasticsearch.core.search.Hit; import co.elastic.clients.elasticsearch.core.search.HighlighterOrder; -import co.elastic.clients.elasticsearch.core.search_mvt.GridType; -import co.elastic.clients.transport.endpoints.BinaryResponse; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; @@ -56,9 +53,6 @@ public class ElasticSearch extends ElasticSearchBase implements Search { @Value("${elasticsearch.search_as_you_type.search_suggestions.fields}") protected String[] searchAsYouTypeEnabledFields; - @Value("${elasticsearch.cloud_optimized_index.name}") - protected String dataIndexName; - @Value("${elasticsearch.search_after.split_regex:\\|\\|}") protected String searchAfterSplitRegex; @@ -709,39 +703,6 @@ public JsonNode explainByUuid(String uuid, List targets, String filter, buildParameterSearchRequestSupplier(targets, filter, properties, sortBy, coor)); } - @Override - public BinaryResponse searchCollectionVectorTile(List ids, Integer tileMatrix, Integer tileRow, Integer tileCol) throws IOException { - - SearchMvtRequest.Builder builder = new SearchMvtRequest.Builder(); - builder.index(indexName) - .field(StacSummeries.Geometry.searchField) - .zoom(tileMatrix) - .x(tileRow) - .y(tileCol) - // If true, the meta layer’s feature is a bounding box resulting from a geo_bounds aggregation. - // The aggregation runs on values that intersect the // tile with wrap_longitude - // set to false. The resulting bounding box may be larger than the vector tile. - .exactBounds(Boolean.FALSE) - .gridType(GridType.Grid); - - if(ids != null && !ids.isEmpty()) { - List values = ids.stream() - .map(FieldValue::of) - .collect(Collectors.toList()); - - List filters = List.of( - TermsQuery.of(t -> t - .field(StacBasicField.UUID.searchField) - .terms(s -> s.value(values)))._toQuery()); - - builder.query(q -> q.bool(b -> b.filter(filters))); - } - - log.debug("Final elastic search mvt payload {}", builder); - - return esClient.searchMvt(builder.build()); - } - protected static FieldValue toFieldValue(String s) { try { Double v = Double.parseDouble(s.trim()); diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/Search.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/Search.java index 49cadeb3..46dc6bbd 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/Search.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/Search.java @@ -2,11 +2,9 @@ import au.org.aodn.stac.model.StacCollectionModel; import au.org.aodn.ogcapi.server.core.model.enumeration.CQLCrsType; -import co.elastic.clients.transport.endpoints.BinaryResponse; import com.fasterxml.jackson.databind.JsonNode; import org.springframework.http.ResponseEntity; -import java.io.IOException; import java.util.List; import java.util.Map; @@ -44,12 +42,5 @@ JsonNode explainByUuid( CQLCrsType coor ) throws Exception; - BinaryResponse searchCollectionVectorTile( - List ids, - Integer tileMatrix, - Integer tileRow, - Integer tileCol - ) throws IOException; - ResponseEntity> getAutocompleteSuggestions(String input, String cql, CQLCrsType coor) throws Exception; } diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/tile/RestApi.java b/server/src/main/java/au/org/aodn/ogcapi/server/tile/RestApi.java index 8ca1edc3..a2a26e44 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/tile/RestApi.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/tile/RestApi.java @@ -1,6 +1,5 @@ package au.org.aodn.ogcapi.server.tile; -import au.org.aodn.ogcapi.server.core.mapper.BinaryResponseToBytes; import au.org.aodn.ogcapi.server.core.mapper.StacToTileSetWmWGS84Q; import au.org.aodn.ogcapi.server.core.model.enumeration.OGCMediaTypeMapper; import au.org.aodn.ogcapi.server.core.mapper.StacToInlineResponse2002; @@ -46,9 +45,6 @@ public class RestApi implements CollectionsApi, MapApi, StylesApi, TileMatrixSet @Autowired protected StacToTileSetWmWGS84Q stacToTileSet; - @Autowired - protected BinaryResponseToBytes binaryResponseToByte; - @Autowired protected DasTilerService dasTilerService; @@ -77,19 +73,22 @@ public ResponseEntity collectionMapGetTile(String tileMatrix, Integer ti * Serves a DAS "visual tile" (colourised raster PNG/WebP for a gridded Zarr ocean product) * through the OGC API - Tiles collection map-tile route *

- * Deliberately non-strict OGC: {@code tileMatrix}=z, {@code tileRow}=x, {@code tileCol}=y - * (slippy {z}/{x}/{y}), matching the convention already shipped by the vector-tile route - * ({@code ElasticSearch.searchCollectionVectorTile}, used by {@code datasetVectorGetTile} - * below) and the frontend's {@code VectorTileLayers.tsx} template — not strict OGC - * row=y/col=x semantics. A generic OGC client would disagree; Mapbox does not care. + * Standard OGC semantics: {@code tileMatrix}=z, {@code tileRow}=y, {@code tileCol}=x — + * matching {@code WebMercatorQuad}'s row/column definition. The frontend never hardcodes + * this path; it substitutes the ready-made {@code visual_tile_url_template} from the + * products listing, whose placeholders are named {@code {tileRow}}/{@code {tileCol}} (not + * {@code {x}}/{@code {y}}) precisely so the template can't be "corrected" back into slippy + * {@code {z}}/{@code {x}}/{@code {y}} order by someone who doesn't know the row/col swap is + * intentional (see {@code RestExtApi.getCollectionProducts}). */ @Operation( summary = "Retrieve a DAS visual map tile of a collection", description = "Proxies a colourised raster tile (PNG/WebP) rendered by the AODN " + "data-access-service from a gridded Zarr ocean product.\n\n" + - "**Not strict OGC:** the path is slippy-map `{z}/{x}/{y}`, so `tileRow` is **x** and " + - "`tileCol` is **y** — the reverse of OGC's row/column order. This matches the vector-tile " + - "route and the frontend's Mapbox templates.\n\n" + + "Standard OGC row/column order: `tileRow` is the **row** (y) and `tileCol` is the " + + "**column** (x), matching `WebMercatorQuad`. Use the ready-made " + + "`visual_tile_url_template` from the products listing rather than building this " + + "path by hand.\n\n" + "Valid `dataset`, `variable` and `datetime` values come from " + "`GET /api/v1/ogc/ext/tiles/collections/{collectionId}/products` — use each product's " + "ready-made `visual_tile_url_template`.", @@ -144,12 +143,12 @@ public ResponseEntity getCollectionVisualMapTile( @PathVariable Integer tileMatrix, @Parameter(in = ParameterIn.PATH, required = true, - description = "Tile **x**, not the OGC row. Range 0 to 2^z - 1.", + description = "Tile row (**y**). Range 0 to 2^z - 1.", schema = @Schema(type = "integer", minimum = "0"), example = "3") @PathVariable Integer tileRow, @Parameter(in = ParameterIn.PATH, required = true, - description = "Tile **y**, not the OGC column. Range 0 to 2^z - 1.", + description = "Tile column (**x**). Range 0 to 2^z - 1.", schema = @Schema(type = "integer", minimum = "0"), example = "2") @PathVariable Integer tileCol, @@ -215,8 +214,9 @@ public ResponseEntity getCollectionVisualMapTile( // DAS identifies a renderable product by the combined {dataset}:{variable} id. String product = dataset + ":" + variable; + // tileRow is the OGC row (y), tileCol is the OGC column (x); getVisualTile takes x before y. DasTilerService.DasTileResult tile = dasTilerService.getVisualTile( - product, datetime, tileMatrix, tileRow, tileCol, f, colormap, rescale); + product, datetime, tileMatrix, tileCol, tileRow, f, colormap, rescale); ResponseEntity.BodyBuilder response = ResponseEntity.ok() .contentType(MediaType.parseMediaType(tile.contentType())); @@ -367,44 +367,12 @@ public ResponseEntity getTileMatrixSet(TileMatrixSets tileMatrixS public ResponseEntity getTileMatrixSetsList(String f) { return null; } - /** - * Return mvt from search instance, type should be protocol buffer - * @param tileMatrix - * @param tileRow - * @param tileCol - * @param tileMatrixSetId - * @param datetime - * @param collections - * @param subset - * @param crs - * @param subsetCrs - * @param f - * @return - */ - @CrossOrigin(origins = "*") //TODO: Just good for testing @Override public ResponseEntity datasetVectorGetTile(String tileMatrix, Integer tileRow, Integer tileCol, TileMatrixSets tileMatrixSetId, String datetime, List collections, List subset, String crs, String subsetCrs, String f) { - - OGCMediaTypeMapper type = OGCMediaTypeMapper.convert(f, OGCMediaTypeMapper.mapbox); - - switch(type) { - case mapbox -> { - return restService.getVectorTileOfCollection( - tileMatrixSetId, - collections, - Integer.valueOf(tileMatrix), - tileRow, - tileCol, - binaryResponseToByte::convert); - } - - default -> { - return ResponseEntity.status(HttpStatus.NOT_IMPLEMENTED).build(); - } - } + return ResponseEntity.status(HttpStatus.NOT_IMPLEMENTED).build(); } @Override diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/tile/RestExtApi.java b/server/src/main/java/au/org/aodn/ogcapi/server/tile/RestExtApi.java index fcfe9bca..df832981 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/tile/RestExtApi.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/tile/RestExtApi.java @@ -67,7 +67,7 @@ public class RestExtApi { "tile_types": ["visual", "data"], "available_dates": ["2024-01-01", "2024-01-02"], "full_date_range": {"start": "2020-01-01", "end": "2024-01-02"}, - "visual_tile_url_template": "/api/v1/ogc/collections/0c9eb39c-9cbe-4c6a-8a10-5867087e703a/map/tiles/WebMercatorQuad/{z}/{x}/{y}?dataset=model_sea_level_anomaly_gridded_realtime&variable=gsla&datetime={datetime}&f=png", + "visual_tile_url_template": "/api/v1/ogc/collections/0c9eb39c-9cbe-4c6a-8a10-5867087e703a/map/tiles/WebMercatorQuad/{z}/{tileRow}/{tileCol}?dataset=model_sea_level_anomaly_gridded_realtime&variable=gsla&datetime={datetime}&f=png", "legend_url": "/api/v1/ogc/ext/tiles/colormaps/{colormap}/legend", "data_tile_url_template": "/api/v1/ogc/ext/tiles/collections/0c9eb39c-9cbe-4c6a-8a10-5867087e703a/data_tiles/{lod}/{x}/{y}?dataset=model_sea_level_anomaly_gridded_realtime&variable=gsla&datetime={datetime}", "data_manifest_url_template": "/api/v1/ogc/ext/tiles/collections/0c9eb39c-9cbe-4c6a-8a10-5867087e703a/data_tiles/manifest?dataset=model_sea_level_anomaly_gridded_realtime&variable=gsla&datetime={datetime}" @@ -161,8 +161,9 @@ public ResponseEntity getCollectionProducts( String encodedVariable = URLEncoder.encode(variablePart, StandardCharsets.UTF_8); if (canVisual) { + // Placeholders are named after the OGC path variables themselves (tileRow/tileCol, row=y col=x), not {x}/{y} entry.put("visual_tile_url_template", - "/api/v1/ogc/collections/" + collectionId + "/map/tiles/WebMercatorQuad/{z}/{x}/{y}" + "/api/v1/ogc/collections/" + collectionId + "/map/tiles/WebMercatorQuad/{z}/{tileRow}/{tileCol}" + "?dataset=" + encodedDataset + "&variable=" + encodedVariable + "&datetime={datetime}&f=png"); entry.put("legend_url", "/api/v1/ogc/ext/tiles/colormaps/{colormap}/legend"); diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/tile/RestService.java b/server/src/main/java/au/org/aodn/ogcapi/server/tile/RestService.java index dfa84cb8..743c725a 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/tile/RestService.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/tile/RestService.java @@ -3,14 +3,11 @@ import au.org.aodn.ogcapi.server.core.model.enumeration.OGCMediaTypeMapper; import au.org.aodn.ogcapi.server.core.service.ElasticSearch; import au.org.aodn.ogcapi.server.core.service.OGCApiService; -import au.org.aodn.ogcapi.server.core.exception.CustomException; -import au.org.aodn.ogcapi.tile.model.TileMatrixSets; import org.opengis.filter.Filter; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; -import java.io.IOException; import java.util.List; import java.util.function.BiFunction; @@ -22,34 +19,6 @@ public List getConformanceDeclaration() { return List.of("http://www.opengis.net/spec/ogcapi-tiles-1/1.0"); } - @SuppressWarnings("unchecked") - public ResponseEntity getVectorTileOfCollection( - TileMatrixSets coordinateSystem, - List ids, - Integer tileMatrix, - Integer tileRow, - Integer tileCol, - BiFunction converter) { - - // TODO: Implements additional filters - try { - switch (coordinateSystem) { - case WEBMERCATORQUAD -> { - return ResponseEntity.ok() - .contentType(OGCMediaTypeMapper.mapbox.getMediaType()) - .body(converter.apply((T) search.searchCollectionVectorTile(ids, tileMatrix, tileRow, tileCol), null)); - } - default -> { - // We support WEBMERCATORQUAD at the moment, so if it isn't return empty set. - throw new IllegalArgumentException("Unknown coordinate system"); - } - } - } - catch(IOException e) { - throw new CustomException(e.getMessage()); - } - } - public ResponseEntity getTileSetsListOfCollection(List id, String sortBy, OGCMediaTypeMapper f, BiFunction converter) { try { diff --git a/server/src/test/java/au/org/aodn/ogcapi/server/tile/RestApiTest.java b/server/src/test/java/au/org/aodn/ogcapi/server/tile/RestApiTest.java index 7704ca88..a9937d3d 100644 --- a/server/src/test/java/au/org/aodn/ogcapi/server/tile/RestApiTest.java +++ b/server/src/test/java/au/org/aodn/ogcapi/server/tile/RestApiTest.java @@ -91,9 +91,9 @@ public void verifyCollectionTiles() throws IOException { Assertions.assertEquals("Impacts of stress on coral reproduction.", tiles.getBody().getTilesets().get(0).getTitle(), "Title matched 1"); } /** - * Verify api call /tiles/{tileMatrixSetId}/{tileMatrix}/{tileRow}/{tileCol}, this call will return the bounding - * box using mvt search, however this mvt search require licence and not enable by default. Without UI it is - * very hard to enable it. + * Verify api call /tiles/{tileMatrixSetId}/{tileMatrix}/{tileRow}/{tileCol}. The vector-tile + * fetch implementation was removed as dead code (its only frontend consumer was deleted), so + * the route is now an unimplemented stub like its siblings. */ @Test public void verifyTilesMatrixSetXYZ() throws IOException { @@ -106,7 +106,7 @@ public void verifyTilesMatrixSetXYZ() throws IOException { byte[].class ); - // No meaningful verify can be done here, we keep it here so that we know that test case considered. + Assertions.assertEquals(HttpStatus.NOT_IMPLEMENTED, tiles.getStatusCode()); } /** @@ -239,7 +239,8 @@ public void verifyVisualMapTileUnknownProductIsForwardedToDas() { @Test public void verifyVisualMapTileForwardsZXYAndReturnsImage() { - when(dasTilerService.getVisualTile(eq("model_sla:gsla"), eq("2024-01-01"), eq(2), eq(1), eq(3), eq("png"), isNull(), isNull())) + // path .../2/1/3 -> tileMatrix=2, tileRow(y)=1, tileCol(x)=3; getVisualTile takes x before y. + when(dasTilerService.getVisualTile(eq("model_sla:gsla"), eq("2024-01-01"), eq(2), eq(3), eq(1), eq("png"), isNull(), isNull())) .thenReturn(new DasTilerService.DasTileResult("tile-bytes".getBytes(), "image/png", "public, max-age=31536000, immutable")); ResponseEntity response = testRestTemplate.getForEntity( @@ -255,7 +256,8 @@ public void verifyVisualMapTileForwardsZXYAndReturnsImage() { @Test public void verifyVisualMapTileRebuildsProductAndMapsWebpExt() { // dataset + variable are recombined into the DAS product id `model_sla:gsla`. - when(dasTilerService.getVisualTile(eq("model_sla:gsla"), eq("2024-01-01"), eq(2), eq(1), eq(3), eq("webp"), isNull(), isNull())) + // path .../2/1/3 -> tileMatrix=2, tileRow(y)=1, tileCol(x)=3; getVisualTile takes x before y. + when(dasTilerService.getVisualTile(eq("model_sla:gsla"), eq("2024-01-01"), eq(2), eq(3), eq(1), eq("webp"), isNull(), isNull())) .thenReturn(new DasTilerService.DasTileResult("webp-bytes".getBytes(), "image/webp", null)); ResponseEntity response = testRestTemplate.getForEntity( @@ -269,7 +271,8 @@ public void verifyVisualMapTileRebuildsProductAndMapsWebpExt() { @Test public void verifyVisualMapTileUpstreamErrorMirrored() { - when(dasTilerService.getVisualTile(eq("model_sla:gsla"), eq("2024-01-01"), eq(2), eq(1), eq(3), eq("png"), isNull(), isNull())) + // path .../2/1/3 -> tileMatrix=2, tileRow(y)=1, tileCol(x)=3; getVisualTile takes x before y. + when(dasTilerService.getVisualTile(eq("model_sla:gsla"), eq("2024-01-01"), eq(2), eq(3), eq(1), eq("png"), isNull(), isNull())) .thenThrow(new DasUpstreamException(HttpStatus.NOT_FOUND, "no such date")); ResponseEntity response = testRestTemplate.getForEntity(