]> git.basschouten.com Git - openhab-addons.git/blob
4118650abd5031b1afb8dd5ce6afc639df04e5de
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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
66     private final @Nullable Bundle bundle = FrameworkUtil.getBundle(getClass());
67     private final RRMapFileParser rmfp;
68     private final Logger logger = LoggerFactory.getLogger(RRMapDraw.class);
69
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;
76
77     public RRMapDraw(RRMapFileParser rmfp) {
78         this.rmfp = rmfp;
79     }
80
81     public int getWidth() {
82         return rmfp.getImgWidth();
83     }
84
85     public int getHeight() {
86         return rmfp.getImgHeight();
87     }
88
89     public void setDrawOptions(RRMapDrawOptions options) {
90         this.drawOptions = options;
91     }
92
93     public RRMapDrawOptions getDrawOptions() {
94         return drawOptions;
95     }
96
97     public RRMapFileParser getMapParseDetails() {
98         return this.rmfp;
99     }
100
101     /**
102      * load Gzipped RR inputstream
103      *
104      * @throws IOException
105      */
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);
110     }
111
112     /**
113      * load Gzipped RR file
114      *
115      * @throws IOException
116      */
117     public static RRMapDraw loadImage(File file) throws IOException {
118         return loadImage(new FileInputStream(file));
119     }
120
121     /**
122      * draws the map from the individual pixels
123      */
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) {
132                     case MAP_OUTSIDE:
133                         g2d.setColor(drawOptions.getColorMapOutside());
134                         break;
135                     case MAP_WALL:
136                         g2d.setColor(drawOptions.getColorMapWall());
137                         break;
138                     case MAP_INSIDE:
139                         g2d.setColor(drawOptions.getColorMapInside());
140                         break;
141                     case MAP_SCAN:
142                         g2d.setColor(drawOptions.getColorScan());
143                         break;
144                     default:
145                         int obstacle = (walltype & 0x07);
146                         int mapId = (walltype & 0xFF) >>> 3;
147                         switch (obstacle) {
148                             case 0:
149                                 g2d.setColor(drawOptions.getColorGreyWall());
150                                 break;
151                             case 1:
152                                 g2d.setColor(Color.BLACK);
153                                 break;
154                             case 7:
155                                 g2d.setColor(drawOptions.getRoomColors()[mapId % 15]);
156                                 roomIds.add(mapId);
157                                 multicolor = true;
158                                 break;
159                             default:
160                                 g2d.setColor(Color.WHITE);
161                                 break;
162                         }
163                 }
164                 float xPos = scale * (rmfp.getImgWidth() - x);
165                 float yP = scale * y;
166                 g2d.draw(new Line2D.Float(xPos, yP, xPos, yP));
167             }
168         }
169         if (logger.isDebugEnabled() && !roomIds.isEmpty()) {
170             StringBuilder sb = new StringBuilder();
171             for (Integer r : roomIds) {
172                 sb.append(" " + r.toString());
173             }
174             logger.debug("Identified rooms in map:{}", sb.toString());
175         }
176     }
177
178     /**
179      * draws the carpet map
180      */
181     private void drawCarpetMap(Graphics2D g2d, float scale) {
182         if (rmfp.getCarpetMap().length == 0) {
183             return;
184         }
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) {
191                     case 0:
192                         // ignore
193                         break;
194                     default:
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));
199                         break;
200                 }
201             }
202         }
203     }
204
205     /**
206      * draws the vacuum path
207      *
208      * @param scale
209      */
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();
215             switch (pathType) {
216                 case RRMapFileParser.PATH:
217                     if (!multicolor) {
218                         g2d.setColor(drawOptions.getColorPath());
219                     } else {
220                         g2d.setColor(Color.WHITE);
221                     }
222                     break;
223                 case RRMapFileParser.GOTO_PATH:
224                     g2d.setColor(Color.GREEN);
225                     break;
226                 case RRMapFileParser.GOTO_PREDICTED_PATH:
227                     g2d.setColor(Color.YELLOW);
228                     break;
229                 default:
230                     g2d.setColor(Color.CYAN);
231             }
232             float prvX = 0;
233             float prvY = 0;
234             for (float[] point : path.getValue()) {
235                 float x = toXCoord(point[0]) * scale;
236                 float y = toYCoord(point[1]) * scale;
237                 if (prvX > 1) {
238                     g2d.draw(new Line2D.Float(prvX, prvY, x, y));
239                 }
240                 prvX = x;
241                 prvY = y;
242             }
243         }
244     }
245
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));
258         }
259     }
260
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();
273                 noGo.moveTo(x, y);
274                 noGo.lineTo(x1, y1);
275                 noGo.lineTo(x2, y2);
276                 noGo.lineTo(x3, y3);
277                 noGo.lineTo(x, y);
278                 g2d.setColor(drawOptions.getColorNoGoZones());
279                 g2d.fill(noGo);
280                 g2d.setColor(area.getKey() == 9 ? Color.RED : Color.WHITE);
281                 g2d.draw(noGo);
282             }
283         }
284     }
285
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));
296         }
297     }
298
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);
308         radius = 3 * scale;
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);
313         if (scale > 1.5) {
314             drawCenteredImg(g2d, scale / 15, "robo.png", roboX, roboY);
315         }
316     }
317
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);
323
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);
330                 if (scale > 1.0) {
331                     drawCenteredImg(g2d, scale / 3, "obstacle-" + obstacle[2] + ".png", obstacleX, obstacleY + 15);
332                 }
333             });
334         }
335     }
336
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);
339         if (fill) {
340             g2d.fill(circle);
341         } else {
342             g2d.draw(circle);
343         }
344     }
345
346     private void drawCenteredImg(Graphics2D g2d, float scale, String imgFile, float x, float y) {
347         URL image = getImageUrl(imgFile);
348         try {
349             if (image != null) {
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);
357             } else {
358                 logger.debug("Error loading image {}: File not be found.", imgFile);
359             }
360         } catch (IOException e) {
361             logger.debug("Error loading image {}: {}", image, e.getMessage());
362         }
363     }
364
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));
374         }
375     }
376
377     private void drawOpenHabRocks(Graphics2D g2d, int width, int height, float scale) {
378         // easter egg gift
379         int offset = 5;
380         int textPos = 55;
381         URL image = getImageUrl("ohlogo.png");
382         try {
383             if (image != null) {
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));
391             } else {
392                 logger.debug("Error loading image ohlogo.png: File not be found.");
393             }
394         } catch (IOException e) {
395             logger.debug("Error loading image ohlogo.png:: {}", e.getMessage());
396         }
397         if (drawOptions.getText().isBlank()) {
398             return;
399         }
400         String fontName = getAvailableFont("Helvetica,Arial,Roboto,Verdana,Times,Serif,Dialog".split(","));
401         if (fontName == null) {
402             return; // no available fonts to draw text
403         }
404         int fz = (int) (drawOptions.getTextFontSize() * scale);
405         Font font = new Font(fontName, Font.BOLD, fz);
406         g2d.setFont(font);
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);
413             g2d.setFont(font);
414         }
415         int stringHeight = fontMetrics.getAscent();
416         g2d.setPaint(Color.white);
417         g2d.drawString(message, textPos, height - offset * scale - stringHeight / 2);
418     }
419
420     private @Nullable String getAvailableFont(String[] preferedFonts) {
421         final GraphicsEnvironment gEv = GraphicsEnvironment.getLocalGraphicsEnvironment();
422         if (gEv == null) {
423             return null;
424         }
425         String[] fonts = gEv.getAvailableFontFamilyNames();
426         if (fonts.length == 0) {
427             return null;
428         }
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];
433                 }
434             }
435         }
436         // Preferred fonts not available... just go with the first one
437         return fonts[0];
438     }
439
440     /**
441      * Finds the perimeter of the used area in the map
442      */
443     private void getMapArea(Graphics2D g2d, float scale) {
444         int firstX = rmfp.getImgWidth();
445         int lastX = 0;
446         int firstY = rmfp.getImgHeight();
447         int lastY = 0;
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) {
452                     if (y < firstY) {
453                         firstY = y;
454                     }
455                     if (y > lastY) {
456                         lastY = y;
457                     }
458                     if (x < firstX) {
459                         firstX = x;
460                     }
461                     if (x > lastX) {
462                         lastX = x;
463                     }
464                 }
465             }
466         }
467         this.firstX = firstX;
468         this.lastX = lastX;
469         this.firstY = rmfp.getImgHeight() - lastY;
470         this.lastY = rmfp.getImgHeight() - firstY;
471     }
472
473     private @Nullable URL getImageUrl(String image) {
474         final Bundle bundle = this.bundle;
475         if (bundle != null) {
476             return bundle.getEntry("images/" + image);
477         }
478         try {
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());
484             return null;
485         }
486     }
487
488     public BufferedImage getImage() {
489         return getImage(drawOptions.getScale());
490     }
491
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);
500         drawMap(g2d, scale);
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);
513             }
514             return bi;
515         }
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);
535         }
536         return bo;
537     }
538
539     public boolean writePic(String filename, String formatName, float scale) throws IOException {
540         return ImageIO.write(getImage(scale), formatName, new File(filename));
541     }
542
543     private float toXCoord(float x) {
544         return rmfp.getImgWidth() + rmfp.getLeft() - (x / MM);
545     }
546
547     private float toYCoord(float y) {
548         return y / MM - rmfp.getTop();
549     }
550
551     @Override
552     public String toString() {
553         return rmfp.toString();
554     }
555 }