]> git.basschouten.com Git - openhab-addons.git/blob
a77249d84abf7657036fbaf2be8d34922a3146f8
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.miio.internal.robot;
14
15 import java.awt.BasicStroke;
16 import java.awt.Color;
17 import java.awt.Font;
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;
30 import java.io.File;
31 import java.io.FileInputStream;
32 import java.io.IOException;
33 import java.io.InputStream;
34 import java.net.MalformedURLException;
35 import java.net.URL;
36 import java.util.ArrayList;
37 import java.util.HashSet;
38 import java.util.Map;
39 import java.util.Map.Entry;
40 import java.util.Set;
41
42 import javax.imageio.ImageIO;
43
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;
50
51 /**
52  * Draws the vacuum map file to an image
53  *
54  * @author Marcel Verpaalen - Initial contribution
55  */
56 @NonNullByDefault
57 public class RRMapDraw {
58
59     private static final float MM = 50.0f;
60
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;
96
97     private final Logger logger = LoggerFactory.getLogger(RRMapDraw.class);
98
99     public RRMapDraw(RRMapFileParser rmfp) {
100         this.rmfp = rmfp;
101     }
102
103     public int getWidth() {
104         return rmfp.getImgWidth();
105     }
106
107     public int getHeight() {
108         return rmfp.getImgHeight();
109     }
110
111     public RRMapFileParser getMapParseDetails() {
112         return this.rmfp;
113     }
114
115     /**
116      * load Gzipped RR inputstream
117      *
118      * @throws IOException
119      */
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);
124     }
125
126     /**
127      * load Gzipped RR file
128      *
129      * @throws IOException
130      */
131     public static RRMapDraw loadImage(File file) throws IOException {
132         return loadImage(new FileInputStream(file));
133     }
134
135     /**
136      * draws the map from the individual pixels
137      */
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) {
146                     case MAP_OUTSIDE:
147                         g2d.setColor(COLOR_MAP_OUTSIDE);
148                         break;
149                     case MAP_WALL:
150                         g2d.setColor(COLOR_MAP_WALL);
151                         break;
152                     case MAP_INSIDE:
153                         g2d.setColor(COLOR_MAP_INSIDE);
154                         break;
155                     case MAP_SCAN:
156                         g2d.setColor(COLOR_SCAN);
157                         break;
158                     default:
159                         int obstacle = (walltype & 0x07);
160                         int mapId = (walltype & 0xFF) >>> 3;
161                         switch (obstacle) {
162                             case 0:
163                                 g2d.setColor(COLOR_GREY_WALL);
164                                 break;
165                             case 1:
166                                 g2d.setColor(Color.BLACK);
167                                 break;
168                             case 7:
169                                 g2d.setColor(ROOM_COLORS[mapId % 15]);
170                                 roomIds.add(mapId);
171                                 multicolor = true;
172                                 break;
173                             default:
174                                 g2d.setColor(Color.WHITE);
175                                 break;
176                         }
177                 }
178                 float xPos = scale * (rmfp.getImgWidth() - x);
179                 float yP = scale * y;
180                 g2d.draw(new Line2D.Float(xPos, yP, xPos, yP));
181             }
182         }
183         if (logger.isDebugEnabled() && roomIds.size() > 0) {
184             StringBuilder sb = new StringBuilder();
185             for (Integer r : roomIds) {
186                 sb.append(" " + r.toString());
187             }
188             logger.debug("Identified rooms in map:{}", sb.toString());
189         }
190     }
191
192     /**
193      * draws the vacuum path
194      *
195      * @param scale
196      */
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();
202             switch (pathType) {
203                 case RRMapFileParser.PATH:
204                     if (!multicolor) {
205                         g2d.setColor(COLOR_PATH);
206                     } else {
207                         g2d.setColor(Color.WHITE);
208                     }
209                     break;
210                 case RRMapFileParser.GOTO_PATH:
211                     g2d.setColor(Color.GREEN);
212                     break;
213                 case RRMapFileParser.GOTO_PREDICTED_PATH:
214                     g2d.setColor(Color.YELLOW);
215                     break;
216                 default:
217                     g2d.setColor(Color.CYAN);
218             }
219             float prvX = 0;
220             float prvY = 0;
221             for (float[] point : path.getValue()) {
222                 float x = toXCoord(point[0]) * scale;
223                 float y = toYCoord(point[1]) * scale;
224                 if (prvX > 1) {
225                     g2d.draw(new Line2D.Float(prvX, prvY, x, y));
226                 }
227                 prvX = x;
228                 prvY = y;
229             }
230         }
231     }
232
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));
245         }
246     }
247
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();
260                 noGo.moveTo(x, y);
261                 noGo.lineTo(x1, y1);
262                 noGo.lineTo(x2, y2);
263                 noGo.lineTo(x3, y3);
264                 noGo.lineTo(x, y);
265                 g2d.setColor(COLOR_NO_GO_ZONES);
266                 g2d.fill(noGo);
267                 g2d.setColor(area.getKey() == 9 ? Color.RED : Color.WHITE);
268                 g2d.draw(noGo);
269             }
270         }
271         ;
272     }
273
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));
284         }
285     }
286
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);
296         radius = 3 * scale;
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);
301         if (scale > 1.5) {
302             drawCenteredImg(g2d, scale / 15, "robo.png", roboX, roboY);
303         }
304     }
305
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);
311
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);
318                 if (scale > 1.0) {
319                     drawCenteredImg(g2d, scale / 3, "obstacle-" + obstacle[2] + ".png", obstacleX, obstacleY + 15);
320                 }
321             });
322         }
323     }
324
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);
327         if (fill) {
328             g2d.fill(circle);
329         } else {
330             g2d.draw(circle);
331         }
332     }
333
334     private void drawCenteredImg(Graphics2D g2d, float scale, String imgFile, float x, float y) {
335         URL image = getImageUrl(imgFile);
336         try {
337             if (image != null) {
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);
345             } else {
346                 logger.debug("Error loading image {}: File not be found.", imgFile);
347             }
348         } catch (IOException e) {
349             logger.debug("Error loading image {}: {}", image, e.getMessage());
350         }
351     }
352
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));
362         }
363     }
364
365     private void drawOpenHabRocks(Graphics2D g2d, int width, int height, float scale) {
366         // easter egg gift
367         int offset = 5;
368         int textPos = 55;
369         URL image = getImageUrl("ohlogo.png");
370         try {
371             if (image != null) {
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));
379             } else {
380                 logger.debug("Error loading image ohlogo.png: File not be found.");
381             }
382         } catch (IOException e) {
383             logger.debug("Error loading image ohlogo.png:: {}", e.getMessage());
384         }
385         String fontName = getAvailableFont("Helvetica,Arial,Roboto,Verdana,Times,Serif,Dialog".split(","));
386         if (fontName == null) {
387             return; // no available fonts to draw text
388         }
389         Font font = new Font(fontName, Font.BOLD, 14);
390         g2d.setFont(font);
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));
397             g2d.setFont(font);
398         }
399         int stringHeight = fontMetrics.getAscent();
400         g2d.setPaint(Color.white);
401         g2d.drawString(message, textPos, height - offset * scale - stringHeight / 2);
402     }
403
404     private @Nullable String getAvailableFont(String[] preferedFonts) {
405         final GraphicsEnvironment gEv = GraphicsEnvironment.getLocalGraphicsEnvironment();
406         if (gEv == null) {
407             return null;
408         }
409         String[] fonts = gEv.getAvailableFontFamilyNames();
410         if (fonts.length == 0) {
411             return null;
412         }
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];
417                 }
418             }
419         }
420         // Preferred fonts not available... just go with the first one
421         return fonts[0];
422     }
423
424     private @Nullable URL getImageUrl(String image) {
425         final Bundle bundle = this.bundle;
426         if (bundle != null) {
427             return bundle.getEntry("images/" + image);
428         }
429         try {
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());
435             return null;
436         }
437     }
438
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);
447         drawMap(g2d, scale);
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);
457         return bi;
458     }
459
460     public boolean writePic(String filename, String formatName, float scale) throws IOException {
461         return ImageIO.write(getImage(scale), formatName, new File(filename));
462     }
463
464     private float toXCoord(float x) {
465         return rmfp.getImgWidth() + rmfp.getLeft() - (x / MM);
466     }
467
468     private float toYCoord(float y) {
469         return y / MM - rmfp.getTop();
470     }
471
472     @Override
473     public String toString() {
474         return rmfp.toString();
475     }
476 }