teichmann@5863: /* Copyright (C) 2011, 2012, 2013 by Bundesanstalt für Gewässerkunde teichmann@5863: * Software engineering by Intevation GmbH teichmann@5863: * teichmann@5994: * This file is Free Software under the GNU AGPL (>=v3) teichmann@5863: * and comes with ABSOLUTELY NO WARRANTY! Check out the teichmann@5994: * documentation coming with Dive4Elements River for details. teichmann@5863: */ teichmann@5863: teichmann@5831: package org.dive4elements.river.artifacts.services; sascha@2615: sascha@3146: import java.awt.BasicStroke; sascha@3146: import java.awt.Color; sascha@2620: import java.awt.Dimension; sascha@2620: import java.awt.Transparency; sascha@3162: import java.awt.geom.Rectangle2D; sascha@2620: import java.awt.image.BufferedImage; sascha@2620: import java.io.ByteArrayOutputStream; sascha@2620: import java.io.IOException; sascha@2620: import java.util.ArrayList; sascha@2620: import java.util.List; sascha@2620: sascha@2620: import javax.imageio.ImageIO; sascha@2620: sascha@2614: import org.apache.log4j.Logger; gernotbelger@9312: import org.dive4elements.artifactdatabase.DefaultService; gernotbelger@9312: import org.dive4elements.artifacts.CallMeta; gernotbelger@9312: import org.dive4elements.artifacts.GlobalContext; gernotbelger@9312: import org.dive4elements.artifacts.Service; gernotbelger@9312: import org.dive4elements.river.artifacts.model.FixingsColumn; gernotbelger@9312: import org.dive4elements.river.artifacts.model.FixingsColumnFactory; gernotbelger@9312: import org.dive4elements.river.artifacts.model.FixingsFilterBuilder; gernotbelger@9312: import org.dive4elements.river.artifacts.model.FixingsOverview; gernotbelger@9312: import org.dive4elements.river.artifacts.model.FixingsOverview.Fixing; gernotbelger@9312: import org.dive4elements.river.artifacts.model.FixingsOverviewFactory; gernotbelger@9312: import org.dive4elements.river.artifacts.model.GaugeFinder; gernotbelger@9312: import org.dive4elements.river.artifacts.model.GaugeFinderFactory; gernotbelger@9312: import org.dive4elements.river.artifacts.model.GaugeRange; gernotbelger@9312: import org.dive4elements.river.artifacts.model.fixings.QWI; gernotbelger@9312: import org.dive4elements.river.artifacts.resources.Resources; gernotbelger@9312: import org.dive4elements.river.backend.SessionHolder; gernotbelger@9312: import org.dive4elements.river.jfree.ShapeRenderer; gernotbelger@9312: import org.dive4elements.river.utils.Formatter; gernotbelger@9312: import org.dive4elements.river.utils.Pair; sascha@2620: import org.jfree.chart.ChartFactory; sascha@2620: import org.jfree.chart.JFreeChart; sascha@3162: import org.jfree.chart.LegendItemCollection; sascha@3162: import org.jfree.chart.axis.NumberAxis; sascha@3146: import org.jfree.chart.plot.Marker; sascha@2620: import org.jfree.chart.plot.PlotOrientation; sascha@3146: import org.jfree.chart.plot.ValueMarker; sascha@2620: import org.jfree.chart.plot.XYPlot; sascha@3162: import org.jfree.data.Range; sascha@3146: import org.jfree.ui.RectangleAnchor; sascha@3146: import org.jfree.ui.TextAnchor; sascha@2614: import org.w3c.dom.Document; sascha@2615: import org.w3c.dom.Element; sascha@2615: import org.w3c.dom.NodeList; sascha@2614: felix@3571: /** Serve chart of Fixings at certain km. */ gernotbelger@9312: public class FixingsKMChartService extends DefaultService { gernotbelger@9312: private static final Logger log = Logger.getLogger(FixingsKMChartService.class); sascha@2614: gernotbelger@9312: public static final int DEFAULT_WIDTH = 240; sascha@2620: public static final int DEFAULT_HEIGHT = 180; sascha@2620: gernotbelger@9312: public static final String[] I18N_Q_SECTOR_BOARDERS = { "fix.km.chart.q.sector.border0", "fix.km.chart.q.sector.border1", "fix.km.chart.q.sector.border2" }; sascha@3170: gernotbelger@9312: public static final String[] DEFAULT_Q_SECTOR_BORDERS = { "(MNQ + MQ)/2", "(MQ + MHQ)/2", "HQ5" }; sascha@3170: gernotbelger@9312: public static final String I18N_CHART_LABEL_DATE = "fix.km.chart.label.date"; sascha@3173: gernotbelger@9312: public static final String DEFAULT_CHART_LABEL_DATE = "yyyy/MM/dd"; sascha@3173: gernotbelger@9312: public static final String I18N_CHART_TITLE = "fix.km.chart.title"; sascha@3173: gernotbelger@9312: public static final String DEFAULT_CHART_TITLE = "Fixings {0} km {1,number,#.###}"; gernotbelger@9312: gernotbelger@9312: public static final String I18N_Q_AXIS = "common.export.csv.header.q"; gernotbelger@9312: gernotbelger@9312: public static final String DEFAULT_Q_AXIS = "Q [m\u00b3/s]"; gernotbelger@9312: gernotbelger@9312: public static final String I18N_W_AXIS = "fix.km.chart.w.axis"; gernotbelger@9312: gernotbelger@9312: public static final String DEFAULT_W_AXIS = "W [NN + m]"; gernotbelger@9312: gernotbelger@9312: public static final String I18N_MEASURED = "fix.km.chart.measured"; gernotbelger@9312: gernotbelger@9312: public static final String DEFAULT_MEASURED = "measured"; gernotbelger@9312: gernotbelger@9312: public static final String I18N_INTERPOLATED = "fix.km.chart.interpolated"; gernotbelger@9312: gernotbelger@9312: public static final String DEFAULT_INTERPOLATED = "interpolated"; sascha@3170: sascha@2620: public static final String DEFAULT_FORMAT = "png"; sascha@2620: sascha@2620: // TODO: Load fancy image from resources. gernotbelger@9312: public static final byte[] EMPTY = { (byte) 0x89, (byte) 0x50, (byte) 0x4e, (byte) 0x47, (byte) 0x0d, (byte) 0x0a, (byte) 0x1a, (byte) 0x0a, (byte) 0x00, gernotbelger@9312: (byte) 0x00, (byte) 0x00, (byte) 0x0d, (byte) 0x49, (byte) 0x48, (byte) 0x44, (byte) 0x52, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, gernotbelger@9312: (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x08, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x3a, (byte) 0x7e, gernotbelger@9312: (byte) 0x9b, (byte) 0x55, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x73, (byte) 0x52, (byte) 0x47, (byte) 0x42, (byte) 0x00, gernotbelger@9312: (byte) 0xae, (byte) 0xce, (byte) 0x1c, (byte) 0xe9, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x09, (byte) 0x70, (byte) 0x48, (byte) 0x59, gernotbelger@9312: (byte) 0x73, (byte) 0x00, (byte) 0x00, (byte) 0x0b, (byte) 0x13, (byte) 0x00, (byte) 0x00, (byte) 0x0b, (byte) 0x13, (byte) 0x01, (byte) 0x00, gernotbelger@9312: (byte) 0x9a, (byte) 0x9c, (byte) 0x18, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x07, (byte) 0x74, (byte) 0x49, (byte) 0x4d, (byte) 0x45, gernotbelger@9312: (byte) 0x07, (byte) 0xdc, (byte) 0x04, (byte) 0x04, (byte) 0x10, (byte) 0x30, (byte) 0x15, (byte) 0x7d, (byte) 0x77, (byte) 0x36, (byte) 0x0b, gernotbelger@9312: (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x08, (byte) 0x74, (byte) 0x45, (byte) 0x58, (byte) 0x74, (byte) 0x43, (byte) 0x6f, (byte) 0x6d, gernotbelger@9312: (byte) 0x6d, (byte) 0x65, (byte) 0x6e, (byte) 0x74, (byte) 0x00, (byte) 0xf6, (byte) 0xcc, (byte) 0x96, (byte) 0xbf, (byte) 0x00, (byte) 0x00, gernotbelger@9312: (byte) 0x00, (byte) 0x0a, (byte) 0x49, (byte) 0x44, (byte) 0x41, (byte) 0x54, (byte) 0x08, (byte) 0xd7, (byte) 0x63, (byte) 0xf8, (byte) 0x0f, gernotbelger@9312: (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x1b, (byte) 0xb6, (byte) 0xee, (byte) 0x56, (byte) 0x00, (byte) 0x00, gernotbelger@9312: (byte) 0x00, (byte) 0x00, (byte) 0x49, (byte) 0x45, (byte) 0x4e, (byte) 0x44, (byte) 0xae, (byte) 0x42, (byte) 0x60, (byte) 0x82 }; sascha@2615: sascha@2615: private static final Output empty() { sascha@2615: return new Output(EMPTY, "image/png"); sascha@2615: } sascha@2614: sascha@2614: @Override gernotbelger@9312: public Service.Output process(final Document data, final GlobalContext globalContext, final CallMeta callMeta) { sascha@2614: log.debug("FixingsKMChartService.process"); sascha@2615: sascha@2615: SessionHolder.acquire(); sascha@2615: try { sascha@2615: return doProcess(data, globalContext, callMeta); gernotbelger@9312: } finally { sascha@2615: SessionHolder.release(); sascha@2615: } sascha@2615: } sascha@2615: gernotbelger@9312: protected Service.Output doProcess(final Document input, final GlobalContext globalContext, final CallMeta callMeta) { gernotbelger@9312: final String river = getRiverName(input); gernotbelger@9312: final Double km = getKM(input); gernotbelger@9312: final Dimension extent = getExtent(input); gernotbelger@9312: final String format = getFormat(input); sascha@2615: sascha@2620: if (river == null || km == null) { sascha@2620: log.warn("River and/or km invalid."); sascha@2615: return empty(); sascha@2615: } sascha@2615: gernotbelger@9312: final FixingsOverview overview = FixingsOverviewFactory.getOverview(river); sascha@2615: sascha@2615: if (overview == null) { sascha@2615: log.warn("No overview found for river '" + river + "'"); sascha@2615: return empty(); sascha@2615: } sascha@2615: gernotbelger@9312: final FixingsFilterBuilder ffb = new FixingsFilterBuilder(input); sascha@2615: gernotbelger@9312: final List columns = overview.filter(ffb.getRange(), ffb.getFilter()); sascha@2620: gernotbelger@9312: final List> cols = new ArrayList<>(); gernotbelger@9312: gernotbelger@9312: for (final Fixing.Column col : columns) { gernotbelger@9312: final FixingsColumn data = FixingsColumnFactory.INSTANCE.getColumnData(col); sascha@2620: if (data != null) { gernotbelger@9312: cols.add(new Pair<>(col, data)); sascha@2615: } sascha@2615: } sascha@2615: gernotbelger@9312: final JFreeChart chart = createChart(cols, river, km, callMeta); sascha@2620: sascha@2620: return encode(chart, extent, format); sascha@2620: } sascha@2620: gernotbelger@9312: protected static Output encode(final JFreeChart chart, final Dimension extent, final String format) { gernotbelger@9312: final BufferedImage image = chart.createBufferedImage(extent.width, extent.height, Transparency.BITMASK, null); sascha@2620: gernotbelger@9312: final ByteArrayOutputStream out = new ByteArrayOutputStream(); sascha@2620: sascha@2620: try { sascha@2620: ImageIO.write(image, format, out); sascha@2620: } gernotbelger@9312: catch (final IOException ioe) { sascha@2620: log.warn("writing image failed", ioe); sascha@2620: return empty(); sascha@2620: } sascha@2620: sascha@2620: return new Output(out.toByteArray(), "image/" + format); sascha@2620: } sascha@2620: gernotbelger@9312: protected static JFreeChart createChart(final List> cols, final String river, final double km, final CallMeta callMeta) { gernotbelger@9312: final String labelFormat = Resources.getMsg(callMeta, I18N_CHART_LABEL_DATE, DEFAULT_CHART_LABEL_DATE); sascha@3170: gernotbelger@9312: final QWSeriesCollection.LabelGenerator lg = new QWSeriesCollection.DateFormatLabelGenerator(labelFormat); sascha@2620: gernotbelger@9312: final QWSeriesCollection dataset = new QWSeriesCollection(lg); gernotbelger@9312: gernotbelger@9312: final double[] w = new double[1]; gernotbelger@9312: for (final Pair col : cols) { gernotbelger@9312: final boolean interpolated = !col.getB().getW(km, w); gernotbelger@9312: final double q = col.getB().getQ(km); sascha@2620: if (!Double.isNaN(w[0]) && !Double.isNaN(q)) { gernotbelger@9312: final QWI qw = new QWI(q, w[0], col.getA().getDescription(), col.getA().getStartTime(), interpolated, 0); sascha@3162: dataset.add(qw); sascha@2620: } sascha@2620: } sascha@2620: gernotbelger@9312: final String title = Resources.format(callMeta, I18N_CHART_TITLE, DEFAULT_CHART_TITLE, river, km); sascha@3170: gernotbelger@9312: final String qAxis = Resources.getMsg(callMeta, I18N_Q_AXIS, DEFAULT_Q_AXIS); sascha@2620: gernotbelger@9312: final String wAxis = Resources.getMsg(callMeta, I18N_W_AXIS, DEFAULT_W_AXIS); sascha@3162: gernotbelger@9312: final JFreeChart chart = ChartFactory.createXYLineChart(title, qAxis, wAxis, null, PlotOrientation.VERTICAL, true, true, false); gernotbelger@9312: gernotbelger@9312: final XYPlot plot = (XYPlot) chart.getPlot(); gernotbelger@9312: gernotbelger@9312: final NumberAxis qA = (NumberAxis) plot.getDomainAxis(); sascha@3162: qA.setNumberFormatOverride(Formatter.getWaterlevelQ(callMeta)); sascha@3162: gernotbelger@9312: final NumberAxis wA = (NumberAxis) plot.getRangeAxis(); sascha@3162: wA.setNumberFormatOverride(Formatter.getWaterlevelW(callMeta)); sascha@3162: sascha@3162: plot.setRenderer(0, dataset.createRenderer()); sascha@3162: plot.setDataset(0, dataset); sascha@3162: gernotbelger@9312: final Rectangle2D area = dataset.getArea(); sascha@3162: sascha@3162: if (area != null) { gernotbelger@9312: final double height = area.getHeight(); gernotbelger@9312: final double wInset = Math.max(height, 0.01) * 0.25d; sascha@3162: sascha@3162: wA.setAutoRangeIncludesZero(false); gernotbelger@9312: wA.setRange(new Range(area.getMinY() - wInset, area.getMaxY() + wInset)); sascha@3162: } sascha@3162: gernotbelger@9312: final String measuredS = Resources.getMsg(callMeta, I18N_MEASURED, DEFAULT_MEASURED); sascha@3173: gernotbelger@9312: final String interpolatedS = Resources.getMsg(callMeta, I18N_INTERPOLATED, DEFAULT_INTERPOLATED); sascha@3173: gernotbelger@9312: final LegendItemCollection lic = plot.getLegendItems(); sascha@3173: dataset.addLegendItems(lic, new ShapeRenderer.LabelGenerator() { sascha@3173: @Override gernotbelger@9312: public String createLabel(final ShapeRenderer.Entry entry) { sascha@3173: return entry.getFilled() ? measuredS : interpolatedS; sascha@3173: } sascha@3173: }); sascha@3162: plot.setFixedLegendItems(lic); sascha@3162: sascha@3170: applyQSectorMarkers(plot, river, km, callMeta); sascha@2620: sascha@3164: chart.setBackgroundPaint(Color.white); sascha@3164: plot.setBackgroundPaint(Color.white); sascha@3164: plot.setDomainGridlinePaint(Color.gray); sascha@3164: plot.setRangeGridlinePaint(Color.gray); sascha@3164: plot.setDomainGridlinesVisible(true); sascha@3164: plot.setRangeGridlinesVisible(true); sascha@2620: sascha@2620: return chart; sascha@2620: } sascha@2620: felix@3577: /** Add domain markers to plot that indicate Q-sectors. */ gernotbelger@9312: protected static void applyQSectorMarkers(final XYPlot plot, final String river, final double km, final CallMeta meta) { gernotbelger@9312: final GaugeFinderFactory ggf = GaugeFinderFactory.getInstance(); gernotbelger@9312: final GaugeFinder gf = ggf.getGaugeFinder(river); sascha@3146: sascha@3146: if (gf == null) { sascha@3146: log.warn("No gauge finder found for river '" + river + "'"); sascha@3146: return; sascha@3146: } sascha@3146: gernotbelger@9312: final GaugeRange gr = gf.find(km); sascha@3146: if (gr == null) { gernotbelger@9312: log.debug("No gauge range found for km " + km + " on river " + river + "."); sascha@3146: return; sascha@3146: } sascha@3146: sascha@3151: if (log.isDebugEnabled()) { sascha@3151: log.debug(gr); sascha@3151: } sascha@3151: sascha@3170: for (int i = 0; i < I18N_Q_SECTOR_BOARDERS.length; ++i) { gernotbelger@9312: final String key = I18N_Q_SECTOR_BOARDERS[i]; gernotbelger@9312: final String def = DEFAULT_Q_SECTOR_BORDERS[i]; gernotbelger@9312: final String label = Resources.getMsg(meta, key, def); sascha@3170: gernotbelger@9312: final Marker m = createQSectorMarker(gr.getSectorBorder(i), label); sascha@3170: sascha@3146: if (m != null) { sascha@3146: plot.addDomainMarker(m); sascha@3146: } sascha@3146: } sascha@3146: } sascha@3146: felix@5373: /** Create Marker at value with label. */ gernotbelger@9312: protected static Marker createQSectorMarker(final double value, final String label) { sascha@3146: if (Double.isNaN(value)) { sascha@3146: return null; sascha@3146: } gernotbelger@9312: final Marker m = new ValueMarker(value); sascha@3146: m.setPaint(Color.black); sascha@3146: m.setStroke(new BasicStroke()); sascha@3146: m.setLabel(label); sascha@3146: m.setLabelAnchor(RectangleAnchor.TOP_LEFT); sascha@3146: m.setLabelTextAnchor(TextAnchor.TOP_LEFT); sascha@3146: return m; sascha@3146: } sascha@3146: gernotbelger@9312: protected static String getRiverName(final Document input) { gernotbelger@9312: final NodeList rivers = input.getElementsByTagName("river"); sascha@2620: sascha@2620: if (rivers.getLength() == 0) { sascha@2620: return null; sascha@2620: } sascha@2620: gernotbelger@9312: final String river = ((Element) rivers.item(0)).getAttribute("name"); sascha@2620: sascha@2621: return river.length() > 0 ? river : null; sascha@2620: } sascha@2620: gernotbelger@9312: protected static Double getKM(final Document input) { gernotbelger@9312: final NodeList kms = input.getElementsByTagName("km"); sascha@2620: sascha@2620: if (kms.getLength() == 0) { sascha@2620: return null; sascha@2620: } sascha@2620: gernotbelger@9312: final String km = ((Element) kms.item(0)).getAttribute("value"); sascha@2620: sascha@2620: try { sascha@2620: return Double.valueOf(km); sascha@2620: } gernotbelger@9312: catch (final NumberFormatException nfe) { sascha@2620: log.warn("Km '" + km + " is not a valid number."); sascha@2620: return null; sascha@2620: } sascha@2620: } sascha@2620: gernotbelger@9312: protected static Dimension getExtent(final Document input) { sascha@2620: gernotbelger@9312: int width = DEFAULT_WIDTH; sascha@2620: int height = DEFAULT_HEIGHT; sascha@2620: gernotbelger@9312: final NodeList extents = input.getElementsByTagName("extent"); sascha@2620: sascha@2620: if (extents.getLength() > 0) { gernotbelger@9312: final Element element = (Element) extents.item(0); gernotbelger@9312: final String w = element.getAttribute("width"); gernotbelger@9312: final String h = element.getAttribute("height"); sascha@2620: sascha@2620: try { sascha@2620: width = Math.max(1, Integer.parseInt(w)); sascha@2620: } gernotbelger@9312: catch (final NumberFormatException nfe) { sascha@2620: log.warn("width '" + w + "' is not a valid."); sascha@2620: } sascha@2620: sascha@2620: try { sascha@2620: height = Math.max(1, Integer.parseInt(h)); sascha@2620: } gernotbelger@9312: catch (final NumberFormatException nfe) { sascha@2620: log.warn("height '" + h + "' is not a valid"); sascha@2620: } sascha@2620: } sascha@2620: sascha@2620: return new Dimension(width, height); sascha@2620: } sascha@2620: gernotbelger@9312: protected static String getFormat(final Document input) { sascha@2620: String format = DEFAULT_FORMAT; sascha@2620: gernotbelger@9312: final NodeList formats = input.getElementsByTagName("format"); sascha@2620: sascha@2620: if (formats.getLength() > 0) { gernotbelger@9312: final String type = ((Element) formats.item(0)).getAttribute("type"); sascha@2620: if (type.length() > 0) { sascha@2620: format = type; sascha@2620: } sascha@2620: } sascha@2620: sascha@2620: return format; sascha@2614: } sascha@2614: } sascha@2614: // vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 :