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;
37 import javax.imageio.ImageIO;
39 import org.eclipse.jdt.annotation.NonNullByDefault;
40 import org.eclipse.jdt.annotation.Nullable;
41 import org.osgi.framework.Bundle;
42 import org.osgi.framework.FrameworkUtil;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
47 * Draws the vacuum map file to an image
49 * @author Marcel Verpaalen - Initial contribution
52 public class RRMapDraw {
54 private static final float MM = 50.0f;
56 private static final int MAP_OUTSIDE = 0x00;
57 private static final int MAP_WALL = 0x01;
58 private static final int MAP_INSIDE = 0xFF;
59 private static final int MAP_SCAN = 0x07;
60 private static final Color COLOR_MAP_INSIDE = new Color(32, 115, 185);
61 private static final Color COLOR_MAP_OUTSIDE = new Color(19, 87, 148);
62 private static final Color COLOR_MAP_WALL = new Color(100, 196, 254);
63 private static final Color COLOR_GREY_WALL = new Color(93, 109, 126);
64 private static final Color COLOR_PATH = new Color(147, 194, 238);
65 private static final Color COLOR_ZONES = new Color(0xAD, 0xD8, 0xFF, 0x8F);
66 private static final Color COLOR_NO_GO_ZONES = new Color(255, 33, 55, 127);
67 private static final Color COLOR_CHARGER_HALO = new Color(0x66, 0xfe, 0xda, 0x7f);
68 private static final Color COLOR_ROBO = new Color(75, 235, 149);
69 private static final Color COLOR_SCAN = new Color(0xDF, 0xDF, 0xDF);
70 private static final Color ROOM1 = new Color(240, 178, 122);
71 private static final Color ROOM2 = new Color(133, 193, 233);
72 private static final Color ROOM3 = new Color(217, 136, 128);
73 private static final Color ROOM4 = new Color(52, 152, 219);
74 private static final Color ROOM5 = new Color(205, 97, 85);
75 private static final Color ROOM6 = new Color(243, 156, 18);
76 private static final Color ROOM7 = new Color(88, 214, 141);
77 private static final Color ROOM8 = new Color(245, 176, 65);
78 private static final Color ROOM9 = new Color(0xFc, 0xD4, 0x51);
79 private static final Color ROOM10 = new Color(72, 201, 176);
80 private static final Color ROOM11 = new Color(84, 153, 199);
81 private static final Color ROOM12 = new Color(133, 193, 233);
82 private static final Color ROOM13 = new Color(245, 176, 65);
83 private static final Color ROOM14 = new Color(82, 190, 128);
84 private static final Color ROOM15 = new Color(72, 201, 176);
85 private static final Color ROOM16 = new Color(165, 105, 189);
86 private static final Color[] ROOM_COLORS = { ROOM1, ROOM2, ROOM3, ROOM4, ROOM5, ROOM6, ROOM7, ROOM8, ROOM9, ROOM10,
87 ROOM11, ROOM12, ROOM13, ROOM14, ROOM15, ROOM16 };
88 private final @Nullable Bundle bundle = FrameworkUtil.getBundle(getClass());
89 private boolean multicolor = false;
90 private final RRMapFileParser rmfp;
92 private final Logger logger = LoggerFactory.getLogger(RRMapDraw.class);
94 public RRMapDraw(RRMapFileParser rmfp) {
98 public int getWidth() {
99 return rmfp.getImgWidth();
102 public int getHeight() {
103 return rmfp.getImgHeight();
106 public RRMapFileParser getMapParseDetails() {
111 * load Gzipped RR inputstream
113 * @throws IOException
115 public static RRMapDraw loadImage(InputStream is) throws IOException {
116 byte[] inputdata = RRMapFileParser.readRRMapFile(is);
117 RRMapFileParser rf = new RRMapFileParser(inputdata);
118 return new RRMapDraw(rf);
122 * load Gzipped RR file
124 * @throws IOException
126 public static RRMapDraw loadImage(File file) throws IOException {
127 return loadImage(new FileInputStream(file));
131 * draws the map from the individual pixels
133 private void drawMap(Graphics2D g2d, float scale) {
134 Stroke stroke = new BasicStroke(1.1f * scale);
135 g2d.setStroke(stroke);
136 for (int y = 0; y < rmfp.getImgHeight() - 1; y++) {
137 for (int x = 0; x < rmfp.getImgWidth() + 1; x++) {
138 byte walltype = rmfp.getImage()[x + rmfp.getImgWidth() * y];
139 switch (walltype & 0xFF) {
141 g2d.setColor(COLOR_MAP_OUTSIDE);
144 g2d.setColor(COLOR_MAP_WALL);
147 g2d.setColor(COLOR_MAP_INSIDE);
150 g2d.setColor(COLOR_SCAN);
153 int obstacle = (walltype & 0x07);
154 int mapId = (walltype & 0xFF) >>> 3;
157 g2d.setColor(COLOR_GREY_WALL);
160 g2d.setColor(Color.BLACK);
163 g2d.setColor(ROOM_COLORS[Math.round(mapId / 2)]);
167 g2d.setColor(Color.WHITE);
171 float xPos = scale * (rmfp.getImgWidth() - x);
172 float yP = scale * y;
173 g2d.draw(new Line2D.Float(xPos, yP, xPos, yP));
179 * draws the vacuum path
183 private void drawPath(Graphics2D g2d, float scale) {
184 Stroke stroke = new BasicStroke(0.5f * scale);
185 g2d.setStroke(stroke);
186 for (Integer pathType : rmfp.getPaths().keySet()) {
188 case RRMapFileParser.PATH:
190 g2d.setColor(COLOR_PATH);
192 g2d.setColor(Color.WHITE);
195 case RRMapFileParser.GOTO_PATH:
196 g2d.setColor(Color.GREEN);
198 case RRMapFileParser.GOTO_PREDICTED_PATH:
199 g2d.setColor(Color.YELLOW);
202 g2d.setColor(Color.CYAN);
206 for (float[] point : rmfp.getPaths().get(pathType)) {
207 float x = toXCoord(point[0]) * scale;
208 float y = toYCoord(point[1]) * scale;
210 g2d.draw(new Line2D.Float(prvX, prvY, x, y));
218 private void drawZones(Graphics2D g2d, float scale) {
219 for (float[] point : rmfp.getZones()) {
220 float x = toXCoord(point[0]) * scale;
221 float y = toYCoord(point[1]) * scale;
222 float x1 = toXCoord(point[2]) * scale;
223 float y1 = toYCoord(point[3]) * scale;
224 float sx = Math.min(x, x1);
225 float w = Math.max(x, x1) - sx;
226 float sy = Math.min(y, y1);
227 float h = Math.max(y, y1) - sy;
228 g2d.setColor(COLOR_ZONES);
229 g2d.fill(new Rectangle2D.Float(sx, sy, w, h));
233 private void drawNoGo(Graphics2D g2d, float scale) {
234 for (Integer area : rmfp.getAreas().keySet()) {
235 for (float[] point : rmfp.getAreas().get(area)) {
236 float x = toXCoord(point[0]) * scale;
237 float y = toYCoord(point[1]) * scale;
238 float x1 = toXCoord(point[2]) * scale;
239 float y1 = toYCoord(point[3]) * scale;
240 float x2 = toXCoord(point[4]) * scale;
241 float y2 = toYCoord(point[5]) * scale;
242 float x3 = toXCoord(point[6]) * scale;
243 float y3 = toYCoord(point[7]) * scale;
244 Path2D noGo = new Path2D.Float();
250 g2d.setColor(COLOR_NO_GO_ZONES);
252 g2d.setColor(area == 9 ? Color.RED : Color.WHITE);
258 private void drawWalls(Graphics2D g2d, float scale) {
259 Stroke stroke = new BasicStroke(3 * scale);
260 g2d.setStroke(stroke);
261 for (float[] point : rmfp.getWalls()) {
262 float x = toXCoord(point[0]) * scale;
263 float y = toYCoord(point[1]) * scale;
264 float x1 = toXCoord(point[2]) * scale;
265 float y1 = toYCoord(point[3]) * scale;
266 g2d.setColor(Color.RED);
267 g2d.draw(new Line2D.Float(x, y, x1, y1));
271 private void drawRobo(Graphics2D g2d, float scale) {
272 float radius = 3 * scale;
273 Stroke stroke = new BasicStroke(2 * scale);
274 g2d.setStroke(stroke);
275 g2d.setColor(COLOR_CHARGER_HALO);
276 final float chargerX = toXCoord(rmfp.getChargerX()) * scale;
277 final float chargerY = toYCoord(rmfp.getChargerY()) * scale;
278 drawCircle(g2d, chargerX, chargerY, radius);
279 drawCenteredImg(g2d, scale / 8, "charger.png", chargerX, chargerY);
281 g2d.setColor(COLOR_ROBO);
282 final float roboX = toXCoord(rmfp.getRoboX()) * scale;
283 final float roboY = toYCoord(rmfp.getRoboY()) * scale;
284 drawCircle(g2d, roboX, roboY, radius);
286 drawCenteredImg(g2d, scale / 15, "robo.png", roboX, roboY);
290 private void drawCircle(Graphics2D g2d, float x, float y, float radius) {
291 g2d.draw(new Ellipse2D.Double(x - radius, y - radius, 2.0 * radius, 2.0 * radius));
294 private void drawCenteredImg(Graphics2D g2d, float scale, String imgFile, float x, float y) {
295 URL image = getImageUrl(imgFile);
298 BufferedImage addImg = ImageIO.read(image);
299 int xpos = Math.round(x - (addImg.getWidth() / 2 * scale));
300 int ypos = Math.round(y - (addImg.getHeight() / 2 * scale));
301 AffineTransform at = new AffineTransform();
302 at.scale(scale, scale);
303 AffineTransformOp scaleOp = new AffineTransformOp(at, AffineTransformOp.TYPE_BILINEAR);
304 g2d.drawImage(addImg, scaleOp, xpos, ypos);
306 logger.debug("Error loading image {}: File not be found.", imgFile);
308 } catch (IOException e) {
309 logger.debug("Error loading image {}: {}", image, e.getMessage());
313 private void drawGoTo(Graphics2D g2d, float scale) {
314 float x = toXCoord(rmfp.getGotoX()) * scale;
315 float y = toYCoord(rmfp.getGotoY()) * scale;
316 if (!(x == 0 && y == 0)) {
317 g2d.setStroke(new BasicStroke());
318 g2d.setColor(Color.YELLOW);
319 int x3[] = { (int) x, (int) (x - 2 * scale), (int) (x + 2 * scale) };
320 int y3[] = { (int) y, (int) (y - 5 * scale), (int) (y - 5 * scale) };
321 g2d.fill(new Polygon(x3, y3, 3));
325 private void drawOpenHabRocks(Graphics2D g2d, int width, int height, float scale) {
329 URL image = getImageUrl("ohlogo.png");
332 BufferedImage ohLogo = ImageIO.read(image);
333 textPos = (int) (ohLogo.getWidth() * scale / 2 + offset * scale);
334 AffineTransform at = new AffineTransform();
335 at.scale(scale / 2, scale / 2);
336 AffineTransformOp scaleOp = new AffineTransformOp(at, AffineTransformOp.TYPE_BILINEAR);
337 g2d.drawImage(ohLogo, scaleOp, offset,
338 height - (int) (ohLogo.getHeight() * scale / 2) - (int) (offset * scale));
340 logger.debug("Error loading image ohlogo.png: File not be found.");
342 } catch (IOException e) {
343 logger.debug("Error loading image ohlogo.png:: {}", e.getMessage());
345 String fontName = getAvailableFont("Helvetica,Arial,Roboto,Verdana,Times,Serif,Dialog".split(","));
346 if (fontName == null) {
347 return; // no available fonts to draw text
349 Font font = new Font(fontName, Font.BOLD, 14);
351 String message = "Openhab rocks your Xiaomi vacuum!";
352 FontMetrics fontMetrics = g2d.getFontMetrics();
353 int stringWidth = fontMetrics.stringWidth(message);
354 if ((stringWidth + textPos) > rmfp.getImgWidth() * scale) {
355 font = new Font(fontName, Font.BOLD,
356 (int) Math.floor(14 * (rmfp.getImgWidth() * scale - textPos - offset * scale) / stringWidth));
359 int stringHeight = fontMetrics.getAscent();
360 g2d.setPaint(Color.white);
361 g2d.drawString(message, textPos, height - offset * scale - stringHeight / 2);
364 private @Nullable String getAvailableFont(String[] preferedFonts) {
365 final GraphicsEnvironment gEv = GraphicsEnvironment.getLocalGraphicsEnvironment();
369 String[] fonts = gEv.getAvailableFontFamilyNames();
370 if (fonts.length == 0) {
373 for (int j = 0; j < preferedFonts.length; j++) {
374 for (int i = 0; i < fonts.length; i++) {
375 if (fonts[i].equalsIgnoreCase(preferedFonts[j])) {
376 return preferedFonts[j];
380 // Preferred fonts not available... just go with the first one
384 private @Nullable URL getImageUrl(String image) {
385 if (bundle != null) {
386 return bundle.getEntry("images/" + image);
389 File fn = new File("src" + File.separator + "main" + File.separator + "resources" + File.separator
390 + "images" + File.separator + image);
391 return fn.toURI().toURL();
392 } catch (MalformedURLException | SecurityException e) {
393 logger.debug("Could create URL for {}: {}", image, e.getMessage());
398 public BufferedImage getImage(float scale) {
399 int width = (int) Math.floor(rmfp.getImgWidth() * scale);
400 int height = (int) Math.floor(rmfp.getImgHeight() * scale);
401 BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
402 Graphics2D g2d = bi.createGraphics();
403 AffineTransform tx = AffineTransform.getScaleInstance(-1, -1);
404 tx.translate(-width, -height);
405 g2d.setTransform(tx);
407 drawZones(g2d, scale);
408 drawNoGo(g2d, scale);
409 drawWalls(g2d, scale);
410 drawPath(g2d, scale);
411 drawRobo(g2d, scale);
412 drawGoTo(g2d, scale);
413 g2d = bi.createGraphics();
414 drawOpenHabRocks(g2d, width, height, scale);
418 public boolean writePic(String filename, String formatName, float scale) throws IOException {
419 return ImageIO.write(getImage(scale), formatName, new File(filename));
422 private float toXCoord(float x) {
423 return rmfp.getImgWidth() + rmfp.getLeft() - (x / MM);
426 private float toYCoord(float y) {
427 return y / MM - rmfp.getTop();
431 public String toString() {
432 return rmfp.toString();