2 * Copyright (c) 2010-2020 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;
41 import javax.imageio.ImageIO;
43 import org.eclipse.jdt.annotation.NonNullByDefault;
44 import org.eclipse.jdt.annotation.Nullable;
45 import org.osgi.framework.Bundle;
46 import org.osgi.framework.FrameworkUtil;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
51 * Draws the vacuum map file to an image
53 * @author Marcel Verpaalen - Initial contribution
56 public class RRMapDraw {
58 private static final float MM = 50.0f;
60 private static final int MAP_OUTSIDE = 0x00;
61 private static final int MAP_WALL = 0x01;
62 private static final int MAP_INSIDE = 0xFF;
63 private static final int MAP_SCAN = 0x07;
64 private static final Color COLOR_MAP_INSIDE = new Color(32, 115, 185);
65 private static final Color COLOR_MAP_OUTSIDE = new Color(19, 87, 148);
66 private static final Color COLOR_MAP_WALL = new Color(100, 196, 254);
67 private static final Color COLOR_GREY_WALL = new Color(93, 109, 126);
68 private static final Color COLOR_PATH = new Color(147, 194, 238);
69 private static final Color COLOR_ZONES = new Color(0xAD, 0xD8, 0xFF, 0x8F);
70 private static final Color COLOR_NO_GO_ZONES = new Color(255, 33, 55, 127);
71 private static final Color COLOR_CHARGER_HALO = new Color(0x66, 0xfe, 0xda, 0x7f);
72 private static final Color COLOR_ROBO = new Color(75, 235, 149);
73 private static final Color COLOR_SCAN = new Color(0xDF, 0xDF, 0xDF);
74 private static final Color ROOM1 = new Color(240, 178, 122);
75 private static final Color ROOM2 = new Color(133, 193, 233);
76 private static final Color ROOM3 = new Color(217, 136, 128);
77 private static final Color ROOM4 = new Color(52, 152, 219);
78 private static final Color ROOM5 = new Color(205, 97, 85);
79 private static final Color ROOM6 = new Color(243, 156, 18);
80 private static final Color ROOM7 = new Color(88, 214, 141);
81 private static final Color ROOM8 = new Color(245, 176, 65);
82 private static final Color ROOM9 = new Color(0xFc, 0xD4, 0x51);
83 private static final Color ROOM10 = new Color(72, 201, 176);
84 private static final Color ROOM11 = new Color(84, 153, 199);
85 private static final Color ROOM12 = new Color(255, 213, 209);
86 private static final Color ROOM13 = new Color(228, 228, 215);
87 private static final Color ROOM14 = new Color(82, 190, 128);
88 private static final Color ROOM15 = new Color(72, 201, 176);
89 private static final Color ROOM16 = new Color(165, 105, 189);
90 private static final Color[] ROOM_COLORS = { ROOM1, ROOM2, ROOM3, ROOM4, ROOM5, ROOM6, ROOM7, ROOM8, ROOM9, ROOM10,
91 ROOM11, ROOM12, ROOM13, ROOM14, ROOM15, ROOM16 };
92 private final @Nullable Bundle bundle = FrameworkUtil.getBundle(getClass());
93 private boolean multicolor = false;
94 private final RRMapFileParser rmfp;
96 private final Logger logger = LoggerFactory.getLogger(RRMapDraw.class);
98 public RRMapDraw(RRMapFileParser rmfp) {
102 public int getWidth() {
103 return rmfp.getImgWidth();
106 public int getHeight() {
107 return rmfp.getImgHeight();
110 public RRMapFileParser getMapParseDetails() {
115 * load Gzipped RR inputstream
117 * @throws IOException
119 public static RRMapDraw loadImage(InputStream is) throws IOException {
120 byte[] inputdata = RRMapFileParser.readRRMapFile(is);
121 RRMapFileParser rf = new RRMapFileParser(inputdata);
122 return new RRMapDraw(rf);
126 * load Gzipped RR file
128 * @throws IOException
130 public static RRMapDraw loadImage(File file) throws IOException {
131 return loadImage(new FileInputStream(file));
135 * draws the map from the individual pixels
137 private void drawMap(Graphics2D g2d, float scale) {
138 Stroke stroke = new BasicStroke(1.1f * scale);
139 Set<Integer> roomIds = new HashSet<Integer>();
140 g2d.setStroke(stroke);
141 for (int y = 0; y < rmfp.getImgHeight() - 1; y++) {
142 for (int x = 0; x < rmfp.getImgWidth() + 1; x++) {
143 byte walltype = rmfp.getImage()[x + rmfp.getImgWidth() * y];
144 switch (walltype & 0xFF) {
146 g2d.setColor(COLOR_MAP_OUTSIDE);
149 g2d.setColor(COLOR_MAP_WALL);
152 g2d.setColor(COLOR_MAP_INSIDE);
155 g2d.setColor(COLOR_SCAN);
158 int obstacle = (walltype & 0x07);
159 int mapId = (walltype & 0xFF) >>> 3;
162 g2d.setColor(COLOR_GREY_WALL);
165 g2d.setColor(Color.BLACK);
168 g2d.setColor(ROOM_COLORS[mapId % 15]);
173 g2d.setColor(Color.WHITE);
177 float xPos = scale * (rmfp.getImgWidth() - x);
178 float yP = scale * y;
179 g2d.draw(new Line2D.Float(xPos, yP, xPos, yP));
182 if (logger.isDebugEnabled() && roomIds.size() > 0) {
183 StringBuilder sb = new StringBuilder();
184 for (Integer r : roomIds) {
185 sb.append(" " + r.toString());
187 logger.debug("Identified rooms in map:{}", sb.toString());
192 * draws the vacuum path
196 private void drawPath(Graphics2D g2d, float scale) {
197 Stroke stroke = new BasicStroke(0.5f * scale);
198 g2d.setStroke(stroke);
199 for (Integer pathType : rmfp.getPaths().keySet()) {
201 case RRMapFileParser.PATH:
203 g2d.setColor(COLOR_PATH);
205 g2d.setColor(Color.WHITE);
208 case RRMapFileParser.GOTO_PATH:
209 g2d.setColor(Color.GREEN);
211 case RRMapFileParser.GOTO_PREDICTED_PATH:
212 g2d.setColor(Color.YELLOW);
215 g2d.setColor(Color.CYAN);
219 for (float[] point : rmfp.getPaths().get(pathType)) {
220 float x = toXCoord(point[0]) * scale;
221 float y = toYCoord(point[1]) * scale;
223 g2d.draw(new Line2D.Float(prvX, prvY, x, y));
231 private void drawZones(Graphics2D g2d, float scale) {
232 for (float[] point : rmfp.getZones()) {
233 float x = toXCoord(point[0]) * scale;
234 float y = toYCoord(point[1]) * scale;
235 float x1 = toXCoord(point[2]) * scale;
236 float y1 = toYCoord(point[3]) * scale;
237 float sx = Math.min(x, x1);
238 float w = Math.max(x, x1) - sx;
239 float sy = Math.min(y, y1);
240 float h = Math.max(y, y1) - sy;
241 g2d.setColor(COLOR_ZONES);
242 g2d.fill(new Rectangle2D.Float(sx, sy, w, h));
246 private void drawNoGo(Graphics2D g2d, float scale) {
247 for (Integer area : rmfp.getAreas().keySet()) {
248 for (float[] point : rmfp.getAreas().get(area)) {
249 float x = toXCoord(point[0]) * scale;
250 float y = toYCoord(point[1]) * scale;
251 float x1 = toXCoord(point[2]) * scale;
252 float y1 = toYCoord(point[3]) * scale;
253 float x2 = toXCoord(point[4]) * scale;
254 float y2 = toYCoord(point[5]) * scale;
255 float x3 = toXCoord(point[6]) * scale;
256 float y3 = toYCoord(point[7]) * scale;
257 Path2D noGo = new Path2D.Float();
263 g2d.setColor(COLOR_NO_GO_ZONES);
265 g2d.setColor(area == 9 ? Color.RED : Color.WHITE);
271 private void drawWalls(Graphics2D g2d, float scale) {
272 Stroke stroke = new BasicStroke(3 * scale);
273 g2d.setStroke(stroke);
274 for (float[] point : rmfp.getWalls()) {
275 float x = toXCoord(point[0]) * scale;
276 float y = toYCoord(point[1]) * scale;
277 float x1 = toXCoord(point[2]) * scale;
278 float y1 = toYCoord(point[3]) * scale;
279 g2d.setColor(Color.RED);
280 g2d.draw(new Line2D.Float(x, y, x1, y1));
284 private void drawRobo(Graphics2D g2d, float scale) {
285 float radius = 3 * scale;
286 Stroke stroke = new BasicStroke(2 * scale);
287 g2d.setStroke(stroke);
288 g2d.setColor(COLOR_CHARGER_HALO);
289 final float chargerX = toXCoord(rmfp.getChargerX()) * scale;
290 final float chargerY = toYCoord(rmfp.getChargerY()) * scale;
291 drawCircle(g2d, chargerX, chargerY, radius, false);
292 drawCenteredImg(g2d, scale / 8, "charger.png", chargerX, chargerY);
294 g2d.setColor(COLOR_ROBO);
295 final float roboX = toXCoord(rmfp.getRoboX()) * scale;
296 final float roboY = toYCoord(rmfp.getRoboY()) * scale;
297 drawCircle(g2d, roboX, roboY, radius, false);
299 drawCenteredImg(g2d, scale / 15, "robo.png", roboX, roboY);
303 private void drawObstacles(Graphics2D g2d, float scale) {
304 float radius = 2 * scale;
305 Stroke stroke = new BasicStroke(3 * scale);
306 g2d.setStroke(stroke);
307 g2d.setColor(Color.MAGENTA);
309 Map<Integer, ArrayList<int[]>> obstacleMap = rmfp.getObstacles();
310 for (ArrayList<int[]> obstacles : obstacleMap.values()) {
311 obstacles.forEach(obstacle -> {
312 final float obstacleX = toXCoord(obstacle[0]) * scale;
313 final float obstacleY = toYCoord(obstacle[1]) * scale;
314 drawCircle(g2d, obstacleX, obstacleY, radius, true);
316 drawCenteredImg(g2d, scale / 3, "obstacle-" + obstacle[2] + ".png", obstacleX, obstacleY + 15);
322 private void drawCircle(Graphics2D g2d, float x, float y, float radius, boolean fill) {
323 Ellipse2D.Double circle = new Ellipse2D.Double(x - radius, y - radius, 2.0 * radius, 2.0 * radius);
331 private void drawCenteredImg(Graphics2D g2d, float scale, String imgFile, float x, float y) {
332 URL image = getImageUrl(imgFile);
335 BufferedImage addImg = ImageIO.read(image);
336 int xpos = Math.round(x + (addImg.getWidth() / 2 * scale));
337 int ypos = Math.round(y + (addImg.getHeight() / 2 * scale));
338 AffineTransform at = new AffineTransform();
339 at.scale(-scale, -scale);
340 AffineTransformOp scaleOp = new AffineTransformOp(at, AffineTransformOp.TYPE_BILINEAR);
341 g2d.drawImage(addImg, scaleOp, xpos, ypos);
343 logger.debug("Error loading image {}: File not be found.", imgFile);
345 } catch (IOException e) {
346 logger.debug("Error loading image {}: {}", image, e.getMessage());
350 private void drawGoTo(Graphics2D g2d, float scale) {
351 float x = toXCoord(rmfp.getGotoX()) * scale;
352 float y = toYCoord(rmfp.getGotoY()) * scale;
353 if (!(x == 0 && y == 0)) {
354 g2d.setStroke(new BasicStroke());
355 g2d.setColor(Color.YELLOW);
356 int x3[] = { (int) x, (int) (x - 2 * scale), (int) (x + 2 * scale) };
357 int y3[] = { (int) y, (int) (y - 5 * scale), (int) (y - 5 * scale) };
358 g2d.fill(new Polygon(x3, y3, 3));
362 private void drawOpenHabRocks(Graphics2D g2d, int width, int height, float scale) {
366 URL image = getImageUrl("ohlogo.png");
369 BufferedImage ohLogo = ImageIO.read(image);
370 textPos = (int) (ohLogo.getWidth() * scale / 2 + offset * scale);
371 AffineTransform at = new AffineTransform();
372 at.scale(scale / 2, scale / 2);
373 AffineTransformOp scaleOp = new AffineTransformOp(at, AffineTransformOp.TYPE_BILINEAR);
374 g2d.drawImage(ohLogo, scaleOp, offset,
375 height - (int) (ohLogo.getHeight() * scale / 2) - (int) (offset * scale));
377 logger.debug("Error loading image ohlogo.png: File not be found.");
379 } catch (IOException e) {
380 logger.debug("Error loading image ohlogo.png:: {}", e.getMessage());
382 String fontName = getAvailableFont("Helvetica,Arial,Roboto,Verdana,Times,Serif,Dialog".split(","));
383 if (fontName == null) {
384 return; // no available fonts to draw text
386 Font font = new Font(fontName, Font.BOLD, 14);
388 String message = "Openhab rocks your Xiaomi vacuum!";
389 FontMetrics fontMetrics = g2d.getFontMetrics();
390 int stringWidth = fontMetrics.stringWidth(message);
391 if ((stringWidth + textPos) > rmfp.getImgWidth() * scale) {
392 font = new Font(fontName, Font.BOLD,
393 (int) Math.floor(14 * (rmfp.getImgWidth() * scale - textPos - offset * scale) / stringWidth));
396 int stringHeight = fontMetrics.getAscent();
397 g2d.setPaint(Color.white);
398 g2d.drawString(message, textPos, height - offset * scale - stringHeight / 2);
401 private @Nullable String getAvailableFont(String[] preferedFonts) {
402 final GraphicsEnvironment gEv = GraphicsEnvironment.getLocalGraphicsEnvironment();
406 String[] fonts = gEv.getAvailableFontFamilyNames();
407 if (fonts.length == 0) {
410 for (int j = 0; j < preferedFonts.length; j++) {
411 for (int i = 0; i < fonts.length; i++) {
412 if (fonts[i].equalsIgnoreCase(preferedFonts[j])) {
413 return preferedFonts[j];
417 // Preferred fonts not available... just go with the first one
421 private @Nullable URL getImageUrl(String image) {
422 if (bundle != null) {
423 return bundle.getEntry("images/" + image);
426 File fn = new File("src" + File.separator + "main" + File.separator + "resources" + File.separator
427 + "images" + File.separator + image);
428 return fn.toURI().toURL();
429 } catch (MalformedURLException | SecurityException e) {
430 logger.debug("Could create URL for {}: {}", image, e.getMessage());
435 public BufferedImage getImage(float scale) {
436 int width = (int) Math.floor(rmfp.getImgWidth() * scale);
437 int height = (int) Math.floor(rmfp.getImgHeight() * scale);
438 BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
439 Graphics2D g2d = bi.createGraphics();
440 AffineTransform tx = AffineTransform.getScaleInstance(-1, -1);
441 tx.translate(-width, -height);
442 g2d.setTransform(tx);
444 drawZones(g2d, scale);
445 drawNoGo(g2d, scale);
446 drawWalls(g2d, scale);
447 drawPath(g2d, scale);
448 drawRobo(g2d, scale);
449 drawGoTo(g2d, scale);
450 drawObstacles(g2d, scale);
451 g2d = bi.createGraphics();
452 drawOpenHabRocks(g2d, width, height, scale);
456 public boolean writePic(String filename, String formatName, float scale) throws IOException {
457 return ImageIO.write(getImage(scale), formatName, new File(filename));
460 private float toXCoord(float x) {
461 return rmfp.getImgWidth() + rmfp.getLeft() - (x / MM);
464 private float toYCoord(float y) {
465 return y / MM - rmfp.getTop();
469 public String toString() {
470 return rmfp.toString();