]> git.basschouten.com Git - openhab-addons.git/commitdiff
[miio] map improvement cropping and customizable colors/parameters (#11110)
authorMarcel <marcel@verpaalen.com>
Mon, 16 Aug 2021 12:03:33 +0000 (14:03 +0200)
committerGitHub <noreply@github.com>
Mon, 16 Aug 2021 12:03:33 +0000 (14:03 +0200)
* [miio] make robo map drawing customizable

Signed-off-by: Marcel Verpaalen <marcel@verpaalen.com>
* [miio] Add cropping possibility for vacuum map

Signed-off-by: Marcel Verpaalen <marcel@verpaalen.com>
* miio minor edit

Signed-off-by: Marcel Verpaalen <marcel@verpaalen.com>
bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoVacuumHandler.java
bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/robot/RRMapDraw.java
bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/robot/RRMapDrawOptions.java [new file with mode: 0644]

index 75f4918e3c5c7ece26074d0f5086792e743e3410..74f7b3e45eda4768b495be9aeceae61426a2035c 100644 (file)
@@ -45,6 +45,7 @@ import org.openhab.binding.miio.internal.cloud.MiCloudException;
 import org.openhab.binding.miio.internal.robot.ConsumablesType;
 import org.openhab.binding.miio.internal.robot.FanModeType;
 import org.openhab.binding.miio.internal.robot.RRMapDraw;
+import org.openhab.binding.miio.internal.robot.RRMapDrawOptions;
 import org.openhab.binding.miio.internal.robot.RobotCababilities;
 import org.openhab.binding.miio.internal.robot.StatusDTO;
 import org.openhab.binding.miio.internal.robot.StatusType;
@@ -88,7 +89,6 @@ import com.google.gson.JsonObject;
 @NonNullByDefault
 public class MiIoVacuumHandler extends MiIoAbstractHandler {
     private final Logger logger = LoggerFactory.getLogger(MiIoVacuumHandler.class);
-    private static final float MAP_SCALE = 2.0f;
     private static final DateTimeFormatter DATEFORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss");
     private static final Gson GSON = new GsonBuilder().serializeNulls().create();
     private final ChannelUID mapChannelUid;
@@ -110,6 +110,7 @@ public class MiIoVacuumHandler extends MiIoAbstractHandler {
     private boolean hasChannelStructure;
     private ConcurrentHashMap<RobotCababilities, Boolean> deviceCapabilities = new ConcurrentHashMap<>();
     private ChannelTypeRegistry channelTypeRegistry;
+    private RRMapDrawOptions mapDrawOptions = new RRMapDrawOptions();
 
     public MiIoVacuumHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService,
             CloudConnector cloudConnector, ChannelTypeRegistry channelTypeRegistry) {
@@ -171,7 +172,6 @@ public class MiIoVacuumHandler extends MiIoAbstractHandler {
             }
             return null;
         });
-        updateState(RobotCababilities.SEGMENT_CLEAN.getChannel(), new StringType("-"));
     }
 
     @Override
@@ -490,6 +490,9 @@ public class MiIoVacuumHandler extends MiIoAbstractHandler {
     public void initialize() {
         super.initialize();
         hasChannelStructure = false;
+        this.mapDrawOptions = RRMapDrawOptions
+                .getOptionsFromFile(BINDING_USERDATA_PATH + File.separator + "mapConfig.json", logger);
+        updateState(RobotCababilities.SEGMENT_CLEAN.getChannel(), new StringType("-"));
     }
 
     @Override
@@ -651,6 +654,7 @@ public class MiIoVacuumHandler extends MiIoAbstractHandler {
                 if (mapDl != null) {
                     byte[] mapData = mapDl.getBytes();
                     RRMapDraw rrMap = RRMapDraw.loadImage(new ByteArrayInputStream(mapData));
+                    rrMap.setDrawOptions(mapDrawOptions);
                     ByteArrayOutputStream baos = new ByteArrayOutputStream();
                     if (logger.isDebugEnabled()) {
                         final String mapPath = BINDING_USERDATA_PATH + File.separator + map
@@ -658,7 +662,7 @@ public class MiIoVacuumHandler extends MiIoAbstractHandler {
                         CloudUtil.writeBytesToFileNio(mapData, mapPath);
                         logger.debug("Mapdata saved to {}", mapPath);
                     }
-                    ImageIO.write(rrMap.getImage(MAP_SCALE), "jpg", baos);
+                    ImageIO.write(rrMap.getImage(), "jpg", baos);
                     byte[] byteArray = baos.toByteArray();
                     if (byteArray != null && byteArray.length > 0) {
                         return new RawType(byteArray, "image/jpeg");
index 183e499f052eaaebdd32f5d8263a8b7a193efaad..3469d877d4a219fde545642599184427c95ee033 100644 (file)
@@ -62,41 +62,18 @@ public class RRMapDraw {
     private static final int MAP_WALL = 0x01;
     private static final int MAP_INSIDE = 0xFF;
     private static final int MAP_SCAN = 0x07;
-    private static final Color COLOR_MAP_INSIDE = new Color(32, 115, 185);
-    private static final Color COLOR_MAP_OUTSIDE = new Color(19, 87, 148);
-    private static final Color COLOR_MAP_WALL = new Color(100, 196, 254);
-    private static final Color COLOR_CARPET = new Color(0xDF, 0xDF, 0xDF, 0xA0);
-    private static final Color COLOR_GREY_WALL = new Color(93, 109, 126);
-    private static final Color COLOR_PATH = new Color(147, 194, 238);
-    private static final Color COLOR_ZONES = new Color(0xAD, 0xD8, 0xFF, 0x8F);
-    private static final Color COLOR_NO_GO_ZONES = new Color(255, 33, 55, 127);
-    private static final Color COLOR_CHARGER_HALO = new Color(0x66, 0xfe, 0xda, 0x7f);
-    private static final Color COLOR_ROBO = new Color(75, 235, 149);
-    private static final Color COLOR_SCAN = new Color(0xDF, 0xDF, 0xDF);
-    private static final Color ROOM1 = new Color(240, 178, 122);
-    private static final Color ROOM2 = new Color(133, 193, 233);
-    private static final Color ROOM3 = new Color(217, 136, 128);
-    private static final Color ROOM4 = new Color(52, 152, 219);
-    private static final Color ROOM5 = new Color(205, 97, 85);
-    private static final Color ROOM6 = new Color(243, 156, 18);
-    private static final Color ROOM7 = new Color(88, 214, 141);
-    private static final Color ROOM8 = new Color(245, 176, 65);
-    private static final Color ROOM9 = new Color(0xFc, 0xD4, 0x51);
-    private static final Color ROOM10 = new Color(72, 201, 176);
-    private static final Color ROOM11 = new Color(84, 153, 199);
-    private static final Color ROOM12 = new Color(255, 213, 209);
-    private static final Color ROOM13 = new Color(228, 228, 215);
-    private static final Color ROOM14 = new Color(82, 190, 128);
-    private static final Color ROOM15 = new Color(72, 201, 176);
-    private static final Color ROOM16 = new Color(165, 105, 189);
-    private static final Color[] ROOM_COLORS = { ROOM1, ROOM2, ROOM3, ROOM4, ROOM5, ROOM6, ROOM7, ROOM8, ROOM9, ROOM10,
-            ROOM11, ROOM12, ROOM13, ROOM14, ROOM15, ROOM16 };
+
     private final @Nullable Bundle bundle = FrameworkUtil.getBundle(getClass());
-    private boolean multicolor = false;
     private final RRMapFileParser rmfp;
-
     private final Logger logger = LoggerFactory.getLogger(RRMapDraw.class);
 
+    private RRMapDrawOptions drawOptions = new RRMapDrawOptions();
+    private boolean multicolor = false;
+    private int firstX = 0;
+    private int lastX = 0;
+    private int firstY = 0;
+    private int lastY = 0;
+
     public RRMapDraw(RRMapFileParser rmfp) {
         this.rmfp = rmfp;
     }
@@ -109,6 +86,14 @@ public class RRMapDraw {
         return rmfp.getImgHeight();
     }
 
+    public void setDrawOptions(RRMapDrawOptions options) {
+        this.drawOptions = options;
+    }
+
+    public RRMapDrawOptions getDrawOptions() {
+        return drawOptions;
+    }
+
     public RRMapFileParser getMapParseDetails() {
         return this.rmfp;
     }
@@ -145,29 +130,29 @@ public class RRMapDraw {
                 byte walltype = rmfp.getImage()[x + rmfp.getImgWidth() * y];
                 switch (walltype & 0xFF) {
                     case MAP_OUTSIDE:
-                        g2d.setColor(COLOR_MAP_OUTSIDE);
+                        g2d.setColor(drawOptions.getColorMapOutside());
                         break;
                     case MAP_WALL:
-                        g2d.setColor(COLOR_MAP_WALL);
+                        g2d.setColor(drawOptions.getColorMapWall());
                         break;
                     case MAP_INSIDE:
-                        g2d.setColor(COLOR_MAP_INSIDE);
+                        g2d.setColor(drawOptions.getColorMapInside());
                         break;
                     case MAP_SCAN:
-                        g2d.setColor(COLOR_SCAN);
+                        g2d.setColor(drawOptions.getColorScan());
                         break;
                     default:
                         int obstacle = (walltype & 0x07);
                         int mapId = (walltype & 0xFF) >>> 3;
                         switch (obstacle) {
                             case 0:
-                                g2d.setColor(COLOR_GREY_WALL);
+                                g2d.setColor(drawOptions.getColorGreyWall());
                                 break;
                             case 1:
                                 g2d.setColor(Color.BLACK);
                                 break;
                             case 7:
-                                g2d.setColor(ROOM_COLORS[mapId % 15]);
+                                g2d.setColor(drawOptions.getRoomColors()[mapId % 15]);
                                 roomIds.add(mapId);
                                 multicolor = true;
                                 break;
@@ -207,7 +192,7 @@ public class RRMapDraw {
                         // ignore
                         break;
                     default:
-                        g2d.setColor(COLOR_CARPET);
+                        g2d.setColor(drawOptions.getColorCarpet());
                         float xPos = scale * (rmfp.getImgWidth() - x);
                         float yP = scale * y;
                         g2d.draw(new Line2D.Float(xPos, yP, xPos, yP));
@@ -230,7 +215,7 @@ public class RRMapDraw {
             switch (pathType) {
                 case RRMapFileParser.PATH:
                     if (!multicolor) {
-                        g2d.setColor(COLOR_PATH);
+                        g2d.setColor(drawOptions.getColorPath());
                     } else {
                         g2d.setColor(Color.WHITE);
                     }
@@ -268,7 +253,7 @@ public class RRMapDraw {
             float w = Math.max(x, x1) - sx;
             float sy = Math.min(y, y1);
             float h = Math.max(y, y1) - sy;
-            g2d.setColor(COLOR_ZONES);
+            g2d.setColor(drawOptions.getColorZones());
             g2d.fill(new Rectangle2D.Float(sx, sy, w, h));
         }
     }
@@ -290,7 +275,7 @@ public class RRMapDraw {
                 noGo.lineTo(x2, y2);
                 noGo.lineTo(x3, y3);
                 noGo.lineTo(x, y);
-                g2d.setColor(COLOR_NO_GO_ZONES);
+                g2d.setColor(drawOptions.getColorNoGoZones());
                 g2d.fill(noGo);
                 g2d.setColor(area.getKey() == 9 ? Color.RED : Color.WHITE);
                 g2d.draw(noGo);
@@ -316,13 +301,13 @@ public class RRMapDraw {
         float radius = 3 * scale;
         Stroke stroke = new BasicStroke(2 * scale);
         g2d.setStroke(stroke);
-        g2d.setColor(COLOR_CHARGER_HALO);
+        g2d.setColor(drawOptions.getColorChargerHalo());
         final float chargerX = toXCoord(rmfp.getChargerX()) * scale;
         final float chargerY = toYCoord(rmfp.getChargerY()) * scale;
         drawCircle(g2d, chargerX, chargerY, radius, false);
         drawCenteredImg(g2d, scale / 8, "charger.png", chargerX, chargerY);
         radius = 3 * scale;
-        g2d.setColor(COLOR_ROBO);
+        g2d.setColor(drawOptions.getColorRobo());
         final float roboX = toXCoord(rmfp.getRoboX()) * scale;
         final float roboY = toYCoord(rmfp.getRoboY()) * scale;
         drawCircle(g2d, roboX, roboY, radius, false);
@@ -410,18 +395,22 @@ public class RRMapDraw {
         } catch (IOException e) {
             logger.debug("Error loading image ohlogo.png:: {}", e.getMessage());
         }
+        if (drawOptions.getText().isBlank()) {
+            return;
+        }
         String fontName = getAvailableFont("Helvetica,Arial,Roboto,Verdana,Times,Serif,Dialog".split(","));
         if (fontName == null) {
             return; // no available fonts to draw text
         }
-        Font font = new Font(fontName, Font.BOLD, 14);
+        int fz = (int) (drawOptions.getTextFontSize() * scale);
+        Font font = new Font(fontName, Font.BOLD, fz);
         g2d.setFont(font);
-        String message = "Openhab rocks your Xiaomi vacuum!";
+        String message = drawOptions.getText();
         FontMetrics fontMetrics = g2d.getFontMetrics();
         int stringWidth = fontMetrics.stringWidth(message);
-        if ((stringWidth + textPos) > rmfp.getImgWidth() * scale) {
-            font = new Font(fontName, Font.BOLD,
-                    (int) Math.floor(14 * (rmfp.getImgWidth() * scale - textPos - offset * scale) / stringWidth));
+        if ((stringWidth + textPos) > width) {
+            int fzn = (int) Math.floor(((float) (width - textPos) / stringWidth) * fz);
+            font = new Font(fontName, Font.BOLD, fzn > 0 ? fzn : 1);
             g2d.setFont(font);
         }
         int stringHeight = fontMetrics.getAscent();
@@ -449,6 +438,39 @@ public class RRMapDraw {
         return fonts[0];
     }
 
+    /**
+     * Finds the perimeter of the used area in the map
+     */
+    private void getMapArea(Graphics2D g2d, float scale) {
+        int firstX = rmfp.getImgWidth();
+        int lastX = 0;
+        int firstY = rmfp.getImgHeight();
+        int lastY = 0;
+        for (int y = 0; y < rmfp.getImgHeight() - 1; y++) {
+            for (int x = 0; x < rmfp.getImgWidth() + 1; x++) {
+                int walltype = rmfp.getImage()[x + rmfp.getImgWidth() * y] & 0xFF;
+                if (walltype > MAP_OUTSIDE) {
+                    if (y < firstY) {
+                        firstY = y;
+                    }
+                    if (y > lastY) {
+                        lastY = y;
+                    }
+                    if (x < firstX) {
+                        firstX = x;
+                    }
+                    if (x > lastX) {
+                        lastX = x;
+                    }
+                }
+            }
+        }
+        this.firstX = firstX;
+        this.lastX = lastX;
+        this.firstY = rmfp.getImgHeight() - lastY;
+        this.lastY = rmfp.getImgHeight() - firstY;
+    }
+
     private @Nullable URL getImageUrl(String image) {
         final Bundle bundle = this.bundle;
         if (bundle != null) {
@@ -464,6 +486,10 @@ public class RRMapDraw {
         }
     }
 
+    public BufferedImage getImage() {
+        return getImage(drawOptions.getScale());
+    }
+
     public BufferedImage getImage(float scale) {
         int width = (int) Math.floor(rmfp.getImgWidth() * scale);
         int height = (int) Math.floor(rmfp.getImgHeight() * scale);
@@ -481,9 +507,34 @@ public class RRMapDraw {
         drawRobo(g2d, scale);
         drawGoTo(g2d, scale);
         drawObstacles(g2d, scale);
-        g2d = bi.createGraphics();
-        drawOpenHabRocks(g2d, width, height, scale);
-        return bi;
+        if (drawOptions.getCropBorder() < 0) {
+            g2d = bi.createGraphics();
+            if (drawOptions.isShowLogo()) {
+                drawOpenHabRocks(g2d, width, height, scale);
+            }
+            return bi;
+        }
+        // crop the image to the used perimeter
+        getMapArea(g2d, scale);
+        int firstX = (this.firstX - drawOptions.getCropBorder()) > 0 ? this.firstX - drawOptions.getCropBorder() : 0;
+        int lastX = (this.lastX + drawOptions.getCropBorder()) < rmfp.getImgWidth()
+                ? this.lastX + drawOptions.getCropBorder()
+                : rmfp.getImgWidth();
+        int firstY = (this.firstY - drawOptions.getCropBorder()) > 0 ? this.firstY - drawOptions.getCropBorder() : 0;
+        int lastY = (this.lastY + drawOptions.getCropBorder() + (int) (8 * scale)) < rmfp.getImgHeight()
+                ? this.lastY + drawOptions.getCropBorder() + (int) (8 * scale)
+                : rmfp.getImgHeight();
+        int nwidth = (int) Math.floor((lastX - firstX) * scale);
+        int nheight = (int) Math.floor((lastY - firstY) * scale);
+        BufferedImage bo = new BufferedImage(nwidth, nheight, BufferedImage.TYPE_3BYTE_BGR);
+        Graphics2D crop = bo.createGraphics();
+        crop.transform(AffineTransform.getTranslateInstance(-firstX * scale, -firstY * scale));
+        crop.drawImage(bi, 0, 0, null);
+        if (drawOptions.isShowLogo()) {
+            crop = bo.createGraphics();
+            drawOpenHabRocks(crop, nwidth, nheight, scale * .75f);
+        }
+        return bo;
     }
 
     public boolean writePic(String filename, String formatName, float scale) throws IOException {
diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/robot/RRMapDrawOptions.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/robot/RRMapDrawOptions.java
new file mode 100644 (file)
index 0000000..260afa4
--- /dev/null
@@ -0,0 +1,317 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.miio.internal.robot;
+
+import java.awt.Color;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.PrintWriter;
+import java.lang.reflect.Type;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.slf4j.Logger;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * This class provides the configuration for the vacuum map drawing
+ *
+ * @author Marcel Verpaalen - Initial contribution
+ */
+@NonNullByDefault
+public class RRMapDrawOptions {
+
+    private static final Color COLOR_MAP_INSIDE = new Color(32, 115, 185);
+    private static final Color COLOR_MAP_OUTSIDE = new Color(19, 87, 148);
+    private static final Color COLOR_MAP_WALL = new Color(100, 196, 254);
+    private static final Color COLOR_CARPET = new Color(0xDF, 0xDF, 0xDF, 0xA0);
+    private static final Color COLOR_GREY_WALL = new Color(93, 109, 126);
+    private static final Color COLOR_PATH = new Color(147, 194, 238);
+    private static final Color COLOR_ZONES = new Color(0xAD, 0xD8, 0xFF, 0x8F);
+    private static final Color COLOR_NO_GO_ZONES = new Color(255, 33, 55, 127);
+    private static final Color COLOR_CHARGER_HALO = new Color(0x66, 0xfe, 0xda, 0x7f);
+    private static final Color COLOR_ROBO = new Color(75, 235, 149);
+    private static final Color COLOR_SCAN = new Color(0xDF, 0xDF, 0xDF);
+    private static final Color ROOM1 = new Color(240, 178, 122);
+    private static final Color ROOM2 = new Color(133, 193, 233);
+    private static final Color ROOM3 = new Color(217, 136, 128);
+    private static final Color ROOM4 = new Color(52, 152, 219);
+    private static final Color ROOM5 = new Color(205, 97, 85);
+    private static final Color ROOM6 = new Color(243, 156, 18);
+    private static final Color ROOM7 = new Color(88, 214, 141);
+    private static final Color ROOM8 = new Color(245, 176, 65);
+    private static final Color ROOM9 = new Color(0xFc, 0xD4, 0x51);
+    private static final Color ROOM10 = new Color(72, 201, 176);
+    private static final Color ROOM11 = new Color(84, 153, 199);
+    private static final Color ROOM12 = new Color(255, 213, 209);
+    private static final Color ROOM13 = new Color(228, 228, 215);
+    private static final Color ROOM14 = new Color(82, 190, 128);
+    private static final Color ROOM15 = new Color(72, 201, 176);
+    private static final Color ROOM16 = new Color(165, 105, 189);
+    private static final Color[] ROOM_COLORS = { ROOM1, ROOM2, ROOM3, ROOM4, ROOM5, ROOM6, ROOM7, ROOM8, ROOM9, ROOM10,
+            ROOM11, ROOM12, ROOM13, ROOM14, ROOM15, ROOM16 };
+
+    private static final Gson GSON = new GsonBuilder().setPrettyPrinting()
+            .registerTypeAdapter(Color.class, new JsonSerializer<Color>() {
+                @Override
+                public JsonElement serialize(Color src, @Nullable Type typeOfSrc,
+                        @Nullable JsonSerializationContext context) {
+                    JsonObject colorSave = new JsonObject();
+                    colorSave.addProperty("red", src.getRed());
+                    colorSave.addProperty("green", src.getGreen());
+                    colorSave.addProperty("blue", src.getBlue());
+                    colorSave.addProperty("alpha", src.getAlpha());
+                    return colorSave;
+                }
+            }).registerTypeAdapter(Color.class, new JsonDeserializer<Color>() {
+                @Override
+                @Nullable
+                public Color deserialize(@Nullable JsonElement json, @Nullable Type typeOfT,
+                        @Nullable JsonDeserializationContext context) throws JsonParseException {
+                    if (json == null) {
+                        throw new JsonParseException("missing json text");
+                    }
+                    JsonObject colorSave = json.getAsJsonObject();
+                    Color color = new Color(colorSave.get("red").getAsInt(), colorSave.get("green").getAsInt(),
+                            colorSave.get("blue").getAsInt(), colorSave.get("alpha").getAsInt());
+                    return color;
+                }
+            }).create();
+
+    @SerializedName("colorMapInside")
+    @Expose
+    private Color colorMapInside = COLOR_MAP_INSIDE;
+    @SerializedName("colorMapOutside")
+    @Expose
+    private Color colorMapOutside = COLOR_MAP_OUTSIDE;
+    @SerializedName("colorMapWall")
+    @Expose
+    private Color colorMapWall = COLOR_MAP_WALL;
+    @SerializedName("colorCarpet")
+    @Expose
+    private Color colorCarpet = COLOR_CARPET;
+    @SerializedName("colorGreyWall")
+    @Expose
+    private Color colorGreyWall = COLOR_GREY_WALL;
+    @SerializedName("colorPath")
+    @Expose
+    private Color colorPath = COLOR_PATH;
+    @SerializedName("colorZones")
+    @Expose
+    private Color colorZones = COLOR_ZONES;
+    @SerializedName("colorNoGoZones")
+    @Expose
+    private Color colorNoGoZones = COLOR_NO_GO_ZONES;
+    @SerializedName("colorChargerHalo")
+    @Expose
+    private Color colorChargerHalo = COLOR_CHARGER_HALO;
+    @SerializedName("colorRobo")
+    @Expose
+    private Color colorRobo = COLOR_ROBO;
+    @SerializedName("colorScan")
+    @Expose
+    private Color colorScan = COLOR_SCAN;
+
+    @SerializedName("roomColors")
+    @Expose
+    private Color[] roomColors = ROOM_COLORS;
+
+    @SerializedName("showLogo")
+    @Expose
+    private boolean showLogo = true;
+    @SerializedName("text")
+    @Expose
+    private String text = "Openhab rocks your Xiaomi vacuum!";
+    @SerializedName("textFontSize")
+    @Expose
+    private int textFontSize = 12;
+    @SerializedName("scale")
+    @Expose
+    private float scale = 2.0f;
+    @SerializedName("cropBorder")
+    @Expose
+    private int cropBorder = 10;
+
+    public Color getColorMapInside() {
+        return colorMapInside;
+    }
+
+    public void setColorMapInside(Color colorMapInside) {
+        this.colorMapInside = colorMapInside;
+    }
+
+    public Color getColorMapOutside() {
+        return colorMapOutside;
+    }
+
+    public void setColorMapOutside(Color colorMapOutside) {
+        this.colorMapOutside = colorMapOutside;
+    }
+
+    public Color getColorMapWall() {
+        return colorMapWall;
+    }
+
+    public void setColorMapWall(Color colorMapWall) {
+        this.colorMapWall = colorMapWall;
+    }
+
+    public Color getColorCarpet() {
+        return colorCarpet;
+    }
+
+    public void setColorCarpet(Color colorCarpet) {
+        this.colorCarpet = colorCarpet;
+    }
+
+    public Color getColorGreyWall() {
+        return colorGreyWall;
+    }
+
+    public void setColorGreyWall(Color colorGreyWall) {
+        this.colorGreyWall = colorGreyWall;
+    }
+
+    public Color getColorPath() {
+        return colorPath;
+    }
+
+    public void setColorPath(Color colorPath) {
+        this.colorPath = colorPath;
+    }
+
+    public Color getColorZones() {
+        return colorZones;
+    }
+
+    public void setColorZones(Color colorZones) {
+        this.colorZones = colorZones;
+    }
+
+    public Color getColorNoGoZones() {
+        return colorNoGoZones;
+    }
+
+    public void setColorNoGoZones(Color colorNoGoZones) {
+        this.colorNoGoZones = colorNoGoZones;
+    }
+
+    public Color getColorChargerHalo() {
+        return colorChargerHalo;
+    }
+
+    public void setColorChargerHalo(Color colorChargerHalo) {
+        this.colorChargerHalo = colorChargerHalo;
+    }
+
+    public Color getColorRobo() {
+        return colorRobo;
+    }
+
+    public void setColorRobo(Color colorRobo) {
+        this.colorRobo = colorRobo;
+    }
+
+    public Color getColorScan() {
+        return colorScan;
+    }
+
+    public void setColorScan(Color colorScan) {
+        this.colorScan = colorScan;
+    }
+
+    public Color[] getRoomColors() {
+        return roomColors;
+    }
+
+    public void setRoomColors(Color[] roomColors) {
+        this.roomColors = roomColors;
+    }
+
+    public boolean isShowLogo() {
+        return showLogo;
+    }
+
+    public void setShowLogo(boolean showLogo) {
+        this.showLogo = showLogo;
+    }
+
+    public String getText() {
+        return text;
+    }
+
+    public void setText(String text) {
+        this.text = text;
+    }
+
+    public final int getTextFontSize() {
+        return textFontSize;
+    }
+
+    public final void setTextFontSize(int textFontSize) {
+        this.textFontSize = textFontSize;
+    }
+
+    public float getScale() {
+        return scale;
+    }
+
+    public void setScale(float scale) {
+        this.scale = scale;
+    }
+
+    public final int getCropBorder() {
+        return cropBorder;
+    }
+
+    public final void setCropBorder(int cropBorder) {
+        this.cropBorder = cropBorder;
+    }
+
+    public static void writeOptionsToFile(RRMapDrawOptions options, String fileName, Logger logger) {
+        String json = GSON.toJson(options, RRMapDrawOptions.class);
+        try (PrintWriter pw = new PrintWriter(fileName)) {
+            pw.println(json);
+            logger.debug("Vacuum map draw options file created: {}", fileName);
+        } catch (FileNotFoundException e) {
+            logger.info("Error writing Vacuum map draw options file: {}", e.getMessage());
+        }
+    }
+
+    public static RRMapDrawOptions getOptionsFromFile(String fileName, Logger logger) {
+        try {
+            RRMapDrawOptions options = GSON.fromJson(new FileReader(fileName), RRMapDrawOptions.class);
+            return options;
+        } catch (FileNotFoundException e) {
+            logger.debug("Vacuum map draw options file {} not found. Using defaults", fileName);
+            return new RRMapDrawOptions();
+        } catch (JsonParseException e) {
+            logger.info("Error reading vacuum map draw options file {}: {}", fileName, e.getMessage());
+        }
+        logger.info("Write default map draw options to {}", fileName);
+        RRMapDrawOptions options = new RRMapDrawOptions();
+        writeOptionsToFile(options, fileName, logger);
+        return new RRMapDrawOptions();
+    }
+}