From f97cf9d7f06f32c5c851b2431c936c0f46a7a5fe Mon Sep 17 00:00:00 2001 From: Dinasoa Date: Thu, 22 Jan 2026 21:25:06 +0300 Subject: [PATCH] feat: implement tile19 on airbus pneo source --- .github/workflows/ci.yml | 3 + .github/workflows/tile19-request-body.json | 11 -- .../birdia/tile19/conf/RestTemplateConf.java | 47 ++++++ .../model/airbus/AirbusAuthResponse.java | 21 +++ .../tile19/model/airbus/AirbusFeature.java | 15 ++ .../model/airbus/AirbusPNEOResponse.java | 14 ++ .../tile19/model/airbus/AirbusProperties.java | 11 ++ .../model/airbus/AirbusRequestBody.java | 22 +++ .../tile19/model/airbus/FeatureLinks.java | 15 ++ .../birdia/tile19/model/airbus/Geometry.java | 17 +++ .../tile19/model/airbus/GeometryCentroid.java | 9 ++ .../tile19/model/airbus/Properties.java | 48 ++++++ .../fr/birdia/tile19/model/airbus/Rights.java | 11 ++ .../tile19/model/airbus/SimpleLink.java | 10 ++ .../tile19/model/exception/ApiException.java | 23 +++ .../tile19/service/ImageExtenderService.java | 42 +++-- .../service/TilesDownloaderService.java | 32 ++++ .../service/airbus/AirbusPNEOService.java | 144 ++++++++++++++++++ .../fr/birdia/tile19/ImageExtenderIT.java | 27 ++++ .../java/fr/birdia/tile19/conf/EnvConf.java | 11 ++ .../tile19/unit/ImageExtenderServiceTest.java | 14 +- .../tile19/unit/TilesDownloaderTest.java | 13 +- 22 files changed, 534 insertions(+), 26 deletions(-) create mode 100644 src/main/java/fr/birdia/tile19/conf/RestTemplateConf.java create mode 100644 src/main/java/fr/birdia/tile19/model/airbus/AirbusAuthResponse.java create mode 100644 src/main/java/fr/birdia/tile19/model/airbus/AirbusFeature.java create mode 100644 src/main/java/fr/birdia/tile19/model/airbus/AirbusPNEOResponse.java create mode 100644 src/main/java/fr/birdia/tile19/model/airbus/AirbusProperties.java create mode 100644 src/main/java/fr/birdia/tile19/model/airbus/AirbusRequestBody.java create mode 100644 src/main/java/fr/birdia/tile19/model/airbus/FeatureLinks.java create mode 100644 src/main/java/fr/birdia/tile19/model/airbus/Geometry.java create mode 100644 src/main/java/fr/birdia/tile19/model/airbus/GeometryCentroid.java create mode 100644 src/main/java/fr/birdia/tile19/model/airbus/Properties.java create mode 100644 src/main/java/fr/birdia/tile19/model/airbus/Rights.java create mode 100644 src/main/java/fr/birdia/tile19/model/airbus/SimpleLink.java create mode 100644 src/main/java/fr/birdia/tile19/model/exception/ApiException.java create mode 100644 src/main/java/fr/birdia/tile19/service/airbus/AirbusPNEOService.java create mode 100644 src/test/java/fr/birdia/tile19/conf/EnvConf.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8865c0b..950686a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,9 @@ jobs: env: AWS_REGION: eu-west-3 + AIRBUS_SEARCHAPI_BASEURL: ${{ secrets.AIRBUS_SEARCHAPI_URL }} + AIRBUS_AUTHENTICATION_BASEURL: ${{ secrets.AIRBUS_AUTHENTICATION_BASEURL }} + AIRBUS_API_KEY: ${{ secrets.AIRBUS_API_KEY }} steps: diff --git a/.github/workflows/tile19-request-body.json b/.github/workflows/tile19-request-body.json index 09e4875..cdf4c86 100644 --- a/.github/workflows/tile19-request-body.json +++ b/.github/workflows/tile19-request-body.json @@ -98,17 +98,6 @@ "latitude": 43.548050434476295, "longitude": 4.681549072265625 }, - { - "x": 524712, - "y": 359841, - "z": 20, - "server": "geoserver", - "layer": "Ortho_Lisieux_Normandie_2022", - "shiftNb": 0, - "isCropped": true, - "latitude": 49.05924487573084, - "longitude": 0.14556884765625 - }, { "x": 519529, "y": 357258, diff --git a/src/main/java/fr/birdia/tile19/conf/RestTemplateConf.java b/src/main/java/fr/birdia/tile19/conf/RestTemplateConf.java new file mode 100644 index 0000000..14241fa --- /dev/null +++ b/src/main/java/fr/birdia/tile19/conf/RestTemplateConf.java @@ -0,0 +1,47 @@ +package fr.birdia.tile19.conf; + +import static fr.birdia.tile19.model.exception.ApiException.ExceptionType.SERVER_EXCEPTION; + +import fr.birdia.tile19.model.exception.ApiException; +import java.io.IOException; +import java.time.Duration; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.web.client.ResponseErrorHandler; +import org.springframework.web.client.RestTemplate; + +@Configuration +@Slf4j +public class RestTemplateConf { + @Bean + public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) { + return restTemplateBuilder + .setReadTimeout(Duration.ofMinutes(10)) + .setConnectTimeout(Duration.ofMinutes(10)) + .errorHandler(new RestTemplateErrorHandler()) + .build(); + } + + private static class RestTemplateErrorHandler implements ResponseErrorHandler { + @Override + public boolean hasError(ClientHttpResponse response) throws IOException { + return response.getStatusCode().is4xxClientError() + || response.getStatusCode().is5xxServerError(); + } + + @Override + public void handleError(ClientHttpResponse response) throws IOException { + var statusCode = response.getStatusCode(); + if (statusCode.is4xxClientError()) { + throw new ApiException( + SERVER_EXCEPTION, "client exception with status " + response.getStatusCode()); + } else if (statusCode.is5xxServerError()) { + throw new ApiException( + SERVER_EXCEPTION, "server exception with status " + response.getStatusCode()); + } + } + } +} diff --git a/src/main/java/fr/birdia/tile19/model/airbus/AirbusAuthResponse.java b/src/main/java/fr/birdia/tile19/model/airbus/AirbusAuthResponse.java new file mode 100644 index 0000000..26ef5fd --- /dev/null +++ b/src/main/java/fr/birdia/tile19/model/airbus/AirbusAuthResponse.java @@ -0,0 +1,21 @@ +package fr.birdia.tile19.model.airbus; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class AirbusAuthResponse { + @JsonProperty("access_token") + private String accessToken; + + @JsonProperty("token_type") + private String tokenType; + + @JsonProperty("expires_in") + private int expiresIn; + + @JsonProperty("refresh_expires_in") + private int refreshExpiresIn; +} diff --git a/src/main/java/fr/birdia/tile19/model/airbus/AirbusFeature.java b/src/main/java/fr/birdia/tile19/model/airbus/AirbusFeature.java new file mode 100644 index 0000000..5656459 --- /dev/null +++ b/src/main/java/fr/birdia/tile19/model/airbus/AirbusFeature.java @@ -0,0 +1,15 @@ +package fr.birdia.tile19.model.airbus; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class AirbusFeature { + @JsonProperty("_links") + private FeatureLinks links; + + private Geometry geometry; + private Properties properties; + private Rights rights; + private String type; +} diff --git a/src/main/java/fr/birdia/tile19/model/airbus/AirbusPNEOResponse.java b/src/main/java/fr/birdia/tile19/model/airbus/AirbusPNEOResponse.java new file mode 100644 index 0000000..850de34 --- /dev/null +++ b/src/main/java/fr/birdia/tile19/model/airbus/AirbusPNEOResponse.java @@ -0,0 +1,14 @@ +package fr.birdia.tile19.model.airbus; + +import java.util.List; +import lombok.Data; + +@Data +public class AirbusPNEOResponse { + private boolean error; + private int itemsPerPage; + private int startIndex; + private int totalResults; + private String type; + private List features; +} diff --git a/src/main/java/fr/birdia/tile19/model/airbus/AirbusProperties.java b/src/main/java/fr/birdia/tile19/model/airbus/AirbusProperties.java new file mode 100644 index 0000000..d99a000 --- /dev/null +++ b/src/main/java/fr/birdia/tile19/model/airbus/AirbusProperties.java @@ -0,0 +1,11 @@ +package fr.birdia.tile19.model.airbus; + +import lombok.Builder; +import lombok.Data; + +@Builder +@Data +public class AirbusProperties { + private String wmtsUrl; + private String bearer; +} diff --git a/src/main/java/fr/birdia/tile19/model/airbus/AirbusRequestBody.java b/src/main/java/fr/birdia/tile19/model/airbus/AirbusRequestBody.java new file mode 100644 index 0000000..f9726cf --- /dev/null +++ b/src/main/java/fr/birdia/tile19/model/airbus/AirbusRequestBody.java @@ -0,0 +1,22 @@ +package fr.birdia.tile19.model.airbus; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.ToString; + +@Data +@AllArgsConstructor +@Builder +@ToString +public class AirbusRequestBody { + private int itemsPerPage; + private int startPage; + private String cloudCover; + private String constellation; + private String sortBy; + private String workspace; + private String processingLevel; + private String relation; + private Geometry geometry; +} diff --git a/src/main/java/fr/birdia/tile19/model/airbus/FeatureLinks.java b/src/main/java/fr/birdia/tile19/model/airbus/FeatureLinks.java new file mode 100644 index 0000000..519ccca --- /dev/null +++ b/src/main/java/fr/birdia/tile19/model/airbus/FeatureLinks.java @@ -0,0 +1,15 @@ +package fr.birdia.tile19.model.airbus; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class FeatureLinks { + private SimpleLink delete; + private SimpleLink monitor; + private SimpleLink quicklook; + private SimpleLink thumbnail; + private SimpleLink wms; + private SimpleLink wmts; +} diff --git a/src/main/java/fr/birdia/tile19/model/airbus/Geometry.java b/src/main/java/fr/birdia/tile19/model/airbus/Geometry.java new file mode 100644 index 0000000..eef86d6 --- /dev/null +++ b/src/main/java/fr/birdia/tile19/model/airbus/Geometry.java @@ -0,0 +1,17 @@ +package fr.birdia.tile19.model.airbus; + +import java.math.BigDecimal; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.ToString; + +@Data +@AllArgsConstructor +@Builder +@ToString +public class Geometry { + private String type; + private List>> coordinates; +} diff --git a/src/main/java/fr/birdia/tile19/model/airbus/GeometryCentroid.java b/src/main/java/fr/birdia/tile19/model/airbus/GeometryCentroid.java new file mode 100644 index 0000000..079a2e5 --- /dev/null +++ b/src/main/java/fr/birdia/tile19/model/airbus/GeometryCentroid.java @@ -0,0 +1,9 @@ +package fr.birdia.tile19.model.airbus; + +import lombok.Data; + +@Data +public class GeometryCentroid { + private double lat; + private double lon; +} diff --git a/src/main/java/fr/birdia/tile19/model/airbus/Properties.java b/src/main/java/fr/birdia/tile19/model/airbus/Properties.java new file mode 100644 index 0000000..5fa19e9 --- /dev/null +++ b/src/main/java/fr/birdia/tile19/model/airbus/Properties.java @@ -0,0 +1,48 @@ +package fr.birdia.tile19.model.airbus; + +import lombok.Data; + +@Data +public class Properties { + private String acquisitionDate; + private String acquisitionIdentifier; + private String activityId; + private double azimuthAngle; + private double cloudCover; + private String commercialReference; + private String constellation; + private String contractId; + private String correlationId; + private String customerReference; + private String expirationDate; + private String format; + private GeometryCentroid geometryCentroid; + private String id; + private double illuminationAzimuthAngle; + private double illuminationElevationAngle; + private double incidenceAngle; + private double incidenceAngleAcrossTrack; + private double incidenceAngleAlongTrack; + private String internalLine; + private String internalReference; + private String lastUpdateDate; + private String organisationName; + private String platform; + private String processingCenter; + private String processingDate; + private String processingLevel; + private String processorName; + private String productCategory; + private String productType; + private String productionStatus; + private String publicationDate; + private boolean qualified; + private double resolution; + private String sensorType; + private double snowCover; + private String spectralRange; + private double surfaceArea; + private String workspaceId; + private String workspaceName; + private String workspaceTitle; +} diff --git a/src/main/java/fr/birdia/tile19/model/airbus/Rights.java b/src/main/java/fr/birdia/tile19/model/airbus/Rights.java new file mode 100644 index 0000000..8dd8394 --- /dev/null +++ b/src/main/java/fr/birdia/tile19/model/airbus/Rights.java @@ -0,0 +1,11 @@ +package fr.birdia.tile19.model.airbus; + +import java.util.Map; +import lombok.Data; + +@Data +public class Rights { + private Map browse; + private Map wms; + private Map wmts; +} diff --git a/src/main/java/fr/birdia/tile19/model/airbus/SimpleLink.java b/src/main/java/fr/birdia/tile19/model/airbus/SimpleLink.java new file mode 100644 index 0000000..6e413f6 --- /dev/null +++ b/src/main/java/fr/birdia/tile19/model/airbus/SimpleLink.java @@ -0,0 +1,10 @@ +package fr.birdia.tile19.model.airbus; + +import lombok.Data; + +@Data +public class SimpleLink { + private String href; + private String name; + private String type; +} diff --git a/src/main/java/fr/birdia/tile19/model/exception/ApiException.java b/src/main/java/fr/birdia/tile19/model/exception/ApiException.java new file mode 100644 index 0000000..01fc007 --- /dev/null +++ b/src/main/java/fr/birdia/tile19/model/exception/ApiException.java @@ -0,0 +1,23 @@ +package fr.birdia.tile19.model.exception; + +import lombok.Getter; + +public class ApiException extends RuntimeException { + + @Getter private final ExceptionType type; + + public ApiException(ExceptionType type, String message) { + super(message); + this.type = type; + } + + public ApiException(ExceptionType type, Exception source) { + super(source); + this.type = type; + } + + public enum ExceptionType { + CLIENT_EXCEPTION, + SERVER_EXCEPTION + } +} diff --git a/src/main/java/fr/birdia/tile19/service/ImageExtenderService.java b/src/main/java/fr/birdia/tile19/service/ImageExtenderService.java index fd9f4e1..ecd8181 100644 --- a/src/main/java/fr/birdia/tile19/service/ImageExtenderService.java +++ b/src/main/java/fr/birdia/tile19/service/ImageExtenderService.java @@ -3,6 +3,8 @@ import static fr.birdia.tile19.service.TilesDownloaderService.tileToLatLon; import fr.birdia.tile19.concurrency.Workers; +import fr.birdia.tile19.model.airbus.AirbusProperties; +import fr.birdia.tile19.service.airbus.AirbusPNEOService; import java.awt.*; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; @@ -27,6 +29,7 @@ public class ImageExtenderService { private final double LAT_KM_PER_DEG = 110.574; private final double LON_KM_PER_DEG = 111.320; private int imageSize = 1024; + private final String AIRBUS_SERVER = "airbus"; private Integer x = null; private Integer y = null; private Integer x1 = null; @@ -35,21 +38,24 @@ public class ImageExtenderService { private Integer y2 = null; private Workers workers; private ImageDegraderService imageDegraderService; + private AirbusPNEOService airbusPNEOService; public ImageExtenderService( TilesDownloaderService downloader, TilesMergerService merger, Workers workers, - ImageDegraderService imageDegraderService) { + ImageDegraderService imageDegraderService, + AirbusPNEOService airbusPNEOService) { this.tileDownloader = downloader; this.tileMerger = merger; this.workers = workers; this.imageDegraderService = imageDegraderService; + this.airbusPNEOService = airbusPNEOService; } public double[] computeXYOffsets(double lat, double lon, int x, int y, int z, String server) { double[] pixelCoords = convertCoordinatesToPixel(lat, lon, x, y, z); - return server.equals("geoserver_ign") + return server.equals("geoserver_ign") || server.equals("airbus") ? new double[] {pixelCoords[0] - 128, pixelCoords[1] - 128, pixelCoords[0], pixelCoords[1]} : new double[] {pixelCoords[0] - 512, pixelCoords[1] - 512, pixelCoords[0], pixelCoords[1]}; } @@ -76,7 +82,7 @@ public String process( if (isCropped) { int cropSize = 1024; - if (server.equals("geoserver_ign")) { + if (server.equals("geoserver_ign") || server.equals("airbus")) { cropSize = 256; this.imageSize = 256; } @@ -84,7 +90,8 @@ public String process( double[] pixelCoords = convertCoordinatesToPixel(lat, lon, x, y, z); byte[] imageBytes = downloadTilesBytes( - this.x, this.y, this.x1, this.x2, this.y1, this.y2, z, server, layer, isOpaque); + this.x, this.y, this.x1, this.x2, this.y1, this.y2, z, server, layer, isOpaque, lat, + lon); BufferedImage image = ImageIO.read(new java.io.ByteArrayInputStream(imageBytes)); BufferedImage cropped = centerImageOnPoint(image, (int) pixelCoords[0], (int) pixelCoords[1], cropSize); @@ -94,14 +101,14 @@ public String process( this.x2 += shiftNb; this.x1 += shiftNb; - return downloadTilesBytesAndConvertToBase64(z, server, layer, isOpaque); + return downloadTilesBytesAndConvertToBase64(z, server, layer, isOpaque, lat, lon); } else if (shiftNb != 0 && Objects.equals(shiftDirection, "UP_DOWN_SIDE")) { this.y2 += shiftNb; this.y1 += shiftNb; - return downloadTilesBytesAndConvertToBase64(z, server, layer, isOpaque); + return downloadTilesBytesAndConvertToBase64(z, server, layer, isOpaque, lat, lon); } - return downloadTilesBytesAndConvertToBase64(z, server, layer, isOpaque); + return downloadTilesBytesAndConvertToBase64(z, server, layer, isOpaque, lat, lon); } public String convertImageToBase64(BufferedImage image) throws IOException { @@ -111,10 +118,12 @@ public String convertImageToBase64(BufferedImage image) throws IOException { } public String downloadTilesBytesAndConvertToBase64( - int z, String server, String layer, boolean isOpaque) throws IOException { + int z, String server, String layer, boolean isOpaque, double lat, double lon) + throws IOException { byte[] imageBytes = downloadTilesBytes( - this.x, this.y, this.x1, this.x2, this.y1, this.y2, z, server, layer, isOpaque); + this.x, this.y, this.x1, this.x2, this.y1, this.y2, z, server, layer, isOpaque, lat, + lon); BufferedImage image = ImageIO.read(new java.io.ByteArrayInputStream(imageBytes)); return convertImageToBase64(image); } @@ -129,11 +138,16 @@ public byte[] downloadTilesBytes( int z, String server, String layer, - boolean isOpaque) + boolean isOpaque, + double lat, + double lon) throws IOException { BufferedImage[][] results = new BufferedImage[y2 - y1][x2 - x1]; List> callables = new ArrayList<>(); + final boolean isAirbusServer = AIRBUS_SERVER.equals(server); + final AirbusProperties airbusProperties = + isAirbusServer ? airbusPNEOService.retrieveAirbusWmtsLink(lat, lon) : null; for (int dy = y1; dy < y2; dy++) { for (int dx = x1; dx < x2; dx++) { @@ -144,7 +158,13 @@ public byte[] downloadTilesBytes( callables.add( () -> { - BufferedImage img = tileDownloader.download(tileX, tileY, z, server, layer); + BufferedImage img; + + if (isAirbusServer) { + img = tileDownloader.download(tileX, tileY, z, airbusProperties); + } else { + img = tileDownloader.download(tileX, tileY, z, server, layer); + } results[row][col] = img; return null; }); diff --git a/src/main/java/fr/birdia/tile19/service/TilesDownloaderService.java b/src/main/java/fr/birdia/tile19/service/TilesDownloaderService.java index 7eb153c..67b7cc2 100644 --- a/src/main/java/fr/birdia/tile19/service/TilesDownloaderService.java +++ b/src/main/java/fr/birdia/tile19/service/TilesDownloaderService.java @@ -1,6 +1,9 @@ package fr.birdia.tile19.service; +import fr.birdia.tile19.model.airbus.AirbusProperties; +import fr.birdia.tile19.service.airbus.AirbusPNEOService; import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -12,10 +15,15 @@ import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.HashMap; +import java.util.Objects; import javax.imageio.ImageIO; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; @Service @AllArgsConstructor @@ -25,6 +33,8 @@ public class TilesDownloaderService { private final String IGN_BASE_URL = "https://data.geopf.fr/wmts"; private final String GEOSERVER = "geoserver"; private final XYZToBBOXService xyzToBBoxService; + private final AirbusPNEOService airbusPNEOService; + private RestTemplate restTemplate; static double[] tileToLatLon(int x, int y, int zoom) { int n = (int) Math.pow(2, zoom); @@ -144,6 +154,28 @@ public BufferedImage download(int xTile, int yTile, int zoom, String server, Str return null; } + public BufferedImage download(int xTile, int yTile, int zoom, AirbusProperties airbusProperties) + throws IOException { + HttpEntity wmtsEntityHeaders = + new HttpEntity<>(airbusPNEOService.customizeHeaders(airbusProperties.getBearer())); + String baseUrl = airbusProperties.getWmtsUrl(); + String xyzUrl = String.format(baseUrl + "%d/%d/%d.png", zoom, xTile, yTile); + log.info("Process download on URL={}", xyzUrl); + ResponseEntity pneoImage = + restTemplate.exchange(xyzUrl, HttpMethod.GET, wmtsEntityHeaders, byte[].class); + log.info("Successfully retrieved image"); + BufferedImage bufferedImage; + try (ByteArrayInputStream bis = + new ByteArrayInputStream(Objects.requireNonNull(pneoImage.getBody()))) { + bufferedImage = ImageIO.read(bis); + } + if (bufferedImage == null) { + throw new IllegalStateException( + "Could not decode image (unsupported format or corrupted data)"); + } + return bufferedImage; + } + private void saveImageToFile(BufferedImage image, int xTile, int yTile, int zoom, String server) throws IOException { String serverName = server.equals(GEOSERVER) ? "geoserver" : "ign"; diff --git a/src/main/java/fr/birdia/tile19/service/airbus/AirbusPNEOService.java b/src/main/java/fr/birdia/tile19/service/airbus/AirbusPNEOService.java new file mode 100644 index 0000000..bed1d32 --- /dev/null +++ b/src/main/java/fr/birdia/tile19/service/airbus/AirbusPNEOService.java @@ -0,0 +1,144 @@ +package fr.birdia.tile19.service.airbus; + +import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED; + +import fr.birdia.tile19.model.airbus.AirbusAuthResponse; +import fr.birdia.tile19.model.airbus.AirbusFeature; +import fr.birdia.tile19.model.airbus.AirbusPNEOResponse; +import fr.birdia.tile19.model.airbus.AirbusProperties; +import fr.birdia.tile19.model.airbus.AirbusRequestBody; +import fr.birdia.tile19.model.airbus.Geometry; +import java.math.BigDecimal; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +@Service +@Slf4j +public class AirbusPNEOService { + private RestTemplate restTemplate; + private String airbusAuthenticationBaseUrl; + private String airbusApiKey; + private final UriComponents baseUrl; + + public AirbusPNEOService( + RestTemplate restTemplate, + @Value("${airbus.authentication.baseurl}") String airbusAuthenticationBaseUrl, + @Value("${airbus.api.key}") String airbusApiKey, + @Value("${airbus.searchapi.baseurl}") String baseUrl) { + this.restTemplate = restTemplate; + this.airbusAuthenticationBaseUrl = airbusAuthenticationBaseUrl; + this.airbusApiKey = airbusApiKey; + this.baseUrl = UriComponentsBuilder.fromHttpUrl(baseUrl).build(); + } + + public String authenticateAirbus() { + log.info("Process airbus authentication ..."); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(APPLICATION_FORM_URLENCODED); + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("apikey", airbusApiKey); + body.add("grant_type", "api_key"); + body.add("client_id", "IDP"); + + HttpEntity> request = new HttpEntity<>(body, headers); + ResponseEntity response = + restTemplate.postForEntity( + URI.create(airbusAuthenticationBaseUrl), request, AirbusAuthResponse.class); + + if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { + log.info("Airbus authentication successful"); + return response.getBody().getAccessToken(); + } + + throw new IllegalArgumentException("Unable to retrieve Airbus Access Token"); + } + + public AirbusProperties retrieveAirbusWmtsLink(double lat, double lon) { + String bearerToken = authenticateAirbus(); + Geometry geometry = convertLatLonToGeometry(lat, lon); + AirbusRequestBody requestBody = + AirbusRequestBody.builder() + .constellation("PNEO") + .workspace("public-pneo") + .cloudCover("[0,10]") + .itemsPerPage(10) + .startPage(1) + .processingLevel("SENSOR") + .relation("intersects") + .sortBy("-acquisitionDate") + .geometry(geometry) + .build(); + HttpHeaders headersWithBearer = customizeHeaders(bearerToken); + headersWithBearer.setAccept(List.of(MediaType.APPLICATION_JSON)); + headersWithBearer.setContentType(MediaType.parseMediaType("application/json;charset=UTF-8")); + HttpEntity entity = new HttpEntity<>(requestBody, headersWithBearer); + + ResponseEntity response = + restTemplate.exchange(baseUrl.toUri(), HttpMethod.POST, entity, AirbusPNEOResponse.class); + + AirbusFeature feature = Objects.requireNonNull(response.getBody()).getFeatures().getFirst(); + String wmtsUrl = + String.format( + "%s/tiles/1.0.0/default/rgb/EPSG3857/", feature.getLinks().getWmts().getHref()); + return AirbusProperties.builder().wmtsUrl(wmtsUrl).bearer(bearerToken).build(); + } + + public Geometry convertLatLonToGeometry(double lat, double lon) { + // Area in square meters + double areaM2 = 900; + // Side length of the square + double sideM = Math.sqrt(areaM2); + // Half side + double halfSideM = sideM / 2; + + // Approximate meters per degree + double metersPerDegLat = 111_320; + double metersPerDegLon = metersPerDegLat * Math.cos(Math.toRadians(lat)); + + // Degree deltas + double deltaLat = halfSideM / metersPerDegLat; + double deltaLon = halfSideM / metersPerDegLon; + + // Bounding box coordinates + double minLat = lat - deltaLat; + double maxLat = lat + deltaLat; + double minLon = lon - deltaLon; + double maxLon = lon + deltaLon; + + List>> polygonCoords = new ArrayList<>(); + List> coordinates = new ArrayList<>(); + coordinates.add(point(minLon, minLat)); + coordinates.add(point(maxLon, minLat)); + coordinates.add(point(maxLon, maxLat)); + coordinates.add(point(minLon, maxLat)); + coordinates.add(point(minLon, minLat)); + polygonCoords.add(coordinates); + + return Geometry.builder().type("Polygon").coordinates(polygonCoords).build(); + } + + public HttpHeaders customizeHeaders(String bearerToken) { + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(bearerToken); + return headers; + } + + private static List point(double lon, double lat) { + return List.of(BigDecimal.valueOf(lon), BigDecimal.valueOf(lat)); + } +} diff --git a/src/test/java/fr/birdia/tile19/ImageExtenderIT.java b/src/test/java/fr/birdia/tile19/ImageExtenderIT.java index a5c20a2..79ed80d 100644 --- a/src/test/java/fr/birdia/tile19/ImageExtenderIT.java +++ b/src/test/java/fr/birdia/tile19/ImageExtenderIT.java @@ -12,6 +12,7 @@ import java.nio.file.Paths; import java.util.Base64; import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; @@ -138,6 +139,32 @@ public TileExtenderRequestBody herault() { .build(); } + public TileExtenderRequestBody airbusBody() { + return TileExtenderRequestBody.builder() + .x(264242) + .y(191449) + .z(19) + .server("airbus") + .layer("PNEO") + .shiftNb(0) + .isCropped(true) + .latitude(43.599621309901735) + .longitude(1.4410986644024693) + .isOpaque(false) + .build(); + } + + @Test + @Disabled("Run locally") + public void extend_airbus_images_ok() throws Exception { + ResponseEntity response = tileExtenderController.extendImage(airbusBody()); + + assertNotNull(response); + + Files.write( + Paths.get("airbus-non-opaque-image.jpg"), Base64.getDecoder().decode(response.getBody())); + } + @Test public void extend_not_full_HD_image_ok() throws Exception { ResponseEntity response = tileExtenderController.extendImage(degraded_body()); diff --git a/src/test/java/fr/birdia/tile19/conf/EnvConf.java b/src/test/java/fr/birdia/tile19/conf/EnvConf.java new file mode 100644 index 0000000..1fe7724 --- /dev/null +++ b/src/test/java/fr/birdia/tile19/conf/EnvConf.java @@ -0,0 +1,11 @@ +package fr.birdia.tile19.conf; + +import org.springframework.test.context.DynamicPropertyRegistry; + +public class EnvConf { + void configureProperties(DynamicPropertyRegistry registry) { + registry.add("airbus.searchapi.baseurl", () -> "https://dummy.searchapi"); + registry.add("airbus.authentication.baseurl", () -> "https://dummy.authentication"); + registry.add("airbus.api.key", () -> "dummy"); + } +} diff --git a/src/test/java/fr/birdia/tile19/unit/ImageExtenderServiceTest.java b/src/test/java/fr/birdia/tile19/unit/ImageExtenderServiceTest.java index 1137f03..45fb334 100644 --- a/src/test/java/fr/birdia/tile19/unit/ImageExtenderServiceTest.java +++ b/src/test/java/fr/birdia/tile19/unit/ImageExtenderServiceTest.java @@ -10,6 +10,7 @@ import fr.birdia.tile19.service.TilesDownloaderService; import fr.birdia.tile19.service.TilesMergerService; import fr.birdia.tile19.service.XYZToBBOXService; +import fr.birdia.tile19.service.airbus.AirbusPNEOService; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.File; @@ -17,16 +18,25 @@ import javax.imageio.ImageIO; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; +import org.springframework.web.client.RestTemplate; @Slf4j public class ImageExtenderServiceTest { XYZToBBOXService xyzToBBOXService = new XYZToBBOXService(); - TilesDownloaderService downloader = new TilesDownloaderService(xyzToBBOXService); + RestTemplate restTemplate = new RestTemplate(); + AirbusPNEOService airbusPNEOService = + new AirbusPNEOService( + restTemplate, + System.getenv("AIRBUS_AUTHENTICATION_BASEURL"), + System.getenv("AIRBUS_API_KEY"), + System.getenv("AIRBUS_SEARCHAPI_BASEURL")); + TilesDownloaderService downloader = + new TilesDownloaderService(xyzToBBOXService, airbusPNEOService, restTemplate); TilesMergerService merger = new TilesMergerService(); Workers workers = new Workers(); ImageDegraderService imageDegrader = new ImageDegraderService(); ImageExtenderService extender = - new ImageExtenderService(downloader, merger, workers, imageDegrader); + new ImageExtenderService(downloader, merger, workers, imageDegrader, airbusPNEOService); @Test public void full_herault_image_extension_ok() throws Exception { diff --git a/src/test/java/fr/birdia/tile19/unit/TilesDownloaderTest.java b/src/test/java/fr/birdia/tile19/unit/TilesDownloaderTest.java index dd05c6b..42dfa04 100644 --- a/src/test/java/fr/birdia/tile19/unit/TilesDownloaderTest.java +++ b/src/test/java/fr/birdia/tile19/unit/TilesDownloaderTest.java @@ -4,15 +4,25 @@ import fr.birdia.tile19.service.TilesDownloaderService; import fr.birdia.tile19.service.XYZToBBOXService; +import fr.birdia.tile19.service.airbus.AirbusPNEOService; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import javax.imageio.ImageIO; import org.junit.jupiter.api.Test; +import org.springframework.web.client.RestTemplate; public class TilesDownloaderTest { XYZToBBOXService xyzToBBOXService = new XYZToBBOXService(); - TilesDownloaderService tilesDownloaderService = new TilesDownloaderService(xyzToBBOXService); + RestTemplate restTemplate = new RestTemplate(); + AirbusPNEOService airbusPNEOService = + new AirbusPNEOService( + restTemplate, + System.getenv("AIRBUS_AUTHENTICATION_BASEURL"), + System.getenv("AIRBUS_API_KEY"), + System.getenv("AIRBUS_SEARCHAPI_BASEURL")); + TilesDownloaderService tilesDownloaderService = + new TilesDownloaderService(xyzToBBOXService, airbusPNEOService, restTemplate); @Test public void tiles_downloader_geoserver_ok() throws IOException, InterruptedException { @@ -21,7 +31,6 @@ public void tiles_downloader_geoserver_ok() throws IOException, InterruptedExcep int zoom = 20; String server = "geoserver"; String layer = "Bas-Rhin_2023_5cm"; - BufferedImage image = tilesDownloaderService.download(haguenauXtile, haguenauYTile, zoom, server, layer);