org.eclipse.smarthome.ui.internal.chart.defaultchartprovider.DefaultChartProvider.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.smarthome.ui.internal.chart.defaultchartprovider.DefaultChartProvider.java

Source

/**
 * Copyright (c) 2014,2018 Contributors to the Eclipse Foundation
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information regarding copyright ownership.
 *
 * 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.eclipse.smarthome.ui.internal.chart.defaultchartprovider;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.apache.commons.lang.StringUtils;
import org.eclipse.smarthome.core.i18n.TimeZoneProvider;
import org.eclipse.smarthome.core.items.GroupItem;
import org.eclipse.smarthome.core.items.Item;
import org.eclipse.smarthome.core.items.ItemNotFoundException;
import org.eclipse.smarthome.core.library.types.DecimalType;
import org.eclipse.smarthome.core.library.types.OnOffType;
import org.eclipse.smarthome.core.library.types.OpenClosedType;
import org.eclipse.smarthome.core.persistence.FilterCriteria;
import org.eclipse.smarthome.core.persistence.FilterCriteria.Ordering;
import org.eclipse.smarthome.core.persistence.HistoricItem;
import org.eclipse.smarthome.core.persistence.PersistenceService;
import org.eclipse.smarthome.core.persistence.PersistenceServiceRegistry;
import org.eclipse.smarthome.core.persistence.QueryablePersistenceService;
import org.eclipse.smarthome.core.types.State;
import org.eclipse.smarthome.ui.chart.ChartProvider;
import org.eclipse.smarthome.ui.internal.chart.ChartServlet;
import org.eclipse.smarthome.ui.items.ItemUIRegistry;
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.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This default chart provider generates time-series charts for a given set of items.
 *
 * See {@link ChartProvider} and {@link ChartServlet} for further details.
 *
 * @author Chris Jackson - Initial contribution
 * @author Holger Reichert - Support for themes, DPI, legend hiding
 * @author Christoph Weitkamp - Consider default persistence service
 */
@Component(immediate = true)
public class DefaultChartProvider implements ChartProvider {

    private final Logger logger = LoggerFactory.getLogger(DefaultChartProvider.class);

    private TimeZoneProvider timeZoneProvider;
    protected ItemUIRegistry itemUIRegistry;
    private PersistenceServiceRegistry persistenceServiceRegistry;

    private int legendPosition = 0;

    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<String, ChartTheme> chartThemes = null;

    public static final int DPI_DEFAULT = 96;

    @Reference
    public void setItemUIRegistry(ItemUIRegistry itemUIRegistry) {
        this.itemUIRegistry = itemUIRegistry;
    }

    public void unsetItemUIRegistry(ItemUIRegistry itemUIRegistry) {
        this.itemUIRegistry = null;
    }

    @Reference
    protected void setPersistenceServiceRegistry(PersistenceServiceRegistry persistenceServiceRegistry) {
        this.persistenceServiceRegistry = persistenceServiceRegistry;
    }

    protected void unsetPersistenceServiceRegistry(PersistenceServiceRegistry persistenceServiceRegistry) {
        this.persistenceServiceRegistry = null;
    }

    @Reference
    public void setTimeZoneProvider(TimeZoneProvider timeZoneProvider) {
        this.timeZoneProvider = timeZoneProvider;
    }

    public void unsetTimeZoneProvider(TimeZoneProvider timeZoneProvider) {
        this.timeZoneProvider = null;
    }

    @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);
    }

    @Override
    public String getName() {
        return "default";
    }

    @Override
    public BufferedImage createChart(String serviceId, String theme, Date startTime, Date endTime, int height,
            int width, String items, String groups, Integer dpiValue, Boolean legend)
            throws ItemNotFoundException, IllegalArgumentException {
        logger.debug(
                "Rendering chart: service: '{}', theme: '{}', startTime: '{}', endTime: '{}', width: '{}', height: '{}', items: '{}', groups: '{}', dpi: '{}', legend: '{}'",
                serviceId, theme, startTime, endTime, width, height, items, groups, dpiValue, legend);

        // If a persistence service is specified, find the provider, or use the default provider
        PersistenceService service = (serviceId == null) ? persistenceServiceRegistry.getDefault()
                : persistenceServiceRegistry.get(serviceId);

        // Did we find a service?
        QueryablePersistenceService persistenceService = (service instanceof QueryablePersistenceService)
                ? (QueryablePersistenceService) service
                : (QueryablePersistenceService) persistenceServiceRegistry.getAll() //
                        .stream() //
                        .filter(it -> it instanceof QueryablePersistenceService) //
                        .findFirst() //
                        .orElseThrow(() -> new IllegalArgumentException("No Persistence service found."));

        int seriesCounter = 0;

        // get theme
        ChartTheme chartTheme = getChartTheme(theme);

        // get DPI
        int dpi;
        if (dpiValue != null && dpiValue > 0) {
            dpi = dpiValue;
        } else {
            dpi = DPI_DEFAULT;
        }

        // Create Chart
        Chart chart = new ChartBuilder().width(width).height(height).build();

        // Define the time axis - the defaults are not very nice
        long period = (endTime.getTime() - startTime.getTime()) / 1000;
        String pattern = "HH:mm";
        if (period <= 600) { // 10 minutes
            pattern = "mm:ss";
        } else if (period <= 86400) { // 1 day
            pattern = "HH:mm";
        } else if (period <= 604800) { // 1 week
            pattern = "EEE d";
        } else {
            pattern = "d MMM";
        }

        chart.getStyleManager().setDatePattern(pattern);
        // axis
        chart.getStyleManager().setAxisTickLabelsFont(chartTheme.getAxisTickLabelsFont(dpi));
        chart.getStyleManager().setAxisTickLabelsColor(chartTheme.getAxisTickLabelsColor());
        chart.getStyleManager().setXAxisMin(startTime.getTime());
        chart.getStyleManager().setXAxisMax(endTime.getTime());
        int yAxisSpacing = Math.max(height / 10, chartTheme.getAxisTickLabelsFont(dpi).getSize());
        chart.getStyleManager().setYAxisTickMarkSpacingHint(yAxisSpacing);
        // chart
        chart.getStyleManager().setChartBackgroundColor(chartTheme.getChartBackgroundColor());
        chart.getStyleManager().setChartFontColor(chartTheme.getChartFontColor());
        chart.getStyleManager().setChartPadding(chartTheme.getChartPadding(dpi));
        chart.getStyleManager().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());
        // legend
        chart.getStyleManager().setLegendBackgroundColor(chartTheme.getLegendBackgroundColor());
        chart.getStyleManager().setLegendFont(chartTheme.getLegendFont(dpi));
        chart.getStyleManager().setLegendSeriesLineLength(chartTheme.getLegendSeriesLineLength(dpi));

        // 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)) {
                    seriesCounter++;
                }
            }
        }

        // Loop through all the groups and add each item from each group
        if (groups != null) {
            String[] groupNames = groups.split(",");
            for (String groupName : groupNames) {
                Item item = itemUIRegistry.getItem(groupName);
                if (item instanceof GroupItem) {
                    GroupItem groupItem = (GroupItem) item;
                    for (Item member : groupItem.getMembers()) {
                        if (addItem(chart, persistenceService, startTime, endTime, member, seriesCounter,
                                chartTheme, dpi)) {
                            seriesCounter++;
                        }
                    }
                } else {
                    throw new ItemNotFoundException(
                            "Item '" + item.getName() + "' defined in groups is not a group.");
                }
            }
        }

        Boolean showLegend = null;

        // If there are no series, render a blank chart
        if (seriesCounter == 0) {
            // always hide the legend
            showLegend = false;

            List<Date> xData = new ArrayList<Date>();
            List<Number> yData = new ArrayList<Number>();

            xData.add(startTime);
            yData.add(0);
            xData.add(endTime);
            yData.add(0);

            Series series = chart.addSeries("NONE", xData, yData);
            series.setMarker(SeriesMarker.NONE);
            series.setLineStyle(new BasicStroke(0f));
        }

        // if the legend is not already hidden, check if legend parameter is supplied, or calculate a sensible value
        if (showLegend == null) {
            if (legend == null) {
                // more than one series, show the legend. otherwise hide it.
                showLegend = seriesCounter > 1;
            } else {
                // take value from supplied legend parameter
                showLegend = legend;
            }
        }

        // 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);
            }
        } else { // hide the whole legend
            chart.getStyleManager().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);
        return lBufferedImage;
    }

    double convertData(State state) {
        if (state instanceof DecimalType) {
            return ((DecimalType) state).doubleValue();
        } else if (state instanceof OnOffType) {
            return (state == OnOffType.OFF) ? 0 : 1;
        } else if (state instanceof OpenClosedType) {
            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) {
        Color color = chartTheme.getLineColor(seriesCounter);

        // Get the item label
        String label = null;
        if (itemUIRegistry != null) {
            // Get the item label
            label = itemUIRegistry.getLabel(item.getName());
            if (label != null && label.contains("[") && label.contains("]")) {
                label = label.substring(0, label.indexOf('['));
            }
        }
        if (label == null) {
            label = item.getName();
        }

        Iterable<HistoricItem> result;
        FilterCriteria filter;

        // Generate data collections
        List<Date> xData = new ArrayList<Date>();
        List<Number> yData = new ArrayList<Number>();

        // Declare state here so it will hold the last value at the end of the process
        State state = null;

        // First, get the value at the start time.
        // 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.setItemName(item.getName());
        filter.setPageSize(1);
        filter.setOrdering(Ordering.DESCENDING);
        result = service.query(filter);
        if (result.iterator().hasNext()) {
            HistoricItem historicItem = result.iterator().next();

            state = historicItem.getState();
            xData.add(timeBegin);
            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.setPageSize(Integer.MAX_VALUE);
        filter.setOrdering(Ordering.ASCENDING);

        // Get the data from the persistence store
        result = service.query(filter);
        Iterator<HistoricItem> it = result.iterator();

        // Iterate through the data
        while (it.hasNext()) {
            HistoricItem historicItem = it.next();

            // For 'binary' states, we need to replicate the data
            // to avoid diagonal lines
            if (state instanceof OnOffType || state instanceof OpenClosedType) {
                Calendar cal = Calendar.getInstance();
                cal.setTime(historicItem.getTimestamp());
                cal.add(Calendar.MILLISECOND, -1);
                xData.add(cal.getTime());
                yData.add(convertData(state));
            }

            state = historicItem.getState();
            xData.add(historicItem.getTimestamp());
            yData.add(convertData(state));
        }

        // Lastly, add the final state at the endtime
        if (state != null) {
            xData.add(timeEnd);
            yData.add(convertData(state));
        }

        // Add the new series to the chart - only if there's data elements to display
        // The chart engine will throw an exception if there's no data
        if (xData.size() == 0) {
            return false;
        }

        // If there's only 1 data point, plot it again!
        if (xData.size() == 1) {
            xData.add(xData.iterator().next());
            yData.add(yData.iterator().next());
        }

        Series 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.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--;
        }

        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 (StringUtils.isBlank(name)) {
            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;
    }

}