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);
+ }
+}