]> git.basschouten.com Git - openhab-addons.git/blob
2ba7c20eb7f50b7449501dc192cf0cb1f0001fe4
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.Set;
40
41 import javax.imageio.ImageIO;
42
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;
49
50 /**
51  * Draws the vacuum map file to an image
52  *
53  * @author Marcel Verpaalen - Initial contribution
54  */
55 @NonNullByDefault
56 public class RRMapDraw {
57
58     private static final float MM = 50.0f;
59
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;
95
96     private final Logger logger = LoggerFactory.getLogger(RRMapDraw.class);
97
98     public RRMapDraw(RRMapFileParser rmfp) {
99         this.rmfp = rmfp;
100     }
101
102     public int getWidth() {
103         return rmfp.getImgWidth();
104     }
105
106     public int getHeight() {
107         return rmfp.getImgHeight();
108     }
109
110     public RRMapFileParser getMapParseDetails() {
111         return this.rmfp;
112     }
113
114     /**
115      * load Gzipped RR inputstream
116      *
117      * @throws IOException
118      */
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);
123     }
124
125     /**
126      * load Gzipped RR file
127      *
128      * @throws IOException
129      */
130     public static RRMapDraw loadImage(File file) throws IOException {
131         return loadImage(new FileInputStream(file));
132     }
133
134     /**
135      * draws the map from the individual pixels
136      */
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) {
145                     case MAP_OUTSIDE:
146                         g2d.setColor(COLOR_MAP_OUTSIDE);
147                         break;
148                     case MAP_WALL:
149                         g2d.setColor(COLOR_MAP_WALL);
150                         break;
151                     case MAP_INSIDE:
152                         g2d.setColor(COLOR_MAP_INSIDE);
153                         break;
154                     case MAP_SCAN:
155                         g2d.setColor(COLOR_SCAN);
156                         break;
157                     default:
158                         int obstacle = (walltype & 0x07);
159                         int mapId = (walltype & 0xFF) >>> 3;
160                         switch (obstacle) {
161                             case 0:
162                                 g2d.setColor(COLOR_GREY_WALL);
163                                 break;
164                             case 1:
165                                 g2d.setColor(Color.BLACK);
166                                 break;
167                             case 7:
168                                 g2d.setColor(ROOM_COLORS[mapId % 15]);
169                                 roomIds.add(mapId);
170                                 multicolor = true;
171                                 break;
172                             default:
173                                 g2d.setColor(Color.WHITE);
174                                 break;
175                         }
176                 }
177                 float xPos = scale * (rmfp.getImgWidth() - x);
178                 float yP = scale * y;
179                 g2d.draw(new Line2D.Float(xPos, yP, xPos, yP));
180             }
181         }
182         if (logger.isDebugEnabled() && roomIds.size() > 0) {
183             StringBuilder sb = new StringBuilder();
184             for (Integer r : roomIds) {
185                 sb.append(" " + r.toString());
186             }
187             logger.debug("Identified rooms in map:{}", sb.toString());
188         }
189     }
190
191     /**
192      * draws the vacuum path
193      *
194      * @param scale
195      */
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()) {
200             switch (pathType) {
201                 case RRMapFileParser.PATH:
202                     if (!multicolor) {
203                         g2d.setColor(COLOR_PATH);
204                     } else {
205                         g2d.setColor(Color.WHITE);
206                     }
207                     break;
208                 case RRMapFileParser.GOTO_PATH:
209                     g2d.setColor(Color.GREEN);
210                     break;
211                 case RRMapFileParser.GOTO_PREDICTED_PATH:
212                     g2d.setColor(Color.YELLOW);
213                     break;
214                 default:
215                     g2d.setColor(Color.CYAN);
216             }
217             float prvX = 0;
218             float prvY = 0;
219             for (float[] point : rmfp.getPaths().get(pathType)) {
220                 float x = toXCoord(point[0]) * scale;
221                 float y = toYCoord(point[1]) * scale;
222                 if (prvX > 1) {
223                     g2d.draw(new Line2D.Float(prvX, prvY, x, y));
224                 }
225                 prvX = x;
226                 prvY = y;
227             }
228         }
229     }
230
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));
243         }
244     }
245
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();
258                 noGo.moveTo(x, y);
259                 noGo.lineTo(x1, y1);
260                 noGo.lineTo(x2, y2);
261                 noGo.lineTo(x3, y3);
262                 noGo.lineTo(x, y);
263                 g2d.setColor(COLOR_NO_GO_ZONES);
264                 g2d.fill(noGo);
265                 g2d.setColor(area == 9 ? Color.RED : Color.WHITE);
266                 g2d.draw(noGo);
267             }
268         }
269     }
270
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));
281         }
282     }
283
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);
293         radius = 3 * scale;
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);
298         if (scale > 1.5) {
299             drawCenteredImg(g2d, scale / 15, "robo.png", roboX, roboY);
300         }
301     }
302
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);
308
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);
315                 if (scale > 1.0) {
316                     drawCenteredImg(g2d, scale / 3, "obstacle-" + obstacle[2] + ".png", obstacleX, obstacleY + 15);
317                 }
318             });
319         }
320     }
321
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);
324         if (fill) {
325             g2d.fill(circle);
326         } else {
327             g2d.draw(circle);
328         }
329     }
330
331     private void drawCenteredImg(Graphics2D g2d, float scale, String imgFile, float x, float y) {
332         URL image = getImageUrl(imgFile);
333         try {
334             if (image != null) {
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);
342             } else {
343                 logger.debug("Error loading image {}: File not be found.", imgFile);
344             }
345         } catch (IOException e) {
346             logger.debug("Error loading image {}: {}", image, e.getMessage());
347         }
348     }
349
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));
359         }
360     }
361
362     private void drawOpenHabRocks(Graphics2D g2d, int width, int height, float scale) {
363         // easter egg gift
364         int offset = 5;
365         int textPos = 55;
366         URL image = getImageUrl("ohlogo.png");
367         try {
368             if (image != null) {
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));
376             } else {
377                 logger.debug("Error loading image ohlogo.png: File not be found.");
378             }
379         } catch (IOException e) {
380             logger.debug("Error loading image ohlogo.png:: {}", e.getMessage());
381         }
382         String fontName = getAvailableFont("Helvetica,Arial,Roboto,Verdana,Times,Serif,Dialog".split(","));
383         if (fontName == null) {
384             return; // no available fonts to draw text
385         }
386         Font font = new Font(fontName, Font.BOLD, 14);
387         g2d.setFont(font);
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));
394             g2d.setFont(font);
395         }
396         int stringHeight = fontMetrics.getAscent();
397         g2d.setPaint(Color.white);
398         g2d.drawString(message, textPos, height - offset * scale - stringHeight / 2);
399     }
400
401     private @Nullable String getAvailableFont(String[] preferedFonts) {
402         final GraphicsEnvironment gEv = GraphicsEnvironment.getLocalGraphicsEnvironment();
403         if (gEv == null) {
404             return null;
405         }
406         String[] fonts = gEv.getAvailableFontFamilyNames();
407         if (fonts.length == 0) {
408             return null;
409         }
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];
414                 }
415             }
416         }
417         // Preferred fonts not available... just go with the first one
418         return fonts[0];
419     }
420
421     private @Nullable URL getImageUrl(String image) {
422         if (bundle != null) {
423             return bundle.getEntry("images/" + image);
424         }
425         try {
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());
431             return null;
432         }
433     }
434
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);
443         drawMap(g2d, scale);
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);
453         return bi;
454     }
455
456     public boolean writePic(String filename, String formatName, float scale) throws IOException {
457         return ImageIO.write(getImage(scale), formatName, new File(filename));
458     }
459
460     private float toXCoord(float x) {
461         return rmfp.getImgWidth() + rmfp.getLeft() - (x / MM);
462     }
463
464     private float toYCoord(float y) {
465         return y / MM - rmfp.getTop();
466     }
467
468     @Override
469     public String toString() {
470         return rmfp.toString();
471     }
472 }