From fbd06ec70991f1a58d2c734d4df7dd8e68d233fc Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=B8rgen=20Austvik?= Date: Sat, 12 Nov 2022 23:00:08 +0100 Subject: [PATCH] [Nanoleaf] Visualize layout (#13552) MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit * Visualize Nanoleaf layout * Only calculate image if channel is linked * White background image * Render more shapes Signed-off-by: Jørgen Austvik --- .../org.openhab.binding.nanoleaf/README.md | 20 +- .../doc/Layout.png | Bin 0 -> 25495 bytes .../internal/NanoleafBindingConstants.java | 7 +- .../nanoleaf/internal/OpenAPIUtils.java | 6 +- .../handler/NanoleafControllerHandler.java | 33 ++++ .../internal/layout/DrawingAlgorithm.java | 30 +++ .../internal/layout/NanoleafLayout.java | 184 ++++++++++++++++++ .../nanoleaf/internal/layout/Point2D.java | 81 ++++++++ .../nanoleaf/internal/layout/ShapeType.java | 87 +++++++++ .../internal/layout/shape/Hexagon.java | 56 ++++++ .../nanoleaf/internal/layout/shape/Point.java | 43 ++++ .../nanoleaf/internal/layout/shape/Shape.java | 85 ++++++++ .../internal/layout/shape/ShapeFactory.java | 44 +++++ .../internal/layout/shape/Square.java | 57 ++++++ .../internal/layout/shape/Triangle.java | 64 ++++++ .../internal/model/GlobalOrientation.java | 37 ++++ .../nanoleaf/internal/model/Layout.java | 53 +++++ .../nanoleaf/internal/model/PanelLayout.java | 73 +++++++ .../internal/model/PositionDatum.java | 56 ++++-- .../resources/OH-INF/i18n/nanoleaf.properties | 2 + .../resources/OH-INF/thing/lightpanels.xml | 7 + .../internal/model/GlobalOrientationTest.java | 66 +++++++ .../nanoleaf/internal/model/LayoutTest.java | 72 +++++++ .../internal/model/PanelLayoutTest.java | 78 ++++++++ .../internal/model/PositionDatumTest.java | 66 +++++++ 25 files changed, 1278 insertions(+), 29 deletions(-) create mode 100644 bundles/org.openhab.binding.nanoleaf/doc/Layout.png create mode 100644 bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/DrawingAlgorithm.java create mode 100644 bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/NanoleafLayout.java create mode 100644 bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/Point2D.java create mode 100644 bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/ShapeType.java create mode 100644 bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Hexagon.java create mode 100644 bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Point.java create mode 100644 bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Shape.java create mode 100644 bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/ShapeFactory.java create mode 100644 bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Square.java create mode 100644 bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Triangle.java create mode 100644 bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/model/GlobalOrientationTest.java create mode 100644 bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/model/LayoutTest.java create mode 100644 bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/model/PanelLayoutTest.java create mode 100644 bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/model/PositionDatumTest.java diff --git a/bundles/org.openhab.binding.nanoleaf/README.md b/bundles/org.openhab.binding.nanoleaf/README.md index 77858fdbc1..1aacf2e413 100644 --- a/bundles/org.openhab.binding.nanoleaf/README.md +++ b/bundles/org.openhab.binding.nanoleaf/README.md @@ -28,9 +28,13 @@ You can set the **color** for each panel and in the case of a Nanoleaf Canvas or | Nanoleaf Name | Type | Description | supported | touch support | | ---------------------- | ---- | ---------------------------------------------------------- | --------- | ------------- | | Light Panels | NL22 | Triangles 1st Generation | X | - | -| Shapes Triangle | NL42 | Triangles 2nd Generation (rounded edges) | X | X | | Shapes Hexagon | NL42 | Hexagons | X | X | -| Shapes Mini Triangles | NL42 | Mini Triangles | x | X | +| Shapes Triangles | NL47 | Triangles | X | X | +| Shapes Mini Triangles | NL48 | Mini Triangles | X | X | +| Elements Hexagon | NL52 | Elements Hexagons | X | X | +| Smart Bulb | NL45 | Smart Bulb | - | | +| Lightstrip | NL55 | Lightstrip | - | | +| Lines | NL59 | Lines | - | | | Canvas | NL29 | Squares | X | X | x = Supported (-) = unknown (no device available to test) @@ -70,9 +74,15 @@ In this case: ### Panel Layout -Unfortunately it is not easy to find out which panel gets which id, and this becomes pretty important if you have lots of them and want to assign rules. +If you want to program individual panels, it can be hard to figure out which panel has which ID. To make this easier, there is Layout channel on the Nanoleaf controller thing in openHAB. +The easiest way to visualize the layout of the individual panels is to open the controller thing in the openHAB UI, go to Channels and add a new item to the Layout channel. +Clicking on that image or adding it to a dashboard will show a picture of your canvas with the individual thing ID in the picture. -For canvas that use square panels, you can request the layout through a [console command](https://www.openhab.org/docs/administration/console.html): +If your canvas has elements we dont know how to draw a layout for yet, please reach out, and we will ask for some information and will try to add support for your elements. + +![Image](doc/Layout.jpg) + +There is an alternative method for canvas that use square panels, you can request the layout through a [console command](https://www.openhab.org/docs/administration/console.html): then issue the following command: @@ -94,7 +104,7 @@ Compare the following output with the right picture at the beginning of the arti 41451 ``` - + ## Thing Configuration The controller thing has the following parameters: diff --git a/bundles/org.openhab.binding.nanoleaf/doc/Layout.png b/bundles/org.openhab.binding.nanoleaf/doc/Layout.png new file mode 100644 index 0000000000000000000000000000000000000000..a8d684a0ce019fbf7009b9d57351c70d473aa48b GIT binary patch literal 25495 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 literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafBindingConstants.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafBindingConstants.java index fef86f5618..e7e8a2fb5a 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafBindingConstants.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafBindingConstants.java @@ -58,6 +58,7 @@ public class NanoleafBindingConstants { public static final String CHANNEL_SWIPE_EVENT_DOWN = "DOWN"; public static final String CHANNEL_SWIPE_EVENT_LEFT = "LEFT"; public static final String CHANNEL_SWIPE_EVENT_RIGHT = "RIGHT"; + public static final String CHANNEL_LAYOUT = "layout"; // List of light panel channels public static final String CHANNEL_PANEL_COLOR = "color"; @@ -78,7 +79,7 @@ public class NanoleafBindingConstants { public static final String API_MIN_FW_VER_CANVAS = "1.1.0"; public static final String MODEL_ID_LIGHTPANELS = "NL22"; - public static final List MODELS_WITH_TOUCHSUPPORT = Arrays.asList("NL29", "NL42"); + public static final List MODELS_WITH_TOUCHSUPPORT = Arrays.asList("NL29", "NL42", "NL47", "NL48", "NL52"); public static final String DEVICE_TYPE_LIGHTPANELS = "lightPanels"; public static final String DEVICE_TYPE_TOUCHSUPPORT = "canvas"; // we need to keep this enum for backward // compatibility even though not only canvas type @@ -93,4 +94,8 @@ public class NanoleafBindingConstants { // Color channels increase/decrease brightness step size public static final int BRIGHTNESS_STEP_SIZE = 5; + + // Layout rendering + public static final int LAYOUT_LIGHT_RADIUS = 8; + public static final int LAYOUT_BORDER_WIDTH = 30; } diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/OpenAPIUtils.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/OpenAPIUtils.java index 55def4c0bd..08a3e5c046 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/OpenAPIUtils.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/OpenAPIUtils.java @@ -154,11 +154,7 @@ public class OpenAPIUtils { for (int i = 0; i < currentVer.length; ++i) { if (currentVer[i] != requiredVer[i]) { - if (currentVer[i] > requiredVer[i]) { - return true; - } - - return false; + return (currentVer[i] > requiredVer[i]); } } diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafControllerHandler.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafControllerHandler.java index 4330890586..bd2efd4554 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafControllerHandler.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafControllerHandler.java @@ -14,6 +14,7 @@ package org.openhab.binding.nanoleaf.internal.handler; import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.*; +import java.io.IOException; import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.Collection; @@ -43,6 +44,7 @@ import org.openhab.binding.nanoleaf.internal.OpenAPIUtils; import org.openhab.binding.nanoleaf.internal.commanddescription.NanoleafCommandDescriptionProvider; import org.openhab.binding.nanoleaf.internal.config.NanoleafControllerConfig; import org.openhab.binding.nanoleaf.internal.discovery.NanoleafPanelsDiscoveryService; +import org.openhab.binding.nanoleaf.internal.layout.NanoleafLayout; import org.openhab.binding.nanoleaf.internal.model.AuthToken; import org.openhab.binding.nanoleaf.internal.model.BooleanState; import org.openhab.binding.nanoleaf.internal.model.Brightness; @@ -65,6 +67,7 @@ import org.openhab.core.library.types.HSBType; import org.openhab.core.library.types.IncreaseDecreaseType; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.RawType; import org.openhab.core.library.types.StringType; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ChannelUID; @@ -72,6 +75,7 @@ import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.binding.BaseBridgeHandler; +import org.openhab.core.thing.binding.ThingHandlerCallback; import org.openhab.core.thing.binding.ThingHandlerService; import org.openhab.core.types.Command; import org.openhab.core.types.RefreshType; @@ -103,6 +107,7 @@ public class NanoleafControllerHandler extends BaseBridgeHandler { private @Nullable HttpClient httpClientSSETouchEvent; private @Nullable Request sseTouchjobRequest; private List controllerListeners = new CopyOnWriteArrayList(); + private PanelLayout previousPanelLayout = new PanelLayout(); private @NonNullByDefault({}) ScheduledFuture pairingJob; private @NonNullByDefault({}) ScheduledFuture updateJob; @@ -664,6 +669,7 @@ public class NanoleafControllerHandler extends BaseBridgeHandler { updateProperties(); updateConfiguration(); + updateLayout(controllerInfo.getPanelLayout()); for (NanoleafControllerListener controllerListener : controllerListeners) { controllerListener.onControllerInfoFetched(getThing().getUID(), controllerInfo); @@ -705,6 +711,33 @@ public class NanoleafControllerHandler extends BaseBridgeHandler { } } + private void updateLayout(PanelLayout panelLayout) { + ChannelUID layoutChannel = new ChannelUID(getThing().getUID(), CHANNEL_LAYOUT); + ThingHandlerCallback callback = getCallback(); + if (callback != null) { + if (!callback.isChannelLinked(layoutChannel)) { + // Don't generate image unless it is used + return; + } + } + + if (previousPanelLayout.equals(panelLayout)) { + logger.trace("Not rendering panel layout as it is the same as previous rendered panel layout"); + return; + } + + try { + byte[] bytes = NanoleafLayout.render(panelLayout); + if (bytes.length > 0) { + updateState(CHANNEL_LAYOUT, new RawType(bytes, "image/png")); + } + + previousPanelLayout = panelLayout; + } catch (IOException ioex) { + logger.warn("Failed to create layout image", ioex); + } + } + private ControllerInfo receiveControllerInfo() throws NanoleafException, NanoleafUnauthorizedException { ContentResponse controllerlInfoJSON = OpenAPIUtils.sendOpenAPIRequest(OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(), API_GET_CONTROLLER_INFO, HttpMethod.GET)); diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/DrawingAlgorithm.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/DrawingAlgorithm.java new file mode 100644 index 0000000000..de9a3b26b2 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/DrawingAlgorithm.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nanoleaf.internal.layout; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Differentiates how shapes must be drawn + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public enum DrawingAlgorithm { + NONE, + SQUARE, + TRIANGLE, + HEXAGON, + CORNER, + LINE; +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/NanoleafLayout.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/NanoleafLayout.java new file mode 100644 index 0000000000..70724333ec --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/NanoleafLayout.java @@ -0,0 +1,184 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package org.openhab.binding.nanoleaf.internal.layout; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import javax.imageio.ImageIO; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants; +import org.openhab.binding.nanoleaf.internal.layout.shape.Shape; +import org.openhab.binding.nanoleaf.internal.layout.shape.ShapeFactory; +import org.openhab.binding.nanoleaf.internal.model.GlobalOrientation; +import org.openhab.binding.nanoleaf.internal.model.Layout; +import org.openhab.binding.nanoleaf.internal.model.PanelLayout; +import org.openhab.binding.nanoleaf.internal.model.PositionDatum; + +/** + * Renders the Nanoleaf layout to an image. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class NanoleafLayout { + + private static final Color COLOR_BACKGROUND = Color.WHITE; + private static final Color COLOR_PANEL = Color.BLACK; + private static final Color COLOR_SIDE = Color.GRAY; + private static final Color COLOR_TEXT = Color.BLACK; + + public static byte[] render(PanelLayout panelLayout) throws IOException { + double rotationRadians = 0; + GlobalOrientation globalOrientation = panelLayout.getGlobalOrientation(); + if (globalOrientation != null) { + rotationRadians = calculateRotationRadians(globalOrientation); + } + + Layout layout = panelLayout.getLayout(); + if (layout == null) { + return new byte[] {}; + } + + List panels = layout.getPositionData(); + if (panels == null) { + return new byte[] {}; + } + + Point2D size[] = findSize(panels, rotationRadians); + final Point2D min = size[0]; + final Point2D max = size[1]; + Point2D prev = null; + Point2D first = null; + + int sideCounter = 0; + BufferedImage image = new BufferedImage( + (max.getX() - min.getX()) + 2 * NanoleafBindingConstants.LAYOUT_BORDER_WIDTH, + (max.getY() - min.getY()) + 2 * NanoleafBindingConstants.LAYOUT_BORDER_WIDTH, + BufferedImage.TYPE_INT_RGB); + Graphics2D g2 = image.createGraphics(); + + g2.setBackground(COLOR_BACKGROUND); + g2.clearRect(0, 0, image.getWidth(), image.getHeight()); + + for (PositionDatum panel : panels) { + final ShapeType shapeType = ShapeType.valueOf(panel.getShapeType()); + + Shape shape = ShapeFactory.CreateShape(shapeType, panel); + List outline = toPictureLayout(shape.generateOutline(), image.getHeight(), min, rotationRadians); + for (int i = 0; i < outline.size(); i++) { + g2.setColor(COLOR_SIDE); + Point2D pos = outline.get(i); + Point2D nextPos = outline.get((i + 1) % outline.size()); + g2.drawLine(pos.getX(), pos.getY(), nextPos.getX(), nextPos.getY()); + } + + for (int i = 0; i < outline.size(); i++) { + Point2D pos = outline.get(i); + g2.setColor(COLOR_PANEL); + g2.fillOval(pos.getX() - NanoleafBindingConstants.LAYOUT_LIGHT_RADIUS / 2, + pos.getY() - NanoleafBindingConstants.LAYOUT_LIGHT_RADIUS / 2, + NanoleafBindingConstants.LAYOUT_LIGHT_RADIUS, NanoleafBindingConstants.LAYOUT_LIGHT_RADIUS); + } + + Point2D current = toPictureLayout(new Point2D(panel.getPosX(), panel.getPosY()), image.getHeight(), min, + rotationRadians); + if (sideCounter == 0) { + first = current; + } + + g2.setColor(COLOR_SIDE); + final int expectedSides = shapeType.getNumSides(); + if (shapeType.getDrawingAlgorithm() == DrawingAlgorithm.CORNER) { + // Special handling of Elements Hexagon Corners, where we get 6 corners instead of 1 shape. They seem to + // come after each other in the JSON, so this algorithm connects them based on the number of sides the + // shape is expected to have. + if (sideCounter > 0 && sideCounter != expectedSides && prev != null) { + g2.drawLine(prev.getX(), prev.getY(), current.getX(), current.getY()); + } + + sideCounter++; + + if (sideCounter == expectedSides && first != null) { + g2.drawLine(current.getX(), current.getY(), first.getX(), first.getY()); + sideCounter = 0; + } + } else { + sideCounter = 0; + } + + prev = current; + + g2.setColor(COLOR_TEXT); + Point2D textPos = shape.labelPosition(g2, outline); + g2.drawString(Integer.toString(panel.getPanelId()), textPos.getX(), textPos.getY()); + } + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ImageIO.write(image, "png", out); + return out.toByteArray(); + } + + private static double calculateRotationRadians(GlobalOrientation globalOrientation) { + Integer maxObj = globalOrientation.getMax(); + int maxValue = maxObj == null ? 360 : (int) maxObj; + int value = globalOrientation.getValue(); // 0 - 360 measured counter clockwise. + return ((double) (maxValue - value)) * (Math.PI / 180); + } + + private static Point2D[] findSize(Collection panels, double rotationRadians) { + int maxX = 0; + int maxY = 0; + int minX = 0; + int minY = 0; + + for (PositionDatum panel : panels) { + ShapeType shapeType = ShapeType.valueOf(panel.getShapeType()); + Shape shape = ShapeFactory.CreateShape(shapeType, panel); + for (Point2D point : shape.generateOutline()) { + var rotated = point.rotate(rotationRadians); + maxX = Math.max(rotated.getX(), maxX); + maxY = Math.max(rotated.getY(), maxY); + minX = Math.min(rotated.getX(), minX); + minY = Math.min(rotated.getY(), minY); + } + } + + return new Point2D[] { new Point2D(minX, minY), new Point2D(maxX, maxY) }; + } + + private static Point2D toPictureLayout(Point2D original, int imageHeight, Point2D min, double rotationRadians) { + Point2D rotated = original.rotate(rotationRadians); + Point2D translated = new Point2D(NanoleafBindingConstants.LAYOUT_BORDER_WIDTH + rotated.getX() - min.getX(), + imageHeight - NanoleafBindingConstants.LAYOUT_BORDER_WIDTH - rotated.getY() + min.getY()); + return translated; + } + + private static List toPictureLayout(List originals, int imageHeight, Point2D min, + double rotationRadians) { + List result = new ArrayList(originals.size()); + for (Point2D original : originals) { + result.add(toPictureLayout(original, imageHeight, min, rotationRadians)); + } + + return result; + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/Point2D.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/Point2D.java new file mode 100644 index 0000000000..921551a2e8 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/Point2D.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package org.openhab.binding.nanoleaf.internal.layout; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Coordinate in 2D space. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class Point2D { + private final int x; + private final int y; + + public Point2D(int x, int y) { + this.x = x; + this.y = y; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + /** + * Rotates the point a given amount of radians. + * + * @param radians The amount to rotate the point + * @return A new point which is rotated + */ + public Point2D rotate(double radians) { + double sinAngle = Math.sin(radians); + double cosAngle = Math.cos(radians); + + int newX = (int) (cosAngle * x - sinAngle * y); + int newY = (int) (sinAngle * x + cosAngle * y); + return new Point2D(newX, newY); + } + + /** + * Move the point in x and y direction. + * + * @param moveX Amount to move in x direction + * @param moveY Amount to move in y direction + * @return + */ + public Point2D move(int moveX, int moveY) { + return new Point2D(getX() + moveX, getY() + moveY); + } + + /** + * Move the point in x and y direction,. + * + * @param offset Offset to move + * @return + */ + public Point2D move(Point2D offset) { + return move(offset.getX(), offset.getY()); + } + + @Override + public String toString() { + return String.format("x:%d, y:%d", x, y); + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/ShapeType.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/ShapeType.java new file mode 100644 index 0000000000..f90262e0e7 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/ShapeType.java @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package org.openhab.binding.nanoleaf.internal.layout; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Information about the different Nanoleaf shapes. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public enum ShapeType { + // side lengths are taken from https://forum.nanoleaf.me/docs chapter 3.3 + UNKNOWN("Unknown", -1, 0, 0, DrawingAlgorithm.NONE), + TRIANGLE("Triangle", 0, 150, 3, DrawingAlgorithm.TRIANGLE), + RHYTHM("Rhythm", 1, 0, 1, DrawingAlgorithm.NONE), + SQUARE("Square", 2, 100, 0, DrawingAlgorithm.SQUARE), + CONTROL_SQUARE_MASTER("Control Square Master", 3, 100, 0, DrawingAlgorithm.SQUARE), + CONTROL_SQUARE_PASSIVE("Control Square Passive", 4, 100, 0, DrawingAlgorithm.SQUARE), + SHAPES_HEXAGON("Hexagon (Shapes)", 7, 67, 6, DrawingAlgorithm.HEXAGON), + SHAPES_TRIANGLE("Triangle (Shapes)", 8, 134, 3, DrawingAlgorithm.TRIANGLE), + SHAPES_MINI_TRIANGLE("Mini Triangle (Shapes)", 9, 67, 3, DrawingAlgorithm.TRIANGLE), + SHAPES_CONTROLLER("Controller (Shapes)", 12, 0, 0, DrawingAlgorithm.NONE), + ELEMENTS_HEXAGON("Elements Hexagon", 14, 134, 6, DrawingAlgorithm.HEXAGON), + ELEMENTS_HEXAGON_CORNER("Elements Hexagon - Corner", 15, 33.5 / 58, 6, DrawingAlgorithm.CORNER), + LINES_CONNECTOR("Lines Connector", 16, 11, 1, DrawingAlgorithm.LINE), + LIGHT_LINES("Light Lines", 17, 154, 1, DrawingAlgorithm.LINE), + LINES_LINES_SINGLE("Light Lines - Single Sone", 18, 77, 1, DrawingAlgorithm.LINE), + CONTROLLER_CAP("Controller Cap", 19, 11, 0, DrawingAlgorithm.NONE), + POWER_CONNECTOR("Power Connector", 20, 11, 0, DrawingAlgorithm.NONE); + + private final String name; + private final int id; + private final double sideLength; + private final int numSides; + private final DrawingAlgorithm drawingAlgorithm; + + ShapeType(String name, int id, double sideLenght, int numSides, DrawingAlgorithm drawingAlgorithm) { + this.name = name; + this.id = id; + this.sideLength = sideLenght; + this.numSides = numSides; + this.drawingAlgorithm = drawingAlgorithm; + } + + public String getName() { + return name; + } + + public int getId() { + return id; + } + + public double getSideLength() { + return sideLength; + } + + public int getNumSides() { + return numSides; + } + + public DrawingAlgorithm getDrawingAlgorithm() { + return drawingAlgorithm; + } + + public static ShapeType valueOf(int id) { + for (ShapeType shapeType : values()) { + if (shapeType.getId() == id) { + return shapeType; + } + } + + return ShapeType.UNKNOWN; + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Hexagon.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Hexagon.java new file mode 100644 index 0000000000..a292335342 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Hexagon.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nanoleaf.internal.layout.shape; + +import java.awt.Graphics2D; +import java.awt.geom.Rectangle2D; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.nanoleaf.internal.layout.Point2D; +import org.openhab.binding.nanoleaf.internal.layout.ShapeType; + +/** + * A hexagon shape. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class Hexagon extends Shape { + public Hexagon(ShapeType shapeType, int panelId, Point2D position, int orientation) { + super(shapeType, panelId, position, orientation); + } + + @Override + public List generateOutline() { + Point2D v1 = new Point2D((int) getShapeType().getSideLength(), 0); + Point2D v2 = v1.rotate((1.0 / 3.0) * Math.PI); + Point2D v3 = v1.rotate((2.0 / 3.0) * Math.PI); + Point2D v4 = v1.rotate((3.0 / 3.0) * Math.PI); + Point2D v5 = v1.rotate((4.0 / 3.0) * Math.PI); + Point2D v6 = v1.rotate((5.0 / 3.0) * Math.PI); + return Arrays.asList(v1.move(getPosition()), v2.move(getPosition()), v3.move(getPosition()), + v4.move(getPosition()), v5.move(getPosition()), v6.move(getPosition())); + } + + @Override + public Point2D labelPosition(Graphics2D graphics, List outline) { + Point2D[] bounds = findBounds(outline); + int midX = bounds[0].getX() + (bounds[1].getX() - bounds[0].getX()) / 2; + int midY = bounds[0].getY() + (bounds[1].getY() - bounds[0].getY()) / 2; + + Rectangle2D rect = graphics.getFontMetrics().getStringBounds(Integer.toString(getPanelId()), graphics); + return new Point2D(midX - (int) (rect.getWidth() / 2), midY - (int) (rect.getHeight() / 2)); + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Point.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Point.java new file mode 100644 index 0000000000..0ae05dc2b5 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Point.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nanoleaf.internal.layout.shape; + +import java.awt.Graphics2D; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.nanoleaf.internal.layout.Point2D; +import org.openhab.binding.nanoleaf.internal.layout.ShapeType; + +/** + * A shape without any area. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class Point extends Shape { + public Point(ShapeType shapeType, int panelId, Point2D position, int orientation) { + super(shapeType, panelId, position, orientation); + } + + @Override + public List generateOutline() { + return Arrays.asList(getPosition()); + } + + @Override + public Point2D labelPosition(Graphics2D graphics, List outline) { + return outline.get(0); + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Shape.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Shape.java new file mode 100644 index 0000000000..99412ba2ea --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Shape.java @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nanoleaf.internal.layout.shape; + +import java.awt.Graphics2D; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.nanoleaf.internal.layout.Point2D; +import org.openhab.binding.nanoleaf.internal.layout.ShapeType; + +/** + * Shape that can be drawn. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public abstract class Shape { + private final ShapeType shapeType; + private final int panelId; + private final Point2D position; + private final int orientation; + + public Shape(ShapeType shapeType, int panelId, Point2D position, int orientation) { + this.shapeType = shapeType; + this.panelId = panelId; + this.position = position; + this.orientation = orientation; + } + + public int getPanelId() { + return panelId; + }; + + public Point2D getPosition() { + return position; + } + + public int getOrientation() { + return orientation; + }; + + public ShapeType getShapeType() { + return shapeType; + } + + /** + * @return The opposite points of the minimum bounding rectangle around this shape. + */ + public Point2D[] findBounds(List outline) { + int minX = Integer.MAX_VALUE; + int minY = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE; + int maxY = Integer.MIN_VALUE; + + for (Point2D point : outline) { + maxX = Math.max(point.getX(), maxX); + maxY = Math.max(point.getY(), maxY); + minX = Math.min(point.getX(), minX); + minY = Math.min(point.getY(), minY); + } + + return new Point2D[] { new Point2D(minX, minY), new Point2D(maxX, maxY) }; + } + + /** + * @return The points that make up this shape. + */ + public abstract List generateOutline(); + + /** + * @return The position where the label of the shape should be placed + */ + public abstract Point2D labelPosition(Graphics2D graphics, List outline); +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/ShapeFactory.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/ShapeFactory.java new file mode 100644 index 0000000000..78e9ec0882 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/ShapeFactory.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nanoleaf.internal.layout.shape; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.nanoleaf.internal.layout.Point2D; +import org.openhab.binding.nanoleaf.internal.layout.ShapeType; +import org.openhab.binding.nanoleaf.internal.model.PositionDatum; + +/** + * Create the correct chape for a given shape type. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class ShapeFactory { + + public static Shape CreateShape(ShapeType shapeType, PositionDatum positionDatum) { + Point2D pos = new Point2D(positionDatum.getPosX(), positionDatum.getPosY()); + switch (shapeType.getDrawingAlgorithm()) { + case SQUARE: + return new Square(shapeType, positionDatum.getPanelId(), pos, positionDatum.getOrientation()); + + case TRIANGLE: + return new Triangle(shapeType, positionDatum.getPanelId(), pos, positionDatum.getOrientation()); + + case HEXAGON: + return new Hexagon(shapeType, positionDatum.getPanelId(), pos, positionDatum.getOrientation()); + + default: + return new Point(shapeType, positionDatum.getPanelId(), pos, positionDatum.getOrientation()); + } + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Square.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Square.java new file mode 100644 index 0000000000..a9cf762843 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Square.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nanoleaf.internal.layout.shape; + +import java.awt.Graphics2D; +import java.awt.geom.Rectangle2D; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.nanoleaf.internal.layout.Point2D; +import org.openhab.binding.nanoleaf.internal.layout.ShapeType; + +/** + * A square shape. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class Square extends Shape { + public Square(ShapeType shapeType, int panelId, Point2D position, int orientation) { + super(shapeType, panelId, position, orientation); + } + + @Override + public List generateOutline() { + int sideLength = (int) getShapeType().getSideLength(); + + Point2D current = getPosition(); + Point2D corner2 = new Point2D(current.getX() + sideLength, current.getY()); + Point2D corner3 = new Point2D(current.getX() + sideLength, current.getY() + sideLength); + Point2D corner4 = new Point2D(current.getX(), current.getY() + sideLength); + return Arrays.asList(getPosition(), corner2, corner3, corner4); + } + + @Override + public Point2D labelPosition(Graphics2D graphics, List outline) { + // Center of square is average of oposite corners + Point2D p0 = outline.get(0); + Point2D p2 = outline.get(2); + + Rectangle2D rect = graphics.getFontMetrics().getStringBounds(Integer.toString(getPanelId()), graphics); + + return new Point2D((p0.getX() + p2.getX()) / 2 - (int) (rect.getWidth() / 2), + (p0.getY() + p2.getY()) / 2 - (int) (rect.getHeight() / 2)); + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Triangle.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Triangle.java new file mode 100644 index 0000000000..586e89fc6e --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Triangle.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nanoleaf.internal.layout.shape; + +import java.awt.Graphics2D; +import java.awt.geom.Rectangle2D; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.nanoleaf.internal.layout.Point2D; +import org.openhab.binding.nanoleaf.internal.layout.ShapeType; + +/** + * A triangular shape. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class Triangle extends Shape { + public Triangle(ShapeType shapeType, int panelId, Point2D position, int orientation) { + super(shapeType, panelId, position, orientation); + } + + @Override + public List generateOutline() { + int height = (int) (getShapeType().getSideLength() * Math.sqrt(3) / 2); + Point2D v1; + if (pointsUp()) { + v1 = new Point2D(0, height * 2 / 3); + } else { + v1 = new Point2D(0, -height * 2 / 3); + } + + Point2D v2 = v1.rotate((2.0 / 3.0) * Math.PI); + Point2D v3 = v1.rotate((-2.0 / 3.0) * Math.PI); + return Arrays.asList(v1.move(getPosition()), v2.move(getPosition()), v3.move(getPosition())); + } + + @Override + public Point2D labelPosition(Graphics2D graphics, List outline) { + Point2D[] bounds = findBounds(outline); + int midX = bounds[0].getX() + (bounds[1].getX() - bounds[0].getX()) / 2; + int midY = bounds[0].getY() + (bounds[1].getY() - bounds[0].getY()) / 2; + + Rectangle2D rect = graphics.getFontMetrics().getStringBounds(Integer.toString(getPanelId()), graphics); + return new Point2D(midX - (int) (rect.getWidth() / 2), midY - (int) (rect.getHeight() / 2)); + } + + private boolean pointsUp() { + // Upward: even multiple of 60 degrees rotation + return ((getOrientation() / 60) % 2) == 0; + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/GlobalOrientation.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/GlobalOrientation.java index cb5d19475f..3bf0751d83 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/GlobalOrientation.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/GlobalOrientation.java @@ -12,6 +12,8 @@ */ package org.openhab.binding.nanoleaf.internal.model; +import java.util.Objects; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -27,6 +29,15 @@ public class GlobalOrientation { private @Nullable Integer max; private @Nullable Integer min; + public GlobalOrientation() { + } + + public GlobalOrientation(Integer min, Integer max, int value) { + this.min = min; + this.max = max; + this.value = value; + } + public int getValue() { return value; } @@ -50,4 +61,30 @@ public class GlobalOrientation { public void setMin(Integer min) { this.min = min; } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + GlobalOrientation go = (GlobalOrientation) o; + return (value == go.getValue()) && (Objects.equals(min, go.getMin())) && (Objects.equals(max, go.getMax())); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + Integer x = max; + Integer i = min; + result = prime * result + value; + result = prime * result + ((x == null) ? 0 : x.hashCode()); + result = prime * result + ((i == null) ? 0 : i.hashCode()); + return result; + } } diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/Layout.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/Layout.java index a46a28b0aa..c5ced2e938 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/Layout.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/Layout.java @@ -12,6 +12,7 @@ */ package org.openhab.binding.nanoleaf.internal.model; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -36,6 +37,14 @@ public class Layout { private @Nullable List positionData = null; + public Layout() { + } + + public Layout(List positionData) { + this.positionData = new ArrayList<>(positionData); + this.numPanels = positionData.size(); + } + public int getNumPanels() { return numPanels; } @@ -143,4 +152,48 @@ public class Layout { return ""; } } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + Layout l = (Layout) o; + + if (numPanels != l.getNumPanels()) { + return false; + } + + List pd = getPositionData(); + List otherPd = l.getPositionData(); + if (pd == null && otherPd == null) { + return true; + } + + if (pd == null || otherPd == null) { + return false; + } + + return pd.equals(otherPd); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + getNumPanels(); + List pd = getPositionData(); + if (pd != null) { + for (PositionDatum p : pd) { + result = prime * result + p.hashCode(); + } + } + + return result; + } } diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/PanelLayout.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/PanelLayout.java index f7ec9e3663..93e822d550 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/PanelLayout.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/PanelLayout.java @@ -26,6 +26,14 @@ public class PanelLayout { private @Nullable Layout layout; private @Nullable GlobalOrientation globalOrientation; + public PanelLayout() { + } + + public PanelLayout(GlobalOrientation globalOrientation, Layout layout) { + this.globalOrientation = globalOrientation; + this.layout = layout; + } + public @Nullable Layout getLayout() { return layout; } @@ -41,4 +49,69 @@ public class PanelLayout { public void setGlobalOrientation(GlobalOrientation globalOrientation) { this.globalOrientation = globalOrientation; } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + PanelLayout pl = (PanelLayout) o; + + // For a panel layout to be equal to another panel layouit, all inner data structures must + // be equal, or they must be null both in this object or the object it is compared with. + + GlobalOrientation go = globalOrientation; + GlobalOrientation otherGo = pl.getGlobalOrientation(); + boolean goEquals = false; + if (go == null || otherGo == null) { + if (go == null && otherGo == null) { + // If one of the global oriantations are null, the other must also be null + // for them to be equal + goEquals = true; + } + } else { + goEquals = go.equals(otherGo); + } + + if (goEquals == false) { + // No reason to compare layout if global oriantation is different + return false; + } + + Layout l = layout; + Layout otherL = pl.getLayout(); + + if (l == null && otherL == null) { + return true; + } + + if (l == null || otherL == null) { + return false; + } + + return l.equals(otherL); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + + GlobalOrientation go = globalOrientation; + if (go != null) { + result = prime * result + go.hashCode(); + } + + Layout l = layout; + if (l != null) { + result = prime * result + l.hashCode(); + } + + return result; + } } diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/PositionDatum.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/PositionDatum.java index 7c717ce7a5..4167339e9f 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/PositionDatum.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/model/PositionDatum.java @@ -12,10 +12,9 @@ */ package org.openhab.binding.nanoleaf.internal.model; -import java.util.HashMap; -import java.util.Map; - import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.nanoleaf.internal.layout.ShapeType; import com.google.gson.annotations.SerializedName; @@ -37,21 +36,15 @@ public class PositionDatum { @SerializedName("shapeType") private int shapeType; - private static Map panelSizes = new HashMap(); - public PositionDatum() { - // initialize constant sidelengths for panels. See https://forum.nanoleaf.me/docs chapter 3.3 - if (panelSizes.isEmpty()) { - panelSizes.put(0, 150); // Triangle - panelSizes.put(1, 0); // Rhythm N/A - panelSizes.put(2, 100); // Square - panelSizes.put(3, 100); // Control Square Master - panelSizes.put(4, 100); // Control Square Passive - panelSizes.put(7, 67); // Hexagon - panelSizes.put(8, 134); // Triangle Shapes - panelSizes.put(9, 67); // Mini Triangle Shapes - panelSizes.put(12, 0); // Shapes Controller (N/A) - } + } + + public PositionDatum(int panelId, int posX, int posY, int orientation, int shapeType) { + this.panelId = panelId; + this.posX = posX; + this.posY = posY; + this.orientation = orientation; + this.shapeType = shapeType; } public int getPanelId() { @@ -105,6 +98,33 @@ public class PositionDatum { } public Integer getPanelSize() { - return panelSizes.getOrDefault(shapeType, 0); + return (int) ShapeType.valueOf(shapeType).getSideLength(); + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + PositionDatum pd = (PositionDatum) o; + return (posX == pd.getPosX()) && (posY == pd.getPosY()) && (orientation == pd.getOrientation()) + && (shapeType == pd.getShapeType()) && (panelId == pd.getPanelId()); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + posX; + result = prime * result + posY; + result = prime * result + orientation; + result = prime * result + shapeType; + result = prime * result + panelId; + return result; } } diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/i18n/nanoleaf.properties b/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/i18n/nanoleaf.properties index 01abe9afac..e3ee3da9db 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/i18n/nanoleaf.properties +++ b/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/i18n/nanoleaf.properties @@ -38,6 +38,8 @@ channel-type.nanoleaf.tap.label = Button channel-type.nanoleaf.tap.description = Button events of the panel channel-type.nanoleaf.swipe.label = Swipe channel-type.nanoleaf.swipe.description = Swipe over the panels +channel-type.nanoleaf.layout.label = Layout +channel-type.nanoleaf.layout.description = Layout of the panels # error messages error.nanoleaf.controller.noIp = IP/host address and/or port are not configured for the controller. diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/thing/lightpanels.xml b/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/thing/lightpanels.xml index 53d2488705..afc9142ad9 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/thing/lightpanels.xml +++ b/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/thing/lightpanels.xml @@ -18,6 +18,7 @@ + @@ -107,4 +108,10 @@ + + Image + + @text/channel-type.nanoleaf.layout.description + + diff --git a/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/model/GlobalOrientationTest.java b/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/model/GlobalOrientationTest.java new file mode 100644 index 0000000000..2290e5260e --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/model/GlobalOrientationTest.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nanoleaf.internal.model; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test for global orientation + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class GlobalOrientationTest { + + @Nullable + GlobalOrientation go1; + + @Nullable + GlobalOrientation go2; // Different from go1 + + @Nullable + GlobalOrientation go3; // Same as go1 + + @BeforeEach + public void setUp() { + go1 = new GlobalOrientation(0, 360, 180); + go2 = new GlobalOrientation(0, 360, 267); + go3 = new GlobalOrientation(0, 360, 180); + } + + @Test + public void testHashCode() { + GlobalOrientation g1 = go1; + GlobalOrientation g2 = go2; + GlobalOrientation g3 = go3; + if (g1 != null && g2 != null && g3 != null) { + assertThat(g1.hashCode(), is(equalTo(g3.hashCode()))); + assertThat(g2.hashCode(), is(not(equalTo(g3.hashCode())))); + } else { + assertThat("Should be initialized", false); + } + } + + @Test + public void testEquals() { + assertThat(go1, is(equalTo(go3))); + assertThat(go2, is(not(equalTo(go3)))); + assertThat(go3, is(not(equalTo(null)))); + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/model/LayoutTest.java b/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/model/LayoutTest.java new file mode 100644 index 0000000000..354afbf74e --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/model/LayoutTest.java @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nanoleaf.internal.model; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.util.Arrays; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test for global orientation + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class LayoutTest { + + @Nullable + private Layout lo1; + + @Nullable + private Layout lo2; // Different from l1 + + @Nullable + private Layout lo3; // Same as l1 + + @BeforeEach + public void setUp() { + PositionDatum pd1 = new PositionDatum(100, 200, 270, 123, 12); + PositionDatum pd2 = new PositionDatum(100, 220, 240, 123, 2); + PositionDatum pd3 = new PositionDatum(100, 200, 270, 123, 12); + + lo1 = new Layout(Arrays.asList(pd1, pd3)); + lo2 = new Layout(Arrays.asList(pd1, pd2)); + lo3 = new Layout(Arrays.asList(pd1, pd3)); + } + + @Test + public void testHashCode() { + Layout l1 = lo1; + Layout l2 = lo2; + Layout l3 = lo3; + if (l1 != null && l2 != null && l3 != null) { + assertThat(l1.hashCode(), is(equalTo(l3.hashCode()))); + assertThat(l2.hashCode(), is(not(equalTo(l3.hashCode())))); + } else { + assertThat("Should be initialized", false); + } + } + + @Test + public void testEquals() { + assertThat(lo1, is(equalTo(lo3))); + assertThat(lo2, is(not(equalTo(lo3)))); + assertThat(lo3, is(not(equalTo(null)))); + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/model/PanelLayoutTest.java b/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/model/PanelLayoutTest.java new file mode 100644 index 0000000000..e1c545f757 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/model/PanelLayoutTest.java @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nanoleaf.internal.model; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.util.Arrays; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test for global orientation + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class PanelLayoutTest { + + @Nullable + private PanelLayout pl1; + + @Nullable + private PanelLayout pl2; // Different from pl1 + + @Nullable + private PanelLayout pl3; // Equal to pl1 + + @BeforeEach + public void setUp() { + PositionDatum pd1 = new PositionDatum(100, 200, 270, 123, 12); + PositionDatum pd2 = new PositionDatum(100, 220, 240, 123, 2); + PositionDatum pd3 = new PositionDatum(100, 200, 270, 123, 12); + + Layout l1 = new Layout(Arrays.asList(pd1, pd3)); + Layout l2 = new Layout(Arrays.asList(pd1, pd2)); + Layout l3 = new Layout(Arrays.asList(pd1, pd3)); + + GlobalOrientation go1 = new GlobalOrientation(0, 360, 180); + + pl1 = new PanelLayout(go1, l1); + pl2 = new PanelLayout(go1, l2); + pl3 = new PanelLayout(go1, l3); + } + + @Test + public void testHashCode() { + PanelLayout p1 = pl1; + PanelLayout p2 = pl2; + PanelLayout p3 = pl3; + if (p1 != null && p2 != null && p3 != null) { + assertThat(p1.hashCode(), is(equalTo(p3.hashCode()))); + assertThat(p2.hashCode(), is(not(equalTo(p3.hashCode())))); + } else { + assertThat("Should be initialized", false); + } + } + + @Test + public void testEquals() { + assertThat(pl1, is(equalTo(pl3))); + assertThat(pl2, is(not(equalTo(pl3)))); + assertThat(pl3, is(not(equalTo(null)))); + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/model/PositionDatumTest.java b/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/model/PositionDatumTest.java new file mode 100644 index 0000000000..cbaba2d044 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/model/PositionDatumTest.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nanoleaf.internal.model; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test for global orientation + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class PositionDatumTest { + + @Nullable + private PositionDatum pd1; + + @Nullable + private PositionDatum pd2; // different from pd1 + + @Nullable + private PositionDatum pd3; // same as pd1 + + @BeforeEach + public void setUp() { + pd1 = new PositionDatum(100, 200, 270, 123, 12); + pd2 = new PositionDatum(100, 220, 240, 123, 2); + pd3 = new PositionDatum(100, 200, 270, 123, 12); + } + + @Test + public void testHashCode() { + PositionDatum p1 = pd1; + PositionDatum p2 = pd2; + PositionDatum p3 = pd3; + if (p1 != null && p2 != null && p3 != null) { + assertThat(p1.hashCode(), is(equalTo(p3.hashCode()))); + assertThat(p2.hashCode(), is(not(equalTo(p3.hashCode())))); + } else { + assertThat("Should be initialized", false); + } + } + + @Test + public void testEquals() { + assertThat(pd1, is(equalTo(pd3))); + assertThat(pd2, is(not(equalTo(pd3)))); + assertThat(pd3, is(not(equalTo(null)))); + } +} -- 2.47.3