]> git.basschouten.com Git - openhab-addons.git/blob
b44f937429dd489ff5f19c6c68f536f09b1e8038
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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
287     private void drawWalls(Graphics2D g2d, float scale) {
288         Stroke stroke = new BasicStroke(3 * scale);
289         g2d.setStroke(stroke);
290         for (float[] point : rmfp.getWalls()) {
291             float x = toXCoord(point[0]) * scale;
292             float y = toYCoord(point[1]) * scale;
293             float x1 = toXCoord(point[2]) * scale;
294             float y1 = toYCoord(point[3]) * scale;
295             g2d.setColor(Color.RED);
296             g2d.draw(new Line2D.Float(x, y, x1, y1));
297         }
298     }
299
300     private void drawRobo(Graphics2D g2d, float scale) {
301         float radius = 3 * scale;
302         Stroke stroke = new BasicStroke(2 * scale);
303         g2d.setStroke(stroke);
304         g2d.setColor(drawOptions.getColorChargerHalo());
305         final float chargerX = toXCoord(rmfp.getChargerX()) * scale;
306         final float chargerY = toYCoord(rmfp.getChargerY()) * scale;
307         drawCircle(g2d, chargerX, chargerY, radius, false);
308         drawCenteredImg(g2d, scale / 8, "charger.png", chargerX, chargerY);
309         radius = 3 * scale;
310         g2d.setColor(drawOptions.getColorRobo());
311         final float roboX = toXCoord(rmfp.getRoboX()) * scale;
312         final float roboY = toYCoord(rmfp.getRoboY()) * scale;
313         drawCircle(g2d, roboX, roboY, radius, false);
314         if (scale > 1.5) {
315             drawCenteredImg(g2d, scale / 15, "robo.png", roboX, roboY);
316         }
317     }
318
319     private void drawObstacles(Graphics2D g2d, float scale) {
320         float radius = 2 * scale;
321         Stroke stroke = new BasicStroke(3 * scale);
322         g2d.setStroke(stroke);
323         g2d.setColor(Color.MAGENTA);
324
325         Map<Integer, ArrayList<int[]>> obstacleMap = rmfp.getObstacles();
326         for (ArrayList<int[]> obstacles : obstacleMap.values()) {
327             obstacles.forEach(obstacle -> {
328                 final float obstacleX = toXCoord(obstacle[0]) * scale;
329                 final float obstacleY = toYCoord(obstacle[1]) * scale;
330                 drawCircle(g2d, obstacleX, obstacleY, radius, true);
331                 if (scale > 1.0) {
332                     drawCenteredImg(g2d, scale / 3, "obstacle-" + obstacle[2] + ".png", obstacleX, obstacleY + 15);
333                 }
334             });
335         }
336     }
337
338     private void drawCircle(Graphics2D g2d, float x, float y, float radius, boolean fill) {
339         Ellipse2D.Double circle = new Ellipse2D.Double(x - radius, y - radius, 2.0 * radius, 2.0 * radius);
340         if (fill) {
341             g2d.fill(circle);
342         } else {
343             g2d.draw(circle);
344         }
345     }
346
347     private void drawCenteredImg(Graphics2D g2d, float scale, String imgFile, float x, float y) {
348         URL image = getImageUrl(imgFile);
349         try {
350             if (image != null) {
351                 BufferedImage addImg = ImageIO.read(image);
352                 int xpos = Math.round(x + (addImg.getWidth() / 2 * scale));
353                 int ypos = Math.round(y + (addImg.getHeight() / 2 * scale));
354                 AffineTransform at = new AffineTransform();
355                 at.scale(-scale, -scale);
356                 AffineTransformOp scaleOp = new AffineTransformOp(at, AffineTransformOp.TYPE_BILINEAR);
357                 g2d.drawImage(addImg, scaleOp, xpos, ypos);
358             } else {
359                 logger.debug("Error loading image {}: File not be found.", imgFile);
360             }
361         } catch (IOException e) {
362             logger.debug("Error loading image {}: {}", image, e.getMessage());
363         }
364     }
365
366     private void drawGoTo(Graphics2D g2d, float scale) {
367         float x = toXCoord(rmfp.getGotoX()) * scale;
368         float y = toYCoord(rmfp.getGotoY()) * scale;
369         if (!(x == 0 && y == 0)) {
370             g2d.setStroke(new BasicStroke());
371             g2d.setColor(Color.YELLOW);
372             int x3[] = { (int) x, (int) (x - 2 * scale), (int) (x + 2 * scale) };
373             int y3[] = { (int) y, (int) (y - 5 * scale), (int) (y - 5 * scale) };
374             g2d.fill(new Polygon(x3, y3, 3));
375         }
376     }
377
378     private void drawOpenHabRocks(Graphics2D g2d, int width, int height, float scale) {
379         // easter egg gift
380         int offset = 5;
381         int textPos = 55;
382         URL image = getImageUrl("ohlogo.png");
383         try {
384             if (image != null) {
385                 BufferedImage ohLogo = ImageIO.read(image);
386                 textPos = (int) (ohLogo.getWidth() * scale / 2 + offset * scale);
387                 AffineTransform at = new AffineTransform();
388                 at.scale(scale / 2, scale / 2);
389                 AffineTransformOp scaleOp = new AffineTransformOp(at, AffineTransformOp.TYPE_BILINEAR);
390                 g2d.drawImage(ohLogo, scaleOp, offset,
391                         height - (int) (ohLogo.getHeight() * scale / 2) - (int) (offset * scale));
392             } else {
393                 logger.debug("Error loading image ohlogo.png: File not be found.");
394             }
395         } catch (IOException e) {
396             logger.debug("Error loading image ohlogo.png:: {}", e.getMessage());
397         }
398         if (drawOptions.getText().isBlank()) {
399             return;
400         }
401         String fontName = getAvailableFont("Helvetica,Arial,Roboto,Verdana,Times,Serif,Dialog".split(","));
402         if (fontName == null) {
403             return; // no available fonts to draw text
404         }
405         int fz = (int) (drawOptions.getTextFontSize() * scale);
406         Font font = new Font(fontName, Font.BOLD, fz);
407         g2d.setFont(font);
408         String message = drawOptions.getText();
409         FontMetrics fontMetrics = g2d.getFontMetrics();
410         int stringWidth = fontMetrics.stringWidth(message);
411         if ((stringWidth + textPos) > width) {
412             int fzn = (int) Math.floor(((float) (width - textPos) / stringWidth) * fz);
413             font = new Font(fontName, Font.BOLD, fzn > 0 ? fzn : 1);
414             g2d.setFont(font);
415         }
416         int stringHeight = fontMetrics.getAscent();
417         g2d.setPaint(Color.white);
418         g2d.drawString(message, textPos, height - offset * scale - stringHeight / 2);
419     }
420
421     private @Nullable String getAvailableFont(String[] preferedFonts) {
422         final GraphicsEnvironment gEv = GraphicsEnvironment.getLocalGraphicsEnvironment();
423         if (gEv == null) {
424             return null;
425         }
426         String[] fonts = gEv.getAvailableFontFamilyNames();
427         if (fonts.length == 0) {
428             return null;
429         }
430         for (int j = 0; j < preferedFonts.length; j++) {
431             for (int i = 0; i < fonts.length; i++) {
432                 if (fonts[i].equalsIgnoreCase(preferedFonts[j])) {
433                     return preferedFonts[j];
434                 }
435             }
436         }
437         // Preferred fonts not available... just go with the first one
438         return fonts[0];
439     }
440
441     /**
442      * Finds the perimeter of the used area in the map
443      */
444     private void getMapArea(Graphics2D g2d, float scale) {
445         int firstX = rmfp.getImgWidth();
446         int lastX = 0;
447         int firstY = rmfp.getImgHeight();
448         int lastY = 0;
449         for (int y = 0; y < rmfp.getImgHeight() - 1; y++) {
450             for (int x = 0; x < rmfp.getImgWidth() + 1; x++) {
451                 int walltype = rmfp.getImage()[x + rmfp.getImgWidth() * y] & 0xFF;
452                 if (walltype > MAP_OUTSIDE) {
453                     if (y < firstY) {
454                         firstY = y;
455                     }
456                     if (y > lastY) {
457                         lastY = y;
458                     }
459                     if (x < firstX) {
460                         firstX = x;
461                     }
462                     if (x > lastX) {
463                         lastX = x;
464                     }
465                 }
466             }
467         }
468         this.firstX = firstX;
469         this.lastX = lastX;
470         this.firstY = rmfp.getImgHeight() - lastY;
471         this.lastY = rmfp.getImgHeight() - firstY;
472     }
473
474     private @Nullable URL getImageUrl(String image) {
475         final Bundle bundle = this.bundle;
476         if (bundle != null) {
477             return bundle.getEntry("images/" + image);
478         }
479         try {
480             File fn = new File("src" + File.separator + "main" + File.separator + "resources" + File.separator
481                     + "images" + File.separator + image);
482             return fn.toURI().toURL();
483         } catch (MalformedURLException | SecurityException e) {
484             logger.debug("Could create URL for {}: {}", image, e.getMessage());
485             return null;
486         }
487     }
488
489     public BufferedImage getImage() {
490         return getImage(drawOptions.getScale());
491     }
492
493     public BufferedImage getImage(float scale) {
494         int width = (int) Math.floor(rmfp.getImgWidth() * scale);
495         int height = (int) Math.floor(rmfp.getImgHeight() * scale);
496         BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
497         Graphics2D g2d = bi.createGraphics();
498         AffineTransform tx = AffineTransform.getScaleInstance(-1, -1);
499         tx.translate(-width, -height);
500         g2d.setTransform(tx);
501         drawMap(g2d, scale);
502         drawCarpetMap(g2d, scale);
503         drawZones(g2d, scale);
504         drawNoGo(g2d, scale);
505         drawWalls(g2d, scale);
506         drawPath(g2d, scale);
507         drawRobo(g2d, scale);
508         drawGoTo(g2d, scale);
509         drawObstacles(g2d, scale);
510         if (drawOptions.getCropBorder() < 0) {
511             g2d = bi.createGraphics();
512             if (drawOptions.isShowLogo()) {
513                 drawOpenHabRocks(g2d, width, height, scale);
514             }
515             return bi;
516         }
517         // crop the image to the used perimeter
518         getMapArea(g2d, scale);
519         int firstX = (this.firstX - drawOptions.getCropBorder()) > 0 ? this.firstX - drawOptions.getCropBorder() : 0;
520         int lastX = (this.lastX + drawOptions.getCropBorder()) < rmfp.getImgWidth()
521                 ? this.lastX + drawOptions.getCropBorder()
522                 : rmfp.getImgWidth();
523         int firstY = (this.firstY - drawOptions.getCropBorder()) > 0 ? this.firstY - drawOptions.getCropBorder() : 0;
524         int lastY = (this.lastY + drawOptions.getCropBorder() + (int) (8 * scale)) < rmfp.getImgHeight()
525                 ? this.lastY + drawOptions.getCropBorder() + (int) (8 * scale)
526                 : rmfp.getImgHeight();
527         int nwidth = (int) Math.floor((lastX - firstX) * scale);
528         int nheight = (int) Math.floor((lastY - firstY) * scale);
529         BufferedImage bo = new BufferedImage(nwidth, nheight, BufferedImage.TYPE_3BYTE_BGR);
530         Graphics2D crop = bo.createGraphics();
531         crop.transform(AffineTransform.getTranslateInstance(-firstX * scale, -firstY * scale));
532         crop.drawImage(bi, 0, 0, null);
533         if (drawOptions.isShowLogo()) {
534             crop = bo.createGraphics();
535             drawOpenHabRocks(crop, nwidth, nheight, scale * .75f);
536         }
537         return bo;
538     }
539
540     public boolean writePic(String filename, String formatName, float scale) throws IOException {
541         return ImageIO.write(getImage(scale), formatName, new File(filename));
542     }
543
544     private float toXCoord(float x) {
545         return rmfp.getImgWidth() + rmfp.getLeft() - (x / MM);
546     }
547
548     private float toYCoord(float y) {
549         return y / MM - rmfp.getTop();
550     }
551
552     @Override
553     public String toString() {
554         return rmfp.toString();
555     }
556 }