Skip to content
Merged
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

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -709,39 +703,6 @@ public JsonNode explainByUuid(String uuid, List<String> targets, String filter,
buildParameterSearchRequestSupplier(targets, filter, properties, sortBy, coor));
}

@Override
public BinaryResponse searchCollectionVectorTile(List<String> 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 <field> values that intersect the <zoom>/<x>/<y> 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<FieldValue> values = ids.stream()
.map(FieldValue::of)
.collect(Collectors.toList());

List<Query> 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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -44,12 +42,5 @@ JsonNode explainByUuid(
CQLCrsType coor
) throws Exception;

BinaryResponse searchCollectionVectorTile(
List<String> ids,
Integer tileMatrix,
Integer tileRow,
Integer tileCol
) throws IOException;

ResponseEntity<Map<String, ?>> getAutocompleteSuggestions(String input, String cql, CQLCrsType coor) throws Exception;
}
64 changes: 16 additions & 48 deletions server/src/main/java/au/org/aodn/ogcapi/server/tile/RestApi.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -46,9 +45,6 @@ public class RestApi implements CollectionsApi, MapApi, StylesApi, TileMatrixSet
@Autowired
protected StacToTileSetWmWGS84Q stacToTileSet;

@Autowired
protected BinaryResponseToBytes binaryResponseToByte;

@Autowired
protected DasTilerService dasTilerService;

Expand Down Expand Up @@ -77,19 +73,22 @@ public ResponseEntity<String> 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
* <p>
* 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`.",
Expand Down Expand Up @@ -144,12 +143,12 @@ public ResponseEntity<byte[]> 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,

Expand Down Expand Up @@ -215,8 +214,9 @@ public ResponseEntity<byte[]> 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()));
Expand Down Expand Up @@ -367,44 +367,12 @@ public ResponseEntity<TileMatrixSet> getTileMatrixSet(TileMatrixSets tileMatrixS
public ResponseEntity<InlineResponse2001> 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<String> collections, List<String> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down Expand Up @@ -161,8 +161,9 @@ public ResponseEntity<JsonNode> 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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -22,34 +19,6 @@ public List<String> getConformanceDeclaration() {
return List.of("http://www.opengis.net/spec/ogcapi-tiles-1/1.0");
}

@SuppressWarnings("unchecked")
public <T, R> ResponseEntity<?> getVectorTileOfCollection(
TileMatrixSets coordinateSystem,
List<String> ids,
Integer tileMatrix,
Integer tileRow,
Integer tileCol,
BiFunction<T, Filter, R> 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 <R> ResponseEntity<R> getTileSetsListOfCollection(List<String> id, String sortBy, OGCMediaTypeMapper f,
BiFunction<ElasticSearch.SearchResult, Filter, R> converter) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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());
}

/**
Expand Down Expand Up @@ -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<byte[]> response = testRestTemplate.getForEntity(
Expand All @@ -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<byte[]> response = testRestTemplate.getForEntity(
Expand All @@ -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<String> response = testRestTemplate.getForEntity(
Expand Down
Loading