diff --git a/backend/pom.xml b/backend/pom.xml index 866ed57..c75573a 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -32,6 +32,10 @@ org.springframework.modulith spring-modulith-starter-core + + org.springframework.boot + spring-boot-starter-cache + org.springframework.boot 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/CacheConfig.java new file mode 100644 index 0000000..eef2526 --- /dev/null +++ b/backend/src/main/java/com/rdkr/tide_display/backend/common/CacheConfig.java @@ -0,0 +1,17 @@ +package com.rdkr.tide_display.backend.common; + +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableCaching +public class CacheConfig { + + @Bean + public CacheManager cacheManager() { + return new ConcurrentMapCacheManager("map", "grayMap", "ePaper", "printHexValues"); + } +} 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 new file mode 100644 index 0000000..6ef5efb --- /dev/null +++ b/backend/src/main/java/com/rdkr/tide_display/backend/images/controller/MapController.java @@ -0,0 +1,43 @@ +package com.rdkr.tide_display.backend.images.controller; + +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 lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/map") +@RequiredArgsConstructor +@Slf4j +public class MapController { + + private final MapService mapService; + private final ImageConverterService converterService; + + @GetMapping + public ResponseEntity getMap( + @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 response = ResponseEntity.ok(); + if (ImageFormat.PNG.equals(format)) { + return response.contentType(MediaType.IMAGE_PNG).body(result); + } else { + val body = converterService.ePaperFormat(result); + return response.contentType(MediaType.APPLICATION_JSON).body(body); + } + } +} diff --git a/backend/src/main/java/com/rdkr/tide_display/backend/images/domain/ImageFormat.java b/backend/src/main/java/com/rdkr/tide_display/backend/images/domain/ImageFormat.java new file mode 100644 index 0000000..bb996ee --- /dev/null +++ b/backend/src/main/java/com/rdkr/tide_display/backend/images/domain/ImageFormat.java @@ -0,0 +1,6 @@ +package com.rdkr.tide_display.backend.images.domain; + +public enum ImageFormat { + PNG, + BINARY +} diff --git a/backend/src/main/java/com/rdkr/tide_display/backend/images/responses/ImageResponse.java b/backend/src/main/java/com/rdkr/tide_display/backend/images/responses/ImageResponse.java new file mode 100644 index 0000000..3224543 --- /dev/null +++ b/backend/src/main/java/com/rdkr/tide_display/backend/images/responses/ImageResponse.java @@ -0,0 +1,5 @@ +package com.rdkr.tide_display.backend.images.responses; + +public record ImageResponse(int width, int height, byte[] data) { + +} 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 new file mode 100644 index 0000000..6d9d730 --- /dev/null +++ b/backend/src/main/java/com/rdkr/tide_display/backend/images/services/ImageConverterService.java @@ -0,0 +1,84 @@ +package com.rdkr.tide_display.backend.images.services; + +import com.rdkr.tide_display.backend.images.responses.ImageResponse; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Base64; +import java.util.HexFormat; +import javax.imageio.ImageIO; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +public class ImageConverterService { + + public byte[] grayscale(byte[] image) throws IOException { + return grayscale_manual(image); + } + + public byte[] grayscale_automatic(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(); + } + + 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); + } + } + val out = new ByteArrayOutputStream(); + ImageIO.write(grayImage, "png", out); + return out.toByteArray(); + } + + public ImageResponse ePaperFormat(byte[] image) throws IOException { + val img = ImageIO.read(new ByteArrayInputStream(image)); + val width = img.getWidth(); + val height = img.getHeight(); + val result = new ByteArrayOutputStream(image.length); + for (int y = 0; y < height; y++) { + int b = 0; + boolean done = true; + for (int x = 0; x < width; x++) { + val color = img.getRGB(x, y); + if (x % 2 == 0) { + b = color >> 4; + done = false; + } else { + b |= color & 0xF0; + result.write(b); + done = true; + } + } + if (!done) { + 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); + } +} 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 new file mode 100644 index 0000000..765123a --- /dev/null +++ b/backend/src/main/java/com/rdkr/tide_display/backend/images/services/MapService.java @@ -0,0 +1,7 @@ +package com.rdkr.tide_display.backend.images.services; + +public interface MapService { + + byte[] getStaticMap(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 new file mode 100644 index 0000000..c33dcc0 --- /dev/null +++ b/backend/src/main/java/com/rdkr/tide_display/backend/images/services/MapServiceImpl.java @@ -0,0 +1,25 @@ +package com.rdkr.tide_display.backend.images.services; + +import lombok.val; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClient; + +@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); + } +}