2 * Copyright (c) 2010-2024 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;
66 private final @Nullable Bundle bundle = FrameworkUtil.getBundle(getClass());
67 private final RRMapFileParser rmfp;
68 private final Logger logger = LoggerFactory.getLogger(RRMapDraw.class);
70 private RRMapDrawOptions drawOptions = new RRMapDrawOptions();
71 private boolean multicolor = false;
72 private int firstX = 0;
73 private int lastX = 0;
74 private int firstY = 0;
75 private int lastY = 0;
77 public RRMapDraw(RRMapFileParser rmfp) {
81 public int getWidth() {
82 return rmfp.getImgWidth();
85 public int getHeight() {
86 return rmfp.getImgHeight();
89 public void setDrawOptions(RRMapDrawOptions options) {
90 this.drawOptions = options;
93 public RRMapDrawOptions getDrawOptions() {
97 public RRMapFileParser getMapParseDetails() {
102 * load Gzipped RR inputstream
104 * @throws IOException
106 public static RRMapDraw loadImage(InputStream is) throws IOException {
107 byte[] inputdata = RRMapFileParser.readRRMapFile(is);
108 RRMapFileParser rf = new RRMapFileParser(inputdata);
109 return new RRMapDraw(rf);
113 * load Gzipped RR file
115 * @throws IOException
117 public static RRMapDraw loadImage(File file) throws IOException {
118 return loadImage(new FileInputStream(file));
122 * draws the map from the individual pixels
124 private void drawMap(Graphics2D g2d, float scale) {
125 Stroke stroke = new BasicStroke(1.1f * scale);
126 Set<Integer> roomIds = new HashSet<Integer>();
127 g2d.setStroke(stroke);
128 for (int y = 0; y < rmfp.getImgHeight() - 1; y++) {
129 for (int x = 0; x < rmfp.getImgWidth() + 1; x++) {
130 byte walltype = rmfp.getImage()[x + rmfp.getImgWidth() * y];
131 switch (walltype & 0xFF) {
133 g2d.setColor(drawOptions.getColorMapOutside());
136 g2d.setColor(drawOptions.getColorMapWall());
139 g2d.setColor(drawOptions.getColorMapInside());
142 g2d.setColor(drawOptions.getColorScan());
145 int obstacle = (walltype & 0x07);
146 int mapId = (walltype & 0xFF) >>> 3;
149 g2d.setColor(drawOptions.getColorGreyWall());
152 g2d.setColor(Color.BLACK);
155 g2d.setColor(drawOptions.getRoomColors()[mapId % 15]);
160 g2d.setColor(Color.WHITE);
164 float xPos = scale * (rmfp.getImgWidth() - x);
165 float yP = scale * y;
166 g2d.draw(new Line2D.Float(xPos, yP, xPos, yP));
169 if (logger.isDebugEnabled() && !roomIds.isEmpty()) {
170 StringBuilder sb = new StringBuilder();
171 for (Integer r : roomIds) {
172 sb.append(" " + r.toString());
174 logger.debug("Identified rooms in map:{}", sb.toString());
179 * draws the carpet map
181 private void drawCarpetMap(Graphics2D g2d, float scale) {
182 if (rmfp.getCarpetMap().length == 0) {
185 Stroke stroke = new BasicStroke(1.1f * scale);
186 g2d.setStroke(stroke);
187 for (int y = 0; y < rmfp.getImgHeight() - 1; y++) {
188 for (int x = 0; x < rmfp.getImgWidth() + 1; x++) {
189 int carpetType = rmfp.getCarpetMap()[x + rmfp.getImgWidth() * y];
190 switch (carpetType) {
195 g2d.setColor(drawOptions.getColorCarpet());
196 float xPos = scale * (rmfp.getImgWidth() - x);
197 float yP = scale * y;
198 g2d.draw(new Line2D.Float(xPos, yP, xPos, yP));
206 * draws the vacuum path
210 private void drawPath(Graphics2D g2d, float scale) {
211 Stroke stroke = new BasicStroke(0.5f * scale);
212 g2d.setStroke(stroke);
213 for (Entry<Integer, ArrayList<float[]>> path : rmfp.getPaths().entrySet()) {
214 Integer pathType = path.getKey();
216 case RRMapFileParser.PATH:
218 g2d.setColor(drawOptions.getColorPath());
220 g2d.setColor(Color.WHITE);
223 case RRMapFileParser.GOTO_PATH:
224 g2d.setColor(Color.GREEN);
226 case RRMapFileParser.GOTO_PREDICTED_PATH:
227 g2d.setColor(Color.YELLOW);
230 g2d.setColor(Color.CYAN);
234 for (float[] point : path.getValue()) {
235 float x = toXCoord(point[0]) * scale;
236 float y = toYCoord(point[1]) * scale;
238 g2d.draw(new Line2D.Float(prvX, prvY, x, y));
246 private void drawZones(Graphics2D g2d, float scale) {
247 for (float[] point : rmfp.getZones()) {
248 float x = toXCoord(point[0]) * scale;
249 float y = toYCoord(point[1]) * scale;
250 float x1 = toXCoord(point[2]) * scale;
251 float y1 = toYCoord(point[3]) * scale;
252 float sx = Math.min(x, x1);
253 float w = Math.max(x, x1) - sx;
254 float sy = Math.min(y, y1);
255 float h = Math.max(y, y1) - sy;
256 g2d.setColor(drawOptions.getColorZones());
257 g2d.fill(new Rectangle2D.Float(sx, sy, w, h));
261 private void drawNoGo(Graphics2D g2d, float scale) {
262 for (Map.Entry<Integer, ArrayList<float[]>> area : rmfp.getAreas().entrySet()) {
263 for (float[] point : area.getValue()) {
264 float x = toXCoord(point[0]) * scale;
265 float y = toYCoord(point[1]) * scale;
266 float x1 = toXCoord(point[2]) * scale;
267 float y1 = toYCoord(point[3]) * scale;
268 float x2 = toXCoord(point[4]) * scale;
269 float y2 = toYCoord(point[5]) * scale;
270 float x3 = toXCoord(point[6]) * scale;
271 float y3 = toYCoord(point[7]) * scale;
272 Path2D noGo = new Path2D.Float();
278 g2d.setColor(drawOptions.getColorNoGoZones());
280 g2d.setColor(area.getKey() == 9 ? Color.RED : Color.WHITE);
286 private void drawWalls(Graphics2D g2d, float scale) {
287 Stroke stroke = new BasicStroke(3 * scale);
288 g2d.setStroke(stroke);
289 for (float[] point : rmfp.getWalls()) {
290 float x = toXCoord(point[0]) * scale;
291 float y = toYCoord(point[1]) * scale;
292 float x1 = toXCoord(point[2]) * scale;
293 float y1 = toYCoord(point[3]) * scale;
294 g2d.setColor(Color.RED);
295 g2d.draw(new Line2D.Float(x, y, x1, y1));
299 private void drawRobo(Graphics2D g2d, float scale) {
300 float radius = 3 * scale;
301 Stroke stroke = new BasicStroke(2 * scale);
302 g2d.setStroke(stroke);
303 g2d.setColor(drawOptions.getColorChargerHalo());
304 final float chargerX = toXCoord(rmfp.getChargerX()) * scale;
305 final float chargerY = toYCoord(rmfp.getChargerY()) * scale;
306 drawCircle(g2d, chargerX, chargerY, radius, false);
307 drawCenteredImg(g2d, scale / 8, "charger.png", chargerX, chargerY);
309 g2d.setColor(drawOptions.getColorRobo());
310 final float roboX = toXCoord(rmfp.getRoboX()) * scale;
311 final float roboY = toYCoord(rmfp.getRoboY()) * scale;
312 drawCircle(g2d, roboX, roboY, radius, false);
314 drawCenteredImg(g2d, scale / 15, "robo.png", roboX, roboY);
318 private void drawObstacles(Graphics2D g2d, float scale) {
319 float radius = 2 * scale;
320 Stroke stroke = new BasicStroke(3 * scale);
321 g2d.setStroke(stroke);
322 g2d.setColor(Color.MAGENTA);
324 Map<Integer, ArrayList<int[]>> obstacleMap = rmfp.getObstacles();
325 for (ArrayList<int[]> obstacles : obstacleMap.values()) {
326 obstacles.forEach(obstacle -> {
327 final float obstacleX = toXCoord(obstacle[0]) * scale;
328 final float obstacleY = toYCoord(obstacle[1]) * scale;
329 drawCircle(g2d, obstacleX, obstacleY, radius, true);
331 drawCenteredImg(g2d, scale / 3, "obstacle-" + obstacle[2] + ".png", obstacleX, obstacleY + 15);
337 private void drawCircle(Graphics2D g2d, float x, float y, float radius, boolean fill) {
338 Ellipse2D.Double circle = new Ellipse2D.Double(x - radius, y - radius, 2.0 * radius, 2.0 * radius);
346 private void drawCenteredImg(Graphics2D g2d, float scale, String imgFile, float x, float y) {
347 URL image = getImageUrl(imgFile);
350 BufferedImage addImg = ImageIO.read(image);
351 int xpos = Math.round(x + (addImg.getWidth() / 2 * scale));
352 int ypos = Math.round(y + (addImg.getHeight() / 2 * scale));
353 AffineTransform at = new AffineTransform();
354 at.scale(-scale, -scale);
355 AffineTransformOp scaleOp = new AffineTransformOp(at, AffineTransformOp.TYPE_BILINEAR);
356 g2d.drawImage(addImg, scaleOp, xpos, ypos);
358 logger.debug("Error loading image {}: File not be found.", imgFile);
360 } catch (IOException e) {
361 logger.debug("Error loading image {}: {}", image, e.getMessage());
365 private void drawGoTo(Graphics2D g2d, float scale) {
366 float x = toXCoord(rmfp.getGotoX()) * scale;
367 float y = toYCoord(rmfp.getGotoY()) * scale;
368 if (!(x == 0 && y == 0)) {
369 g2d.setStroke(new BasicStroke());
370 g2d.setColor(Color.YELLOW);
371 int[] x3 = { (int) x, (int) (x - 2 * scale), (int) (x + 2 * scale) };
372 int[] y3 = { (int) y, (int) (y - 5 * scale), (int) (y - 5 * scale) };
373 g2d.fill(new Polygon(x3, y3, 3));
377 private void drawOpenHabRocks(Graphics2D g2d, int width, int height, float scale) {
381 URL image = getImageUrl("ohlogo.png");
384 BufferedImage ohLogo = ImageIO.read(image);
385 textPos = (int) (ohLogo.getWidth() * scale / 2 + offset * scale);
386 AffineTransform at = new AffineTransform();
387 at.scale(scale / 2, scale / 2);
388 AffineTransformOp scaleOp = new AffineTransformOp(at, AffineTransformOp.TYPE_BILINEAR);
389 g2d.drawImage(ohLogo, scaleOp, offset,
390 height - (int) (ohLogo.getHeight() * scale / 2) - (int) (offset * scale));
392 logger.debug("Error loading image ohlogo.png: File not be found.");
394 } catch (IOException e) {
395 logger.debug("Error loading image ohlogo.png:: {}", e.getMessage());
397 if (drawOptions.getText().isBlank()) {
400 String fontName = getAvailableFont("Helvetica,Arial,Roboto,Verdana,Times,Serif,Dialog".split(","));
401 if (fontName == null) {
402 return; // no available fonts to draw text
404 int fz = (int) (drawOptions.getTextFontSize() * scale);
405 Font font = new Font(fontName, Font.BOLD, fz);
407 String message = drawOptions.getText();
408 FontMetrics fontMetrics = g2d.getFontMetrics();
409 int stringWidth = fontMetrics.stringWidth(message);
410 if ((stringWidth + textPos) > width) {
411 int fzn = (int) Math.floor(((float) (width - textPos) / stringWidth) * fz);
412 font = new Font(fontName, Font.BOLD, fzn > 0 ? fzn : 1);
415 int stringHeight = fontMetrics.getAscent();
416 g2d.setPaint(Color.white);
417 g2d.drawString(message, textPos, height - offset * scale - stringHeight / 2);
420 private @Nullable String getAvailableFont(String[] preferedFonts) {
421 final GraphicsEnvironment gEv = GraphicsEnvironment.getLocalGraphicsEnvironment();
425 String[] fonts = gEv.getAvailableFontFamilyNames();
426 if (fonts.length == 0) {
429 for (int j = 0; j < preferedFonts.length; j++) {
430 for (int i = 0; i < fonts.length; i++) {
431 if (fonts[i].equalsIgnoreCase(preferedFonts[j])) {
432 return preferedFonts[j];
436 // Preferred fonts not available... just go with the first one
441 * Finds the perimeter of the used area in the map
443 private void getMapArea(Graphics2D g2d, float scale) {
444 int firstX = rmfp.getImgWidth();
446 int firstY = rmfp.getImgHeight();
448 for (int y = 0; y < rmfp.getImgHeight() - 1; y++) {
449 for (int x = 0; x < rmfp.getImgWidth() + 1; x++) {
450 int walltype = rmfp.getImage()[x + rmfp.getImgWidth() * y] & 0xFF;
451 if (walltype > MAP_OUTSIDE) {
467 this.firstX = firstX;
469 this.firstY = rmfp.getImgHeight() - lastY;
470 this.lastY = rmfp.getImgHeight() - firstY;
473 private @Nullable URL getImageUrl(String image) {
474 final Bundle bundle = this.bundle;
475 if (bundle != null) {
476 return bundle.getEntry("images/" + image);
479 File fn = new File("src" + File.separator + "main" + File.separator + "resources" + File.separator
480 + "images" + File.separator + image);
481 return fn.toURI().toURL();
482 } catch (MalformedURLException | SecurityException e) {
483 logger.debug("Could create URL for {}: {}", image, e.getMessage());
488 public BufferedImage getImage() {
489 return getImage(drawOptions.getScale());
492 public BufferedImage getImage(float scale) {
493 int width = (int) Math.floor(rmfp.getImgWidth() * scale);
494 int height = (int) Math.floor(rmfp.getImgHeight() * scale);
495 BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
496 Graphics2D g2d = bi.createGraphics();
497 AffineTransform tx = AffineTransform.getScaleInstance(-1, -1);
498 tx.translate(-width, -height);
499 g2d.setTransform(tx);
501 drawCarpetMap(g2d, scale);
502 drawZones(g2d, scale);
503 drawNoGo(g2d, scale);
504 drawWalls(g2d, scale);
505 drawPath(g2d, scale);
506 drawRobo(g2d, scale);
507 drawGoTo(g2d, scale);
508 drawObstacles(g2d, scale);
509 if (drawOptions.getCropBorder() < 0) {
510 g2d = bi.createGraphics();
511 if (drawOptions.isShowLogo()) {
512 drawOpenHabRocks(g2d, width, height, scale);
516 // crop the image to the used perimeter
517 getMapArea(g2d, scale);
518 int firstX = (this.firstX - drawOptions.getCropBorder()) > 0 ? this.firstX - drawOptions.getCropBorder() : 0;
519 int lastX = (this.lastX + drawOptions.getCropBorder()) < rmfp.getImgWidth()
520 ? this.lastX + drawOptions.getCropBorder()
521 : rmfp.getImgWidth();
522 int firstY = (this.firstY - drawOptions.getCropBorder()) > 0 ? this.firstY - drawOptions.getCropBorder() : 0;
523 int lastY = (this.lastY + drawOptions.getCropBorder() + (int) (8 * scale)) < rmfp.getImgHeight()
524 ? this.lastY + drawOptions.getCropBorder() + (int) (8 * scale)
525 : rmfp.getImgHeight();
526 int nwidth = (int) Math.floor((lastX - firstX) * scale);
527 int nheight = (int) Math.floor((lastY - firstY) * scale);
528 BufferedImage bo = new BufferedImage(nwidth, nheight, BufferedImage.TYPE_3BYTE_BGR);
529 Graphics2D crop = bo.createGraphics();
530 crop.transform(AffineTransform.getTranslateInstance(-firstX * scale, -firstY * scale));
531 crop.drawImage(bi, 0, 0, null);
532 if (drawOptions.isShowLogo()) {
533 crop = bo.createGraphics();
534 drawOpenHabRocks(crop, nwidth, nheight, scale * .75f);
539 public boolean writePic(String filename, String formatName, float scale) throws IOException {
540 return ImageIO.write(getImage(scale), formatName, new File(filename));
543 private float toXCoord(float x) {
544 return rmfp.getImgWidth() + rmfp.getLeft() - (x / MM);
547 private float toYCoord(float y) {
548 return y / MM - rmfp.getTop();
552 public String toString() {
553 return rmfp.toString();