View Javadoc
1   package fr.ifremer.dali.ui.swing.util.map.layer;
2   
3   /*-
4    * #%L
5    * Dali :: UI
6    * $Id:$
7    * $HeadURL:$
8    * %%
9    * Copyright (C) 2014 - 2017 Ifremer
10   * %%
11   * This program is free software: you can redistribute it and/or modify
12   * it under the terms of the GNU Affero General Public License as published by
13   * the Free Software Foundation, either version 3 of the License, or
14   * (at your option) any later version.
15   * 
16   * This program is distributed in the hope that it will be useful,
17   * but WITHOUT ANY WARRANTY; without even the implied warranty of
18   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19   * GNU General Public License for more details.
20   * 
21   * You should have received a copy of the GNU Affero General Public License
22   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
23   * #L%
24   */
25  
26  import fr.ifremer.dali.map.MapProjection;
27  import org.apache.commons.logging.Log;
28  import org.apache.commons.logging.LogFactory;
29  import org.geotools.geometry.jts.ReferencedEnvelope;
30  import org.geotools.map.DirectLayer;
31  import org.geotools.map.MapContent;
32  import org.geotools.map.MapViewport;
33  import org.geotools.referencing.CRS;
34  import org.geotools.referencing.crs.DefaultGeographicCRS;
35  import org.opengis.referencing.FactoryException;
36  import org.opengis.referencing.crs.CoordinateReferenceSystem;
37  import org.opengis.referencing.operation.TransformException;
38  
39  import java.awt.*;
40  import java.awt.geom.Line2D;
41  import java.awt.geom.Point2D;
42  import java.awt.geom.Rectangle2D;
43  import java.math.RoundingMode;
44  import java.text.DecimalFormat;
45  import java.util.ArrayList;
46  import java.util.Iterator;
47  import java.util.List;
48  
49  /**
50   * Layer drawing the graticule
51   *
52   * @author peck7 on 09/06/2017.
53   */
54  public class GraticuleDirectLayer extends DirectLayer implements GraticuleLayer {
55  
56      private static Log log = LogFactory.getLog(GraticuleDirectLayer.class);
57  
58      private static XWilkinson xWilkinson = XWilkinson.base10();
59  
60      private final DecimalFormat df;
61  
62      public GraticuleDirectLayer() {
63          setTitle("graticuleDirectLayer");
64          df = new DecimalFormat("#.####");
65          df.setRoundingMode(RoundingMode.HALF_EVEN);
66      }
67  
68      /**
69       * Draw layer contents onto screen
70       *
71       * @param map      Map being drawn; check map bounds and crs
72       * @param graphics Graphics to draw into
73       * @param viewport Area to draw the map into; including screen area
74       */
75      @Override
76      public void draw(Graphics2D graphics, MapContent map, MapViewport viewport) {
77          if (viewport == null) {
78              viewport = map.getViewport(); // use the map viewport if one has not been provided
79          }
80          if (viewport == null || viewport.getScreenArea() == null) {
81              return; // renderer is not set up for use yet
82          }
83          try {
84  
85              // transform to WGS84 if needed
86              ReferencedEnvelope envelope = viewport.getBounds();
87              CoordinateReferenceSystem crs = envelope.getCoordinateReferenceSystem();
88              if (!CRS.equalsIgnoreMetadata(crs, DefaultGeographicCRS.WGS84)) {
89                  envelope = envelope.transform(DefaultGeographicCRS.WGS84, true);
90              }
91              // restrict to overall WGS84 envelope
92              envelope = envelope.intersection(MapProjection.WGS84.getEnvelope());
93  
94              double aspectRatio = envelope.getWidth() / envelope.getHeight();
95  
96              int nbXLines = 10;
97              int nbYLines = Math.max((int) ((nbXLines / aspectRatio) + 0.5), 2);
98              XWilkinson.Label latitudeLabel = xWilkinson.search(envelope.getMinX(), envelope.getMaxX(), nbXLines);
99              XWilkinson.Label longitudeLabel = xWilkinson.search(envelope.getMinY(), envelope.getMaxY(), nbYLines);
100             if (longitudeLabel.getScore() < 0.5)
101                 longitudeLabel = xWilkinson.search(envelope.getMinY(), envelope.getMaxY(), nbYLines + 1);
102 
103             if (log.isDebugEnabled()) {
104                 log.debug("aspect ratio = " + aspectRatio + " nbXLines = " + nbXLines + " nbYLines = " + nbYLines);
105                 log.debug("latitudes  = " + latitudeLabel);
106                 log.debug("longitudes = " + longitudeLabel);
107             }
108 
109             // latitude points
110             List<Point2D> pointList = new ArrayList<>();
111             List<String> xPointNames = new ArrayList<>();
112             for (Double latitude : latitudeLabel.getList()) {
113                 // include only inbound lines
114                 if (latitude > envelope.getMinX() && latitude < envelope.getMaxX()) {
115                     xPointNames.add(df.format(latitude));
116                     ReferencedEnvelope pointEnv = new ReferencedEnvelope(latitude, latitude, envelope.getMinY(), envelope.getMaxY(), DefaultGeographicCRS.WGS84);
117                     if (!CRS.equalsIgnoreMetadata(crs, DefaultGeographicCRS.WGS84)) {
118                         pointEnv = pointEnv.transform(crs, true);
119                     }
120 
121                     pointList.add(new Point2D.Double(pointEnv.getMaxX(), pointEnv.getMaxY()));
122                     pointList.add(new Point2D.Double(pointEnv.getMinX(), pointEnv.getMinY()));
123                 }
124             }
125 
126             int xPointsCount = pointList.size();
127             Point2D[] latitudePoints = pointList.toArray(new Point2D[xPointsCount]);
128             Point2D[] xPoints = new Point2D[xPointsCount];
129             viewport.getWorldToScreen().transform(latitudePoints, 0, xPoints, 0, xPointsCount);
130 
131             // longitude points
132             pointList.clear();
133             List<String> yPointNames = new ArrayList<>();
134             for (Double longitude : longitudeLabel.getList()) {
135                 // include only inbound lines
136                 if (longitude >= envelope.getMinY() && longitude <= envelope.getMaxY()) {
137                     yPointNames.add(df.format(longitude));
138                     ReferencedEnvelope pointEnv = new ReferencedEnvelope(envelope.getMinX(), envelope.getMaxX(), longitude, longitude, DefaultGeographicCRS.WGS84);
139                     if (!CRS.equalsIgnoreMetadata(crs, DefaultGeographicCRS.WGS84))
140                         pointEnv = pointEnv.transform(crs, true);
141 
142                     pointList.add(new Point2D.Double(pointEnv.getMinX(), pointEnv.getMinY()));
143                     pointList.add(new Point2D.Double(pointEnv.getMaxX(), pointEnv.getMaxY()));
144                 }
145             }
146 
147             int yPointsCount = pointList.size();
148             Point2D[] longitudePoints = pointList.toArray(new Point2D[yPointsCount]);
149             Point2D[] yPoints = new Point2D[yPointsCount];
150             viewport.getWorldToScreen().transform(longitudePoints, 0, yPoints, 0, yPointsCount);
151 
152             // draw
153             Stroke oldStroke = graphics.getStroke();
154             graphics.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, new float[]{9}, 0));
155             graphics.setColor(Color.BLUE.brighter().brighter());
156 
157             // latitude (x) lines
158             for (int i = 0; i <= xPointsCount - 2; i += 2) {
159                 graphics.draw(new Line2D.Double(xPoints[i], xPoints[i + 1]));
160             }
161             // longitude (y) lines
162             for (int i = 0; i <= yPointsCount - 2; i += 2) {
163                 graphics.draw(new Line2D.Double(yPoints[i], yPoints[i + 1]));
164             }
165 
166             // latitude (x) text
167             for (int i = 0; i < xPointNames.size(); i++) {
168                 String text = xPointNames.get(i);
169                 FontMetrics fm = graphics.getFontMetrics();
170                 Rectangle2D textRect = fm.getStringBounds(text, graphics);
171                 int textX = (int) ((int) xPoints[i * 2].getX() - (textRect.getWidth() / 2));
172                 int textY = (int) xPoints[i * 2].getY();
173 
174                 graphics.setColor(Color.WHITE);
175                 graphics.fillRect(textX, textY, (int) textRect.getWidth() + 2, (int) textRect.getHeight() + 1);
176                 graphics.setColor(Color.BLACK);
177                 graphics.drawString(text, textX + 1, textY + 13);
178             }
179             // longitude (y) text
180             for (int i = 0; i < yPointNames.size(); i++) {
181                 String text = yPointNames.get(i);
182                 FontMetrics fm = graphics.getFontMetrics();
183                 Rectangle2D textRect = fm.getStringBounds(text, graphics);
184                 int textX = (int) yPoints[i * 2].getX() + 1;
185                 int textY = (int) ((int) yPoints[i * 2].getY() - (textRect.getHeight() / 2));
186 
187                 graphics.setColor(Color.WHITE);
188                 graphics.fillRect(textX, textY, (int) textRect.getWidth() + 2, (int) textRect.getHeight() + 1);
189                 graphics.setColor(Color.BLACK);
190                 graphics.drawString(text, textX + 1, textY + 13);
191             }
192 
193             graphics.setStroke(oldStroke);
194 
195         } catch (TransformException | FactoryException e) {
196             log.warn(e.getMessage(), e);
197         }
198     }
199 
200     /**
201      * Does not contribute a bounding box to the map.
202      *
203      * @return null
204      */
205     @Override
206     public ReferencedEnvelope getBounds() {
207         return null;
208     }
209 
210     public static class XWilkinson {
211 
212         private XWilkinson(double[] Q, double base, double[] w, double eps) {
213             this.w = w;
214             this.Q = Q;
215             this.base = base;
216             this.eps = eps;
217         }
218 
219         private XWilkinson(double[] Q, double base) {
220             this(Q, base, new double[]{0.25, 0.2, 0.5, 0.05}, 1e-10);
221         }
222 
223         public static XWilkinson of(double[] Q, double base) {
224             return new XWilkinson(Q, base);
225         }
226 
227         public static XWilkinson base10() {
228             return XWilkinson.of(new double[]{1, 5, 2, 2.5, 4, 3}, 10);
229         }
230 
231         public static XWilkinson base2() {
232             return XWilkinson.of(new double[]{1}, 2);
233         }
234 
235         public static XWilkinson base16() {
236             return XWilkinson.of(new double[]{1, 2, 4, 8}, 16);
237         }
238 
239         //- Factory methods that may be useful for time-axis implementations
240         public static XWilkinson forSeconds() {
241             return XWilkinson.of(new double[]{1, 2, 3, 5, 10, 15, 20, 30}, 60);
242         }
243 
244         public static XWilkinson forMinutes() {
245             return XWilkinson.of(new double[]{1, 2, 3, 5, 10, 15, 20, 30}, 60);
246         }
247 
248         public static XWilkinson forHours24() {
249             return XWilkinson.of(new double[]{1, 2, 3, 4, 6, 8, 12}, 24);
250         }
251 
252         public static XWilkinson forHours12() {
253             return XWilkinson.of(new double[]{1, 2, 3, 4, 6}, 12);
254         }
255 
256         public static XWilkinson forDays() {
257             return XWilkinson.of(new double[]{1, 2}, 7);
258         }
259 
260         public static XWilkinson forWeeks() {
261             return XWilkinson.of(new double[]{1, 2, 4, 13, 26}, 52);
262         }
263 
264         public static XWilkinson forMonths() {
265             return XWilkinson.of(new double[]{1, 2, 3, 4, 6}, 12);
266         }
267 
268         public static XWilkinson forYears() {
269             return XWilkinson.of(new double[]{1, 2, 5}, 10);
270         }
271 
272         // Loose flag
273         public boolean loose = false;
274 
275         // scale-goodness weights for simplicity, coverage, density, legibility
276         final private double w[];
277 
278         // calculation of scale-goodness
279         private double w(double s, double c, double d, double l) {
280             return w[0] * s + w[1] * c + w[2] * d + w[3] * l;
281         }
282 
283         // Initial step sizes which we use as seed of generator
284         final private double[] Q;
285 
286         // Number base used to calculate logarithms
287         final private double base;
288 
289         private double logB(double a) {
290             return Math.log(a) / Math.log(base);
291         }
292 
293         /*
294          * a mod b for float numbers (reminder of a/b)
295          */
296         private double flooredMod(double a, double n) {
297             return a - n * Math.floor(a / n);
298         }
299 
300         // can be injected via c'tor depending on your application, default is 1e-10
301         final private double eps;
302 
303         private double v(double min, double max, double step) {
304             return (flooredMod(min, step) < eps && min <= 0 && max >= 0) ? 1 : 0;
305         }
306 
307         private double simplicity(int i, int j, double min, double max, double step) {
308             if (Q.length > 1) {
309                 return 1 - (double) i / (Q.length - 1) - j + v(min, max, step);
310             } else {
311                 return 1 - j + v(min, max, step);
312             }
313         }
314 
315         private double simplicity_max(int i, int j) {
316             if (Q.length > 1) {
317                 return 1 - (double) i / (Q.length - 1) - j + 1.0;
318             } else {
319                 return 1 - j + 1.0;
320             }
321         }
322 
323         private double coverage(double dmin, double dmax, double lmin, double lmax) {
324             double a = dmax - lmax;
325             double b = dmin - lmin;
326             double c = 0.1 * (dmax - dmin);
327             return 1 - 0.5 * ((a * a + b * b) / (c * c));
328         }
329 
330         private double coverage_max(double dmin, double dmax, double span) {
331             double range = dmax - dmin;
332             if (span > range) {
333                 double half = (span - range) / 2;
334                 double r = 0.1 * range;
335                 return 1 - half * half / (r * r);
336             } else {
337                 return 1.0;
338             }
339         }
340 
341         /**
342          * @param k    number of labels
343          * @param m    number of desired labels
344          * @param dmin data range minimum
345          * @param dmax data range maximum
346          * @param lmin label range minimum
347          * @param lmax label range maximum
348          * @return density k-1 number of intervals between labels
349          * m-1 number of intervals between desired number of labels
350          * r   label interval length/label range
351          * rt  desired label interval length/actual range
352          */
353         private double density(int k, int m, double dmin, double dmax, double lmin, double lmax) {
354             double r = (k - 1) / (lmax - lmin);
355             double rt = (m - 1) / (Math.max(lmax, dmax) - Math.min(lmin, dmin));
356             return 2 - Math.max(r / rt, rt / r);   // return 1-Math.max(r/rt, rt/r); (paper is wrong)
357         }
358 
359         private double density_max(int k, int m) {
360             if (k >= m) {
361                 return 2 - (double)(k - 1) / (m - 1);        // return 2-(k-1)/(m-1); (paper is wrong)
362             } else {
363                 return 1;
364             }
365         }
366 
367         private double legibility(double min, double max, double step) {
368             return 1; // Maybe later more...
369         }
370 
371         public class Label implements Iterable<Double> {
372 
373             private double min, max, step, score;
374 
375             @Override
376             public String toString() {
377                 DecimalFormat df = new DecimalFormat("00.00");
378                 StringBuilder s = new StringBuilder("(Score: " + df.format(score) + ") ");
379                 for (double x = min; x <= max; x = x + step) {
380                     s.append(df.format(x)).append("\t");
381                 }
382                 return s.toString();
383             }
384 
385             @Override
386             public Iterator<Double> iterator() {
387                 return getList().iterator();
388             }
389 
390             public List<Double> getList() {
391                 List<Double> list = new ArrayList<>();
392                 for (double i = min; i <= max; i += step) {
393                     list.add(i);
394                 }
395                 return list;
396             }
397 
398             public double getMin() {
399                 return min;
400             }
401 
402             public double getMax() {
403                 return max;
404             }
405 
406             public double getStep() {
407                 return step;
408             }
409 
410             public double getScore() {
411                 return score;
412             }
413 
414         }
415 
416         /**
417          * @param dmin data range min
418          * @param dmax data range max
419          * @param m    desired number of labels
420          * @return XWilkinson.Label
421          */
422         public Label search(double dmin, double dmax, int m) {
423             Label best = new Label();
424             double bestScore = -2;
425             double sm, dm, cm, delta;
426             int j = 1;
427 
428             main_loop:
429             while (j < Integer.MAX_VALUE) {
430                 for (int _i = 0; _i < Q.length; _i++) {
431                     int i = _i + 1;
432                     double q = Q[_i];
433                     sm = simplicity_max(i, j);
434                     if (w(sm, 1, 1, 1) < bestScore) {
435                         break main_loop;
436                     }
437                     int k = 2;
438                     while (k < Integer.MAX_VALUE) {
439                         dm = density_max(k, m);
440                         if (w(sm, 1, dm, 1) < bestScore) {
441                             break;
442                         }
443                         delta = (dmax - dmin) / (k + 1) / (j * q);
444                         int z = (int) Math.ceil(logB(delta));
445                         while (z < Integer.MAX_VALUE) {
446                             double step = j * q * Math.pow(base, z);
447                             cm = coverage_max(dmin, dmax, step * (k - 1));
448                             if (w(sm, cm, dm, 1) < bestScore) {
449                                 break;
450                             }
451                             int min_start = (int) (Math.floor(dmax / step - (k - 1)) * j);
452                             int max_start = (int) (Math.ceil(dmin / step)) * j;
453 
454                             for (int start = min_start; start <= max_start; start++) {
455                                 double lmin = start * step / j;
456                                 double lmax = lmin + step * (k - 1);
457                                 double c = coverage(dmin, dmax, lmin, lmax);
458                                 double s = simplicity(i, j, lmin, lmax, step);
459                                 double d = density(k, m, dmin, dmax, lmin, lmax);
460                                 double l = legibility(lmin, lmax, step);
461                                 double score = w(s, c, d, l);
462 
463                                 // later legibility logic can be implemented here
464 
465                                 if (score > bestScore && (!loose || (lmin <= dmin && lmax >= dmax))) {
466                                     best.min = lmin;
467                                     best.max = lmax;
468                                     best.step = step;
469                                     best.score = score;
470                                     bestScore = score;
471                                 }
472                             }
473                             z = z + 1;
474                         }
475                         k = k + 1;
476                     }
477                 }
478                 j = j + 1;
479             }
480             return best;
481         }
482 
483     }
484 
485 }