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_CARPET = new Color(0xDF, 0xDF, 0xDF, 0xA0);
69 private static final Color COLOR_GREY_WALL = new Color(93, 109, 126);
70 private static final Color COLOR_PATH = new Color(147, 194, 238);
71 private static final Color COLOR_ZONES = new Color(0xAD, 0xD8, 0xFF, 0x8F);
72 private static final Color COLOR_NO_GO_ZONES = new Color(255, 33, 55, 127);
73 private static final Color COLOR_CHARGER_HALO = new Color(0x66, 0xfe, 0xda, 0x7f);
74 private static final Color COLOR_ROBO = new Color(75, 235, 149);
75 private static final Color COLOR_SCAN = new Color(0xDF, 0xDF, 0xDF);
76 private static final Color ROOM1 = new Color(240, 178, 122);
77 private static final Color ROOM2 = new Color(133, 193, 233);
78 private static final Color ROOM3 = new Color(217, 136, 128);
79 private static final Color ROOM4 = new Color(52, 152, 219);
80 private static final Color ROOM5 = new Color(205, 97, 85);
81 private static final Color ROOM6 = new Color(243, 156, 18);
82 private static final Color ROOM7 = new Color(88, 214, 141);
83 private static final Color ROOM8 = new Color(245, 176, 65);
84 private static final Color ROOM9 = new Color(0xFc, 0xD4, 0x51);
85 private static final Color ROOM10 = new Color(72, 201, 176);
86 private static final Color ROOM11 = new Color(84, 153, 199);
87 private static final Color ROOM12 = new Color(255, 213, 209);
88 private static final Color ROOM13 = new Color(228, 228, 215);
89 private static final Color ROOM14 = new Color(82, 190, 128);
90 private static final Color ROOM15 = new Color(72, 201, 176);
91 private static final Color ROOM16 = new Color(165, 105, 189);
92 private static final Color[] ROOM_COLORS = { ROOM1, ROOM2, ROOM3, ROOM4, ROOM5, ROOM6, ROOM7, ROOM8, ROOM9, ROOM10,
93 ROOM11, ROOM12, ROOM13, ROOM14, ROOM15, ROOM16 };
94 private final @Nullable Bundle bundle = FrameworkUtil.getBundle(getClass());
95 private boolean multicolor = false;
96 private final RRMapFileParser rmfp;
98 private final Logger logger = LoggerFactory.getLogger(RRMapDraw.class);
100 public RRMapDraw(RRMapFileParser rmfp) {
104 public int getWidth() {
105 return rmfp.getImgWidth();
108 public int getHeight() {
109 return rmfp.getImgHeight();
112 public RRMapFileParser getMapParseDetails() {
117 * load Gzipped RR inputstream
119 * @throws IOException
121 public static RRMapDraw loadImage(InputStream is) throws IOException {
122 byte[] inputdata = RRMapFileParser.readRRMapFile(is);
123 RRMapFileParser rf = new RRMapFileParser(inputdata);
124 return new RRMapDraw(rf);
128 * load Gzipped RR file
130 * @throws IOException
132 public static RRMapDraw loadImage(File file) throws IOException {
133 return loadImage(new FileInputStream(file));
137 * draws the map from the individual pixels
139 private void drawMap(Graphics2D g2d, float scale) {
140 Stroke stroke = new BasicStroke(1.1f * scale);
141 Set<Integer> roomIds = new HashSet<Integer>();
142 g2d.setStroke(stroke);
143 for (int y = 0; y < rmfp.getImgHeight() - 1; y++) {
144 for (int x = 0; x < rmfp.getImgWidth() + 1; x++) {
145 byte walltype = rmfp.getImage()[x + rmfp.getImgWidth() * y];
146 switch (walltype & 0xFF) {
148 g2d.setColor(COLOR_MAP_OUTSIDE);
151 g2d.setColor(COLOR_MAP_WALL);
154 g2d.setColor(COLOR_MAP_INSIDE);
157 g2d.setColor(COLOR_SCAN);
160 int obstacle = (walltype & 0x07);
161 int mapId = (walltype & 0xFF) >>> 3;
164 g2d.setColor(COLOR_GREY_WALL);
167 g2d.setColor(Color.BLACK);
170 g2d.setColor(ROOM_COLORS[mapId % 15]);
175 g2d.setColor(Color.WHITE);
179 float xPos = scale * (rmfp.getImgWidth() - x);
180 float yP = scale * y;
181 g2d.draw(new Line2D.Float(xPos, yP, xPos, yP));
184 if (logger.isDebugEnabled() && !roomIds.isEmpty()) {
185 StringBuilder sb = new StringBuilder();
186 for (Integer r : roomIds) {
187 sb.append(" " + r.toString());
189 logger.debug("Identified rooms in map:{}", sb.toString());
194 * draws the carpet map
196 private void drawCarpetMap(Graphics2D g2d, float scale) {
197 if (rmfp.getCarpetMap().length == 0) {
200 Stroke stroke = new BasicStroke(1.1f * scale);
201 g2d.setStroke(stroke);
202 for (int y = 0; y < rmfp.getImgHeight() - 1; y++) {
203 for (int x = 0; x < rmfp.getImgWidth() + 1; x++) {
204 int carpetType = rmfp.getCarpetMap()[x + rmfp.getImgWidth() * y];
205 switch (carpetType) {
210 g2d.setColor(COLOR_CARPET);
211 float xPos = scale * (rmfp.getImgWidth() - x);
212 float yP = scale * y;
213 g2d.draw(new Line2D.Float(xPos, yP, xPos, yP));
221 * draws the vacuum path
225 private void drawPath(Graphics2D g2d, float scale) {
226 Stroke stroke = new BasicStroke(0.5f * scale);
227 g2d.setStroke(stroke);
228 for (Entry<Integer, ArrayList<float[]>> path : rmfp.getPaths().entrySet()) {
229 Integer pathType = path.getKey();
231 case RRMapFileParser.PATH:
233 g2d.setColor(COLOR_PATH);
235 g2d.setColor(Color.WHITE);
238 case RRMapFileParser.GOTO_PATH:
239 g2d.setColor(Color.GREEN);
241 case RRMapFileParser.GOTO_PREDICTED_PATH:
242 g2d.setColor(Color.YELLOW);
245 g2d.setColor(Color.CYAN);
249 for (float[] point : path.getValue()) {
250 float x = toXCoord(point[0]) * scale;
251 float y = toYCoord(point[1]) * scale;
253 g2d.draw(new Line2D.Float(prvX, prvY, x, y));
261 private void drawZones(Graphics2D g2d, float scale) {
262 for (float[] point : rmfp.getZones()) {
263 float x = toXCoord(point[0]) * scale;
264 float y = toYCoord(point[1]) * scale;
265 float x1 = toXCoord(point[2]) * scale;
266 float y1 = toYCoord(point[3]) * scale;
267 float sx = Math.min(x, x1);
268 float w = Math.max(x, x1) - sx;
269 float sy = Math.min(y, y1);
270 float h = Math.max(y, y1) - sy;
271 g2d.setColor(COLOR_ZONES);
272 g2d.fill(new Rectangle2D.Float(sx, sy, w, h));
276 private void drawNoGo(Graphics2D g2d, float scale) {
277 for (Map.Entry<Integer, ArrayList<float[]>> area : rmfp.getAreas().entrySet()) {
278 for (float[] point : area.getValue()) {
279 float x = toXCoord(point[0]) * scale;
280 float y = toYCoord(point[1]) * scale;
281 float x1 = toXCoord(point[2]) * scale;
282 float y1 = toYCoord(point[3]) * scale;
283 float x2 = toXCoord(point[4]) * scale;
284 float y2 = toYCoord(point[5]) * scale;
285 float x3 = toXCoord(point[6]) * scale;
286 float y3 = toYCoord(point[7]) * scale;
287 Path2D noGo = new Path2D.Float();
293 g2d.setColor(COLOR_NO_GO_ZONES);
295 g2d.setColor(area.getKey() == 9 ? Color.RED : Color.WHITE);
302 private void drawWalls(Graphics2D g2d, float scale) {
303 Stroke stroke = new BasicStroke(3 * scale);
304 g2d.setStroke(stroke);
305 for (float[] point : rmfp.getWalls()) {
306 float x = toXCoord(point[0]) * scale;
307 float y = toYCoord(point[1]) * scale;
308 float x1 = toXCoord(point[2]) * scale;
309 float y1 = toYCoord(point[3]) * scale;
310 g2d.setColor(Color.RED);
311 g2d.draw(new Line2D.Float(x, y, x1, y1));
315 private void drawRobo(Graphics2D g2d, float scale) {
316 float radius = 3 * scale;
317 Stroke stroke = new BasicStroke(2 * scale);
318 g2d.setStroke(stroke);
319 g2d.setColor(COLOR_CHARGER_HALO);
320 final float chargerX = toXCoord(rmfp.getChargerX()) * scale;
321 final float chargerY = toYCoord(rmfp.getChargerY()) * scale;
322 drawCircle(g2d, chargerX, chargerY, radius, false);
323 drawCenteredImg(g2d, scale / 8, "charger.png", chargerX, chargerY);
325 g2d.setColor(COLOR_ROBO);
326 final float roboX = toXCoord(rmfp.getRoboX()) * scale;
327 final float roboY = toYCoord(rmfp.getRoboY()) * scale;
328 drawCircle(g2d, roboX, roboY, radius, false);
330 drawCenteredImg(g2d, scale / 15, "robo.png", roboX, roboY);
334 private void drawObstacles(Graphics2D g2d, float scale) {
335 float radius = 2 * scale;
336 Stroke stroke = new BasicStroke(3 * scale);
337 g2d.setStroke(stroke);
338 g2d.setColor(Color.MAGENTA);
340 Map<Integer, ArrayList<int[]>> obstacleMap = rmfp.getObstacles();
341 for (ArrayList<int[]> obstacles : obstacleMap.values()) {
342 obstacles.forEach(obstacle -> {
343 final float obstacleX = toXCoord(obstacle[0]) * scale;
344 final float obstacleY = toYCoord(obstacle[1]) * scale;
345 drawCircle(g2d, obstacleX, obstacleY, radius, true);
347 drawCenteredImg(g2d, scale / 3, "obstacle-" + obstacle[2] + ".png", obstacleX, obstacleY + 15);
353 private void drawCircle(Graphics2D g2d, float x, float y, float radius, boolean fill) {
354 Ellipse2D.Double circle = new Ellipse2D.Double(x - radius, y - radius, 2.0 * radius, 2.0 * radius);
362 private void drawCenteredImg(Graphics2D g2d, float scale, String imgFile, float x, float y) {
363 URL image = getImageUrl(imgFile);
366 BufferedImage addImg = ImageIO.read(image);
367 int xpos = Math.round(x + (addImg.getWidth() / 2 * scale));
368 int ypos = Math.round(y + (addImg.getHeight() / 2 * scale));
369 AffineTransform at = new AffineTransform();
370 at.scale(-scale, -scale);
371 AffineTransformOp scaleOp = new AffineTransformOp(at, AffineTransformOp.TYPE_BILINEAR);
372 g2d.drawImage(addImg, scaleOp, xpos, ypos);
374 logger.debug("Error loading image {}: File not be found.", imgFile);
376 } catch (IOException e) {
377 logger.debug("Error loading image {}: {}", image, e.getMessage());
381 private void drawGoTo(Graphics2D g2d, float scale) {
382 float x = toXCoord(rmfp.getGotoX()) * scale;
383 float y = toYCoord(rmfp.getGotoY()) * scale;
384 if (!(x == 0 && y == 0)) {
385 g2d.setStroke(new BasicStroke());
386 g2d.setColor(Color.YELLOW);
387 int x3[] = { (int) x, (int) (x - 2 * scale), (int) (x + 2 * scale) };
388 int y3[] = { (int) y, (int) (y - 5 * scale), (int) (y - 5 * scale) };
389 g2d.fill(new Polygon(x3, y3, 3));
393 private void drawOpenHabRocks(Graphics2D g2d, int width, int height, float scale) {
397 URL image = getImageUrl("ohlogo.png");
400 BufferedImage ohLogo = ImageIO.read(image);
401 textPos = (int) (ohLogo.getWidth() * scale / 2 + offset * scale);
402 AffineTransform at = new AffineTransform();
403 at.scale(scale / 2, scale / 2);
404 AffineTransformOp scaleOp = new AffineTransformOp(at, AffineTransformOp.TYPE_BILINEAR);
405 g2d.drawImage(ohLogo, scaleOp, offset,
406 height - (int) (ohLogo.getHeight() * scale / 2) - (int) (offset * scale));
408 logger.debug("Error loading image ohlogo.png: File not be found.");
410 } catch (IOException e) {
411 logger.debug("Error loading image ohlogo.png:: {}", e.getMessage());
413 String fontName = getAvailableFont("Helvetica,Arial,Roboto,Verdana,Times,Serif,Dialog".split(","));
414 if (fontName == null) {
415 return; // no available fonts to draw text
417 Font font = new Font(fontName, Font.BOLD, 14);
419 String message = "Openhab rocks your Xiaomi vacuum!";
420 FontMetrics fontMetrics = g2d.getFontMetrics();
421 int stringWidth = fontMetrics.stringWidth(message);
422 if ((stringWidth + textPos) > rmfp.getImgWidth() * scale) {
423 font = new Font(fontName, Font.BOLD,
424 (int) Math.floor(14 * (rmfp.getImgWidth() * scale - textPos - offset * scale) / stringWidth));
427 int stringHeight = fontMetrics.getAscent();
428 g2d.setPaint(Color.white);
429 g2d.drawString(message, textPos, height - offset * scale - stringHeight / 2);
432 private @Nullable String getAvailableFont(String[] preferedFonts) {
433 final GraphicsEnvironment gEv = GraphicsEnvironment.getLocalGraphicsEnvironment();
437 String[] fonts = gEv.getAvailableFontFamilyNames();
438 if (fonts.length == 0) {
441 for (int j = 0; j < preferedFonts.length; j++) {
442 for (int i = 0; i < fonts.length; i++) {
443 if (fonts[i].equalsIgnoreCase(preferedFonts[j])) {
444 return preferedFonts[j];
448 // Preferred fonts not available... just go with the first one
452 private @Nullable URL getImageUrl(String image) {
453 final Bundle bundle = this.bundle;
454 if (bundle != null) {
455 return bundle.getEntry("images/" + image);
458 File fn = new File("src" + File.separator + "main" + File.separator + "resources" + File.separator
459 + "images" + File.separator + image);
460 return fn.toURI().toURL();
461 } catch (MalformedURLException | SecurityException e) {
462 logger.debug("Could create URL for {}: {}", image, e.getMessage());
467 public BufferedImage getImage(float scale) {
468 int width = (int) Math.floor(rmfp.getImgWidth() * scale);
469 int height = (int) Math.floor(rmfp.getImgHeight() * scale);
470 BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
471 Graphics2D g2d = bi.createGraphics();
472 AffineTransform tx = AffineTransform.getScaleInstance(-1, -1);
473 tx.translate(-width, -height);
474 g2d.setTransform(tx);
476 drawCarpetMap(g2d, scale);
477 drawZones(g2d, scale);
478 drawNoGo(g2d, scale);
479 drawWalls(g2d, scale);
480 drawPath(g2d, scale);
481 drawRobo(g2d, scale);
482 drawGoTo(g2d, scale);
483 drawObstacles(g2d, scale);
484 g2d = bi.createGraphics();
485 drawOpenHabRocks(g2d, width, height, scale);
489 public boolean writePic(String filename, String formatName, float scale) throws IOException {
490 return ImageIO.write(getImage(scale), formatName, new File(filename));
493 private float toXCoord(float x) {
494 return rmfp.getImgWidth() + rmfp.getLeft() - (x / MM);
497 private float toYCoord(float y) {
498 return y / MM - rmfp.getTop();
502 public String toString() {
503 return rmfp.toString();