View Javadoc
1   package au.gov.amsa.geo.distance;
2   
3   import static au.gov.amsa.geo.model.Util.formatDate;
4   
5   import java.awt.Color;
6   import java.awt.Font;
7   import java.awt.Graphics2D;
8   import java.awt.Point;
9   import java.awt.image.BufferedImage;
10  import java.io.File;
11  import java.io.IOException;
12  import java.text.DecimalFormat;
13  
14  import javax.imageio.ImageIO;
15  
16  import org.apache.log4j.Logger;
17  
18  import rx.Observable;
19  import rx.functions.Action1;
20  import rx.functions.Func1;
21  import rx.schedulers.Schedulers;
22  import au.gov.amsa.geo.distance.DistanceTravelledCalculator.CalculationResult;
23  import au.gov.amsa.geo.model.Bounds;
24  import au.gov.amsa.geo.model.CellValue;
25  import au.gov.amsa.geo.model.Options;
26  import au.gov.amsa.geo.model.Position;
27  import au.gov.amsa.geo.projection.FeatureUtil;
28  import au.gov.amsa.geo.projection.Projector;
29  import au.gov.amsa.geo.projection.ProjectorBounds;
30  import au.gov.amsa.geo.projection.ProjectorTarget;
31  
32  import com.google.common.util.concurrent.AtomicDouble;
33  
34  /**
35   * Draws a vessel traffic density plot into a {@link Graphics2D}.
36   *
37   */
38  public class Renderer {
39  
40      private static Logger log = Logger.getLogger(Renderer.class);
41      private final static boolean coloured = false;
42      private static final String UNICODE_GREATER_THAN_OR_EQUAL = "\u2265 ";
43  
44      public static void paintAll(Graphics2D g, Options options, int numberStandardDeviations, int w,
45              int h, CalculationResult calculationResult, boolean addLegend, boolean addParameters) {
46          paintMap(g, options.getBounds(), options.getCellSizeDegreesAsDouble(),
47                  numberStandardDeviations, w, h, calculationResult.getCells(), addLegend);
48          if (addParameters)
49              paintParameters(g, options, calculationResult, w, h);
50      }
51  
52      private static Func1<Position, Point> epsg4326Locator(Bounds b, int w, int h) {
53  
54          ProjectorBounds projectorBounds = new ProjectorBounds(FeatureUtil.EPSG_4326,
55                  b.getTopLeftLon(), b.getBottomRightLat(), b.getBottomRightLon(), b.getTopLeftLat());
56          ProjectorTarget projectorTarget = new ProjectorTarget(w, h);
57          final Projector projector = new Projector(projectorBounds, projectorTarget);
58  
59          return new Func1<Position, Point>() {
60  
61              @Override
62              public Point call(Position p) {
63                  return projector.toPoint(p.lat(), p.lon());
64              }
65          };
66      }
67  
68      public static void paintMap(Graphics2D g, final Bounds b, double cellSizeDegrees,
69              double numberStandardDeviationsForHighValue, final int w, final int h,
70              Observable<CellValue> cells, boolean addLegend) {
71  
72          paintMap(g, b, cellSizeDegrees, numberStandardDeviationsForHighValue, w, h, cells,
73                  addLegend, epsg4326Locator(b, w, h));
74      }
75  
76      public static void paintMap(final Graphics2D g, Bounds b, final double cellSizeDegrees,
77              double numberStandardDeviationsForHighValue, final int w, final int h,
78              Observable<CellValue> cells, final boolean addLegend,
79              final Func1<Position, Point> locator) {
80  
81          final Statistics metrics = getStatistics(cells);
82          final double maxNmForColour = metrics.mean + numberStandardDeviationsForHighValue
83                  * metrics.sd;
84  
85          final double minSaturation = 0.05;
86          cells.observeOn(Schedulers.immediate()).subscribeOn(Schedulers.immediate())
87          // add to sum and sumSquares
88                  .doOnNext(new Action1<CellValue>() {
89                      @Override
90                      public void call(CellValue cell) {
91                          double topLeftLat = cell.getCentreLat() + cellSizeDegrees / 2;
92                          double topLeftLon = cell.getCentreLon() - cellSizeDegrees / 2;
93                          double bottomRightLat = cell.getCentreLat() - cellSizeDegrees / 2;
94                          double bottomRightLon = cell.getCentreLon() + cellSizeDegrees / 2;
95                          Point topLeft = locator.call(new Position(topLeftLat, topLeftLon));
96                          Point bottomRight = locator.call(new Position(bottomRightLat,
97                                  bottomRightLon));
98                          double d = cell.getValue();
99                          double prop = Math.min(d, maxNmForColour) / maxNmForColour;
100                         Color color = toColor(minSaturation, prop);
101                         g.setColor(color);
102                         g.fillRect(topLeft.x, topLeft.y, bottomRight.x - topLeft.x, bottomRight.y
103                                 - topLeft.y);
104                     }
105                 })
106                 // count
107                 .count()
108                 // block and get
109                 .toBlocking().single();
110 
111         if (addLegend)
112             paintLegend(g, cellSizeDegrees, metrics, maxNmForColour, minSaturation, w, h);
113 
114     }
115 
116     public static void paintParameters(Graphics2D g, Options options,
117             CalculationResult calculationResult, int w, int h) {
118 
119         g.setColor(Color.darkGray);
120         Font font = g.getFont();
121         g.setFont(font.deriveFont(10f));
122         {
123             String label = "startTime=" + formatDate(options.getStartTime()) + ", finishTime="
124                     + formatDate(options.getFinishTime()) + ", "
125                     + options.getSegmentOptions().toString();
126             int labelWidth = g.getFontMetrics().stringWidth(label);
127             g.drawString(label, (w - labelWidth) / 2, h - 50);
128         }
129         {
130             String label = metricsToString(calculationResult);
131             int labelWidth = g.getFontMetrics().stringWidth(label);
132             g.drawString(label, (w - labelWidth) / 2, h - 50 + g.getFontMetrics().getHeight());
133         }
134 
135         g.setFont(font);
136     }
137 
138     private static String metricsToString(CalculationResult r) {
139         StringBuilder s = new StringBuilder();
140         // s.append("cells=" + r.getCells().size());
141         DistanceCalculationMetrics m = r.getMetrics();
142         s.append(", fixes=" + m.fixes.get());
143         s.append(", inTime=" + m.fixesInTimeRange.get());
144         s.append(", inRegion=" + m.fixesWithinRegion.get());
145         s.append(", effSpdOk=" + m.fixesPassedEffectiveSpeedCheck.get());
146         s.append(", segs=" + m.segments.get());
147         s.append(", segsTimeDiffOk=" + m.segmentsTimeDifferenceOk.get());
148         s.append(", segCells=" + m.segmentCells.get());
149         s.append(", totalNm=" + m.totalNauticalMiles);
150         return s.toString();
151     }
152 
153     private static void paintLegend(Graphics2D g, double cellSizeDegrees, Statistics metrics,
154             double maxNmForColour, double minSaturation, int w, int h) {
155         int legendWidth = 120;
156         int rightMargin = 50;
157         int legendHeight = 200;
158         int topMargin = 50;
159         g.setColor(Color.darkGray);
160         int legendLeft = w - rightMargin - legendWidth;
161         int legendTop = topMargin;
162         g.clearRect(legendLeft, legendTop, legendWidth, legendHeight);
163         g.drawRect(legendLeft, legendTop, legendWidth, legendHeight);
164         int innerTopMargin = 15;
165         int innerBottomMargin = 15;
166         int innerRightMargin = 5;
167         int innerHeight = legendHeight - 2 - innerTopMargin - innerBottomMargin;
168         int innerWidth = (legendWidth - innerRightMargin) / 2;
169 
170         for (int i = 1; i < innerHeight; i++) {
171             int y = legendTop + innerHeight + innerTopMargin - i;
172             double prop = (double) i / innerHeight;
173             Color color = toColor(minSaturation, prop);
174             g.setColor(color);
175             g.drawLine(legendLeft + legendWidth - innerWidth - innerRightMargin, y, legendLeft
176                     + legendWidth - innerRightMargin, y);
177         }
178         int numMarkers = 5;
179         int markerLeftMargin = 5;
180         Font font = g.getFont();
181         g.setFont(new Font("Arial", Font.PLAIN, 10));
182         g.setColor(Color.black);
183         DecimalFormat df = new DecimalFormat("0.##E0");
184         int markerLineLength = 8;
185         for (int i = 0; i <= numMarkers; i++) {
186             int y = legendTop + innerHeight + innerTopMargin
187                     - (int) Math.round((double) i * innerHeight / numMarkers);
188             String label = df.format(maxNmForColour * i / numMarkers);
189             if (i == numMarkers)
190                 label = UNICODE_GREATER_THAN_OR_EQUAL + label;
191             g.drawString(label, legendLeft + markerLeftMargin, y);
192 
193             int markerLineX = legendLeft + legendWidth - innerRightMargin - innerWidth
194                     - markerLineLength / 2;
195             g.drawLine(markerLineX, y, markerLineX + markerLineLength, y);
196         }
197         g.drawString("nm/nm2", legendLeft + legendWidth - innerRightMargin - innerWidth + 5,
198                 legendTop + innerTopMargin - 2);
199         g.drawString("cellSize=" + new DecimalFormat("0.###").format(cellSizeDegrees) + "degs",
200                 legendLeft + markerLeftMargin, legendTop + legendHeight - 3);
201         g.setFont(font);
202 
203     }
204 
205     private static Color toColor(double minSaturation, double prop) {
206         if (coloured) {
207             return Color.getHSBColor((float) (1 - prop) * 0.5f, 1.0f, 1.0f);
208         } else {
209             return Color.getHSBColor(0.0f, (float) (prop * (1 - minSaturation) + minSaturation),
210                     1.0f);
211         }
212     }
213 
214     private static Statistics getStatistics(Observable<CellValue> cells) {
215         final AtomicDouble sum = new AtomicDouble(0);
216         final AtomicDouble sumSquares = new AtomicDouble(0);
217         log.info("calculating mean and sd");
218         long count = cells
219         // add to sum and sumSquares
220                 .doOnNext(new Action1<CellValue>() {
221                     @Override
222                     public void call(CellValue cell) {
223                         sum.addAndGet(cell.getValue());
224                         sumSquares.addAndGet(cell.getValue() * cell.getValue());
225                     }
226                 })
227                 // count
228                 .count()
229                 // block and get
230                 .toBlocking().single();
231         double mean = sum.get() / count;
232         double variance = sumSquares.get() / count - mean * mean;
233         double sd = Math.sqrt(variance);
234         log.info("calculated");
235         Statistics stats = new Statistics(mean, sd, count);
236         log.info(stats);
237         return stats;
238     }
239 
240     public static BufferedImage createImage(Options options, int numStandardDeviations, int width,
241             CalculationResult calculationResult) {
242         log.info("creating image");
243         int height = (int) Math.round(width / options.getBounds().getWidthDegrees()
244                 * options.getBounds().getHeightDegrees());
245         BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
246         Graphics2D g = image.createGraphics();
247         g.setBackground(Color.white);
248         g.clearRect(0, 0, width, height);
249         Renderer.paintAll(g, options, numStandardDeviations, width, height, calculationResult,
250                 true, true);
251         log.info("created image");
252         return image;
253     }
254 
255     public static void saveAsPng(BufferedImage image, File file) {
256         log.info("saving png to " + file);
257         try {
258             ImageIO.write(image, "png", file);
259             log.info("saved");
260         } catch (IOException e) {
261             throw new RuntimeException(e);
262         }
263     }
264 
265     private static class Statistics {
266         final double mean;
267         final double sd;
268         private final long count;
269 
270         Statistics(double mean, double sd, long count) {
271             this.mean = mean;
272             this.sd = sd;
273             this.count = count;
274         }
275 
276         @Override
277         public String toString() {
278             StringBuilder builder = new StringBuilder();
279             builder.append("Statistics [mean=");
280             builder.append(mean);
281             builder.append(", sd=");
282             builder.append(sd);
283             builder.append(", count=");
284             builder.append(count);
285             builder.append("]");
286             return builder.toString();
287         }
288 
289     }
290 
291 }