1 package fr.ifremer.dali.ui.swing.util.map.layer;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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
51
52
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
70
71
72
73
74
75 @Override
76 public void draw(Graphics2D graphics, MapContent map, MapViewport viewport) {
77 if (viewport == null) {
78 viewport = map.getViewport();
79 }
80 if (viewport == null || viewport.getScreenArea() == null) {
81 return;
82 }
83 try {
84
85
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
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
110 List<Point2D> pointList = new ArrayList<>();
111 List<String> xPointNames = new ArrayList<>();
112 for (Double latitude : latitudeLabel.getList()) {
113
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
132 pointList.clear();
133 List<String> yPointNames = new ArrayList<>();
134 for (Double longitude : longitudeLabel.getList()) {
135
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
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
158 for (int i = 0; i <= xPointsCount - 2; i += 2) {
159 graphics.draw(new Line2D.Double(xPoints[i], xPoints[i + 1]));
160 }
161
162 for (int i = 0; i <= yPointsCount - 2; i += 2) {
163 graphics.draw(new Line2D.Double(yPoints[i], yPoints[i + 1]));
164 }
165
166
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
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
202
203
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
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
273 public boolean loose = false;
274
275
276 final private double w[];
277
278
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
284 final private double[] Q;
285
286
287 final private double base;
288
289 private double logB(double a) {
290 return Math.log(a) / Math.log(base);
291 }
292
293
294
295
296 private double flooredMod(double a, double n) {
297 return a - n * Math.floor(a / n);
298 }
299
300
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
343
344
345
346
347
348
349
350
351
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);
357 }
358
359 private double density_max(int k, int m) {
360 if (k >= m) {
361 return 2 - (double)(k - 1) / (m - 1);
362 } else {
363 return 1;
364 }
365 }
366
367 private double legibility(double min, double max, double step) {
368 return 1;
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
418
419
420
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
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 }