sascha@2614: package de.intevation.flys.artifacts.services;
sascha@2614: 
sascha@2614: import de.intevation.artifactdatabase.DefaultService;
sascha@2614: 
sascha@2614: import de.intevation.artifacts.CallMeta;
sascha@2614: import de.intevation.artifacts.GlobalContext;
sascha@2614: import de.intevation.artifacts.Service;
sascha@2614: 
sascha@2615: import de.intevation.flys.artifacts.model.FixingsColumn;
sascha@2615: import de.intevation.flys.artifacts.model.FixingsColumnFactory;
sascha@2620: import de.intevation.flys.artifacts.model.FixingsFilterBuilder;
sascha@2615: 
sascha@2615: import de.intevation.flys.artifacts.model.FixingsOverview.Fixing;
sascha@2615: 
sascha@2620: import de.intevation.flys.artifacts.model.FixingsOverview;
sascha@2620: import de.intevation.flys.artifacts.model.FixingsOverviewFactory;
sascha@3146: import de.intevation.flys.artifacts.model.GaugeFinder;
sascha@3146: import de.intevation.flys.artifacts.model.GaugeFinderFactory;
sascha@3146: import de.intevation.flys.artifacts.model.GaugeRange;
sascha@2620: 
sascha@3729: import de.intevation.flys.artifacts.model.fixings.QWI;
sascha@3162: 
sascha@3170: import de.intevation.flys.artifacts.resources.Resources;
sascha@3170: 
sascha@2615: import de.intevation.flys.backend.SessionHolder;
sascha@2615: 
sascha@3173: import de.intevation.flys.jfree.ShapeRenderer;
sascha@3173: 
sascha@3162: import de.intevation.flys.utils.Formatter;
sascha@2620: import de.intevation.flys.utils.Pair;
sascha@2620: 
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@2620: 
sascha@3162: import java.awt.geom.Rectangle2D;
sascha@3162: 
sascha@2620: import java.awt.image.BufferedImage;
sascha@2620: 
sascha@2620: import java.io.ByteArrayOutputStream;
sascha@2620: import java.io.IOException;
sascha@2620: 
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;
sascha@2614: 
sascha@2620: import org.jfree.chart.ChartFactory;
sascha@2620: import org.jfree.chart.JFreeChart;
sascha@3162: import org.jfree.chart.LegendItemCollection;
sascha@3162: 
sascha@3162: import org.jfree.chart.axis.NumberAxis;
sascha@2620: 
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@2620: 
sascha@3162: import org.jfree.data.Range;
sascha@2620: 
sascha@3146: import org.jfree.ui.RectangleAnchor;
sascha@3146: import org.jfree.ui.TextAnchor;
sascha@3146: 
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: 
felix@3571: /** Serve chart of Fixings at certain km. */
sascha@2614: public class FixingsKMChartService
sascha@2614: extends      DefaultService
sascha@2614: {
sascha@2614:     private static final Logger log =
sascha@2614:         Logger.getLogger(FixingsKMChartService.class);
sascha@2614: 
sascha@2620:     public static final int DEFAULT_WIDTH  = 240;
sascha@2620:     public static final int DEFAULT_HEIGHT = 180;
sascha@2620: 
sascha@3170:     public static final String [] I18N_Q_SECTOR_BOARDERS = {
sascha@3170:         "fix.km.chart.q.sector.border0",
sascha@3170:         "fix.km.chart.q.sector.border1",
sascha@3170:         "fix.km.chart.q.sector.border2"
sascha@3170:     };
sascha@3170: 
sascha@3170:     public static final String [] DEFAULT_Q_SECTOR_BORDERS = {
sascha@3146:         "(MNQ + MQ)/2",
sascha@3146:         "(MQ + MHQ)/2",
sascha@3146:         "HQ5"
sascha@3146:     };
sascha@3146: 
sascha@3170:     public static final String I18N_CHART_LABEL_DATE =
sascha@3170:         "fix.km.chart.label.date";
sascha@3170: 
sascha@3170:     public static final String DEFAULT_CHART_LABEL_DATE =
sascha@3170:         "yyyy/MM/dd";
sascha@3170: 
sascha@3170:     public static final String I18N_CHART_TITLE =
sascha@3170:         "fix.km.chart.title";
sascha@3170: 
sascha@3170:     public static final String DEFAULT_CHART_TITLE =
sascha@3170:         "Fixings {0} km {1,number,#.###}";
sascha@3170: 
sascha@3170:     public static final String I18N_Q_AXIS =
sascha@3170:         "fix.km.chart.q.axis";
sascha@3170: 
sascha@3170:     public static final String DEFAULT_Q_AXIS =
sascha@3170:         "Q [m\u00b3/s]";
sascha@3170: 
sascha@3170:     public static final String I18N_W_AXIS =
sascha@3170:         "fix.km.chart.w.axis";
sascha@3170: 
sascha@3170:     public static final String DEFAULT_W_AXIS =
sascha@3170:         "W [NN + m]";
sascha@3170: 
sascha@3173:     public static final String I18N_MEASURED =
sascha@3174:         "fix.km.chart.measured";
sascha@3173: 
sascha@3173:     public static final String DEFAULT_MEASURED =
sascha@3173:         "measured";
sascha@3173: 
sascha@3173:     public static final String I18N_INTERPOLATED =
sascha@3173:         "fix.km.chart.interpolated";
sascha@3173: 
sascha@3173:     public static final String DEFAULT_INTERPOLATED =
sascha@3173:         "interpolated";
sascha@3170: 
sascha@2620:     public static final String DEFAULT_FORMAT = "png";
sascha@2620: 
sascha@2620:     // TODO: Load fancy image from resources.
sascha@2615:     public static final byte [] EMPTY = {
sascha@3076:         (byte)0x89, (byte)0x50, (byte)0x4e, (byte)0x47,
sascha@3076:         (byte)0x0d, (byte)0x0a, (byte)0x1a, (byte)0x0a,
sascha@3076:         (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x0d,
sascha@3076:         (byte)0x49, (byte)0x48, (byte)0x44, (byte)0x52,
sascha@3076:         (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x01,
sascha@3076:         (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x01,
sascha@3076:         (byte)0x08, (byte)0x00, (byte)0x00, (byte)0x00,
sascha@3076:         (byte)0x00, (byte)0x3a, (byte)0x7e, (byte)0x9b,
sascha@3076:         (byte)0x55, (byte)0x00, (byte)0x00, (byte)0x00,
sascha@3076:         (byte)0x01, (byte)0x73, (byte)0x52, (byte)0x47,
sascha@3076:         (byte)0x42, (byte)0x00, (byte)0xae, (byte)0xce,
sascha@3076:         (byte)0x1c, (byte)0xe9, (byte)0x00, (byte)0x00,
sascha@3076:         (byte)0x00, (byte)0x09, (byte)0x70, (byte)0x48,
sascha@3076:         (byte)0x59, (byte)0x73, (byte)0x00, (byte)0x00,
sascha@3076:         (byte)0x0b, (byte)0x13, (byte)0x00, (byte)0x00,
sascha@3076:         (byte)0x0b, (byte)0x13, (byte)0x01, (byte)0x00,
sascha@3076:         (byte)0x9a, (byte)0x9c, (byte)0x18, (byte)0x00,
sascha@3076:         (byte)0x00, (byte)0x00, (byte)0x07, (byte)0x74,
sascha@3076:         (byte)0x49, (byte)0x4d, (byte)0x45, (byte)0x07,
sascha@3076:         (byte)0xdc, (byte)0x04, (byte)0x04, (byte)0x10,
sascha@3076:         (byte)0x30, (byte)0x15, (byte)0x7d, (byte)0x77,
sascha@3076:         (byte)0x36, (byte)0x0b, (byte)0x00, (byte)0x00,
sascha@3076:         (byte)0x00, (byte)0x08, (byte)0x74, (byte)0x45,
sascha@3076:         (byte)0x58, (byte)0x74, (byte)0x43, (byte)0x6f,
sascha@3076:         (byte)0x6d, (byte)0x6d, (byte)0x65, (byte)0x6e,
sascha@3076:         (byte)0x74, (byte)0x00, (byte)0xf6, (byte)0xcc,
sascha@3076:         (byte)0x96, (byte)0xbf, (byte)0x00, (byte)0x00,
sascha@3076:         (byte)0x00, (byte)0x0a, (byte)0x49, (byte)0x44,
sascha@3076:         (byte)0x41, (byte)0x54, (byte)0x08, (byte)0xd7,
sascha@3076:         (byte)0x63, (byte)0xf8, (byte)0x0f, (byte)0x00,
sascha@3076:         (byte)0x01, (byte)0x01, (byte)0x01, (byte)0x00,
sascha@3076:         (byte)0x1b, (byte)0xb6, (byte)0xee, (byte)0x56,
sascha@3076:         (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00,
sascha@3076:         (byte)0x49, (byte)0x45, (byte)0x4e, (byte)0x44,
sascha@2615:         (byte)0xae, (byte)0x42, (byte)0x60, (byte)0x82
sascha@2615:     };
sascha@2615: 
sascha@2615:     private static final Output empty() {
sascha@2615:         return new Output(EMPTY, "image/png");
sascha@2615:     }
sascha@2614: 
sascha@2614:     @Override
sascha@2614:     public Service.Output process(
sascha@2614:         Document      data,
sascha@2614:         GlobalContext globalContext,
sascha@2614:         CallMeta      callMeta
sascha@2614:     ) {
sascha@2614:         log.debug("FixingsKMChartService.process");
sascha@2615: 
sascha@2615:         SessionHolder.acquire();
sascha@2615:         try {
sascha@2615:             return doProcess(data, globalContext, callMeta);
sascha@2615:         }
sascha@2615:         finally {
sascha@2615:             SessionHolder.HOLDER.get().close();
sascha@2615:             SessionHolder.release();
sascha@2615:         }
sascha@2615:     }
sascha@2615: 
sascha@2615:     protected Service.Output doProcess(
sascha@2620:         Document      input,
sascha@2615:         GlobalContext globalContext,
sascha@2615:         CallMeta      callMeta
sascha@2615:     ) {
sascha@2620:         String    river  = getRiverName(input);
sascha@2620:         Double    km     = getKM(input);
sascha@2620:         Dimension extent = getExtent(input);
sascha@2620:         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: 
sascha@2615:         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: 
sascha@2620:         FixingsFilterBuilder ffb = new FixingsFilterBuilder(input);
sascha@2615: 
sascha@2615:         List<Fixing.Column> columns = overview.filter(
sascha@2615:             ffb.getRange(),
sascha@2615:             ffb.getFilter());
sascha@2615: 
sascha@2620:         List<Pair<Fixing.Column, FixingsColumn>> cols =
sascha@2620:             new ArrayList<Pair<Fixing.Column, FixingsColumn>>();
sascha@2620: 
sascha@2620:         for (Fixing.Column col: columns) {
sascha@2620:             FixingsColumn data =
sascha@2620:                 FixingsColumnFactory.INSTANCE.getColumnData(col);
sascha@2620:             if (data != null) {
sascha@2620:                 cols.add(new Pair<Fixing.Column, FixingsColumn>(col, data));
sascha@2615:             }
sascha@2615:         }
sascha@2615: 
sascha@3162:         JFreeChart chart = createChart(cols, river, km, callMeta);
sascha@2620: 
sascha@2620:         return encode(chart, extent, format);
sascha@2620:     }
sascha@2620: 
sascha@2620:     protected static Output encode(
sascha@2620:         JFreeChart chart,
sascha@3076:         Dimension  extent,
sascha@2620:         String     format
sascha@2620:     ) {
sascha@2620:         BufferedImage image = chart.createBufferedImage(
sascha@2620:             extent.width, extent.height,
sascha@2620:             Transparency.BITMASK,
sascha@2620:             null);
sascha@2620: 
sascha@2620:         ByteArrayOutputStream out = new ByteArrayOutputStream();
sascha@2620: 
sascha@2620:         try {
sascha@2620:             ImageIO.write(image, format, out);
sascha@2620:         }
sascha@2620:         catch (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: 
sascha@2620:     protected static JFreeChart createChart(
sascha@2620:         List<Pair<Fixing.Column, FixingsColumn>> cols,
sascha@3162:         String      river,
sascha@3162:         double      km,
sascha@3162:         CallMeta    callMeta
sascha@2620:     ) {
sascha@3170:         String labelFormat = Resources.getMsg(
sascha@3170:             callMeta, I18N_CHART_LABEL_DATE, DEFAULT_CHART_LABEL_DATE);
sascha@3170: 
sascha@3170:         QWSeriesCollection.LabelGenerator lg =
sascha@3170:             new QWSeriesCollection.DateFormatLabelGenerator(labelFormat);
sascha@3170: 
sascha@3170:         QWSeriesCollection dataset = new QWSeriesCollection(lg);
sascha@2620: 
sascha@2620:         double [] w = new double[1];
sascha@2620:         for (Pair<Fixing.Column, FixingsColumn> col: cols) {
sascha@3162:             boolean interpolated = !col.getB().getW(km, w);
sascha@2620:             double q = col.getB().getQ(km);
sascha@2620:             if (!Double.isNaN(w[0]) && !Double.isNaN(q)) {
sascha@3729:                 QWI qw = new QWI(
sascha@3162:                     q, w[0],
sascha@3162:                     col.getA().getDescription(),
sascha@3162:                     col.getA().getStartTime(),
sascha@3604:                     interpolated, 0);
sascha@3162:                 dataset.add(qw);
sascha@2620:             }
sascha@2620:         }
sascha@2620: 
sascha@3170:         String title = Resources.format(
sascha@3170:             callMeta, I18N_CHART_TITLE, DEFAULT_CHART_TITLE, river, km);
sascha@3170: 
sascha@3170:         String qAxis = Resources.getMsg(
sascha@3170:             callMeta, I18N_Q_AXIS, DEFAULT_Q_AXIS);
sascha@3170: 
sascha@3170:         String wAxis = Resources.getMsg(
sascha@3170:             callMeta, I18N_W_AXIS, DEFAULT_W_AXIS);
sascha@3170: 
sascha@3162:         JFreeChart chart = ChartFactory.createXYLineChart(
sascha@3170:             title,
sascha@3170:             qAxis,
sascha@3170:             wAxis,
sascha@3162:             null,
sascha@2620:             PlotOrientation.VERTICAL,
sascha@2620:             true,
sascha@3162:             true,
sascha@2620:             false);
sascha@2620: 
sascha@3162:         XYPlot plot = (XYPlot)chart.getPlot();
sascha@3162: 
sascha@3162:         NumberAxis qA = (NumberAxis)plot.getDomainAxis();
sascha@3162:         qA.setNumberFormatOverride(Formatter.getWaterlevelQ(callMeta));
sascha@3162: 
sascha@3162:         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: 
sascha@3162:         Rectangle2D area = dataset.getArea();
sascha@3162: 
sascha@3162:         if (area != null) {
sascha@3166:             double height = area.getHeight();
sascha@3166:             double wInset = Math.max(height, 0.01) * 0.25d;
sascha@3162: 
sascha@3162:             wA.setAutoRangeIncludesZero(false);
sascha@3162:             wA.setRange(new Range(
sascha@3162:                 area.getMinY() - wInset,
sascha@3162:                 area.getMaxY() + wInset));
sascha@3162:         }
sascha@3162: 
sascha@3173:         final String measuredS = Resources.getMsg(
sascha@3173:             callMeta, I18N_MEASURED, DEFAULT_MEASURED);
sascha@3173: 
sascha@3173:         final String interpolatedS = Resources.getMsg(
sascha@3173:             callMeta, I18N_INTERPOLATED, DEFAULT_INTERPOLATED);
sascha@3173: 
sascha@3162:         LegendItemCollection lic = plot.getLegendItems();
sascha@3173:         dataset.addLegendItems(lic, new ShapeRenderer.LabelGenerator() {
sascha@3173:             @Override
sascha@3173:             public String createLabel(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. */
sascha@3146:     protected static void applyQSectorMarkers(
sascha@3170:         XYPlot   plot,
sascha@3170:         String   river,
sascha@3170:         double   km,
sascha@3170:         CallMeta meta
sascha@3146:     ) {
sascha@3146:         GaugeFinderFactory ggf = GaugeFinderFactory.getInstance();
sascha@3146:         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: 
sascha@3152:         GaugeRange gr = gf.find(km);
sascha@3146:         if (gr == null) {
sascha@3146:             log.debug("No gauge range found for km "
sascha@3146:                 + 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) {
sascha@3170:             String key   = I18N_Q_SECTOR_BOARDERS[i];
sascha@3170:             String def   = DEFAULT_Q_SECTOR_BORDERS[i];
sascha@3170:             String label = Resources.getMsg(meta, key, def);
sascha@3170: 
sascha@3146:             Marker m = createQSectorMarker(
sascha@3146:                 gr.getSectorBorder(i),
sascha@3170:                 label);
sascha@3170: 
sascha@3146:             if (m != null) {
sascha@3146:                 plot.addDomainMarker(m);
sascha@3146:             }
sascha@3146:         }
sascha@3146:     }
sascha@3146: 
sascha@3146:     protected static Marker createQSectorMarker(double value, String label) {
sascha@3146:         if (Double.isNaN(value)) {
sascha@3146:             return null;
sascha@3146:         }
sascha@3146:         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: 
sascha@2620:     protected static String getRiverName(Document input) {
sascha@2620:         NodeList rivers = input.getElementsByTagName("river");
sascha@2620: 
sascha@2620:         if (rivers.getLength() == 0) {
sascha@2620:             return null;
sascha@2620:         }
sascha@2620: 
sascha@2620:         String river = ((Element)rivers.item(0)).getAttribute("name");
sascha@2620: 
sascha@2621:         return river.length() > 0 ? river : null;
sascha@2620:     }
sascha@2620: 
sascha@2620:     protected static Double getKM(Document input) {
sascha@2620:         NodeList kms = input.getElementsByTagName("km");
sascha@2620: 
sascha@2620:         if (kms.getLength() == 0) {
sascha@2620:             return null;
sascha@2620:         }
sascha@2620: 
sascha@2620:         String km = ((Element)kms.item(0)).getAttribute("value");
sascha@2620: 
sascha@2620:         try {
sascha@2620:             return Double.valueOf(km);
sascha@2620:         }
sascha@2620:         catch (NumberFormatException nfe) {
sascha@2620:             log.warn("Km '" + km + " is not a valid number.");
sascha@2620:             return null;
sascha@2620:         }
sascha@2620:     }
sascha@2620: 
sascha@2620:     protected static Dimension getExtent(Document input) {
sascha@2620: 
sascha@2620:         int width  = DEFAULT_WIDTH;
sascha@2620:         int height = DEFAULT_HEIGHT;
sascha@2620: 
sascha@2620:         NodeList extents = input.getElementsByTagName("extent");
sascha@2620: 
sascha@2620:         if (extents.getLength() > 0) {
sascha@2620:             Element element = (Element)extents.item(0);
sascha@2620:             String w = element.getAttribute("width");
sascha@2620:             String h = element.getAttribute("height");
sascha@2620: 
sascha@2620:             try {
sascha@2620:                 width = Math.max(1, Integer.parseInt(w));
sascha@2620:             }
sascha@2620:             catch (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:             }
sascha@2620:             catch (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: 
sascha@2620:     protected static String getFormat(Document input) {
sascha@2620:         String format = DEFAULT_FORMAT;
sascha@2620: 
sascha@2620:         NodeList formats = input.getElementsByTagName("format");
sascha@2620: 
sascha@2620:         if (formats.getLength() > 0) {
sascha@2620:             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 :