]> git.basschouten.com Git - openhab-addons.git/blob
183e499f052eaaebdd32f5d8263a8b7a193efaad
[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_CARPET = new Color(0xDF, 0xDF, 0xDF, 0xA0);
69     private static final Color COLOR_GREY_WALL = new Color(93, 109, 126);
70     private static final Color COLOR_PATH = new Color(147, 194, 238);
71     private static final Color COLOR_ZONES = new Color(0xAD, 0xD8, 0xFF, 0x8F);
72     private static final Color COLOR_NO_GO_ZONES = new Color(255, 33, 55, 127);
73     private static final Color COLOR_CHARGER_HALO = new Color(0x66, 0xfe, 0xda, 0x7f);
74     private static final Color COLOR_ROBO = new Color(75, 235, 149);
75     private static final Color COLOR_SCAN = new Color(0xDF, 0xDF, 0xDF);
76     private static final Color ROOM1 = new Color(240, 178, 122);
77     private static final Color ROOM2 = new Color(133, 193, 233);
78     private static final Color ROOM3 = new Color(217, 136, 128);
79     private static final Color ROOM4 = new Color(52, 152, 219);
80     private static final Color ROOM5 = new Color(205, 97, 85);
81     private static final Color ROOM6 = new Color(243, 156, 18);
82     private static final Color ROOM7 = new Color(88, 214, 141);
83     private static final Color ROOM8 = new Color(245, 176, 65);
84     private static final Color ROOM9 = new Color(0xFc, 0xD4, 0x51);
85     private static final Color ROOM10 = new Color(72, 201, 176);
86     private static final Color ROOM11 = new Color(84, 153, 199);
87     private static final Color ROOM12 = new Color(255, 213, 209);
88     private static final Color ROOM13 = new Color(228, 228, 215);
89     private static final Color ROOM14 = new Color(82, 190, 128);
90     private static final Color ROOM15 = new Color(72, 201, 176);
91     private static final Color ROOM16 = new Color(165, 105, 189);
92     private static final Color[] ROOM_COLORS = { ROOM1, ROOM2, ROOM3, ROOM4, ROOM5, ROOM6, ROOM7, ROOM8, ROOM9, ROOM10,
93             ROOM11, ROOM12, ROOM13, ROOM14, ROOM15, ROOM16 };
94     private final @Nullable Bundle bundle = FrameworkUtil.getBundle(getClass());
95     private boolean multicolor = false;
96     private final RRMapFileParser rmfp;
97
98     private final Logger logger = LoggerFactory.getLogger(RRMapDraw.class);
99
100     public RRMapDraw(RRMapFileParser rmfp) {
101         this.rmfp = rmfp;
102     }
103
104     public int getWidth() {
105         return rmfp.getImgWidth();
106     }
107
108     public int getHeight() {
109         return rmfp.getImgHeight();
110     }
111
112     public RRMapFileParser getMapParseDetails() {
113         return this.rmfp;
114     }
115
116     /**
117      * load Gzipped RR inputstream
118      *
119      * @throws IOException
120      */
121     public static RRMapDraw loadImage(InputStream is) throws IOException {
122         byte[] inputdata = RRMapFileParser.readRRMapFile(is);
123         RRMapFileParser rf = new RRMapFileParser(inputdata);
124         return new RRMapDraw(rf);
125     }
126
127     /**
128      * load Gzipped RR file
129      *
130      * @throws IOException
131      */
132     public static RRMapDraw loadImage(File file) throws IOException {
133         return loadImage(new FileInputStream(file));
134     }
135
136     /**
137      * draws the map from the individual pixels
138      */
139     private void drawMap(Graphics2D g2d, float scale) {
140         Stroke stroke = new BasicStroke(1.1f * scale);
141         Set<Integer> roomIds = new HashSet<Integer>();
142         g2d.setStroke(stroke);
143         for (int y = 0; y < rmfp.getImgHeight() - 1; y++) {
144             for (int x = 0; x < rmfp.getImgWidth() + 1; x++) {
145                 byte walltype = rmfp.getImage()[x + rmfp.getImgWidth() * y];
146                 switch (walltype & 0xFF) {
147                     case MAP_OUTSIDE:
148                         g2d.setColor(COLOR_MAP_OUTSIDE);
149                         break;
150                     case MAP_WALL:
151                         g2d.setColor(COLOR_MAP_WALL);
152                         break;
153                     case MAP_INSIDE:
154                         g2d.setColor(COLOR_MAP_INSIDE);
155                         break;
156                     case MAP_SCAN:
157                         g2d.setColor(COLOR_SCAN);
158                         break;
159                     default:
160                         int obstacle = (walltype & 0x07);
161                         int mapId = (walltype & 0xFF) >>> 3;
162                         switch (obstacle) {
163                             case 0:
164                                 g2d.setColor(COLOR_GREY_WALL);
165                                 break;
166                             case 1:
167                                 g2d.setColor(Color.BLACK);
168                                 break;
169                             case 7:
170                                 g2d.setColor(ROOM_COLORS[mapId % 15]);
171                                 roomIds.add(mapId);
172                                 multicolor = true;
173                                 break;
174                             default:
175                                 g2d.setColor(Color.WHITE);
176                                 break;
177                         }
178                 }
179                 float xPos = scale * (rmfp.getImgWidth() - x);
180                 float yP = scale * y;
181                 g2d.draw(new Line2D.Float(xPos, yP, xPos, yP));
182             }
183         }
184         if (logger.isDebugEnabled() && !roomIds.isEmpty()) {
185             StringBuilder sb = new StringBuilder();
186             for (Integer r : roomIds) {
187                 sb.append(" " + r.toString());
188             }
189             logger.debug("Identified rooms in map:{}", sb.toString());
190         }
191     }
192
193     /**
194      * draws the carpet map
195      */
196     private void drawCarpetMap(Graphics2D g2d, float scale) {
197         if (rmfp.getCarpetMap().length == 0) {
198             return;
199         }
200         Stroke stroke = new BasicStroke(1.1f * scale);
201         g2d.setStroke(stroke);
202         for (int y = 0; y < rmfp.getImgHeight() - 1; y++) {
203             for (int x = 0; x < rmfp.getImgWidth() + 1; x++) {
204                 int carpetType = rmfp.getCarpetMap()[x + rmfp.getImgWidth() * y];
205                 switch (carpetType) {
206                     case 0:
207                         // ignore
208                         break;
209                     default:
210                         g2d.setColor(COLOR_CARPET);
211                         float xPos = scale * (rmfp.getImgWidth() - x);
212                         float yP = scale * y;
213                         g2d.draw(new Line2D.Float(xPos, yP, xPos, yP));
214                         break;
215                 }
216             }
217         }
218     }
219
220     /**
221      * draws the vacuum path
222      *
223      * @param scale
224      */
225     private void drawPath(Graphics2D g2d, float scale) {
226         Stroke stroke = new BasicStroke(0.5f * scale);
227         g2d.setStroke(stroke);
228         for (Entry<Integer, ArrayList<float[]>> path : rmfp.getPaths().entrySet()) {
229             Integer pathType = path.getKey();
230             switch (pathType) {
231                 case RRMapFileParser.PATH:
232                     if (!multicolor) {
233                         g2d.setColor(COLOR_PATH);
234                     } else {
235                         g2d.setColor(Color.WHITE);
236                     }
237                     break;
238                 case RRMapFileParser.GOTO_PATH:
239                     g2d.setColor(Color.GREEN);
240                     break;
241                 case RRMapFileParser.GOTO_PREDICTED_PATH:
242                     g2d.setColor(Color.YELLOW);
243                     break;
244                 default:
245                     g2d.setColor(Color.CYAN);
246             }
247             float prvX = 0;
248             float prvY = 0;
249             for (float[] point : path.getValue()) {
250                 float x = toXCoord(point[0]) * scale;
251                 float y = toYCoord(point[1]) * scale;
252                 if (prvX > 1) {
253                     g2d.draw(new Line2D.Float(prvX, prvY, x, y));
254                 }
255                 prvX = x;
256                 prvY = y;
257             }
258         }
259     }
260
261     private void drawZones(Graphics2D g2d, float scale) {
262         for (float[] point : rmfp.getZones()) {
263             float x = toXCoord(point[0]) * scale;
264             float y = toYCoord(point[1]) * scale;
265             float x1 = toXCoord(point[2]) * scale;
266             float y1 = toYCoord(point[3]) * scale;
267             float sx = Math.min(x, x1);
268             float w = Math.max(x, x1) - sx;
269             float sy = Math.min(y, y1);
270             float h = Math.max(y, y1) - sy;
271             g2d.setColor(COLOR_ZONES);
272             g2d.fill(new Rectangle2D.Float(sx, sy, w, h));
273         }
274     }
275
276     private void drawNoGo(Graphics2D g2d, float scale) {
277         for (Map.Entry<Integer, ArrayList<float[]>> area : rmfp.getAreas().entrySet()) {
278             for (float[] point : area.getValue()) {
279                 float x = toXCoord(point[0]) * scale;
280                 float y = toYCoord(point[1]) * scale;
281                 float x1 = toXCoord(point[2]) * scale;
282                 float y1 = toYCoord(point[3]) * scale;
283                 float x2 = toXCoord(point[4]) * scale;
284                 float y2 = toYCoord(point[5]) * scale;
285                 float x3 = toXCoord(point[6]) * scale;
286                 float y3 = toYCoord(point[7]) * scale;
287                 Path2D noGo = new Path2D.Float();
288                 noGo.moveTo(x, y);
289                 noGo.lineTo(x1, y1);
290                 noGo.lineTo(x2, y2);
291                 noGo.lineTo(x3, y3);
292                 noGo.lineTo(x, y);
293                 g2d.setColor(COLOR_NO_GO_ZONES);
294                 g2d.fill(noGo);
295                 g2d.setColor(area.getKey() == 9 ? Color.RED : Color.WHITE);
296                 g2d.draw(noGo);
297             }
298         }
299         ;
300     }
301
302     private void drawWalls(Graphics2D g2d, float scale) {
303         Stroke stroke = new BasicStroke(3 * scale);
304         g2d.setStroke(stroke);
305         for (float[] point : rmfp.getWalls()) {
306             float x = toXCoord(point[0]) * scale;
307             float y = toYCoord(point[1]) * scale;
308             float x1 = toXCoord(point[2]) * scale;
309             float y1 = toYCoord(point[3]) * scale;
310             g2d.setColor(Color.RED);
311             g2d.draw(new Line2D.Float(x, y, x1, y1));
312         }
313     }
314
315     private void drawRobo(Graphics2D g2d, float scale) {
316         float radius = 3 * scale;
317         Stroke stroke = new BasicStroke(2 * scale);
318         g2d.setStroke(stroke);
319         g2d.setColor(COLOR_CHARGER_HALO);
320         final float chargerX = toXCoord(rmfp.getChargerX()) * scale;
321         final float chargerY = toYCoord(rmfp.getChargerY()) * scale;
322         drawCircle(g2d, chargerX, chargerY, radius, false);
323         drawCenteredImg(g2d, scale / 8, "charger.png", chargerX, chargerY);
324         radius = 3 * scale;
325         g2d.setColor(COLOR_ROBO);
326         final float roboX = toXCoord(rmfp.getRoboX()) * scale;
327         final float roboY = toYCoord(rmfp.getRoboY()) * scale;
328         drawCircle(g2d, roboX, roboY, radius, false);
329         if (scale > 1.5) {
330             drawCenteredImg(g2d, scale / 15, "robo.png", roboX, roboY);
331         }
332     }
333
334     private void drawObstacles(Graphics2D g2d, float scale) {
335         float radius = 2 * scale;
336         Stroke stroke = new BasicStroke(3 * scale);
337         g2d.setStroke(stroke);
338         g2d.setColor(Color.MAGENTA);
339
340         Map<Integer, ArrayList<int[]>> obstacleMap = rmfp.getObstacles();
341         for (ArrayList<int[]> obstacles : obstacleMap.values()) {
342             obstacles.forEach(obstacle -> {
343                 final float obstacleX = toXCoord(obstacle[0]) * scale;
344                 final float obstacleY = toYCoord(obstacle[1]) * scale;
345                 drawCircle(g2d, obstacleX, obstacleY, radius, true);
346                 if (scale > 1.0) {
347                     drawCenteredImg(g2d, scale / 3, "obstacle-" + obstacle[2] + ".png", obstacleX, obstacleY + 15);
348                 }
349             });
350         }
351     }
352
353     private void drawCircle(Graphics2D g2d, float x, float y, float radius, boolean fill) {
354         Ellipse2D.Double circle = new Ellipse2D.Double(x - radius, y - radius, 2.0 * radius, 2.0 * radius);
355         if (fill) {
356             g2d.fill(circle);
357         } else {
358             g2d.draw(circle);
359         }
360     }
361
362     private void drawCenteredImg(Graphics2D g2d, float scale, String imgFile, float x, float y) {
363         URL image = getImageUrl(imgFile);
364         try {
365             if (image != null) {
366                 BufferedImage addImg = ImageIO.read(image);
367                 int xpos = Math.round(x + (addImg.getWidth() / 2 * scale));
368                 int ypos = Math.round(y + (addImg.getHeight() / 2 * scale));
369                 AffineTransform at = new AffineTransform();
370                 at.scale(-scale, -scale);
371                 AffineTransformOp scaleOp = new AffineTransformOp(at, AffineTransformOp.TYPE_BILINEAR);
372                 g2d.drawImage(addImg, scaleOp, xpos, ypos);
373             } else {
374                 logger.debug("Error loading image {}: File not be found.", imgFile);
375             }
376         } catch (IOException e) {
377             logger.debug("Error loading image {}: {}", image, e.getMessage());
378         }
379     }
380
381     private void drawGoTo(Graphics2D g2d, float scale) {
382         float x = toXCoord(rmfp.getGotoX()) * scale;
383         float y = toYCoord(rmfp.getGotoY()) * scale;
384         if (!(x == 0 && y == 0)) {
385             g2d.setStroke(new BasicStroke());
386             g2d.setColor(Color.YELLOW);
387             int x3[] = { (int) x, (int) (x - 2 * scale), (int) (x + 2 * scale) };
388             int y3[] = { (int) y, (int) (y - 5 * scale), (int) (y - 5 * scale) };
389             g2d.fill(new Polygon(x3, y3, 3));
390         }
391     }
392
393     private void drawOpenHabRocks(Graphics2D g2d, int width, int height, float scale) {
394         // easter egg gift
395         int offset = 5;
396         int textPos = 55;
397         URL image = getImageUrl("ohlogo.png");
398         try {
399             if (image != null) {
400                 BufferedImage ohLogo = ImageIO.read(image);
401                 textPos = (int) (ohLogo.getWidth() * scale / 2 + offset * scale);
402                 AffineTransform at = new AffineTransform();
403                 at.scale(scale / 2, scale / 2);
404                 AffineTransformOp scaleOp = new AffineTransformOp(at, AffineTransformOp.TYPE_BILINEAR);
405                 g2d.drawImage(ohLogo, scaleOp, offset,
406                         height - (int) (ohLogo.getHeight() * scale / 2) - (int) (offset * scale));
407             } else {
408                 logger.debug("Error loading image ohlogo.png: File not be found.");
409             }
410         } catch (IOException e) {
411             logger.debug("Error loading image ohlogo.png:: {}", e.getMessage());
412         }
413         String fontName = getAvailableFont("Helvetica,Arial,Roboto,Verdana,Times,Serif,Dialog".split(","));
414         if (fontName == null) {
415             return; // no available fonts to draw text
416         }
417         Font font = new Font(fontName, Font.BOLD, 14);
418         g2d.setFont(font);
419         String message = "Openhab rocks your Xiaomi vacuum!";
420         FontMetrics fontMetrics = g2d.getFontMetrics();
421         int stringWidth = fontMetrics.stringWidth(message);
422         if ((stringWidth + textPos) > rmfp.getImgWidth() * scale) {
423             font = new Font(fontName, Font.BOLD,
424                     (int) Math.floor(14 * (rmfp.getImgWidth() * scale - textPos - offset * scale) / stringWidth));
425             g2d.setFont(font);
426         }
427         int stringHeight = fontMetrics.getAscent();
428         g2d.setPaint(Color.white);
429         g2d.drawString(message, textPos, height - offset * scale - stringHeight / 2);
430     }
431
432     private @Nullable String getAvailableFont(String[] preferedFonts) {
433         final GraphicsEnvironment gEv = GraphicsEnvironment.getLocalGraphicsEnvironment();
434         if (gEv == null) {
435             return null;
436         }
437         String[] fonts = gEv.getAvailableFontFamilyNames();
438         if (fonts.length == 0) {
439             return null;
440         }
441         for (int j = 0; j < preferedFonts.length; j++) {
442             for (int i = 0; i < fonts.length; i++) {
443                 if (fonts[i].equalsIgnoreCase(preferedFonts[j])) {
444                     return preferedFonts[j];
445                 }
446             }
447         }
448         // Preferred fonts not available... just go with the first one
449         return fonts[0];
450     }
451
452     private @Nullable URL getImageUrl(String image) {
453         final Bundle bundle = this.bundle;
454         if (bundle != null) {
455             return bundle.getEntry("images/" + image);
456         }
457         try {
458             File fn = new File("src" + File.separator + "main" + File.separator + "resources" + File.separator
459                     + "images" + File.separator + image);
460             return fn.toURI().toURL();
461         } catch (MalformedURLException | SecurityException e) {
462             logger.debug("Could create URL for {}: {}", image, e.getMessage());
463             return null;
464         }
465     }
466
467     public BufferedImage getImage(float scale) {
468         int width = (int) Math.floor(rmfp.getImgWidth() * scale);
469         int height = (int) Math.floor(rmfp.getImgHeight() * scale);
470         BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
471         Graphics2D g2d = bi.createGraphics();
472         AffineTransform tx = AffineTransform.getScaleInstance(-1, -1);
473         tx.translate(-width, -height);
474         g2d.setTransform(tx);
475         drawMap(g2d, scale);
476         drawCarpetMap(g2d, scale);
477         drawZones(g2d, scale);
478         drawNoGo(g2d, scale);
479         drawWalls(g2d, scale);
480         drawPath(g2d, scale);
481         drawRobo(g2d, scale);
482         drawGoTo(g2d, scale);
483         drawObstacles(g2d, scale);
484         g2d = bi.createGraphics();
485         drawOpenHabRocks(g2d, width, height, scale);
486         return bi;
487     }
488
489     public boolean writePic(String filename, String formatName, float scale) throws IOException {
490         return ImageIO.write(getImage(scale), formatName, new File(filename));
491     }
492
493     private float toXCoord(float x) {
494         return rmfp.getImgWidth() + rmfp.getLeft() - (x / MM);
495     }
496
497     private float toYCoord(float y) {
498         return y / MM - rmfp.getTop();
499     }
500
501     @Override
502     public String toString() {
503         return rmfp.toString();
504     }
505 }