# HG changeset patch # User Sascha L. Teichmann # Date 1340624313 0 # Node ID 0d81469890120accbce2fe06dade2a2e97690f9e # Parent cd8d81b2824de014a679755bdf5783eeb610c973 Added labeling for Q/W points FixingsKMChartService. flys-artifacts/trunk@4774 c6561f87-3c4e-4783-a992-168aeb5c3f6f diff -r cd8d81b2824d -r 0d8146989012 flys-artifacts/ChangeLog --- a/flys-artifacts/ChangeLog Mon Jun 25 07:59:22 2012 +0000 +++ b/flys-artifacts/ChangeLog Mon Jun 25 11:38:33 2012 +0000 @@ -1,3 +1,21 @@ +2012-06-25 Sascha L. Teichmann + + * src/main/java/de/intevation/flys/artifacts/services/FixingsKMChartService.java: + Label the points in diagram and show if they are interpolated or not. + + * src/main/java/de/intevation/flys/artifacts/services/QWSeriesCollection.java: + New. Extended XYSeriesCollection to cope with QWs + + * src/main/java/de/intevation/flys/utils/Formatter.java: Added formatters + to be fetched only over CallMeta. CallContext are not present in services. + + * src/main/java/de/intevation/flys/java2d/ShapeUtils.java: New. Some code + to handle Shapes. + + * src/main/java/de/intevation/flys/jfree/ShapeRenderer.java: New. Shape + renderer. This is a simplified version of the shape renderer + from fixings analysis in desktop FLYS. + 2012-06-25 Sascha L. Teichmann * src/main/java/de/intevation/flys/artifacts/model/fixings/FixCalculation.java: diff -r cd8d81b2824d -r 0d8146989012 flys-artifacts/src/main/java/de/intevation/flys/artifacts/services/FixingsKMChartService.java --- a/flys-artifacts/src/main/java/de/intevation/flys/artifacts/services/FixingsKMChartService.java Mon Jun 25 07:59:22 2012 +0000 +++ b/flys-artifacts/src/main/java/de/intevation/flys/artifacts/services/FixingsKMChartService.java Mon Jun 25 11:38:33 2012 +0000 @@ -18,17 +18,20 @@ import de.intevation.flys.artifacts.model.GaugeFinderFactory; import de.intevation.flys.artifacts.model.GaugeRange; +import de.intevation.flys.artifacts.model.fixings.QW; + import de.intevation.flys.backend.SessionHolder; +import de.intevation.flys.utils.Formatter; import de.intevation.flys.utils.Pair; -import gnu.trove.TDoubleArrayList; - import java.awt.BasicStroke; import java.awt.Color; import java.awt.Dimension; import java.awt.Transparency; +import java.awt.geom.Rectangle2D; + import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; @@ -44,13 +47,16 @@ import org.jfree.chart.ChartFactory; import org.jfree.chart.ChartUtilities; import org.jfree.chart.JFreeChart; +import org.jfree.chart.LegendItemCollection; + +import org.jfree.chart.axis.NumberAxis; import org.jfree.chart.plot.Marker; import org.jfree.chart.plot.PlotOrientation; import org.jfree.chart.plot.ValueMarker; import org.jfree.chart.plot.XYPlot; -import org.jfree.data.xy.DefaultXYDataset; +import org.jfree.data.Range; import org.jfree.ui.RectangleAnchor; import org.jfree.ui.TextAnchor; @@ -177,7 +183,7 @@ } } - JFreeChart chart = createChart(cols, river, km); + JFreeChart chart = createChart(cols, river, km, callMeta); return encode(chart, extent, format); } @@ -207,42 +213,66 @@ protected static JFreeChart createChart( List> cols, - String river, - double km + String river, + double km, + CallMeta callMeta ) { - - TDoubleArrayList ws = new TDoubleArrayList(cols.size()); - TDoubleArrayList qs = new TDoubleArrayList(cols.size()); + // TODO: I18N + QWSeriesCollection dataset = new QWSeriesCollection(); double [] w = new double[1]; for (Pair col: cols) { - boolean interpolated = col.getB().getW(km, w); - // TODO: Do something special with the interpolated values. + boolean interpolated = !col.getB().getW(km, w); double q = col.getB().getQ(km); if (!Double.isNaN(w[0]) && !Double.isNaN(q)) { - ws.add(w[0]); - qs.add(q); - // TODO: Generate labels depending on sectors. + QW qw = new QW( + q, w[0], + col.getA().getDescription(), + col.getA().getStartTime(), + interpolated); + dataset.add(qw); } } - DefaultXYDataset dataset = new DefaultXYDataset(); - - dataset.addSeries( - "Fixierungen", // TODO: i18n - new double [][] { qs.toNativeArray(), ws.toNativeArray() }); - - JFreeChart chart = ChartFactory.createScatterPlot( + JFreeChart chart = ChartFactory.createXYLineChart( "Fixierungen " + river + ": km " + km, // TODO: i18n - "Q", // TODO: i18n - "W", // TODO: i18n - dataset, + "Q [m\u00b3/s]", + "W [NN + m]", + null, PlotOrientation.VERTICAL, true, - false, + true, false); - applyQSectorMarkers(chart.getXYPlot(), river, km); + XYPlot plot = (XYPlot)chart.getPlot(); + + NumberAxis qA = (NumberAxis)plot.getDomainAxis(); + qA.setNumberFormatOverride(Formatter.getWaterlevelQ(callMeta)); + + NumberAxis wA = (NumberAxis)plot.getRangeAxis(); + wA.setNumberFormatOverride(Formatter.getWaterlevelW(callMeta)); + + plot.setRenderer(0, dataset.createRenderer()); + plot.setDataset(0, dataset); + + Rectangle2D area = dataset.getArea(); + + if (area != null) { + double wInset = area.getHeight() * 0.25d; + log.debug("width: " + area.getWidth()); + log.debug("height: "+ area.getHeight()); + + wA.setAutoRangeIncludesZero(false); + wA.setRange(new Range( + area.getMinY() - wInset, + area.getMaxY() + wInset)); + } + + LegendItemCollection lic = plot.getLegendItems(); + dataset.addLegendItems(lic); + plot.setFixedLegendItems(lic); + + applyQSectorMarkers(plot, river, km); ChartUtilities.applyCurrentTheme(chart); diff -r cd8d81b2824d -r 0d8146989012 flys-artifacts/src/main/java/de/intevation/flys/artifacts/services/QWSeriesCollection.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/flys-artifacts/src/main/java/de/intevation/flys/artifacts/services/QWSeriesCollection.java Mon Jun 25 11:38:33 2012 +0000 @@ -0,0 +1,222 @@ +package de.intevation.flys.artifacts.services; + +import de.intevation.flys.artifacts.model.fixings.QW; + +import de.intevation.flys.java2d.ShapeUtils; + +import de.intevation.flys.jfree.ShapeRenderer; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Paint; +import java.awt.Shape; + +import java.awt.geom.Rectangle2D; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.jfree.chart.LegendItem; +import org.jfree.chart.LegendItemCollection; + +import org.jfree.chart.labels.XYItemLabelGenerator; + +import org.jfree.chart.renderer.xy.StandardXYItemRenderer; + +import org.jfree.data.xy.XYDataset; +import org.jfree.data.xy.XYSeries; +import org.jfree.data.xy.XYSeriesCollection; + +public class QWSeriesCollection +extends XYSeriesCollection +implements XYItemLabelGenerator +{ + public interface LabelGenerator { + String createLabel(QW qw); + } // interface LabelGenerator + + public static class DateFormatLabelGenerator + implements LabelGenerator + { + protected DateFormat format; + + public DateFormatLabelGenerator() { + this(new SimpleDateFormat("dd.MM.yyyy")); + } + + public DateFormatLabelGenerator(DateFormat format) { + this.format = format; + } + + @Override + public String createLabel(QW qw) { + Date date = qw.getDate(); + return date != null ? format.format(date) : ""; + } + } // class DateFormatLabelGenerator + + public static final LabelGenerator SIMPLE_GENERATOR = + new DateFormatLabelGenerator(); + + protected Date minDate; + protected Date maxDate; + + protected List> labels; + + protected Rectangle2D area; + + protected LabelGenerator labelGenerator; + + protected Map knownShapes = + new HashMap(); + + public QWSeriesCollection() { + labels = new ArrayList>(); + labelGenerator = SIMPLE_GENERATOR; + } + + public QWSeriesCollection(LabelGenerator labelGenerator) { + this(); + this.labelGenerator = labelGenerator; + } + + protected static ShapeRenderer.Entry classify(QW qw) { + Shape shape = qw.getInterpolated() + ? ShapeUtils.INTERPOLATED_SHAPE + : ShapeUtils.MEASURED_SHAPE; + + boolean filled = !qw.getInterpolated(); + + return new ShapeRenderer.Entry( + shape, Color.black, filled); + } + + public void add(QW qw) { + + ShapeRenderer.Entry key = classify(qw); + + Integer seriesNo = knownShapes.get(key); + + XYSeries series; + + if (seriesNo == null) { + seriesNo = Integer.valueOf(getSeriesCount()); + knownShapes.put(key, seriesNo); + series = new XYSeries(seriesNo, false); + addSeries(series); + labels.add(new ArrayList()); + } + else { + series = getSeries(seriesNo); + } + + series.add(qw.getQ(), qw.getW()); + + labels.get(seriesNo).add(qw); + + extendDateRange(qw); + extendArea(qw); + } + + protected void extendDateRange(QW qw) { + Date date = qw.getDate(); + if (date != null) { + if (minDate == null) { + minDate = maxDate = date; + } + else { + if (date.compareTo(minDate) < 0) { + minDate = date; + } + if (date.compareTo(maxDate) > 0) { + maxDate = date; + } + } + } + } + + protected void extendArea(QW qw) { + if (area == null) { + area = new Rectangle2D.Double( + qw.getQ(), qw.getW(), 0d, 0d); + } + else { + area.add(qw.getQ(), qw.getW()); + } + } + + public Rectangle2D getArea() { + return area; + } + + public Date getMinDate() { + return minDate; + } + + public Date getMaxDate() { + return maxDate; + } + + public LabelGenerator getLabelGenerator() { + return labelGenerator; + } + + @Override + public String generateLabel(XYDataset dataset, int series, int item) { + return labelGenerator.createLabel(labels.get(series).get(item)); + } + + public StandardXYItemRenderer createRenderer() { + StandardXYItemRenderer renderer = new ShapeRenderer(knownShapes); + renderer.setBaseItemLabelGenerator(this); + renderer.setBaseSeriesVisibleInLegend(false); + renderer.setBaseItemLabelsVisible(true); + return renderer; + } + + public static final LegendItem legendItem( + String label, + Paint paint, + Shape shape, + boolean filled + ) { + BasicStroke stroke = new BasicStroke(); + return new LegendItem( + label, // label + null, // description + null, // tooltip + null, // url + true, // shape visible + shape, // shape + filled, // shape filled + filled ? paint : Color.white, // fill paint + true, // shape outline + paint, // outline paint + stroke, // outline stroke + false, // line visible + shape, // line + stroke, // line stroke + Color.white); + } + + public void addLegendItems(LegendItemCollection lic) { + for (ShapeRenderer.Entry entry: knownShapes.keySet()) { + // TODO: i18n + String label = entry.getFilled() + ? "gemessene Werte" + : "interpolierte Werte"; + lic.add(legendItem( + label, + entry.getPaint(), + entry.getShape(), + entry.getFilled())); + } + } +} +// vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 : diff -r cd8d81b2824d -r 0d8146989012 flys-artifacts/src/main/java/de/intevation/flys/java2d/ShapeUtils.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/flys-artifacts/src/main/java/de/intevation/flys/java2d/ShapeUtils.java Mon Jun 25 11:38:33 2012 +0000 @@ -0,0 +1,83 @@ +package de.intevation.flys.java2d; + +import java.awt.Shape; + +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.GeneralPath; +import java.awt.geom.Rectangle2D; + +import java.util.HashMap; +import java.util.Map; + +public class ShapeUtils +{ + // TODO: Use enum + public static final int MEASURED = 0; + public static final int DIGITIZED = 1; + public static final int INTERPOLATED = 2; + + public static final boolean DIGITIZED_FILL = false; + public static final boolean MEASURED_FILL = true; + public static final boolean INTERPOLATED_FILL = false; + + public static final Shape DIGITIZED_SHAPE = + createCross(4f); + + public static final Shape MEASURED_SHAPE = + new Rectangle2D.Double(-2, -2, 4, 4); + + public static final Shape INTERPOLATED_SHAPE = + new Ellipse2D.Double(-2, -2, 4, 4); + + protected static Map scaledShapesCache = + new HashMap(); + + public static final Shape createCross(float size) { + float half = size * 0.5f; + GeneralPath p = new GeneralPath(); + p.moveTo(-half, -half); + p.lineTo(half, half); + p.closePath(); + p.moveTo(-half, half); + p.lineTo(half, -half); + p.closePath(); + return p; + } + + public static Shape scale(Shape shape, float factor) { + if (factor == 1f) { + return shape; + } + AffineTransform xform = + AffineTransform.getScaleInstance(factor, factor); + + GeneralPath gp = new GeneralPath(shape); + return gp.createTransformedShape(xform); + } + + public static synchronized Shape getScaledShape(int type, float size) { + + Long hash = Long.valueOf( + (((long)type) << 32) | Float.floatToIntBits(size)); + + Shape shape = scaledShapesCache.get(hash); + + if (shape == null) { + switch (type) { + case MEASURED: + shape = MEASURED_SHAPE; + break; + case DIGITIZED: + shape = DIGITIZED_SHAPE; + break; + default: + shape = INTERPOLATED_SHAPE; + } + scaledShapesCache.put(hash, shape = scale(shape, size)); + } + + return shape; + } +} +// vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 : diff -r cd8d81b2824d -r 0d8146989012 flys-artifacts/src/main/java/de/intevation/flys/jfree/ShapeRenderer.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/flys-artifacts/src/main/java/de/intevation/flys/jfree/ShapeRenderer.java Mon Jun 25 11:38:33 2012 +0000 @@ -0,0 +1,342 @@ +package de.intevation.flys.jfree; + +/** + * Copyright (c) 2006, 2012 by Intevation GmbH + * + * @author Sascha L. Teichmann (teichmann@intevation.de) + * + * This program is free software under the LGPL (>=v2.1) + * Read the file LGPL coming with FLYS for details. + */ + +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Paint; +import java.awt.Shape; + +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.jfree.chart.axis.ValueAxis; + +import org.jfree.chart.labels.ItemLabelPosition; +import org.jfree.chart.labels.XYItemLabelGenerator; + +import org.jfree.chart.plot.CrosshairState; +import org.jfree.chart.plot.PlotOrientation; +import org.jfree.chart.plot.PlotRenderingInfo; +import org.jfree.chart.plot.XYPlot; + +import org.jfree.chart.renderer.xy.StandardXYItemRenderer; +import org.jfree.chart.renderer.xy.XYItemRendererState; + +import org.jfree.data.xy.XYDataset; + +import org.jfree.text.TextUtilities; + +import org.jfree.ui.RectangleEdge; + +public class ShapeRenderer +extends StandardXYItemRenderer { + + public static class Entry { + protected Shape shape; + protected Shape frame; + protected Paint paint; + protected boolean filled; + + public Entry( + Shape shape, + Paint paint, + boolean filled + ) { + this.shape = shape; + this.paint = paint; + this.filled = filled; + } + + public Entry( + Shape shape, + Shape frame, + Paint paint, + boolean filled + ) { + this.shape = shape; + this.frame = frame; + this.paint = paint; + this.filled = filled; + } + + public Shape getShape() { + return shape; + } + + public void setShape(Shape shape) { + this.shape = shape; + } + + + public Paint getPaint() { + return paint; + } + + public void setPaint(Paint paint) { + this.paint = paint; + } + + public boolean getFilled() { + return filled; + } + + public void setFilled(boolean filled) { + this.filled = filled; + } + + public boolean equals(Object other) { + Entry entry = (Entry)other; + return filled == entry.filled + && paint.equals(entry.paint) + && shape.equals(entry.shape); + } + + public int hashCode() { + return + shape.hashCode() ^ + paint.hashCode() ^ + (filled ? 1231 : 1237); + } + } // class Entry + + protected Entry [] entries; + + protected List labelBoundingBoxes; + + protected Rectangle2D area; + + public ShapeRenderer() { + this(SHAPES); + } + + public ShapeRenderer(int type) { + super(type); + } + + public ShapeRenderer(Map map) { + super(SHAPES); + setEntries(map); + } + + public void setEntries(Entry [] entries) { + this.entries = entries; + } + + public void setEntries(Map map) { + Entry [] entries = new Entry[map.size()]; + + for (Map.Entry entry: map.entrySet()) { + entries[entry.getValue()] = entry.getKey(); + } + + setEntries(entries); + } + + @Override + public Shape getSeriesShape(int series) { + return entries[series].shape; + } + + public Shape getSeriesFrame(int series) { + return entries[series].frame; + } + + @Override + public Paint getSeriesPaint(int series) { + return entries[series].paint; + } + + @Override + public boolean getItemShapeFilled(int series, int item) { + return entries[series].filled; + } + + @Override + public XYItemRendererState initialise( + Graphics2D g2, + Rectangle2D dataArea, + XYPlot plot, + XYDataset data, + PlotRenderingInfo info + ) { + if (labelBoundingBoxes == null) { + labelBoundingBoxes = new ArrayList(32); + } + else { + labelBoundingBoxes.clear(); + } + + area = dataArea; + + return super.initialise(g2, dataArea, plot, data, info); + } + + @Override + public void drawItem( + Graphics2D g2, + XYItemRendererState state, + Rectangle2D dataArea, + PlotRenderingInfo info, + XYPlot plot, + ValueAxis domainAxis, + ValueAxis rangeAxis, + XYDataset dataset, + int series, + int item, + CrosshairState crosshairState, + int pass + ) { + if (!getItemVisible(series, item)) { + return; + } + + // get the data point... + double x1 = dataset.getXValue(series, item); + double y1 = dataset.getYValue(series, item); + if (Double.isNaN(x1) || Double.isNaN(y1)) { + return; + } + + RectangleEdge xAxisLocation = plot.getDomainAxisEdge(); + RectangleEdge yAxisLocation = plot.getRangeAxisEdge(); + double x = domainAxis.valueToJava2D(x1, dataArea, xAxisLocation); + double y = rangeAxis.valueToJava2D(y1, dataArea, yAxisLocation); + + if (dataArea.contains(x, y)) + super.drawItem( + g2, + state, + dataArea, + info, + plot, + domainAxis, + rangeAxis, + dataset, + series, + item, + crosshairState, + pass); + } + + protected Point2D shiftBox(Rectangle2D box) { + + double cx1 = area.getX(); + double cy1 = area.getY(); + double cx2 = cx1 + area.getWidth(); + double cy2 = cy1 + area.getHeight(); + + double bx1 = box.getX(); + double by1 = box.getY(); + double bx2 = bx1 + box.getWidth(); + double by2 = by1 + box.getHeight(); + + double dx; + double dy; + + if (bx1 < cx1) { + dx = cx1 - bx1; + } + else if (bx2 > cx2) { + dx = cx2 - bx2; + } + else { + dx = 0d; + } + + if (by1 < cy1) { + dy = cy1 - by1; + } + else if (by2 > cy2) { + dy = cy2 - by2; + } + else { + dy = 0d; + } + + return new Point2D.Double(dx, dy); + } + + @Override + protected void drawItemLabel( + Graphics2D g2, + PlotOrientation orientation, + XYDataset dataset, + int series, + int item, + double x, + double y, + boolean negative + ) { + XYItemLabelGenerator generator = getItemLabelGenerator(series, item); + if (generator == null) { + return; + } + + Font labelFont = getItemLabelFont(series, item); + + Paint paint = getItemLabelPaint(series, item); + + g2.setFont(labelFont); + g2.setPaint(paint); + + String label = generator.generateLabel(dataset, series, item); + + // get the label position.. + ItemLabelPosition position = null; + if (!negative) { + position = getPositiveItemLabelPosition(series, item); + } + else { + position = getNegativeItemLabelPosition(series, item); + } + + // work out the label anchor point... + Point2D anchorPoint = calculateLabelAnchorPoint( + position.getItemLabelAnchor(), x, y, orientation); + + Shape labelShape = TextUtilities.calculateRotatedStringBounds( + label, g2, + (float)anchorPoint.getX(), (float)anchorPoint.getY(), + position.getTextAnchor(), position.getAngle(), + position.getRotationAnchor()); + + Rectangle2D bbox = labelShape.getBounds2D(); + + Point2D shift = shiftBox(bbox); + + bbox = new Rectangle2D.Double( + bbox.getX() + shift.getX(), + bbox.getY() + shift.getY(), + bbox.getWidth(), + bbox.getHeight()); + + if (labelBoundingBoxes != null) { + for (Rectangle2D old: labelBoundingBoxes) { + if (old.intersects(bbox)) { + return; + } + } + labelBoundingBoxes.add(bbox); + } + + TextUtilities.drawRotatedString( + label, g2, + (float)(anchorPoint.getX() + shift.getX()), + (float)(anchorPoint.getY() + shift.getY()), + position.getTextAnchor(), position.getAngle(), + position.getRotationAnchor()); + } +} +// vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 : diff -r cd8d81b2824d -r 0d8146989012 flys-artifacts/src/main/java/de/intevation/flys/utils/Formatter.java --- a/flys-artifacts/src/main/java/de/intevation/flys/utils/Formatter.java Mon Jun 25 07:59:22 2012 +0000 +++ b/flys-artifacts/src/main/java/de/intevation/flys/utils/Formatter.java Mon Jun 25 11:38:33 2012 +0000 @@ -142,6 +142,12 @@ WATERLEVEL_KM_MAX_DIGITS); } + public static NumberFormat getWaterlevelW(CallMeta meta) { + return getFormatter( + meta, + WATERLEVEL_W_MIN_DIGITS, + WATERLEVEL_W_MAX_DIGITS); + } /** * Returns the number formatter for W values in waterlevel exports. @@ -168,6 +174,12 @@ WATERLEVEL_Q_MAX_DIGITS); } + public static NumberFormat getWaterlevelQ(CallMeta meta) { + return getFormatter( + meta, + WATERLEVEL_Q_MIN_DIGITS, + WATERLEVEL_Q_MAX_DIGITS); + } /** * Returns the number formatter for W values in exports of computed