1 package org.opentrafficsim.web.animation.d2;
2
3 import java.awt.Canvas;
4 import java.awt.Color;
5 import java.awt.Dimension;
6 import java.awt.Font;
7 import java.awt.FontMetrics;
8 import java.awt.Graphics;
9 import java.awt.Image;
10 import java.awt.geom.Point2D;
11 import java.awt.geom.RectangularShape;
12 import java.awt.image.ImageObserver;
13 import java.text.NumberFormat;
14 import java.util.Optional;
15
16 import org.djutils.draw.bounds.Bounds2d;
17 import org.djutils.draw.point.Point2d;
18 import org.opentrafficsim.web.animation.HtmlGraphics2d;
19
20 import nl.tudelft.simulation.dsol.animation.d2.RenderableScale;
21
22
23
24
25
26
27
28
29
30 public class HtmlGridPanel implements ImageObserver
31 {
32
33 public static final int UP = 1;
34
35
36 public static final int DOWN = 2;
37
38
39 public static final int LEFT = 3;
40
41
42 public static final int RIGHT = 4;
43
44
45 public static final double ZOOMFACTOR = 1.2;
46
47
48 protected static final Color GRIDCOLOR = Color.BLACK;
49
50
51 @SuppressWarnings("checkstyle:visibilitymodifier")
52 protected Bounds2d extent = null;
53
54
55 @SuppressWarnings("checkstyle:visibilitymodifier")
56 protected Bounds2d homeExtent = null;
57
58
59 @SuppressWarnings("checkstyle:visibilitymodifier")
60 protected boolean showGrid = true;
61
62
63 @SuppressWarnings("checkstyle:visibilitymodifier")
64 protected double gridSizeX = 100.0;
65
66
67 @SuppressWarnings("checkstyle:visibilitymodifier")
68 protected double gridSizeY = 100.0;
69
70
71 @SuppressWarnings("checkstyle:visibilitymodifier")
72 protected NumberFormat formatter = NumberFormat.getInstance();
73
74
75 @SuppressWarnings("checkstyle:visibilitymodifier")
76 protected Dimension lastDimension = null;
77
78
79 @SuppressWarnings("checkstyle:visibilitymodifier")
80 protected Dimension lastScreen = null;
81
82
83 @SuppressWarnings("checkstyle:visibilitymodifier")
84 protected Double lastXScale = null;
85
86
87 @SuppressWarnings("checkstyle:visibilitymodifier")
88 protected Double lastYScale = null;
89
90
91 @SuppressWarnings("checkstyle:visibilitymodifier")
92 protected Dimension size = null;
93
94
95 @SuppressWarnings("checkstyle:visibilitymodifier")
96 protected Dimension preferredSize = null;
97
98
99 @SuppressWarnings("checkstyle:visibilitymodifier")
100 protected Point2d worldCoordinate = new Point2d(0, 0);
101
102
103 @SuppressWarnings("checkstyle:visibilitymodifier")
104 protected boolean showToolTip = true;
105
106
107 private Color background;
108
109
110 private String toolTipText = "";
111
112
113 private boolean showing = true;
114
115
116 private Font currentFont = new Font(Font.SANS_SERIF, Font.PLAIN, 10);
117
118
119 private Canvas canvas = new Canvas();
120
121
122 @SuppressWarnings("checkstyle:visibilitymodifier")
123 protected HtmlGraphics2d htmlGraphics2D;
124
125
126 @SuppressWarnings("checkstyle:visibilitymodifier")
127 protected RenderableScale renderableScale;
128
129
130 private boolean dirty = false;
131
132
133
134
135
136 public HtmlGridPanel(final Bounds2d extent)
137 {
138 this(extent, new Dimension(600, 600));
139 }
140
141
142
143
144
145
146 public HtmlGridPanel(final Bounds2d homeExtent, final Dimension size)
147 {
148 this.htmlGraphics2D = new HtmlGraphics2d();
149 this.extent = homeExtent;
150 this.homeExtent = homeExtent;
151 this.renderableScale = new RenderableScale();
152 this.setBackground(Color.WHITE);
153 this.setPreferredSize(size);
154 this.size = (Dimension) size.clone();
155 this.lastDimension = this.getSize();
156 this.lastScreen = this.getSize();
157 setExtent(this.homeExtent);
158 }
159
160
161
162
163
164 public String getDrawingCommands()
165 {
166 this.htmlGraphics2D.clearCommand();
167 this.paintComponent(this.htmlGraphics2D);
168 return this.htmlGraphics2D.closeAndGetCommands();
169 }
170
171
172
173
174
175 public void paintComponent(final HtmlGraphics2d g)
176 {
177 if (!this.getSize().equals(this.lastDimension))
178 {
179 this.lastDimension = this.getSize();
180 setExtent(computeVisibleExtent(this.extent).get());
181 }
182 if (this.showGrid)
183 {
184 this.drawGrid(g);
185 }
186 }
187
188
189
190
191
192 public final synchronized void showGrid(final boolean bool)
193 {
194 this.showGrid = bool;
195 this.repaint();
196 }
197
198
199
200
201
202 public final Bounds2d getExtent()
203 {
204 return this.extent;
205 }
206
207
208
209
210
211 public void setExtent(final Bounds2d extent)
212 {
213 if (this.lastScreen != null)
214 {
215
216 this.lastXScale = this.getRenderableScale().getXScale(extent, this.lastScreen);
217 this.lastYScale = this.getRenderableScale().getYScale(extent, this.lastScreen);
218 }
219 this.extent = extent;
220 this.repaint();
221 }
222
223
224
225
226
227 public final synchronized void setWorldCoordinate(final Point2d point)
228 {
229 this.worldCoordinate = point;
230 }
231
232
233
234
235
236 public final synchronized Point2d getWorldCoordinate()
237 {
238 return this.worldCoordinate;
239 }
240
241
242
243
244 public final synchronized void displayWorldCoordinateToolTip()
245 {
246 if (this.showToolTip)
247 {
248 String worldPoint = "(x=" + this.formatter.format(this.worldCoordinate.getX()) + " ; y="
249 + this.formatter.format(this.worldCoordinate.getY()) + ")";
250 setToolTipText(worldPoint);
251 }
252 }
253
254
255
256
257
258 public final synchronized boolean isShowToolTip()
259 {
260 return this.showToolTip;
261 }
262
263
264
265
266
267 public final synchronized void setShowToolTip(final boolean showToolTip)
268 {
269 this.showToolTip = showToolTip;
270 }
271
272
273
274
275
276
277 public final synchronized void pan(final int direction, final double percentage)
278 {
279 if (percentage <= 0 || percentage > 1.0)
280 {
281 throw new IllegalArgumentException("percentage<=0 || >1.0");
282 }
283 switch (direction)
284 {
285 case LEFT:
286 setExtent(new Bounds2d(this.extent.getMinX() - percentage * this.extent.getDeltaX(),
287 this.extent.getMaxX() - percentage * this.extent.getDeltaX(), this.extent.getMinY(),
288 this.extent.getMaxY()));
289 break;
290 case RIGHT:
291 setExtent(new Bounds2d(this.extent.getMinX() + percentage * this.extent.getDeltaX(),
292 this.extent.getMaxX() + percentage * this.extent.getDeltaX(), this.extent.getMinY(),
293 this.extent.getMaxY()));
294 break;
295 case UP:
296 setExtent(new Bounds2d(this.extent.getMinX(), this.extent.getMaxX(),
297 this.extent.getMinY() + percentage * this.extent.getDeltaY(),
298 this.extent.getMaxY() + percentage * this.extent.getDeltaY()));
299 break;
300 case DOWN:
301 setExtent(new Bounds2d(this.extent.getMinX(), this.extent.getMaxX(),
302 this.extent.getMinY() - percentage * this.extent.getDeltaY(),
303 this.extent.getMaxY() - percentage * this.extent.getDeltaY()));
304 break;
305 default:
306 throw new IllegalArgumentException("direction unkown");
307 }
308 this.repaint();
309 }
310
311
312
313
314 public final synchronized void home()
315 {
316 setExtent(computeVisibleExtent(this.homeExtent).get());
317 this.repaint();
318 }
319
320
321
322
323
324 public final boolean isShowGrid()
325 {
326 return this.showGrid;
327 }
328
329
330
331
332
333 public final void setShowGrid(final boolean showGrid)
334 {
335 this.showGrid = showGrid;
336 }
337
338
339
340
341
342 public final synchronized void zoom(final double factor)
343 {
344 zoom(factor, (int) (this.getWidth() / 2.0), (int) (this.getHeight() / 2.0));
345 }
346
347
348
349
350
351
352
353 public final synchronized void zoom(final double factor, final int mouseX, final int mouseY)
354 {
355 Point2d mwc = this.renderableScale.getWorldCoordinates(new Point2D.Double(mouseX, mouseY), this.extent, this.getSize());
356 double minX = mwc.getX() - (mwc.getX() - this.extent.getMinX()) * factor;
357 double minY = mwc.getY() - (mwc.getY() - this.extent.getMinY()) * factor;
358 double w = this.extent.getDeltaX() * factor;
359 double h = this.extent.getDeltaY() * factor;
360
361 setExtent(new Bounds2d(minX, minX + w, minY, minY + h));
362 this.repaint();
363 }
364
365
366
367
368
369
370 protected synchronized void drawGrid(final Graphics g)
371 {
372
373 g.setFont(g.getFont().deriveFont(11.0f));
374 g.setColor(GRIDCOLOR);
375 double scaleX = this.renderableScale.getXScale(this.extent, this.getSize());
376 double scaleY = this.renderableScale.getYScale(this.extent, this.getSize());
377
378 int count = 0;
379 int gridSizePixelsX = (int) Math.round(this.gridSizeX / scaleX);
380 while (gridSizePixelsX < 40)
381 {
382 this.gridSizeX = 10 * this.gridSizeX;
383 int maximumNumberOfDigits = (int) Math.max(0, 1 + Math.ceil(Math.log(1 / this.gridSizeX) / Math.log(10)));
384 this.formatter.setMaximumFractionDigits(maximumNumberOfDigits);
385 gridSizePixelsX = (int) Math.round(this.gridSizeX / scaleX);
386 if (count++ > 10)
387 {
388 break;
389 }
390 }
391
392 count = 0;
393 while (gridSizePixelsX > 10 * 40)
394 {
395 int maximumNumberOfDigits = (int) Math.max(0, 2 + Math.ceil(Math.log(1 / this.gridSizeX) / Math.log(10)));
396 this.formatter.setMaximumFractionDigits(maximumNumberOfDigits);
397 this.gridSizeX = this.gridSizeX / 10;
398 gridSizePixelsX = (int) Math.round(this.gridSizeX / scaleX);
399 if (count++ > 10)
400 {
401 break;
402 }
403 }
404
405 int gridSizePixelsY = (int) Math.round(this.gridSizeY / scaleY);
406 while (gridSizePixelsY < 40)
407 {
408 this.gridSizeY = 10 * this.gridSizeY;
409 int maximumNumberOfDigits = (int) Math.max(0, 1 + Math.ceil(Math.log(1 / this.gridSizeY) / Math.log(10)));
410 this.formatter.setMaximumFractionDigits(maximumNumberOfDigits);
411 gridSizePixelsY = (int) Math.round(this.gridSizeY / scaleY);
412 if (count++ > 10)
413 {
414 break;
415 }
416 }
417
418 count = 0;
419 while (gridSizePixelsY > 10 * 40)
420 {
421 int maximumNumberOfDigits = (int) Math.max(0, 2 + Math.ceil(Math.log(1 / this.gridSizeY) / Math.log(10)));
422 this.formatter.setMaximumFractionDigits(maximumNumberOfDigits);
423 this.gridSizeY = this.gridSizeY / 10;
424 gridSizePixelsY = (int) Math.round(this.gridSizeY / scaleY);
425 if (count++ > 10)
426 {
427 break;
428 }
429 }
430
431
432 double mod = this.extent.getMinX() % this.gridSizeX;
433 int x = (int) -Math.round(mod / scaleX);
434 while (x < this.getWidth())
435 {
436 Point2d point = this.renderableScale.getWorldCoordinates(new Point2D.Double(x, 0), this.extent, this.getSize());
437 if (point != null)
438 {
439 String label = this.formatter.format(Math.round(point.getX() / this.gridSizeX) * this.gridSizeX);
440 double labelWidth = this.getFontMetrics(this.getFont()).getStringBounds(label, g).getWidth();
441 if (x > labelWidth + 4)
442 {
443 g.drawLine(x, 15, x, this.getHeight());
444 g.drawString(label, (int) Math.round(x - 0.5 * labelWidth), 11);
445 }
446 }
447 x = x + gridSizePixelsX;
448 }
449
450
451 mod = Math.abs(this.extent.getMinY()) % this.gridSizeY;
452 int y = (int) Math.round(this.getSize().getHeight() - (mod / scaleY));
453 while (y > 15)
454 {
455 Point2d point = this.renderableScale.getWorldCoordinates(new Point2D.Double(0, y), this.extent, this.getSize());
456 if (point != null)
457 {
458 String label = this.formatter.format(Math.round(point.getY() / this.gridSizeY) * this.gridSizeY);
459 RectangularShape labelBounds = this.getFontMetrics(this.getFont()).getStringBounds(label, g);
460 g.drawLine((int) Math.round(labelBounds.getWidth() + 4), y, this.getWidth(), y);
461 g.drawString(label, 2, (int) Math.round(y + labelBounds.getHeight() * 0.3));
462 }
463 y = y - gridSizePixelsY;
464 }
465 }
466
467
468
469
470
471 public final RenderableScale getRenderableScale()
472 {
473 return this.renderableScale;
474 }
475
476
477
478
479
480 public final void setRenderableScale(final RenderableScale renderableScale)
481 {
482 this.renderableScale = renderableScale;
483 }
484
485
486
487
488 public void repaint()
489 {
490
491 this.dirty = true;
492 }
493
494
495
496
497
498 public final Dimension getSize()
499 {
500 return this.size;
501 }
502
503
504
505
506
507 public final void setSize(final Dimension size)
508 {
509 this.size = size;
510 }
511
512
513
514
515
516 public final Color getBackground()
517 {
518 return this.background;
519 }
520
521
522
523
524
525 public final void setBackground(final Color background)
526 {
527 this.background = background;
528 }
529
530
531
532
533
534 public final int getWidth()
535 {
536 return this.size.width;
537 }
538
539
540
541
542
543 public final int getHeight()
544 {
545 return this.size.height;
546 }
547
548
549
550
551
552 public final Dimension getPreferredSize()
553 {
554 return this.preferredSize;
555 }
556
557
558
559
560
561 public final void setPreferredSize(final Dimension preferredSize)
562 {
563 this.preferredSize = preferredSize;
564 }
565
566
567
568
569
570 public final String getToolTipText()
571 {
572 return this.toolTipText;
573 }
574
575
576
577
578
579 public final void setToolTipText(final String toolTipText)
580 {
581 this.toolTipText = toolTipText;
582 }
583
584
585
586
587
588 public final boolean isShowing()
589 {
590 return this.showing;
591 }
592
593
594
595
596
597 public final void setShowing(final boolean showing)
598 {
599 this.showing = showing;
600 }
601
602
603
604
605
606 public final Font getFont()
607 {
608 return this.currentFont;
609 }
610
611
612
613
614
615 public final void setFont(final Font font)
616 {
617 this.currentFont = font;
618 }
619
620
621
622
623
624
625 public final FontMetrics getFontMetrics(final Font font)
626 {
627 return this.canvas.getFontMetrics(font);
628 }
629
630
631
632
633
634 public final boolean isDirty()
635 {
636 return this.dirty;
637 }
638
639 @Override
640 public boolean imageUpdate(final Image img, final int infoflags, final int x, final int y, final int width,
641 final int height)
642 {
643 return false;
644 }
645
646
647
648
649
650
651
652 public Optional<Bounds2d> computeVisibleExtent(final Bounds2d extent)
653 {
654 Dimension screen = getSize();
655 double xScale = this.renderableScale.getXScale(extent, screen);
656 double yScale = this.renderableScale.getYScale(extent, screen);
657 Bounds2d result;
658 if (this.lastYScale != null && yScale == this.lastYScale)
659 {
660 result = new Bounds2d(extent.midPoint().getX() - 0.5 * screen.getWidth() * yScale,
661 extent.midPoint().getX() + 0.5 * screen.getWidth() * yScale, extent.getMinY(), extent.getMaxY());
662 xScale = yScale;
663 }
664 else if (this.lastXScale != null && xScale == this.lastXScale)
665 {
666 result = new Bounds2d(extent.getMinX(), extent.getMaxX(),
667 extent.midPoint().getY() - 0.5 * screen.getHeight() * xScale * this.renderableScale.getYScaleRatio(),
668 extent.midPoint().getY() + 0.5 * screen.getHeight() * xScale * this.renderableScale.getYScaleRatio());
669 yScale = xScale;
670 }
671 else
672 {
673 double scale = this.lastXScale == null ? Math.min(xScale, yScale)
674 : this.lastXScale * this.lastScreen.getWidth() / screen.getWidth();
675 result = new Bounds2d(extent.midPoint().getX() - 0.5 * screen.getWidth() * scale,
676 extent.midPoint().getX() + 0.5 * screen.getWidth() * scale,
677 extent.midPoint().getY() - 0.5 * screen.getHeight() * scale * this.renderableScale.getYScaleRatio(),
678 extent.midPoint().getY() + 0.5 * screen.getHeight() * scale * this.renderableScale.getYScaleRatio());
679 yScale = scale;
680 xScale = scale;
681 }
682 this.lastXScale = xScale;
683 this.lastYScale = yScale;
684 this.lastScreen = screen;
685 return Optional.of(result);
686 }
687
688 }