2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.miio.internal.robot;
15 import java.awt.BasicStroke;
16 import java.awt.Color;
18 import java.awt.FontMetrics;
19 import java.awt.Graphics2D;
20 import java.awt.GraphicsEnvironment;
21 import java.awt.Polygon;
22 import java.awt.Stroke;
23 import java.awt.geom.AffineTransform;
24 import java.awt.geom.Ellipse2D;
25 import java.awt.geom.Line2D;
26 import java.awt.geom.Path2D;
27 import java.awt.geom.Rectangle2D;
28 import java.awt.image.AffineTransformOp;
29 import java.awt.image.BufferedImage;
31 import java.io.FileInputStream;
32 import java.io.IOException;
33 import java.io.InputStream;
34 import java.net.MalformedURLException;
36 import java.util.ArrayList;
37 import java.util.HashSet;
39 import java.util.Map.Entry;
42 import javax.imageio.ImageIO;
44 import org.eclipse.jdt.annotation.NonNullByDefault;
45 import org.eclipse.jdt.annotation.Nullable;
46 import org.osgi.framework.Bundle;
47 import org.osgi.framework.FrameworkUtil;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
52 * Draws the vacuum map file to an image
54 * @author Marcel Verpaalen - Initial contribution
57 public class RRMapDraw {
59 private static final float MM = 50.0f;
61 private static final int MAP_OUTSIDE = 0x00;
62 private static final int MAP_WALL = 0x01;
63 private static final int MAP_INSIDE = 0xFF;
64 private static final int MAP_SCAN = 0x07;
65 private static final Color COLOR_MAP_INSIDE = new Color(32, 115, 185);
66 private static final Color COLOR_MAP_OUTSIDE = new Color(19, 87, 148);
67 private static final Color COLOR_MAP_WALL = new Color(100, 196, 254);
68 private static final Color COLOR_GREY_WALL = new Color(93, 109, 126);
69 private static final Color COLOR_PATH = new Color(147, 194, 238);
70 private static final Color COLOR_ZONES = new Color(0xAD, 0xD8, 0xFF, 0x8F);
71 private static final Color COLOR_NO_GO_ZONES = new Color(255, 33, 55, 127);
72 private static final Color COLOR_CHARGER_HALO = new Color(0x66, 0xfe, 0xda, 0x7f);
73 private static final Color COLOR_ROBO = new Color(75, 235, 149);
74 private static final Color COLOR_SCAN = new Color(0xDF, 0xDF, 0xDF);
75 private static final Color ROOM1 = new Color(240, 178, 122);
76 private static final Color ROOM2 = new Color(133, 193, 233);
77 private static final Color ROOM3 = new Color(217, 136, 128);
78 private static final Color ROOM4 = new Color(52, 152, 219);
79 private static final Color ROOM5 = new Color(205, 97, 85);
80 private static final Color ROOM6 = new Color(243, 156, 18);
81 private static final Color ROOM7 = new Color(88, 214, 141);
82 private static final Color ROOM8 = new Color(245, 176, 65);
83 private static final Color ROOM9 = new Color(0xFc, 0xD4, 0x51);
84 private static final Color ROOM10 = new Color(72, 201, 176);
85 private static final Color ROOM11 = new Color(84, 153, 199);
86 private static final Color ROOM12 = new Color(255, 213, 209);
87 private static final Color ROOM13 = new Color(228, 228, 215);
88 private static final Color ROOM14 = new Color(82, 190, 128);
89 private static final Color ROOM15 = new Color(72, 201, 176);
90 private static final Color ROOM16 = new Color(165, 105, 189);
91 private static final Color[] ROOM_COLORS = { ROOM1, ROOM2, ROOM3, ROOM4, ROOM5, ROOM6, ROOM7, ROOM8, ROOM9, ROOM10,
92 ROOM11, ROOM12, ROOM13, ROOM14, ROOM15, ROOM16 };
93 private final @Nullable Bundle bundle = FrameworkUtil.getBundle(getClass());
94 private boolean multicolor = false;
95 private final RRMapFileParser rmfp;
97 private final Logger logger = LoggerFactory.getLogger(RRMapDraw.class);
99 public RRMapDraw(RRMapFileParser rmfp) {
103 public int getWidth() {
104 return rmfp.getImgWidth();
107 public int getHeight() {
108 return rmfp.getImgHeight();
111 public RRMapFileParser getMapParseDetails() {
116 * load Gzipped RR inputstream
118 * @throws IOException
120 public static RRMapDraw loadImage(InputStream is) throws IOException {
121 byte[] inputdata = RRMapFileParser.readRRMapFile(is);
122 RRMapFileParser rf = new RRMapFileParser(inputdata);
123 return new RRMapDraw(rf);
127 * load Gzipped RR file
129 * @throws IOException
131 public static RRMapDraw loadImage(File file) throws IOException {
132 return loadImage(new FileInputStream(file));
136 * draws the map from the individual pixels
138 private void drawMap(Graphics2D g2d, float scale) {
139 Stroke stroke = new BasicStroke(1.1f * scale);
140 Set<Integer> roomIds = new HashSet<Integer>();
141 g2d.setStroke(stroke);
142 for (int y = 0; y < rmfp.getImgHeight() - 1; y++) {
143 for (int x = 0; x < rmfp.getImgWidth() + 1; x++) {
144 byte walltype = rmfp.getImage()[x + rmfp.getImgWidth() * y];
145 switch (walltype & 0xFF) {
147 g2d.setColor(COLOR_MAP_OUTSIDE);
150 g2d.setColor(COLOR_MAP_WALL);
153 g2d.setColor(COLOR_MAP_INSIDE);
156 g2d.setColor(COLOR_SCAN);
159 int obstacle = (walltype & 0x07);
160 int mapId = (walltype & 0xFF) >>> 3;
163 g2d.setColor(COLOR_GREY_WALL);
166 g2d.setColor(Color.BLACK);
169 g2d.setColor(ROOM_COLORS[mapId % 15]);
174 g2d.setColor(Color.WHITE);
178 float xPos = scale * (rmfp.getImgWidth() - x);
179 float yP = scale * y;
180 g2d.draw(new Line2D.Float(xPos, yP, xPos, yP));
183 if (logger.isDebugEnabled() && roomIds.size() > 0) {
184 StringBuilder sb = new StringBuilder();
185 for (Integer r : roomIds) {
186 sb.append(" " + r.toString());
188 logger.debug("Identified rooms in map:{}", sb.toString());
193 * draws the vacuum path
197 private void drawPath(Graphics2D g2d, float scale) {
198 Stroke stroke = new BasicStroke(0.5f * scale);
199 g2d.setStroke(stroke);
200 for (Entry<Integer, ArrayList<float[]>> path : rmfp.getPaths().entrySet()) {
201 Integer pathType = path.getKey();
203 case RRMapFileParser.PATH:
205 g2d.setColor(COLOR_PATH);
207 g2d.setColor(Color.WHITE);
210 case RRMapFileParser.GOTO_PATH:
211 g2d.setColor(Color.GREEN);
213 case RRMapFileParser.GOTO_PREDICTED_PATH:
214 g2d.setColor(Color.YELLOW);
217 g2d.setColor(Color.CYAN);
221 for (float[] point : path.getValue()) {
222 float x = toXCoord(point[0]) * scale;
223 float y = toYCoord(point[1]) * scale;
225 g2d.draw(new Line2D.Float(prvX, prvY, x, y));
233 private void drawZones(Graphics2D g2d, float scale) {
234 for (float[] point : rmfp.getZones()) {
235 float x = toXCoord(point[0]) * scale;
236 float y = toYCoord(point[1]) * scale;
237 float x1 = toXCoord(point[2]) * scale;
238 float y1 = toYCoord(point[3]) * scale;
239 float sx = Math.min(x, x1);
240 float w = Math.max(x, x1) - sx;
241 float sy = Math.min(y, y1);
242 float h = Math.max(y, y1) - sy;
243 g2d.setColor(COLOR_ZONES);
244 g2d.fill(new Rectangle2D.Float(sx, sy, w, h));
248 private void drawNoGo(Graphics2D g2d, float scale) {
249 for (Map.Entry<Integer, ArrayList<float[]>> area : rmfp.getAreas().entrySet()) {
250 for (float[] point : area.getValue()) {
251 float x = toXCoord(point[0]) * scale;
252 float y = toYCoord(point[1]) * scale;
253 float x1 = toXCoord(point[2]) * scale;
254 float y1 = toYCoord(point[3]) * scale;
255 float x2 = toXCoord(point[4]) * scale;
256 float y2 = toYCoord(point[5]) * scale;
257 float x3 = toXCoord(point[6]) * scale;
258 float y3 = toYCoord(point[7]) * scale;
259 Path2D noGo = new Path2D.Float();
265 g2d.setColor(COLOR_NO_GO_ZONES);
267 g2d.setColor(area.getKey() == 9 ? Color.RED : Color.WHITE);
274 private void drawWalls(Graphics2D g2d, float scale) {
275 Stroke stroke = new BasicStroke(3 * scale);
276 g2d.setStroke(stroke);
277 for (float[] point : rmfp.getWalls()) {
278 float x = toXCoord(point[0]) * scale;
279 float y = toYCoord(point[1]) * scale;
280 float x1 = toXCoord(point[2]) * scale;
281 float y1 = toYCoord(point[3]) * scale;
282 g2d.setColor(Color.RED);
283 g2d.draw(new Line2D.Float(x, y, x1, y1));
287 private void drawRobo(Graphics2D g2d, float scale) {
288 float radius = 3 * scale;
289 Stroke stroke = new BasicStroke(2 * scale);
290 g2d.setStroke(stroke);
291 g2d.setColor(COLOR_CHARGER_HALO);
292 final float chargerX = toXCoord(rmfp.getChargerX()) * scale;
293 final float chargerY = toYCoord(rmfp.getChargerY()) * scale;
294 drawCircle(g2d, chargerX, chargerY, radius, false);
295 drawCenteredImg(g2d, scale / 8, "charger.png", chargerX, chargerY);
297 g2d.setColor(COLOR_ROBO);
298 final float roboX = toXCoord(rmfp.getRoboX()) * scale;
299 final float roboY = toYCoord(rmfp.getRoboY()) * scale;
300 drawCircle(g2d, roboX, roboY, radius, false);
302 drawCenteredImg(g2d, scale / 15, "robo.png", roboX, roboY);
306 private void drawObstacles(Graphics2D g2d, float scale) {
307 float radius = 2 * scale;
308 Stroke stroke = new BasicStroke(3 * scale);
309 g2d.setStroke(stroke);
310 g2d.setColor(Color.MAGENTA);
312 Map<Integer, ArrayList<int[]>> obstacleMap = rmfp.getObstacles();
313 for (ArrayList<int[]> obstacles : obstacleMap.values()) {
314 obstacles.forEach(obstacle -> {
315 final float obstacleX = toXCoord(obstacle[0]) * scale;
316 final float obstacleY = toYCoord(obstacle[1]) * scale;
317 drawCircle(g2d, obstacleX, obstacleY, radius, true);
319 drawCenteredImg(g2d, scale / 3, "obstacle-" + obstacle[2] + ".png", obstacleX, obstacleY + 15);
325 private void drawCircle(Graphics2D g2d, float x, float y, float radius, boolean fill) {
326 Ellipse2D.Double circle = new Ellipse2D.Double(x - radius, y - radius, 2.0 * radius, 2.0 * radius);
334 private void drawCenteredImg(Graphics2D g2d, float scale, String imgFile, float x, float y) {
335 URL image = getImageUrl(imgFile);
338 BufferedImage addImg = ImageIO.read(image);
339 int xpos = Math.round(x + (addImg.getWidth() / 2 * scale));
340 int ypos = Math.round(y + (addImg.getHeight() / 2 * scale));
341 AffineTransform at = new AffineTransform();
342 at.scale(-scale, -scale);
343 AffineTransformOp scaleOp = new AffineTransformOp(at, AffineTransformOp.TYPE_BILINEAR);
344 g2d.drawImage(addImg, scaleOp, xpos, ypos);
346 logger.debug("Error loading image {}: File not be found.", imgFile);
348 } catch (IOException e) {
349 logger.debug("Error loading image {}: {}", image, e.getMessage());
353 private void drawGoTo(Graphics2D g2d, float scale) {
354 float x = toXCoord(rmfp.getGotoX()) * scale;
355 float y = toYCoord(rmfp.getGotoY()) * scale;
356 if (!(x == 0 && y == 0)) {
357 g2d.setStroke(new BasicStroke());
358 g2d.setColor(Color.YELLOW);
359 int x3[] = { (int) x, (int) (x - 2 * scale), (int) (x + 2 * scale) };
360 int y3[] = { (int) y, (int) (y - 5 * scale), (int) (y - 5 * scale) };
361 g2d.fill(new Polygon(x3, y3, 3));
365 private void drawOpenHabRocks(Graphics2D g2d, int width, int height, float scale) {
369 URL image = getImageUrl("ohlogo.png");
372 BufferedImage ohLogo = ImageIO.read(image);
373 textPos = (int) (ohLogo.getWidth() * scale / 2 + offset * scale);
374 AffineTransform at = new AffineTransform();
375 at.scale(scale / 2, scale / 2);
376 AffineTransformOp scaleOp = new AffineTransformOp(at, AffineTransformOp.TYPE_BILINEAR);
377 g2d.drawImage(ohLogo, scaleOp, offset,
378 height - (int) (ohLogo.getHeight() * scale / 2) - (int) (offset * scale));
380 logger.debug("Error loading image ohlogo.png: File not be found.");
382 } catch (IOException e) {
383 logger.debug("Error loading image ohlogo.png:: {}", e.getMessage());
385 String fontName = getAvailableFont("Helvetica,Arial,Roboto,Verdana,Times,Serif,Dialog".split(","));
386 if (fontName == null) {
387 return; // no available fonts to draw text
389 Font font = new Font(fontName, Font.BOLD, 14);
391 String message = "Openhab rocks your Xiaomi vacuum!";
392 FontMetrics fontMetrics = g2d.getFontMetrics();
393 int stringWidth = fontMetrics.stringWidth(message);
394 if ((stringWidth + textPos) > rmfp.getImgWidth() * scale) {
395 font = new Font(fontName, Font.BOLD,
396 (int) Math.floor(14 * (rmfp.getImgWidth() * scale - textPos - offset * scale) / stringWidth));
399 int stringHeight = fontMetrics.getAscent();
400 g2d.setPaint(Color.white);
401 g2d.drawString(message, textPos, height - offset * scale - stringHeight / 2);
404 private @Nullable String getAvailableFont(String[] preferedFonts) {
405 final GraphicsEnvironment gEv = GraphicsEnvironment.getLocalGraphicsEnvironment();
409 String[] fonts = gEv.getAvailableFontFamilyNames();
410 if (fonts.length == 0) {
413 for (int j = 0; j < preferedFonts.length; j++) {
414 for (int i = 0; i < fonts.length; i++) {
415 if (fonts[i].equalsIgnoreCase(preferedFonts[j])) {
416 return preferedFonts[j];
420 // Preferred fonts not available... just go with the first one
424 private @Nullable URL getImageUrl(String image) {
425 final Bundle bundle = this.bundle;
426 if (bundle != null) {
427 return bundle.getEntry("images/" + image);
430 File fn = new File("src" + File.separator + "main" + File.separator + "resources" + File.separator
431 + "images" + File.separator + image);
432 return fn.toURI().toURL();
433 } catch (MalformedURLException | SecurityException e) {
434 logger.debug("Could create URL for {}: {}", image, e.getMessage());
439 public BufferedImage getImage(float scale) {
440 int width = (int) Math.floor(rmfp.getImgWidth() * scale);
441 int height = (int) Math.floor(rmfp.getImgHeight() * scale);
442 BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
443 Graphics2D g2d = bi.createGraphics();
444 AffineTransform tx = AffineTransform.getScaleInstance(-1, -1);
445 tx.translate(-width, -height);
446 g2d.setTransform(tx);
448 drawZones(g2d, scale);
449 drawNoGo(g2d, scale);
450 drawWalls(g2d, scale);
451 drawPath(g2d, scale);
452 drawRobo(g2d, scale);
453 drawGoTo(g2d, scale);
454 drawObstacles(g2d, scale);
455 g2d = bi.createGraphics();
456 drawOpenHabRocks(g2d, width, height, scale);
460 public boolean writePic(String filename, String formatName, float scale) throws IOException {
461 return ImageIO.write(getImage(scale), formatName, new File(filename));
464 private float toXCoord(float x) {
465 return rmfp.getImgWidth() + rmfp.getLeft() - (x / MM);
468 private float toYCoord(float y) {
469 return y / MM - rmfp.getTop();
473 public String toString() {
474 return rmfp.toString();