diff --git a/backend/pom.xml b/backend/pom.xml index c75573a..e4af898 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -37,6 +37,12 @@ spring-boot-starter-cache + + org.bouncycastle + bcpkix-jdk18on + 1.78.1 + + org.springframework.boot spring-boot-devtools diff --git a/backend/src/main/java/com/rdkr/tide_display/backend/Application.java b/backend/src/main/java/com/rdkr/tide_display/backend/Application.java index 3c693cd..5e93cf6 100644 --- a/backend/src/main/java/com/rdkr/tide_display/backend/Application.java +++ b/backend/src/main/java/com/rdkr/tide_display/backend/Application.java @@ -1,13 +1,17 @@ package com.rdkr.tide_display.backend; +import java.security.Security; +import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class Application { - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } + public static void main(String[] args) { + Security.addProvider(new BouncyCastleProvider()); + + SpringApplication.run(Application.class, args); + } } diff --git a/backend/src/main/java/com/rdkr/tide_display/backend/apple/Snapshot.java b/backend/src/main/java/com/rdkr/tide_display/backend/apple/Snapshot.java new file mode 100644 index 0000000..6449bd7 --- /dev/null +++ b/backend/src/main/java/com/rdkr/tide_display/backend/apple/Snapshot.java @@ -0,0 +1,9 @@ +package com.rdkr.tide_display.backend.apple; + +import java.net.URI; + +public interface Snapshot { + + URI generateSnapshotURI(double latitude, double longitude); + +} diff --git a/backend/src/main/java/com/rdkr/tide_display/backend/apple/snapshot/SnapshotImpl.java b/backend/src/main/java/com/rdkr/tide_display/backend/apple/snapshot/SnapshotImpl.java new file mode 100644 index 0000000..52ac97e --- /dev/null +++ b/backend/src/main/java/com/rdkr/tide_display/backend/apple/snapshot/SnapshotImpl.java @@ -0,0 +1,60 @@ +package com.rdkr.tide_display.backend.apple.snapshot; + +import com.rdkr.tide_display.backend.apple.Snapshot; +import com.rdkr.tide_display.backend.crypto.Platforms; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.security.Signature; +import java.util.Base64; +import lombok.val; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.util.UriComponentsBuilder; + +@Component() +public class SnapshotImpl implements Snapshot { + + private final Platforms platforms; + + @Value("${APPLE_TEAM_ID:dummy}") + private String teamId; + + @Value("${APPLE_KEY_ID:dummy}") + private String keyId; + + public SnapshotImpl(@Qualifier("AppleCrypto") Platforms platforms) { + this.platforms = platforms; + } + + public URI generateSnapshotURI(double latitude, double longitude) { + val uriBuilder = UriComponentsBuilder.fromUriString("https://snapshot.apple-mapkit.com/api/v1/snapshot") + .queryParam("center", latitude + "," + longitude) + .queryParam("t", "standard") + .queryParam("scale", "1") + .queryParam("size", "540x540") + .queryParam("lang", "de-DE") + .queryParam("poi", "0") + .queryParam("teamId", teamId) + .queryParam("keyId", keyId); + return signRequest(uriBuilder); + } + + private URI signRequest(UriComponentsBuilder uriBuilder) { + try { + val pk = platforms.loadPrivateKey("MapKit_D28AJ2A3UT.p8"); + val signature = Signature.getInstance("SHA256withECDSA", "BC"); + signature.initSign(pk); + signature.update(uriBuilder.toString().getBytes()); + val signatureBytes = new String(signature.sign(), StandardCharsets.UTF_8); + val sign = Base64.getEncoder().encodeToString(signatureBytes.getBytes()); + return uriBuilder + .queryParam("signature", sign) + .build() + .toUri(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + +} diff --git a/backend/src/main/java/com/rdkr/tide_display/backend/common/config/AppConfig.java b/backend/src/main/java/com/rdkr/tide_display/backend/common/config/AppConfig.java new file mode 100644 index 0000000..b2a1688 --- /dev/null +++ b/backend/src/main/java/com/rdkr/tide_display/backend/common/config/AppConfig.java @@ -0,0 +1,17 @@ +package com.rdkr.tide_display.backend.common.config; + +import com.rdkr.tide_display.backend.common.interceptor.RequestResponseLoggingInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; + +@Component +public class AppConfig { + + @Bean + public RestClient.Builder restClient() { + return RestClient + .builder() + .requestInterceptor(new RequestResponseLoggingInterceptor()); + } +} diff --git a/backend/src/main/java/com/rdkr/tide_display/backend/common/CacheConfig.java b/backend/src/main/java/com/rdkr/tide_display/backend/common/config/CacheConfig.java similarity index 67% rename from backend/src/main/java/com/rdkr/tide_display/backend/common/CacheConfig.java rename to backend/src/main/java/com/rdkr/tide_display/backend/common/config/CacheConfig.java index eef2526..a56c9c7 100644 --- a/backend/src/main/java/com/rdkr/tide_display/backend/common/CacheConfig.java +++ b/backend/src/main/java/com/rdkr/tide_display/backend/common/config/CacheConfig.java @@ -1,4 +1,4 @@ -package com.rdkr.tide_display.backend.common; +package com.rdkr.tide_display.backend.common.config; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; @@ -12,6 +12,11 @@ public class CacheConfig { @Bean public CacheManager cacheManager() { - return new ConcurrentMapCacheManager("map", "grayMap", "ePaper", "printHexValues"); + return new ConcurrentMapCacheManager( + "privateKey", + "map", + "grayMap", + "ePaper", + "printHexValues"); } } diff --git a/backend/src/main/java/com/rdkr/tide_display/backend/common/interceptor/RequestResponseLoggingInterceptor.java b/backend/src/main/java/com/rdkr/tide_display/backend/common/interceptor/RequestResponseLoggingInterceptor.java new file mode 100644 index 0000000..d6bc283 --- /dev/null +++ b/backend/src/main/java/com/rdkr/tide_display/backend/common/interceptor/RequestResponseLoggingInterceptor.java @@ -0,0 +1,54 @@ +package com.rdkr.tide_display.backend.common.interceptor; + +import java.nio.charset.StandardCharsets; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.util.StreamUtils; + +import java.io.IOException; +import java.nio.charset.Charset; + +@Slf4j +public class RequestResponseLoggingInterceptor implements ClientHttpRequestInterceptor { + + @Override + public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException + { + logRequest(request, body); + ClientHttpResponse response = execution.execute(request, body); + logResponse(response); + + return response; + } + + private void logRequest(HttpRequest request, byte[] body) throws IOException + { + if (log.isDebugEnabled()) + { + log.debug("=========================== request begin ================================================"); + log.debug("URI : {}", request.getURI()); + log.debug("Method : {}", request.getMethod()); + log.debug("Headers : {}", request.getHeaders()); + //log.debug("Request body: {}", new String(body, StandardCharsets.UTF_8)); + log.debug("=========================== request end =================================================="); + } + } + + private void logResponse(ClientHttpResponse response) throws IOException + { + if (log.isDebugEnabled()) + { + log.debug("=========================== response begin ==============================================="); + log.debug("Status code : {}", response.getStatusCode()); + log.debug("Status text : {}", response.getStatusText()); + log.debug("Headers : {}", response.getHeaders()); + //log.debug("Response body: {}", StreamUtils.copyToString(response.getBody(), StandardCharsets.UTF_8)); + log.debug("=========================== response end ================================================="); + } + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/rdkr/tide_display/backend/crypto/Platforms.java b/backend/src/main/java/com/rdkr/tide_display/backend/crypto/Platforms.java new file mode 100644 index 0000000..9c06c7b --- /dev/null +++ b/backend/src/main/java/com/rdkr/tide_display/backend/crypto/Platforms.java @@ -0,0 +1,9 @@ +package com.rdkr.tide_display.backend.crypto; + +import java.security.PrivateKey; + +public interface Platforms { + + PrivateKey loadPrivateKey(String filename); + +} diff --git a/backend/src/main/java/com/rdkr/tide_display/backend/crypto/platforms/AppleCrypto.java b/backend/src/main/java/com/rdkr/tide_display/backend/crypto/platforms/AppleCrypto.java new file mode 100644 index 0000000..7418a5c --- /dev/null +++ b/backend/src/main/java/com/rdkr/tide_display/backend/crypto/platforms/AppleCrypto.java @@ -0,0 +1,31 @@ +package com.rdkr.tide_display.backend.crypto.platforms; + +import com.rdkr.tide_display.backend.crypto.Platforms; +import java.io.FileReader; +import java.security.PrivateKey; +import lombok.val; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Component; +import org.springframework.util.ResourceUtils; + +@Component("AppleCrypto") +public class AppleCrypto implements Platforms { + + @Cacheable(value = "privateKey", key = "#filename") + public PrivateKey loadPrivateKey(String filename) { + try { + val file = ResourceUtils.getFile("classpath:apple/" + filename ); + val pemParser = new PEMParser(new FileReader(file)); + val converter = new JcaPEMKeyConverter(); + val object = (PrivateKeyInfo) pemParser.readObject(); + val pKey = converter.getPrivateKey(object); + pemParser.close(); + return pKey; + } catch (Exception e) { + throw new RuntimeException("Failed to load private key", e); + } + } +} diff --git a/backend/src/main/java/com/rdkr/tide_display/backend/crypto/platforms/GoogleCrypto.java b/backend/src/main/java/com/rdkr/tide_display/backend/crypto/platforms/GoogleCrypto.java new file mode 100644 index 0000000..a7f9b5b --- /dev/null +++ b/backend/src/main/java/com/rdkr/tide_display/backend/crypto/platforms/GoogleCrypto.java @@ -0,0 +1,15 @@ +package com.rdkr.tide_display.backend.crypto.platforms; + +import com.rdkr.tide_display.backend.crypto.Platforms; +import java.security.PrivateKey; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Component; + +@Component("GoogleCrypto") +public class GoogleCrypto implements Platforms { + + @Cacheable(value = "privateKey", key = "#filename") + public PrivateKey loadPrivateKey(String filename) { + return null; + } +} diff --git a/backend/src/main/java/com/rdkr/tide_display/backend/firmware/controller/RootController.java b/backend/src/main/java/com/rdkr/tide_display/backend/firmware/controller/RootController.java index dd62293..10cce4d 100644 --- a/backend/src/main/java/com/rdkr/tide_display/backend/firmware/controller/RootController.java +++ b/backend/src/main/java/com/rdkr/tide_display/backend/firmware/controller/RootController.java @@ -8,8 +8,8 @@ import org.springframework.web.bind.annotation.RestController; @RequestMapping("/") public class RootController { - @GetMapping() - public String index() { - return "Tide Display Backend"; - } + @GetMapping() + public String index() { + return "Tide Display Backend"; + } } diff --git a/backend/src/main/java/com/rdkr/tide_display/backend/gcp/Firmware.java b/backend/src/main/java/com/rdkr/tide_display/backend/gcp/Firmware.java index 7f5375d..a324be1 100644 --- a/backend/src/main/java/com/rdkr/tide_display/backend/gcp/Firmware.java +++ b/backend/src/main/java/com/rdkr/tide_display/backend/gcp/Firmware.java @@ -41,5 +41,5 @@ public class Firmware { private String espIdf; private String compiled; -} + } } diff --git a/backend/src/main/java/com/rdkr/tide_display/backend/gcp/StorageService.java b/backend/src/main/java/com/rdkr/tide_display/backend/gcp/StorageService.java index e42c7bb..8096c03 100644 --- a/backend/src/main/java/com/rdkr/tide_display/backend/gcp/StorageService.java +++ b/backend/src/main/java/com/rdkr/tide_display/backend/gcp/StorageService.java @@ -5,9 +5,12 @@ import java.util.List; import org.springframework.web.multipart.MultipartFile; public interface StorageService { + List getFirmwareVersions(); byte[] download(String version, String filename); String upload(MultipartFile file) throws IOException; + + void save(String filename, byte[] data, String contentType, int expire); } diff --git a/backend/src/main/java/com/rdkr/tide_display/backend/gcp/bean/GoogleCloudBean.java b/backend/src/main/java/com/rdkr/tide_display/backend/gcp/bean/GoogleCloudBean.java index ff4f4c6..7d55474 100644 --- a/backend/src/main/java/com/rdkr/tide_display/backend/gcp/bean/GoogleCloudBean.java +++ b/backend/src/main/java/com/rdkr/tide_display/backend/gcp/bean/GoogleCloudBean.java @@ -9,7 +9,7 @@ import org.springframework.stereotype.Component; public class GoogleCloudBean { @Bean - public Storage getStorage(){ + public Storage getStorage() { return StorageOptions.getDefaultInstance().getService(); } } diff --git a/backend/src/main/java/com/rdkr/tide_display/backend/gcp/service/StorageServiceImpl.java b/backend/src/main/java/com/rdkr/tide_display/backend/gcp/service/StorageServiceImpl.java index 5330063..18d8644 100644 --- a/backend/src/main/java/com/rdkr/tide_display/backend/gcp/service/StorageServiceImpl.java +++ b/backend/src/main/java/com/rdkr/tide_display/backend/gcp/service/StorageServiceImpl.java @@ -8,9 +8,11 @@ import com.rdkr.tide_display.backend.gcp.StorageService; import jakarta.annotation.Nullable; import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import lombok.RequiredArgsConstructor; import lombok.val; +import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; @@ -34,7 +36,8 @@ public class StorageServiceImpl implements StorageService { val result = new ArrayList(); val bucket = storage.list(bucketName, BlobListOption.currentDirectory(), BlobListOption.prefix("firmware/")); for (val directory : bucket.iterateAll()) { - val files = storage.list(bucketName, BlobListOption.currentDirectory(), BlobListOption.prefix(directory.getName())); + val files = storage.list(bucketName, BlobListOption.currentDirectory(), + BlobListOption.prefix(directory.getName())); for (val file : files.iterateAll()) { val name = file.getName(); if (name.endsWith(".bin")) { @@ -53,7 +56,8 @@ public class StorageServiceImpl implements StorageService { val meta = new Firmware.FirmwareMeta(projectName, flashSize, espIdf, compiled); result.add( - new Firmware(major, minor, patch, "/files/" + major + "." + minor + "." + patch + "/firmware.bin", file.getEtag(), file.getUpdateTimeOffsetDateTime(), meta)); + new Firmware(major, minor, patch, "/files/" + major + "." + minor + "." + patch + "/firmware.bin", + file.getEtag(), file.getUpdateTimeOffsetDateTime(), meta)); } } } @@ -84,7 +88,19 @@ public class StorageServiceImpl implements StorageService { return null; } val version = extract(bytes, 48, 30); - storage.create(BlobInfo.newBuilder(bucketName, "firmware/" + version + "/firmware.bin").build(), bytes); + save("firmware/" + version + "/firmware.bin", bytes, MediaType.APPLICATION_OCTET_STREAM_VALUE, 0); return version; } + + @Override + public void save(String filename, byte[] data, String contentType, int expire) { + val metadata = new HashMap(); + metadata.put("Cache-Control", "public, max-age=" + expire); + val blobInfo = BlobInfo + .newBuilder(bucketName, filename) + .setContentType(contentType) + .setMetadata(metadata) + .build(); + storage.create(blobInfo, data); + } } diff --git a/backend/src/main/java/com/rdkr/tide_display/backend/images/controller/MapController.java b/backend/src/main/java/com/rdkr/tide_display/backend/images/controller/MapController.java index 6ef5efb..20ebfc7 100644 --- a/backend/src/main/java/com/rdkr/tide_display/backend/images/controller/MapController.java +++ b/backend/src/main/java/com/rdkr/tide_display/backend/images/controller/MapController.java @@ -4,6 +4,7 @@ import com.rdkr.tide_display.backend.images.domain.ImageFormat; import com.rdkr.tide_display.backend.images.services.ImageConverterService; import com.rdkr.tide_display.backend.images.services.MapService; import java.io.IOException; +import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import lombok.val; @@ -25,13 +26,14 @@ public class MapController { @GetMapping public ResponseEntity getMap( + @RequestParam(required = false, defaultValue = "GoogleMaps") String mapProvider, @RequestParam(required = false, defaultValue = "53.541962") double latitude, @RequestParam(required = false, defaultValue = "9.993402") double longitude, @RequestParam(required = false, defaultValue = "15") int zoom, @RequestParam(required = false, defaultValue = "BINARY") ImageFormat format) throws IOException { - val map = mapService.getStaticMap(latitude, longitude, zoom); - var result = converterService.grayscale(map); + val map = mapService.getStaticMap(mapProvider, latitude, longitude, zoom); + var result = converterService.convert(map); val response = ResponseEntity.ok(); if (ImageFormat.PNG.equals(format)) { return response.contentType(MediaType.IMAGE_PNG).body(result); @@ -40,4 +42,9 @@ public class MapController { return response.contentType(MediaType.APPLICATION_JSON).body(body); } } + + @GetMapping("/providers") + public ResponseEntity> getMapProviders() { + return ResponseEntity.ok(mapService.getMapProviders()); + } } diff --git a/backend/src/main/java/com/rdkr/tide_display/backend/images/services/ImageConverterService.java b/backend/src/main/java/com/rdkr/tide_display/backend/images/services/ImageConverterService.java index 6d9d730..ba900e5 100644 --- a/backend/src/main/java/com/rdkr/tide_display/backend/images/services/ImageConverterService.java +++ b/backend/src/main/java/com/rdkr/tide_display/backend/images/services/ImageConverterService.java @@ -16,41 +16,35 @@ import org.springframework.stereotype.Service; @Slf4j public class ImageConverterService { - public byte[] grayscale(byte[] image) throws IOException { - return grayscale_manual(image); - } - - public byte[] grayscale_automatic(byte[] image) throws IOException { + public byte[] convert(byte[] image) throws IOException { val img = ImageIO.read(new ByteArrayInputStream(image)); - val grayImage = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_BYTE_GRAY); - val g = grayImage.getGraphics(); - g.drawImage(img, 0, 0, null); - g.dispose(); - val out = new ByteArrayOutputStream(); - ImageIO.write(grayImage, "png", out); - return out.toByteArray(); - } + val celShadedImage = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_RGB); - public byte[] grayscale_manual(byte[] image) throws IOException { - val img = ImageIO.read(new ByteArrayInputStream(image)); - val grayImage = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_BYTE_GRAY); - int rgb = 0, r = 0, g = 0, b = 0; for (int y = 0; y < img.getHeight(); y++) { for (int x = 0; x < img.getWidth(); x++) { - rgb = img.getRGB(x, y); - r = (rgb >> 16) & 0xFF; - g = (rgb >> 8) & 0xFF; - b = (rgb & 0xFF); - rgb = (int) (r * 0.299 + g * 0.587 + b * 0.114); - rgb = (255 << 24) | (rgb << 16) | (rgb << 8) | rgb; - grayImage.setRGB(x, y, rgb); + // Pixel-Farbe auslesen + int rgb = img.getRGB(x, y); + + int r = (rgb >> 16) & 0xFF; + int g = (rgb >> 8) & 0xFF; + int b = rgb & 0xFF; + + int gray = (r + g + b) / 3; + + int celShadeLevel = gray / 16; + int newGray = celShadeLevel * 16; + + int newRGB = (newGray << 16) | (newGray << 8) | newGray; + + celShadedImage.setRGB(x, y, newRGB); } } val out = new ByteArrayOutputStream(); - ImageIO.write(grayImage, "png", out); + ImageIO.write(celShadedImage, "png", out); return out.toByteArray(); } + public ImageResponse ePaperFormat(byte[] image) throws IOException { val img = ImageIO.read(new ByteArrayInputStream(image)); val width = img.getWidth(); @@ -74,11 +68,11 @@ public class ImageConverterService { result.write(b); } } - return new ImageResponse(width, height, Base64.getEncoder().encode(result.toByteArray())); - } - - public void printHexValues(byte[] bytes) throws IOException { - val hex = HexFormat.of().withUpperCase().withPrefix("0x").withSuffix(", ").formatHex(bytes); - log.info("Hex value: {}", hex); + val bytes = result.toByteArray(); + if (log.isDebugEnabled()) { + val hex = HexFormat.of().withUpperCase().withPrefix("0x").withSuffix(", ").formatHex(bytes); + log.debug("Image: {}", hex); + } + return new ImageResponse(width, height, Base64.getEncoder().encode(bytes)); } } diff --git a/backend/src/main/java/com/rdkr/tide_display/backend/images/services/MapService.java b/backend/src/main/java/com/rdkr/tide_display/backend/images/services/MapService.java index 765123a..004840e 100644 --- a/backend/src/main/java/com/rdkr/tide_display/backend/images/services/MapService.java +++ b/backend/src/main/java/com/rdkr/tide_display/backend/images/services/MapService.java @@ -1,7 +1,11 @@ package com.rdkr.tide_display.backend.images.services; +import java.util.List; + public interface MapService { - byte[] getStaticMap(double latitude, double longitude, int zoom); + List getMapProviders(); + + byte[] getStaticMap(String provider, double latitude, double longitude, int zoom); } diff --git a/backend/src/main/java/com/rdkr/tide_display/backend/images/services/MapServiceImpl.java b/backend/src/main/java/com/rdkr/tide_display/backend/images/services/MapServiceImpl.java index c33dcc0..bbb9956 100644 --- a/backend/src/main/java/com/rdkr/tide_display/backend/images/services/MapServiceImpl.java +++ b/backend/src/main/java/com/rdkr/tide_display/backend/images/services/MapServiceImpl.java @@ -1,25 +1,47 @@ package com.rdkr.tide_display.backend.images.services; +import com.rdkr.tide_display.backend.gcp.StorageService; +import com.rdkr.tide_display.backend.images.services.mapProvider.MapProvider; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; import lombok.val; import org.springframework.cache.annotation.Cacheable; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; -import org.springframework.web.client.RestClient; +import org.springframework.util.MimeType; @Service public class MapServiceImpl implements MapService { - @Cacheable("map") - public byte[] getStaticMap(double latitude, double longitude, int zoom) { - val client = RestClient.create("https://maps.googleapis.com/maps/api/staticmap"); - return client.get() - .uri(uriBuilder -> uriBuilder - .queryParam("center", latitude + "," + longitude) - .queryParam("zoom", "" + zoom) - .queryParam("size", "540x540") - .queryParam("map_id", "2f371c2346218fe8") - .queryParam("key", "AIzaSyARgP_FKFXsrcgVd_HVWoIfH5N8-a88wlQ") - .build()) - .retrieve() - .body(byte[].class); + private final Map providers = new HashMap<>(); + private final StorageService storageService; + + public MapServiceImpl(List mapProviders, StorageService storageService) { + this.storageService = storageService; + mapProviders.forEach( + provider -> providers.put(provider.getClass().getAnnotation(Component.class).value().toUpperCase(), provider)); + } + + public List getMapProviders() { + return List.copyOf(providers.keySet()); + } + + @Cacheable(value = "map", key = "{#provider, #latitude, #longitude, #zoom}") + public byte[] getStaticMap(String provider, double latitude, double longitude, int zoom) { + val mapProvider = providers.get(provider.toUpperCase()); + if (mapProvider == null) { + throw new UnsupportedOperationException("Map provider not found: " + provider); + } + val imageData = mapProvider.getStaticMap(latitude, longitude, zoom); + cacheImage(provider, latitude, longitude, zoom, imageData); + return imageData; + } + + private void cacheImage(String provider, double latitude, double longitude, int zoom, byte[] image) { + storageService.save("mapProvider/" + provider.toUpperCase() + "/" + latitude + "/" + longitude + "/" + zoom + "/image.png", image, MediaType.IMAGE_PNG_VALUE, 60); } } diff --git a/backend/src/main/java/com/rdkr/tide_display/backend/images/services/mapProvider/AppleMapKit.java b/backend/src/main/java/com/rdkr/tide_display/backend/images/services/mapProvider/AppleMapKit.java new file mode 100644 index 0000000..bcb3d94 --- /dev/null +++ b/backend/src/main/java/com/rdkr/tide_display/backend/images/services/mapProvider/AppleMapKit.java @@ -0,0 +1,29 @@ +package com.rdkr.tide_display.backend.images.services.mapProvider; + +import com.rdkr.tide_display.backend.apple.Snapshot; +import lombok.RequiredArgsConstructor; +import lombok.val; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; + +@Component("AppleMapKit") +@RequiredArgsConstructor +public class AppleMapKit implements MapProvider { + + private final RestClient.Builder restClient; + private final Snapshot snapshot; + + @Override + public byte[] getStaticMap(double latitude, double longitude, int zoom) { + try { + val uri = snapshot.generateSnapshotURI(latitude, longitude); + val client = restClient.baseUrl(uri.toString()).build(); + return client.get() + .retrieve() + .body(byte[].class); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + +} diff --git a/backend/src/main/java/com/rdkr/tide_display/backend/images/services/mapProvider/GoogleMaps.java b/backend/src/main/java/com/rdkr/tide_display/backend/images/services/mapProvider/GoogleMaps.java new file mode 100644 index 0000000..199ed32 --- /dev/null +++ b/backend/src/main/java/com/rdkr/tide_display/backend/images/services/mapProvider/GoogleMaps.java @@ -0,0 +1,36 @@ +package com.rdkr.tide_display.backend.images.services.mapProvider; + +import lombok.RequiredArgsConstructor; +import lombok.val; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; + +@Component("GoogleMaps") +@RequiredArgsConstructor +public class GoogleMaps implements MapProvider { + + private final RestClient.Builder restClient; + + @Value("${GCP_MAP_ID:dummy}") + private String mapId; + + @Value("${GCP_API_KEY:dummy}") + private String apiKey; + + @Override + public byte[] getStaticMap(double latitude, double longitude, int zoom) { + val client = restClient.baseUrl("https://maps.googleapis.com/maps/api/staticmap").build(); + return client.get() + .uri(builder -> builder + .queryParam("center", latitude + "," + longitude) + .queryParam("zoom", "" + zoom) + .queryParam("size", "540x540") + .queryParam("map_id", mapId) + .queryParam("key", apiKey) + .build()) + .retrieve() + .body(byte[].class); + } + +} diff --git a/backend/src/main/java/com/rdkr/tide_display/backend/images/services/mapProvider/MapBox.java b/backend/src/main/java/com/rdkr/tide_display/backend/images/services/mapProvider/MapBox.java new file mode 100644 index 0000000..a501156 --- /dev/null +++ b/backend/src/main/java/com/rdkr/tide_display/backend/images/services/mapProvider/MapBox.java @@ -0,0 +1,31 @@ +package com.rdkr.tide_display.backend.images.services.mapProvider; + +import lombok.RequiredArgsConstructor; +import lombok.val; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; + +@Component("MapBox") +@RequiredArgsConstructor +public class MapBox implements MapProvider { + + private final RestClient.Builder restClient; + + @Value("${MAPBOX_TOKEN:dummy}") + private String accessToken; + + @Override + public byte[] getStaticMap(double latitude, double longitude, int zoom) { + val client = restClient.baseUrl("https://api.mapbox.com/styles/v1/mapbox/streets-v12/static").build(); + return client.get() + .uri(builder -> builder + .path("/" + longitude + "," + latitude + "," + (zoom - 1)) + .path("/540x540") + .queryParam("access_token", accessToken) + .build()) + .retrieve() + .body(byte[].class); + } + +} diff --git a/backend/src/main/java/com/rdkr/tide_display/backend/images/services/mapProvider/MapProvider.java b/backend/src/main/java/com/rdkr/tide_display/backend/images/services/mapProvider/MapProvider.java new file mode 100644 index 0000000..b63c7f1 --- /dev/null +++ b/backend/src/main/java/com/rdkr/tide_display/backend/images/services/mapProvider/MapProvider.java @@ -0,0 +1,7 @@ +package com.rdkr.tide_display.backend.images.services.mapProvider; + +public interface MapProvider { + + byte[] getStaticMap(double latitude, double longitude, int zoom); + +} diff --git a/backend/src/main/resources/application-dev.yaml b/backend/src/main/resources/application-dev.yaml new file mode 100644 index 0000000..2368f1e --- /dev/null +++ b/backend/src/main/resources/application-dev.yaml @@ -0,0 +1,3 @@ +logging: + level: + com.rdkr: trace