]> git.basschouten.com Git - openhab-addons.git/blob
851215bc123b2dec365f967e4b8a7df0e1f64f3a
[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
37 import javax.imageio.ImageIO;
38
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;
45
46 /**
47  * Draws the vacuum map file to an image
48  *
49  * @author Marcel Verpaalen - Initial contribution
50  */
51 @NonNullByDefault
52 public class RRMapDraw {
53
54     private static final float MM = 50.0f;
55
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;
91
92     private final Logger logger = LoggerFactory.getLogger(RRMapDraw.class);
93
94     public RRMapDraw(RRMapFileParser rmfp) {
95         this.rmfp = rmfp;
96     }
97
98     public int getWidth() {
99         return rmfp.getImgWidth();
100     }
101
102     public int getHeight() {
103         return rmfp.getImgHeight();
104     }
105
106     public RRMapFileParser getMapParseDetails() {
107         return this.rmfp;
108     }
109
110     /**
111      * load Gzipped RR inputstream
112      *
113      * @throws IOException
114      */
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);
119     }
120
121     /**
122      * load Gzipped RR file
123      *
124      * @throws IOException
125      */
126     public static RRMapDraw loadImage(File file) throws IOException {
127         return loadImage(new FileInputStream(file));
128     }
129
130     /**
131      * draws the map from the individual pixels
132      */
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) {
140                     case MAP_OUTSIDE:
141                         g2d.setColor(COLOR_MAP_OUTSIDE);
142                         break;
143                     case MAP_WALL:
144                         g2d.setColor(COLOR_MAP_WALL);
145                         break;
146                     case MAP_INSIDE:
147                         g2d.setColor(COLOR_MAP_INSIDE);
148                         break;
149                     case MAP_SCAN:
150                         g2d.setColor(COLOR_SCAN);
151                         break;
152                     default:
153                         int obstacle = (walltype & 0x07);
154                         int mapId = (walltype & 0xFF) >>> 3;
155                         switch (obstacle) {
156                             case 0:
157                                 g2d.setColor(COLOR_GREY_WALL);
158                                 break;
159                             case 1:
160                                 g2d.setColor(Color.BLACK);
161                                 break;
162                             case 7:
163                                 g2d.setColor(ROOM_COLORS[Math.round(mapId / 2)]);
164                                 multicolor = true;
165                                 break;
166                             default:
167                                 g2d.setColor(Color.WHITE);
168                                 break;
169                         }
170                 }
171                 float xPos = scale * (rmfp.getImgWidth() - x);
172                 float yP = scale * y;
173                 g2d.draw(new Line2D.Float(xPos, yP, xPos, yP));
174             }
175         }
176     }
177
178     /**
179      * draws the vacuum path
180      *
181      * @param scale
182      */
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()) {
187             switch (pathType) {
188                 case RRMapFileParser.PATH:
189                     if (!multicolor) {
190                         g2d.setColor(COLOR_PATH);
191                     } else {
192                         g2d.setColor(Color.WHITE);
193                     }
194                     break;
195                 case RRMapFileParser.GOTO_PATH:
196                     g2d.setColor(Color.GREEN);
197                     break;
198                 case RRMapFileParser.GOTO_PREDICTED_PATH:
199                     g2d.setColor(Color.YELLOW);
200                     break;
201                 default:
202                     g2d.setColor(Color.CYAN);
203             }
204             float prvX = 0;
205             float prvY = 0;
206             for (float[] point : rmfp.getPaths().get(pathType)) {
207                 float x = toXCoord(point[0]) * scale;
208                 float y = toYCoord(point[1]) * scale;
209                 if (prvX > 1) {
210                     g2d.draw(new Line2D.Float(prvX, prvY, x, y));
211                 }
212                 prvX = x;
213                 prvY = y;
214             }
215         }
216     }
217
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));
230         }
231     }
232
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();
245                 noGo.moveTo(x, y);
246                 noGo.lineTo(x1, y1);
247                 noGo.lineTo(x2, y2);
248                 noGo.lineTo(x3, y3);
249                 noGo.lineTo(x, y);
250                 g2d.setColor(COLOR_NO_GO_ZONES);
251                 g2d.fill(noGo);
252                 g2d.setColor(area == 9 ? Color.RED : Color.WHITE);
253                 g2d.draw(noGo);
254             }
255         }
256     }
257
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));
268         }
269     }
270
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);
280         radius = 3 * scale;
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);
285         if (scale > 1.5) {
286             drawCenteredImg(g2d, scale / 15, "robo.png", roboX, roboY);
287         }
288     }
289
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));
292     }
293
294     private void drawCenteredImg(Graphics2D g2d, float scale, String imgFile, float x, float y) {
295         URL image = getImageUrl(imgFile);
296         try {
297             if (image != null) {
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);
305             } else {
306                 logger.debug("Error loading image {}: File not be found.", imgFile);
307             }
308         } catch (IOException e) {
309             logger.debug("Error loading image {}: {}", image, e.getMessage());
310         }
311     }
312
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));
322         }
323     }
324
325     private void drawOpenHabRocks(Graphics2D g2d, int width, int height, float scale) {
326         // easter egg gift
327         int offset = 5;
328         int textPos = 55;
329         URL image = getImageUrl("ohlogo.png");
330         try {
331             if (image != null) {
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));
339             } else {
340                 logger.debug("Error loading image ohlogo.png: File not be found.");
341             }
342         } catch (IOException e) {
343             logger.debug("Error loading image ohlogo.png:: {}", e.getMessage());
344         }
345         String fontName = getAvailableFont("Helvetica,Arial,Roboto,Verdana,Times,Serif,Dialog".split(","));
346         if (fontName == null) {
347             return; // no available fonts to draw text
348         }
349         Font font = new Font(fontName, Font.BOLD, 14);
350         g2d.setFont(font);
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));
357             g2d.setFont(font);
358         }
359         int stringHeight = fontMetrics.getAscent();
360         g2d.setPaint(Color.white);
361         g2d.drawString(message, textPos, height - offset * scale - stringHeight / 2);
362     }
363
364     private @Nullable String getAvailableFont(String[] preferedFonts) {
365         final GraphicsEnvironment gEv = GraphicsEnvironment.getLocalGraphicsEnvironment();
366         if (gEv == null) {
367             return null;
368         }
369         String[] fonts = gEv.getAvailableFontFamilyNames();
370         if (fonts.length == 0) {
371             return null;
372         }
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];
377                 }
378             }
379         }
380         // Preferred fonts not available... just go with the first one
381         return fonts[0];
382     }
383
384     private @Nullable URL getImageUrl(String image) {
385         if (bundle != null) {
386             return bundle.getEntry("images/" + image);
387         }
388         try {
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());
394             return null;
395         }
396     }
397
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);
406         drawMap(g2d, scale);
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);
415         return bi;
416     }
417
418     public boolean writePic(String filename, String formatName, float scale) throws IOException {
419         return ImageIO.write(getImage(scale), formatName, new File(filename));
420     }
421
422     private float toXCoord(float x) {
423         return rmfp.getImgWidth() + rmfp.getLeft() - (x / MM);
424     }
425
426     private float toYCoord(float y) {
427         return y / MM - rmfp.getTop();
428     }
429
430     @Override
431     public String toString() {
432         return rmfp.toString();
433     }
434 }