teichmann@5831: package org.dive4elements.river.artifacts.services; sascha@2615: teichmann@5831: import org.dive4elements.artifactdatabase.DefaultService; sascha@3162: teichmann@5831: import org.dive4elements.artifacts.CallMeta; teichmann@5831: import org.dive4elements.artifacts.GlobalContext; teichmann@5831: import org.dive4elements.artifacts.Service; sascha@2615: teichmann@5831: import org.dive4elements.river.artifacts.model.FixingsColumn; teichmann@5831: import org.dive4elements.river.artifacts.model.FixingsColumnFactory; teichmann@5831: import org.dive4elements.river.artifacts.model.FixingsFilterBuilder; sascha@3173: teichmann@5831: import org.dive4elements.river.artifacts.model.FixingsOverview.Fixing; teichmann@5831: teichmann@5831: import org.dive4elements.river.artifacts.model.FixingsOverview; teichmann@5831: import org.dive4elements.river.artifacts.model.FixingsOverviewFactory; teichmann@5831: import org.dive4elements.river.artifacts.model.GaugeFinder; teichmann@5831: import org.dive4elements.river.artifacts.model.GaugeFinderFactory; teichmann@5831: import org.dive4elements.river.artifacts.model.GaugeRange; teichmann@5831: teichmann@5831: import org.dive4elements.river.artifacts.model.fixings.QWI; teichmann@5831: teichmann@5831: import org.dive4elements.river.artifacts.resources.Resources; teichmann@5831: teichmann@5831: import org.dive4elements.river.backend.SessionHolder; teichmann@5831: teichmann@5831: import org.dive4elements.river.jfree.ShapeRenderer; teichmann@5831: teichmann@5831: import org.dive4elements.river.utils.Formatter; teichmann@5831: import org.dive4elements.river.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 columns = overview.filter( sascha@2615: ffb.getRange(), sascha@2615: ffb.getFilter()); sascha@2615: sascha@2620: List> cols = sascha@2620: new ArrayList>(); 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(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> 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 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: felix@5373: /** Create Marker at value with label. */ felix@5373: protected static Marker createQSectorMarker( felix@5373: double value, String label felix@5373: ) { 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 :