From c0ad308493aa30523d4ba064d67d43f723e58ce8 Mon Sep 17 00:00:00 2001 From: Wouter Born Date: Sun, 3 Oct 2021 00:10:23 +0200 Subject: [PATCH] ChartServlet bug fixes and improvements * Improve exception handling * Add transparent themes * Add null annotations * Use java.time classes instead of Date and magic numbers * Upgrade XChart to 3.1.0 * Fix buggy legend position logic: Reinitialize counter to 0. So it does not work on legend position counter values of previously created charts. Use a local variable for the position counter instead of a field. This prevents issues when creating multiple charts simultanuously. For XChart release notes see: https://knowm.org/open-source/xchart/xchart-change-log/ On newer XChart versions there is an issue when using customized grid lines: https://github.com/knowm/XChart/issues/628 Fixes #1183 Related to #2501 Supersedes #2415 Signed-off-by: Wouter Born --- .../openhab/core/io/rest/RESTConstants.java | 2 +- bundles/org.openhab.core.ui/pom.xml | 3 +- .../openhab/core/ui/chart/ChartProvider.java | 10 +- .../core/ui/internal/chart/ChartServlet.java | 102 ++++---- .../defaultchartprovider/ChartTheme.java | 3 + .../defaultchartprovider/ChartThemeBlack.java | 3 + .../ChartThemeBlackTransparent.java | 43 ++++ .../ChartThemeBright.java | 3 + .../ChartThemeBrightTransparent.java | 43 ++++ .../defaultchartprovider/ChartThemeDark.java | 3 + .../ChartThemeDarkTransparent.java | 43 ++++ .../defaultchartprovider/ChartThemeWhite.java | 3 + .../ChartThemeWhiteTransparent.java | 43 ++++ .../DefaultChartProvider.java | 227 ++++++++---------- 14 files changed, 351 insertions(+), 180 deletions(-) create mode 100644 bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartThemeBlackTransparent.java create mode 100644 bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartThemeBrightTransparent.java create mode 100644 bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartThemeDarkTransparent.java create mode 100644 bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartThemeWhiteTransparent.java diff --git a/bundles/org.openhab.core.io.rest/src/main/java/org/openhab/core/io/rest/RESTConstants.java b/bundles/org.openhab.core.io.rest/src/main/java/org/openhab/core/io/rest/RESTConstants.java index 4ff2d1bd3a5..c83dc4eb13b 100644 --- a/bundles/org.openhab.core.io.rest/src/main/java/org/openhab/core/io/rest/RESTConstants.java +++ b/bundles/org.openhab.core.io.rest/src/main/java/org/openhab/core/io/rest/RESTConstants.java @@ -24,5 +24,5 @@ public class RESTConstants { public static final String JAX_RS_NAME = "openhab"; - public static final String API_VERSION = "4"; + public static final String API_VERSION = "5"; } diff --git a/bundles/org.openhab.core.ui/pom.xml b/bundles/org.openhab.core.ui/pom.xml index ad61893bb30..ec985177b00 100644 --- a/bundles/org.openhab.core.ui/pom.xml +++ b/bundles/org.openhab.core.ui/pom.xml @@ -18,7 +18,8 @@ org.knowm.xchart xchart - 2.6.1 + + 3.1.0 org.openhab.core.bundles diff --git a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/chart/ChartProvider.java b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/chart/ChartProvider.java index 8d76b45da5b..c7b7a70ebcf 100644 --- a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/chart/ChartProvider.java +++ b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/chart/ChartProvider.java @@ -13,8 +13,10 @@ package org.openhab.core.ui.chart; import java.awt.image.BufferedImage; -import java.util.Date; +import java.time.ZonedDateTime; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.items.ItemNotFoundException; /** @@ -25,6 +27,7 @@ * @author Chris Jackson - Initial contribution * @author Holger Reichert - Support for themes, DPI, legend hiding */ +@NonNullByDefault public interface ChartProvider { /** * Gets the name of this chart provider. @@ -56,8 +59,9 @@ public interface ChartProvider { * @throws ItemNotFoundException if an item or group is not found * @throws IllegalArgumentException if an invalid argument is passed */ - BufferedImage createChart(String service, String theme, Date startTime, Date endTime, int height, int width, - String items, String groups, Integer dpi, Boolean legend) throws ItemNotFoundException; + BufferedImage createChart(@Nullable String service, @Nullable String theme, ZonedDateTime startTime, + ZonedDateTime endTime, int height, int width, @Nullable String items, @Nullable String groups, + @Nullable Integer dpi, @Nullable Boolean legend) throws ItemNotFoundException; /** * Gets the type of data that will be written by the chart. diff --git a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/ChartServlet.java b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/ChartServlet.java index 00e12d149e6..f7088bdd977 100644 --- a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/ChartServlet.java +++ b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/ChartServlet.java @@ -15,16 +15,16 @@ import static java.util.Map.entry; import java.awt.image.BufferedImage; -import java.io.EOFException; import java.io.IOException; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; -import javax.imageio.IIOException; import javax.imageio.ImageIO; import javax.imageio.stream.ImageOutputStream; import javax.servlet.ServletConfig; @@ -32,7 +32,10 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.config.core.ConfigurableService; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.io.http.servlet.OpenHABServlet; import org.openhab.core.items.ItemNotFoundException; import org.openhab.core.ui.chart.ChartProvider; @@ -65,6 +68,7 @@ * @author Chris Jackson - Initial contribution * @author Holger Reichert - Support for themes, DPI, legend hiding */ +@NonNullByDefault @Component(immediate = true, service = ChartServlet.class, configurationPid = "org.openhab.chart", // property = Constants.SERVICE_PID + "=org.openhab.chart") @ConfigurableService(category = "system", label = "Charts", description_uri = ChartServlet.CONFIG_URI) @@ -76,6 +80,9 @@ public class ChartServlet extends OpenHABServlet { private static final int CHART_HEIGHT = 240; private static final int CHART_WIDTH = 480; private static final String DATE_FORMAT = "yyyyMMddHHmm"; + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(DATE_FORMAT); + + private final TimeZoneProvider timeZoneProvider; private String providerName = "default"; private int defaultHeight = CHART_HEIGHT; @@ -86,21 +93,25 @@ public class ChartServlet extends OpenHABServlet { // The URI of this servlet public static final String SERVLET_NAME = "/chart"; - protected static final Map PERIODS = Map.ofEntries( // - entry("h", 3600000L), entry("4h", 14400000L), // - entry("8h", 28800000L), entry("12h", 43200000L), // - entry("D", 86400000L), entry("2D", 172800000L), // - entry("3D", 259200000L), entry("W", 604800000L), // - entry("2W", 1209600000L), entry("M", 2592000000L), // - entry("2M", 5184000000L), entry("4M", 10368000000L), // - entry("Y", 31536000000L)// + private static final Duration DEFAULT_PERIOD = Duration.ofDays(1); + + private static final Map PERIODS = Map.ofEntries( // + entry("h", Duration.ofHours(1)), entry("4h", Duration.ofHours(4)), // + entry("8h", Duration.ofHours(8)), entry("12h", Duration.ofHours(12)), // + entry("D", Duration.ofDays(1)), entry("2D", Duration.ofDays(2)), // + entry("3D", Duration.ofDays(3)), entry("W", Duration.ofDays(7)), // + entry("2W", Duration.ofDays(14)), entry("M", Duration.ofDays(30)), // + entry("2M", Duration.ofDays(60)), entry("4M", Duration.ofDays(120)), // + entry("Y", Duration.ofDays(365))// ); protected static final Map CHART_PROVIDERS = new ConcurrentHashMap<>(); @Activate - public ChartServlet(final @Reference HttpService httpService, final @Reference HttpContext httpContext) { + public ChartServlet(final @Reference HttpService httpService, final @Reference HttpContext httpContext, + final @Reference TimeZoneProvider timeZoneProvider) { super(httpService, httpContext); + this.timeZoneProvider = timeZoneProvider; } @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) @@ -128,7 +139,7 @@ protected void deactivate() { } @Modified - protected void modified(Map config) { + protected void modified(@Nullable Map config) { applyConfig(config); } @@ -137,7 +148,7 @@ protected void modified(Map config) { * * @param config the configuration */ - private void applyConfig(Map config) { + private void applyConfig(@Nullable Map config) { if (config == null) { return; } @@ -224,22 +235,14 @@ protected void doGet(HttpServletRequest req, HttpServletResponse res) throws Ser } // Read out the parameter period, begin and end and save them. - Long period = null; - Date timeBegin = null; - Date timeEnd = null; - - if (periodParam != null) { - period = PERIODS.get(periodParam); - } - if (period == null) { - // use a day as the default period - period = PERIODS.get("D"); - } + Duration period = periodParam == null ? DEFAULT_PERIOD : PERIODS.getOrDefault(periodParam, DEFAULT_PERIOD); + ZonedDateTime timeBegin = null; + ZonedDateTime timeEnd = null; if (timeBeginParam != null) { try { - timeBegin = new SimpleDateFormat(DATE_FORMAT).parse(timeBeginParam); - } catch (ParseException e) { + timeBegin = LocalDateTime.parse(timeBeginParam, FORMATTER).atZone(timeZoneProvider.getTimeZone()); + } catch (DateTimeParseException e) { res.sendError(HttpServletResponse.SC_BAD_REQUEST, "Begin and end must have this format: " + DATE_FORMAT + "."); return; @@ -248,8 +251,8 @@ protected void doGet(HttpServletRequest req, HttpServletResponse res) throws Ser if (timeEndParam != null) { try { - timeEnd = new SimpleDateFormat(DATE_FORMAT).parse(timeEndParam); - } catch (ParseException e) { + timeEnd = LocalDateTime.parse(timeEndParam, FORMATTER).atZone(timeZoneProvider.getTimeZone()); + } catch (DateTimeParseException e) { res.sendError(HttpServletResponse.SC_BAD_REQUEST, "Begin and end must have this format: " + DATE_FORMAT + "."); return; @@ -258,16 +261,16 @@ protected void doGet(HttpServletRequest req, HttpServletResponse res) throws Ser // Set begin and end time and check legality. if (timeBegin == null && timeEnd == null) { - timeEnd = new Date(); - timeBegin = new Date(timeEnd.getTime() - period); + timeEnd = ZonedDateTime.now(timeZoneProvider.getTimeZone()); + timeBegin = timeEnd.minus(period); logger.debug("No begin or end is specified, use now as end and now-period as begin."); } else if (timeEnd == null) { - timeEnd = new Date(timeBegin.getTime() + period); + timeEnd = timeBegin.plus(period); logger.debug("No end is specified, use begin + period as end."); } else if (timeBegin == null) { - timeBegin = new Date(timeEnd.getTime() - period); + timeBegin = timeEnd.minus(period); logger.debug("No begin is specified, use end-period as begin"); - } else if (timeEnd.before(timeBegin)) { + } else if (timeEnd.isBefore(timeBegin)) { throw new ServletException("The end is before the begin."); } @@ -311,46 +314,43 @@ protected void doGet(HttpServletRequest req, HttpServletResponse res) throws Ser width = maxWidth; } - // Set the content type to that provided by the chart provider - res.setContentType("image/" + provider.getChartType()); logger.debug("chart building with width {} height {} dpi {}", width, height, dpi); - try (ImageOutputStream imageOutputStream = ImageIO.createImageOutputStream(res.getOutputStream())) { + try { BufferedImage chart = provider.createChart(serviceName, req.getParameter("theme"), timeBegin, timeEnd, height, width, req.getParameter("items"), req.getParameter("groups"), dpi, legend); - ImageIO.write(chart, provider.getChartType().toString(), imageOutputStream); - logger.debug("Chart successfully generated and written to the response."); + // Set the content type to that provided by the chart provider + res.setContentType("image/" + provider.getChartType()); + try (ImageOutputStream imageOutputStream = ImageIO.createImageOutputStream(res.getOutputStream())) { + ImageIO.write(chart, provider.getChartType().toString(), imageOutputStream); + logger.debug("Chart successfully generated and written to the response."); + } } catch (ItemNotFoundException e) { logger.debug("{}", e.getMessage()); res.sendError(HttpServletResponse.SC_BAD_REQUEST, e.getMessage()); } catch (IllegalArgumentException e) { logger.warn("Illegal argument in chart: {}", e.getMessage()); res.sendError(HttpServletResponse.SC_BAD_REQUEST, "Illegal argument in chart: " + e.getMessage()); - } catch (IIOException | EOFException e) { + } catch (IOException e) { // this can happen if the request is terminated while the image is streamed, see // https://github.com/openhab/openhab-distro/issues/684 logger.debug("Failed writing image to response stream", e); } catch (RuntimeException e) { - if (logger.isDebugEnabled()) { - // we also attach the stack trace - logger.warn("Chart generation failed: {}", e.getMessage(), e); - } else { - logger.warn("Chart generation failed: {}", e.getMessage()); - } + logger.warn("Chart generation failed: {}", e.getMessage(), logger.isDebugEnabled() ? e : null); res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage()); } } @Override - public void init(ServletConfig config) throws ServletException { + public void init(@Nullable ServletConfig config) throws ServletException { } @Override - public ServletConfig getServletConfig() { + public @Nullable ServletConfig getServletConfig() { return null; } @Override - public String getServletInfo() { + public @Nullable String getServletInfo() { return null; } diff --git a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartTheme.java b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartTheme.java index f8b29333cbb..9ed04a4008d 100644 --- a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartTheme.java +++ b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartTheme.java @@ -15,11 +15,14 @@ import java.awt.Color; import java.awt.Font; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * Chart styling theme for the {@link DefaultChartProvider}. * * @author Holger Reichert - Initial contribution */ +@NonNullByDefault public interface ChartTheme { /** diff --git a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartThemeBlack.java b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartThemeBlack.java index 2f77327156c..63483240cb1 100644 --- a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartThemeBlack.java +++ b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartThemeBlack.java @@ -15,11 +15,14 @@ import java.awt.Color; import java.awt.Font; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * Implementation of the black {@link ChartTheme chart theme}. * * @author Holger Reichert - Initial contribution */ +@NonNullByDefault public class ChartThemeBlack implements ChartTheme { private static final String THEME_NAME = "black"; diff --git a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartThemeBlackTransparent.java b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartThemeBlackTransparent.java new file mode 100644 index 00000000000..afe25633eea --- /dev/null +++ b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartThemeBlackTransparent.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2010-2021 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.core.ui.internal.chart.defaultchartprovider; + +import java.awt.Color; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Implementation of the black {@link ChartTheme chart theme} with transparent background. + * + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public class ChartThemeBlackTransparent extends ChartThemeBlack { + + private static final String THEME_NAME = "black_transparent"; + + @Override + public String getThemeName() { + return THEME_NAME; + } + + @Override + public Color getPlotBackgroundColor() { + return new Color(0, 0, 0, 0); + } + + @Override + public Color getChartBackgroundColor() { + return new Color(0, 0, 0, 0); + } +} diff --git a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartThemeBright.java b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartThemeBright.java index 8d37f403cfa..960d495d259 100644 --- a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartThemeBright.java +++ b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartThemeBright.java @@ -15,11 +15,14 @@ import java.awt.Color; import java.awt.Font; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * Implementation of the default bright {@link ChartTheme chart theme}. * * @author Holger Reichert - Initial contribution */ +@NonNullByDefault public class ChartThemeBright implements ChartTheme { private static final String THEME_NAME = "bright"; diff --git a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartThemeBrightTransparent.java b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartThemeBrightTransparent.java new file mode 100644 index 00000000000..2d48f06cf92 --- /dev/null +++ b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartThemeBrightTransparent.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2010-2021 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.core.ui.internal.chart.defaultchartprovider; + +import java.awt.Color; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Implementation of the bright {@link ChartTheme chart theme} with transparent background. + * + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public class ChartThemeBrightTransparent extends ChartThemeBright { + + private static final String THEME_NAME = "bright_transparent"; + + @Override + public String getThemeName() { + return THEME_NAME; + } + + @Override + public Color getPlotBackgroundColor() { + return new Color(0, 0, 0, 0); + } + + @Override + public Color getChartBackgroundColor() { + return new Color(0, 0, 0, 0); + } +} diff --git a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartThemeDark.java b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartThemeDark.java index 6ca36f004ca..39620f2b4b0 100644 --- a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartThemeDark.java +++ b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartThemeDark.java @@ -15,11 +15,14 @@ import java.awt.Color; import java.awt.Font; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * Implementation of the dark {@link ChartTheme chart theme}. * * @author Holger Reichert - Initial contribution */ +@NonNullByDefault public class ChartThemeDark implements ChartTheme { private static final String THEME_NAME = "dark"; diff --git a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartThemeDarkTransparent.java b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartThemeDarkTransparent.java new file mode 100644 index 00000000000..42b365d7d6d --- /dev/null +++ b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartThemeDarkTransparent.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2010-2021 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.core.ui.internal.chart.defaultchartprovider; + +import java.awt.Color; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Implementation of the dark {@link ChartTheme chart theme} with transparent background. + * + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public class ChartThemeDarkTransparent extends ChartThemeDark { + + private static final String THEME_NAME = "dark_transparent"; + + @Override + public String getThemeName() { + return THEME_NAME; + } + + @Override + public Color getPlotBackgroundColor() { + return new Color(0, 0, 0, 0); + } + + @Override + public Color getChartBackgroundColor() { + return new Color(0, 0, 0, 0); + } +} diff --git a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartThemeWhite.java b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartThemeWhite.java index 20194b0c684..b18adf879db 100644 --- a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartThemeWhite.java +++ b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartThemeWhite.java @@ -15,11 +15,14 @@ import java.awt.Color; import java.awt.Font; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * Implementation of the white {@link ChartTheme chart theme}. * * @author Holger Reichert - Initial contribution */ +@NonNullByDefault public class ChartThemeWhite implements ChartTheme { private static final String THEME_NAME = "white"; diff --git a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartThemeWhiteTransparent.java b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartThemeWhiteTransparent.java new file mode 100644 index 00000000000..6d6559932b8 --- /dev/null +++ b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/ChartThemeWhiteTransparent.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2010-2021 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.core.ui.internal.chart.defaultchartprovider; + +import java.awt.Color; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Implementation of the white {@link ChartTheme chart theme} with transparent background. + * + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public class ChartThemeWhiteTransparent extends ChartThemeWhite { + + private static final String THEME_NAME = "white_transparent"; + + @Override + public String getThemeName() { + return THEME_NAME; + } + + @Override + public Color getPlotBackgroundColor() { + return new Color(0, 0, 0, 0); + } + + @Override + public Color getChartBackgroundColor() { + return new Color(0, 0, 0, 0); + } +} diff --git a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/DefaultChartProvider.java b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/DefaultChartProvider.java index 53154213513..7a644da2311 100644 --- a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/DefaultChartProvider.java +++ b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/chart/defaultchartprovider/DefaultChartProvider.java @@ -16,23 +16,26 @@ import java.awt.Color; import java.awt.Graphics2D; import java.awt.image.BufferedImage; +import java.time.Duration; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.ArrayList; -import java.util.Arrays; import java.util.Date; -import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.function.Function; import java.util.stream.Collectors; - -import org.knowm.xchart.Chart; -import org.knowm.xchart.ChartBuilder; -import org.knowm.xchart.Series; -import org.knowm.xchart.SeriesMarker; -import org.knowm.xchart.StyleManager.LegendPosition; -import org.openhab.core.i18n.TimeZoneProvider; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.knowm.xchart.XYChart; +import org.knowm.xchart.XYChartBuilder; +import org.knowm.xchart.XYSeries; +import org.knowm.xchart.style.Styler.LegendPosition; +import org.knowm.xchart.style.XYStyler; +import org.knowm.xchart.style.markers.None; import org.openhab.core.items.GroupItem; import org.openhab.core.items.Item; import org.openhab.core.items.ItemNotFoundException; @@ -65,40 +68,54 @@ * @author Holger Reichert - Support for themes, DPI, legend hiding * @author Christoph Weitkamp - Consider default persistence service */ +@NonNullByDefault @Component(immediate = true) public class DefaultChartProvider implements ChartProvider { - private final Logger logger = LoggerFactory.getLogger(DefaultChartProvider.class); + private static class LegendPositionDecider { + private int counter = 0; - private final TimeZoneProvider timeZoneProvider; - protected final ItemUIRegistry itemUIRegistry; - private final PersistenceServiceRegistry persistenceServiceRegistry; + private void addData(XYSeries series, List yData) { + // If the start value is below the median, then count legend position down + // Otherwise count up. + // We use this to decide whether to put the legend in the top or bottom corner. + if (yData.iterator().next().floatValue() > ((series.getYMax() - series.getYMin()) / 2 + series.getYMin())) { + counter++; + } else { + counter--; + } + } - private int legendPosition = 0; + private LegendPosition getLegendPosition() { + return counter < 0 ? LegendPosition.InsideNW : LegendPosition.InsideSW; + } + } - private static final ChartTheme[] CHART_THEMES_AVAILABLE = { new ChartThemeWhite(), new ChartThemeBright(), - new ChartThemeDark(), new ChartThemeBlack() }; - public static final String CHART_THEME_DEFAULT_NAME = "bright"; - private Map chartThemes = null; + private static final ChartTheme CHART_THEME_DEFAULT = new ChartThemeBright(); + private static final Map CHART_THEMES = Stream + .of(CHART_THEME_DEFAULT, new ChartThemeBrightTransparent(), // + new ChartThemeWhite(), new ChartThemeWhiteTransparent(), // + new ChartThemeDark(), new ChartThemeDarkTransparent(), // + new ChartThemeBlack(), new ChartThemeBlackTransparent()) // + .collect(Collectors.toMap(ChartTheme::getThemeName, Function.identity())); - public static final int DPI_DEFAULT = 96; + private static final int DPI_DEFAULT = 96; + + private final Logger logger = LoggerFactory.getLogger(DefaultChartProvider.class); + + private final ItemUIRegistry itemUIRegistry; + private final PersistenceServiceRegistry persistenceServiceRegistry; @Activate - public DefaultChartProvider(final @Reference TimeZoneProvider timeZoneProvider, - final @Reference ItemUIRegistry itemUIRegistry, + public DefaultChartProvider(final @Reference ItemUIRegistry itemUIRegistry, final @Reference PersistenceServiceRegistry persistenceServiceRegistry) { - this.timeZoneProvider = timeZoneProvider; this.itemUIRegistry = itemUIRegistry; this.persistenceServiceRegistry = persistenceServiceRegistry; - } - @Activate - protected void activate() { - logger.debug("Starting up default chart provider."); - String themeNames = Arrays.stream(CHART_THEMES_AVAILABLE) // - .map(t -> t.getThemeName()) // - .collect(Collectors.joining(", ")); - logger.debug("Available themes for default chart provider: {}", themeNames); + if (logger.isDebugEnabled()) { + logger.debug("Available themes for default chart provider: {}", + CHART_THEMES.keySet().stream().collect(Collectors.joining(", "))); + } } @Override @@ -107,8 +124,9 @@ public String getName() { } @Override - public BufferedImage createChart(String serviceId, String theme, Date startTime, Date endTime, int height, - int width, String items, String groups, Integer dpiValue, Boolean legend) + public BufferedImage createChart(@Nullable String serviceId, @Nullable String theme, ZonedDateTime startTime, + ZonedDateTime endTime, int height, int width, @Nullable String items, @Nullable String groups, + @Nullable Integer dpiValue, @Nullable Boolean legend) throws ItemNotFoundException, IllegalArgumentException { logger.debug( "Rendering chart: service: '{}', theme: '{}', startTime: '{}', endTime: '{}', width: '{}', height: '{}', items: '{}', groups: '{}', dpi: '{}', legend: '{}'", @@ -130,61 +148,62 @@ public BufferedImage createChart(String serviceId, String theme, Date startTime, int seriesCounter = 0; // get theme - ChartTheme chartTheme = getChartTheme(theme); + ChartTheme chartTheme = theme == null || theme.isBlank() ? CHART_THEME_DEFAULT + : CHART_THEMES.getOrDefault(theme, CHART_THEME_DEFAULT); // get DPI - int dpi; - if (dpiValue != null && dpiValue > 0) { - dpi = dpiValue; - } else { - dpi = DPI_DEFAULT; - } + int dpi = dpiValue != null && dpiValue > 0 ? dpiValue : DPI_DEFAULT; // Create Chart - Chart chart = new ChartBuilder().width(width).height(height).build(); + XYChart chart = new XYChartBuilder().width(width).height(height).build(); // Define the time axis - the defaults are not very nice - long period = (endTime.getTime() - startTime.getTime()) / 1000; + Duration period = Duration.between(startTime, endTime); String pattern = "HH:mm"; - if (period <= 600) { // 10 minutes + + if (period.compareTo(Duration.ofMinutes(10)) <= 0) { pattern = "mm:ss"; - } else if (period <= 86400) { // 1 day + } else if (period.compareTo(Duration.ofDays(1)) <= 0) { pattern = "HH:mm"; - } else if (period <= 604800) { // 1 week + } else if (period.compareTo(Duration.ofDays(7)) <= 0) { pattern = "EEE d"; } else { pattern = "d MMM"; } - chart.getStyleManager().setDatePattern(pattern); + XYStyler styler = chart.getStyler(); + styler.setDatePattern(pattern); // axis - chart.getStyleManager().setAxisTickLabelsFont(chartTheme.getAxisTickLabelsFont(dpi)); - chart.getStyleManager().setAxisTickLabelsColor(chartTheme.getAxisTickLabelsColor()); - chart.getStyleManager().setXAxisMin(startTime.getTime()); - chart.getStyleManager().setXAxisMax(endTime.getTime()); + styler.setAxisTickLabelsFont(chartTheme.getAxisTickLabelsFont(dpi)); + styler.setAxisTickLabelsColor(chartTheme.getAxisTickLabelsColor()); + styler.setXAxisMin(Double.valueOf(startTime.toInstant().toEpochMilli())); + styler.setXAxisMax(Double.valueOf(endTime.toInstant().toEpochMilli())); int yAxisSpacing = Math.max(height / 10, chartTheme.getAxisTickLabelsFont(dpi).getSize()); - chart.getStyleManager().setYAxisTickMarkSpacingHint(yAxisSpacing); + styler.setYAxisTickMarkSpacingHint(yAxisSpacing); // chart - chart.getStyleManager().setChartBackgroundColor(chartTheme.getChartBackgroundColor()); - chart.getStyleManager().setChartFontColor(chartTheme.getChartFontColor()); - chart.getStyleManager().setChartPadding(chartTheme.getChartPadding(dpi)); - chart.getStyleManager().setPlotBackgroundColor(chartTheme.getPlotBackgroundColor()); + styler.setChartBackgroundColor(chartTheme.getChartBackgroundColor()); + styler.setChartFontColor(chartTheme.getChartFontColor()); + styler.setChartPadding(chartTheme.getChartPadding(dpi)); + styler.setPlotBackgroundColor(chartTheme.getPlotBackgroundColor()); float plotGridLinesDash = (float) chartTheme.getPlotGridLinesDash(dpi); float[] plotGridLinesDashArray = { plotGridLinesDash, plotGridLinesDash }; - chart.getStyleManager().setPlotGridLinesStroke( - new BasicStroke((float) chartTheme.getPlotGridLinesWidth(dpi), 0, 2, 10, plotGridLinesDashArray, 0)); - chart.getStyleManager().setPlotGridLinesColor(chartTheme.getPlotGridLinesColor()); + styler.setPlotGridLinesStroke(new BasicStroke((float) chartTheme.getPlotGridLinesWidth(dpi), + BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 10, plotGridLinesDashArray, 0)); + styler.setPlotGridLinesColor(chartTheme.getPlotGridLinesColor()); // legend - chart.getStyleManager().setLegendBackgroundColor(chartTheme.getLegendBackgroundColor()); - chart.getStyleManager().setLegendFont(chartTheme.getLegendFont(dpi)); - chart.getStyleManager().setLegendSeriesLineLength(chartTheme.getLegendSeriesLineLength(dpi)); + styler.setLegendBackgroundColor(chartTheme.getLegendBackgroundColor()); + styler.setLegendFont(chartTheme.getLegendFont(dpi)); + styler.setLegendSeriesLineLength(chartTheme.getLegendSeriesLineLength(dpi)); + + LegendPositionDecider legendPositionDecider = new LegendPositionDecider(); // Loop through all the items if (items != null) { String[] itemNames = items.split(","); for (String itemName : itemNames) { Item item = itemUIRegistry.getItem(itemName); - if (addItem(chart, persistenceService, startTime, endTime, item, seriesCounter, chartTheme, dpi)) { + if (addItem(chart, persistenceService, startTime, endTime, item, seriesCounter, chartTheme, dpi, + legendPositionDecider)) { seriesCounter++; } } @@ -199,7 +218,7 @@ public BufferedImage createChart(String serviceId, String theme, Date startTime, GroupItem groupItem = (GroupItem) item; for (Item member : groupItem.getMembers()) { if (addItem(chart, persistenceService, startTime, endTime, member, seriesCounter, chartTheme, - dpi)) { + dpi, legendPositionDecider)) { seriesCounter++; } } @@ -219,13 +238,13 @@ public BufferedImage createChart(String serviceId, String theme, Date startTime, List xData = new ArrayList<>(); List yData = new ArrayList<>(); - xData.add(startTime); + xData.add(Date.from(startTime.toInstant())); yData.add(0); - xData.add(endTime); + xData.add(Date.from(endTime.toInstant())); yData.add(0); - Series series = chart.addSeries("NONE", xData, yData); - series.setMarker(SeriesMarker.NONE); + XYSeries series = chart.addSeries("NONE", xData, yData); + series.setMarker(new None()); series.setLineStyle(new BasicStroke(0f)); } @@ -243,40 +262,37 @@ public BufferedImage createChart(String serviceId, String theme, Date startTime, // Legend position (top-left or bottom-left) is dynamically selected based on the data // This won't be perfect, but it's a good compromise if (showLegend) { - if (legendPosition < 0) { - chart.getStyleManager().setLegendPosition(LegendPosition.InsideNW); - } else { - chart.getStyleManager().setLegendPosition(LegendPosition.InsideSW); - } + styler.setLegendPosition(legendPositionDecider.getLegendPosition()); } else { // hide the whole legend - chart.getStyleManager().setLegendVisible(false); + styler.setLegendVisible(false); } // Write the chart as a PNG image BufferedImage lBufferedImage = new BufferedImage(chart.getWidth(), chart.getHeight(), BufferedImage.TYPE_INT_ARGB); Graphics2D lGraphics2D = lBufferedImage.createGraphics(); - chart.paint(lGraphics2D); + chart.paint(lGraphics2D, chart.getWidth(), chart.getHeight()); return lBufferedImage; } - double convertData(State state) { + private double convertData(State state) { if (state instanceof DecimalType) { return ((DecimalType) state).doubleValue(); } else if (state instanceof QuantityType) { return ((QuantityType) state).doubleValue(); } else if (state instanceof OnOffType) { - return (state == OnOffType.OFF) ? 0 : 1; + return state == OnOffType.OFF ? 0 : 1; } else if (state instanceof OpenClosedType) { - return (state == OpenClosedType.CLOSED) ? 0 : 1; + return state == OpenClosedType.CLOSED ? 0 : 1; } else { logger.debug("Unsupported item type in chart: {}", state.getClass().toString()); return 0; } } - boolean addItem(Chart chart, QueryablePersistenceService service, Date timeBegin, Date timeEnd, Item item, - int seriesCounter, ChartTheme chartTheme, int dpi) { + private boolean addItem(XYChart chart, QueryablePersistenceService service, ZonedDateTime timeBegin, + ZonedDateTime timeEnd, Item item, int seriesCounter, ChartTheme chartTheme, int dpi, + LegendPositionDecider legendPositionDecider) { Color color = chartTheme.getLineColor(seriesCounter); // Get the item label @@ -301,7 +317,7 @@ boolean addItem(Chart chart, QueryablePersistenceService service, Date timeBegin // This is necessary for values that don't change often otherwise data will start // after the start of the graph (or not at all if there's no change during the graph period) filter = new FilterCriteria(); - filter.setEndDate(ZonedDateTime.ofInstant(timeBegin.toInstant(), timeZoneProvider.getTimeZone())); + filter.setEndDate(timeBegin); filter.setItemName(item.getName()); filter.setPageSize(1); filter.setOrdering(Ordering.DESCENDING); @@ -310,13 +326,13 @@ boolean addItem(Chart chart, QueryablePersistenceService service, Date timeBegin HistoricItem historicItem = result.iterator().next(); state = historicItem.getState(); - xData.add(timeBegin); + xData.add(Date.from(timeBegin.toInstant())); yData.add(convertData(state)); } // Now, get all the data between the start and end time - filter.setBeginDate(ZonedDateTime.ofInstant(timeBegin.toInstant(), timeZoneProvider.getTimeZone())); - filter.setEndDate(ZonedDateTime.ofInstant(timeEnd.toInstant(), timeZoneProvider.getTimeZone())); + filter.setBeginDate(timeBegin); + filter.setEndDate(timeEnd); filter.setPageSize(Integer.MAX_VALUE); filter.setOrdering(Ordering.ASCENDING); @@ -342,7 +358,7 @@ boolean addItem(Chart chart, QueryablePersistenceService service, Date timeBegin // Lastly, add the final state at the endtime if (state != null) { - xData.add(timeEnd); + xData.add(Date.from(timeEnd.toInstant())); yData.add(convertData(state)); } @@ -358,55 +374,18 @@ boolean addItem(Chart chart, QueryablePersistenceService service, Date timeBegin yData.add(yData.iterator().next()); } - Series series = chart.addSeries(label, xData, yData); + XYSeries series = chart.addSeries(label, xData, yData); float lineWidth = (float) chartTheme.getLineWidth(dpi); series.setLineStyle(new BasicStroke(lineWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER)); - series.setMarker(SeriesMarker.NONE); + series.setMarker(new None()); series.setLineColor(color); - // If the start value is below the median, then count legend position down - // Otherwise count up. - // We use this to decide whether to put the legend in the top or bottom corner. - if (yData.iterator().next().floatValue() > ((series.getYMax() - series.getYMin()) / 2 + series.getYMin())) { - legendPosition++; - } else { - legendPosition--; - } - + legendPositionDecider.addData(series, yData); return true; } @Override public ImageType getChartType() { - return (ImageType.png); - } - - /** - * Retrieve a chart theme by it's name. If no name is given or no theme with the given name exists, the - * {@link DefaultChartProvider#CHART_THEME_DEFAULT_NAME default theme} gets returned. - * - * @param name the {@link ChartTheme#getThemeName() theme name} - * @return {@link ChartTheme} - */ - private ChartTheme getChartTheme(String name) { - // if the static chartThemes hashmap is nul, we have to fill it first with all available themes, - // based on the theme name - if (chartThemes == null) { - chartThemes = new HashMap<>(); - for (ChartTheme theme : CHART_THEMES_AVAILABLE) { - chartThemes.put(theme.getThemeName(), theme); - } - } - String chartThemeName = name; - // no theme name -> default theme - if (name == null || name.isBlank()) { - chartThemeName = CHART_THEME_DEFAULT_NAME; - } - ChartTheme chartTheme = chartThemes.get(chartThemeName); - if (chartTheme == null) { - // no theme with the given name found -> default theme - chartTheme = chartThemes.get(CHART_THEME_DEFAULT_NAME); - } - return chartTheme; + return ImageType.png; } }