View Javadoc
1   package nl.tudelft.simulation.dsol.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  
15  import org.djutils.draw.bounds.Bounds2d;
16  import org.djutils.draw.point.Point2d;
17  
18  import nl.tudelft.simulation.dsol.animation.D2.RenderableScale;
19  import nl.tudelft.simulation.dsol.web.animation.HTMLGraphics2D;
20  
21  /**
22   * The GridPanel introduces the gridPanel.
23   * <p>
24   * <p>
25   * Copyright (c) 2003-2022 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved.
26   * BSD-style license. See <a href="https://opentrafficsim.org/docs/v2/license.html">OpenTrafficSim License</a>.
27   * </p>
28   * </p>
29   * @author <a href="mailto:nlang@fbk.eur.nl">Niels Lang </a>, <a href="https://www.peter-jacobs.com">Peter Jacobs </a>
30   */
31  public class HTMLGridPanel implements ImageObserver
32  {
33      /** the UP directions for moving/zooming. */
34      public static final int UP = 1;
35  
36      /** the DOWN directions for moving/zooming. */
37      public static final int DOWN = 2;
38  
39      /** the LEFT directions for moving/zooming. */
40      public static final int LEFT = 3;
41  
42      /** the RIGHT directions for moving/zooming. */
43      public static final int RIGHT = 4;
44  
45      /** the ZOOM factor. */
46      public static final double ZOOMFACTOR = 1.2;
47  
48      /** gridColor. */
49      protected static final Color GRIDCOLOR = Color.BLACK;
50  
51      /** the extent of this panel. */
52      @SuppressWarnings("checkstyle:visibilitymodifier")
53      protected Bounds2d extent = null;
54  
55      /** the extent of this panel. */
56      @SuppressWarnings("checkstyle:visibilitymodifier")
57      protected Bounds2d homeExtent = null;
58  
59      /** show the grid. */
60      @SuppressWarnings("checkstyle:visibilitymodifier")
61      protected boolean showGrid = true;
62  
63      /** the gridSize for the X-direction in world Units. */
64      @SuppressWarnings("checkstyle:visibilitymodifier")
65      protected double gridSizeX = 100.0;
66  
67      /** the gridSize for the Y-direction in world Units. */
68      @SuppressWarnings("checkstyle:visibilitymodifier")
69      protected double gridSizeY = 100.0;
70  
71      /** the formatter to use. */
72      @SuppressWarnings("checkstyle:visibilitymodifier")
73      protected NumberFormat formatter = NumberFormat.getInstance();
74  
75      /** the last computed Dimension. */
76      @SuppressWarnings("checkstyle:visibilitymodifier")
77      protected Dimension lastDimension = null;
78  
79      /** the last computed Dimension. */
80      @SuppressWarnings("checkstyle:visibilitymodifier")
81      protected Dimension size = null;
82  
83      /** the last computed Dimension. */
84      @SuppressWarnings("checkstyle:visibilitymodifier")
85      protected Dimension preferredSize = null;
86  
87      /** the last known world coordinate of the mouse. */
88      @SuppressWarnings("checkstyle:visibilitymodifier")
89      protected Point2d worldCoordinate = new Point2d(0, 0);
90  
91      /** whether to show a tooltip with the coordinates or not. */
92      @SuppressWarnings("checkstyle:visibilitymodifier")
93      protected boolean showToolTip = true;
94  
95      /** the background color. */
96      private Color background;
97  
98      /** The tooltip text which shows the coordinates. */
99      private String toolTipText = "";
100 
101     /** Whether the panel is showing or not. */
102     private boolean showing = true;
103 
104     /** the current font. */
105     private Font currentFont = new Font(Font.SANS_SERIF, Font.PLAIN, 10);
106 
107     /** the canvas to determine the font metrics. */
108     private Canvas canvas = new Canvas();
109 
110     /** the HTMLGraphics2D 'shadow' canvas. */
111     @SuppressWarnings("checkstyle:visibilitymodifier")
112     protected HTMLGraphics2D htmlGraphics2D;
113 
114     /** the renderable scale (X/Y ratio) to use. */
115     @SuppressWarnings("checkstyle:visibilitymodifier")
116     protected RenderableScale renderableScale;
117 
118     /** dirty flag. */
119     private boolean dirty = false;
120 
121     /**
122      * constructs a new GridPanel.
123      * @param extent Bounds2d; the extent to show.
124      */
125     public HTMLGridPanel(final Bounds2d extent)
126     {
127         this(extent, new Dimension(600, 600));
128     }
129 
130     /**
131      * constructs a new GridPanel.
132      * @param extent Bounds2d; the initial extent.
133      * @param size Dimension; the size of the panel in pixels.
134      */
135     public HTMLGridPanel(final Bounds2d extent, final Dimension size)
136     {
137         this.htmlGraphics2D = new HTMLGraphics2D();
138         this.homeExtent = extent;
139         this.renderableScale = new RenderableScale();
140         this.setBackground(Color.WHITE);
141         this.setPreferredSize(size);
142         this.size = (Dimension) size.clone();
143         this.lastDimension = this.getSize();
144         setExtent(this.homeExtent);
145     }
146 
147     /**
148      * Return the set of drawing commands.
149      * @return the set of drawing commands
150      */
151     public String getDrawingCommands()
152     {
153         this.htmlGraphics2D.clearCommand();
154         this.paintComponent(this.htmlGraphics2D);
155         return this.htmlGraphics2D.closeAndGetCommands();
156     }
157 
158     /**
159      * Draw the grid.
160      * @param g HTMLGraphics2D; the virtual Graphics2D canvas to enable writing to the browser
161      */
162     public void paintComponent(final HTMLGraphics2D g)
163     {
164         if (!this.getSize().equals(this.lastDimension))
165         {
166             this.lastDimension = this.getSize();
167             setExtent(this.renderableScale.computeVisibleExtent(this.extent, this.getSize()));
168         }
169         if (this.showGrid)
170         {
171             this.drawGrid(g);
172         }
173     }
174 
175     /**
176      * show the grid?
177      * @param bool boolean; true/false
178      */
179     public final synchronized void showGrid(final boolean bool)
180     {
181         this.showGrid = bool;
182         this.repaint();
183     }
184 
185     /**
186      * returns the extent of this panel.
187      * @return Bounds2d
188      */
189     public final Bounds2d getExtent()
190     {
191         return this.extent;
192     }
193 
194     /**
195      * set a new extent for this panel.
196      * @param extent Bounds2d; set a new extent
197      */
198     public final void setExtent(final Bounds2d extent)
199     {
200         this.extent = extent;
201         this.repaint();
202     }
203 
204     /**
205      * Set the world coordinates based on a mouse move.
206      * @param point Point2d; the x,y world coordinates
207      */
208     public final synchronized void setWorldCoordinate(final Point2d point)
209     {
210         this.worldCoordinate = point;
211     }
212 
213     /**
214      * @return worldCoordinate
215      */
216     public final synchronized Point2d getWorldCoordinate()
217     {
218         return this.worldCoordinate;
219     }
220 
221     /**
222      * Display a tooltip with the last known world coordinates of the mouse, in case the tooltip should be displayed.
223      */
224     public final synchronized void displayWorldCoordinateToolTip()
225     {
226         if (this.showToolTip)
227         {
228             String worldPoint = "(x=" + this.formatter.format(this.worldCoordinate.getX()) + " ; y="
229                     + this.formatter.format(this.worldCoordinate.getY()) + ")";
230             setToolTipText(worldPoint);
231         }
232     }
233 
234     /**
235      * @return showToolTip
236      */
237     public final synchronized boolean isShowToolTip()
238     {
239         return this.showToolTip;
240     }
241 
242     /**
243      * @param showToolTip boolean; set showToolTip
244      */
245     public final synchronized void setShowToolTip(final boolean showToolTip)
246     {
247         this.showToolTip = showToolTip;
248     }
249 
250     /**
251      * pans the panel in a specified direction.
252      * @param direction int; the direction
253      * @param percentage double; the percentage
254      */
255     public final synchronized void pan(final int direction, final double percentage)
256     {
257         if (percentage <= 0 || percentage > 1.0)
258         {
259             throw new IllegalArgumentException("percentage<=0 || >1.0");
260         }
261         switch (direction)
262         {
263             case LEFT:
264                 setExtent(new Bounds2d(this.extent.getMinX() - percentage * this.extent.getDeltaX(),
265                         this.extent.getMaxX() - percentage * this.extent.getDeltaX(), this.extent.getMinY(),
266                         this.extent.getMaxY()));
267                 break;
268             case RIGHT:
269                 setExtent(new Bounds2d(this.extent.getMinX() + percentage * this.extent.getDeltaX(),
270                         this.extent.getMaxX() + percentage * this.extent.getDeltaX(), this.extent.getMinY(),
271                         this.extent.getMaxY()));
272                 break;
273             case UP:
274                 setExtent(new Bounds2d(this.extent.getMinX(), this.extent.getMaxX(),
275                         this.extent.getMinY() + percentage * this.extent.getDeltaY(),
276                         this.extent.getMaxY() + percentage * this.extent.getDeltaY()));
277                 break;
278             case DOWN:
279                 setExtent(new Bounds2d(this.extent.getMinX(), this.extent.getMaxX(),
280                         this.extent.getMinY() - percentage * this.extent.getDeltaY(),
281                         this.extent.getMaxY() - percentage * this.extent.getDeltaY()));
282                 break;
283             default:
284                 throw new IllegalArgumentException("direction unkown");
285         }
286         this.repaint();
287     }
288 
289     /**
290      * resets the panel to its original extent.
291      */
292     public final synchronized void home()
293     {
294         setExtent(this.renderableScale.computeVisibleExtent(this.homeExtent, this.getSize()));
295         this.repaint();
296     }
297 
298     /**
299      * @return Returns the showGrid.
300      */
301     public final boolean isShowGrid()
302     {
303         return this.showGrid;
304     }
305 
306     /**
307      * @param showGrid boolean; The showGrid to set.
308      */
309     public final void setShowGrid(final boolean showGrid)
310     {
311         this.showGrid = showGrid;
312     }
313 
314     /**
315      * zooms in/out.
316      * @param factor double; The zoom factor
317      */
318     public final synchronized void zoom(final double factor)
319     {
320         zoom(factor, (int) (this.getWidth() / 2.0), (int) (this.getHeight() / 2.0));
321     }
322 
323     /**
324      * zooms in/out.
325      * @param factor double; The zoom factor
326      * @param mouseX int; x-position of the mouse around which we zoom
327      * @param mouseY int; y-position of the mouse around which we zoom
328      */
329     public final synchronized void zoom(final double factor, final int mouseX, final int mouseY)
330     {
331         Point2d mwc = this.renderableScale.getWorldCoordinates(new Point2D.Double(mouseX, mouseY), this.extent, this.getSize());
332         double minX = mwc.getX() - (mwc.getX() - this.extent.getMinX()) * factor;
333         double minY = mwc.getY() - (mwc.getY() - this.extent.getMinY()) * factor;
334         double w = this.extent.getDeltaX() * factor;
335         double h = this.extent.getDeltaY() * factor;
336 
337         setExtent(new Bounds2d(minX, minX + w, minY, minY + h));
338         this.repaint();
339     }
340 
341     /**
342      * Added to make sure the recursive render-call calls THIS render method instead of a potential super-class defined
343      * 'paintComponent' render method.
344      * @param g Graphics; the graphics object
345      */
346     protected synchronized void drawGrid(final Graphics g)
347     {
348         // we prepare the graphics object for the grid
349         g.setFont(g.getFont().deriveFont(11.0f));
350         g.setColor(GRIDCOLOR);
351         double scaleX = this.renderableScale.getXScale(this.extent, this.getSize());
352         double scaleY = this.renderableScale.getYScale(this.extent, this.getSize());
353 
354         int count = 0;
355         int gridSizePixelsX = (int) Math.round(this.gridSizeX / scaleX);
356         while (gridSizePixelsX < 40)
357         {
358             this.gridSizeX = 10 * this.gridSizeX;
359             int maximumNumberOfDigits = (int) Math.max(0, 1 + Math.ceil(Math.log(1 / this.gridSizeX) / Math.log(10)));
360             this.formatter.setMaximumFractionDigits(maximumNumberOfDigits);
361             gridSizePixelsX = (int) Math.round(this.gridSizeX / scaleX);
362             if (count++ > 10)
363             {
364                 break;
365             }
366         }
367 
368         count = 0;
369         while (gridSizePixelsX > 10 * 40)
370         {
371             int maximumNumberOfDigits = (int) Math.max(0, 2 + Math.ceil(Math.log(1 / this.gridSizeX) / Math.log(10)));
372             this.formatter.setMaximumFractionDigits(maximumNumberOfDigits);
373             this.gridSizeX = this.gridSizeX / 10;
374             gridSizePixelsX = (int) Math.round(this.gridSizeX / scaleX);
375             if (count++ > 10)
376             {
377                 break;
378             }
379         }
380 
381         int gridSizePixelsY = (int) Math.round(this.gridSizeY / scaleY);
382         while (gridSizePixelsY < 40)
383         {
384             this.gridSizeY = 10 * this.gridSizeY;
385             int maximumNumberOfDigits = (int) Math.max(0, 1 + Math.ceil(Math.log(1 / this.gridSizeY) / Math.log(10)));
386             this.formatter.setMaximumFractionDigits(maximumNumberOfDigits);
387             gridSizePixelsY = (int) Math.round(this.gridSizeY / scaleY);
388             if (count++ > 10)
389             {
390                 break;
391             }
392         }
393 
394         count = 0;
395         while (gridSizePixelsY > 10 * 40)
396         {
397             int maximumNumberOfDigits = (int) Math.max(0, 2 + Math.ceil(Math.log(1 / this.gridSizeY) / Math.log(10)));
398             this.formatter.setMaximumFractionDigits(maximumNumberOfDigits);
399             this.gridSizeY = this.gridSizeY / 10;
400             gridSizePixelsY = (int) Math.round(this.gridSizeY / scaleY);
401             if (count++ > 10)
402             {
403                 break;
404             }
405         }
406 
407         // Let's draw the vertical lines
408         double mod = this.extent.getMinX() % this.gridSizeX;
409         int x = (int) -Math.round(mod / scaleX);
410         while (x < this.getWidth())
411         {
412             Point2d point = this.renderableScale.getWorldCoordinates(new Point2D.Double(x, 0), this.extent, this.getSize());
413             if (point != null)
414             {
415                 String label = this.formatter.format(Math.round(point.getX() / this.gridSizeX) * this.gridSizeX);
416                 double labelWidth = this.getFontMetrics(this.getFont()).getStringBounds(label, g).getWidth();
417                 if (x > labelWidth + 4)
418                 {
419                     g.drawLine(x, 15, x, this.getHeight());
420                     g.drawString(label, (int) Math.round(x - 0.5 * labelWidth), 11);
421                 }
422             }
423             x = x + gridSizePixelsX;
424         }
425 
426         // Let's draw the horizontal lines
427         mod = Math.abs(this.extent.getMinY()) % this.gridSizeY;
428         int y = (int) Math.round(this.getSize().getHeight() - (mod / scaleY));
429         while (y > 15)
430         {
431             Point2d point = this.renderableScale.getWorldCoordinates(new Point2D.Double(0, y), this.extent, this.getSize());
432             if (point != null)
433             {
434                 String label = this.formatter.format(Math.round(point.getY() / this.gridSizeY) * this.gridSizeY);
435                 RectangularShape labelBounds = this.getFontMetrics(this.getFont()).getStringBounds(label, g);
436                 g.drawLine((int) Math.round(labelBounds.getWidth() + 4), y, this.getWidth(), y);
437                 g.drawString(label, 2, (int) Math.round(y + labelBounds.getHeight() * 0.3));
438             }
439             y = y - gridSizePixelsY;
440         }
441     }
442 
443     /**
444      * @return renderableScale
445      */
446     public final RenderableScale getRenderableScale()
447     {
448         return this.renderableScale;
449     }
450 
451     /**
452      * @param renderableScale RenderableScale; set renderableScale
453      */
454     public final void setRenderableScale(final RenderableScale renderableScale)
455     {
456         this.renderableScale = renderableScale;
457     }
458 
459     /**
460      * Repaint the shadow canvas.
461      */
462     public void repaint()
463     {
464         // repaint does not do any painting -- information is pulled from the browser
465         this.dirty = true;
466     }
467 
468     /**
469      * @return size
470      */
471     public final Dimension getSize()
472     {
473         return this.size;
474     }
475 
476     /**
477      * @param size Dimension; set size
478      */
479     public final void setSize(final Dimension size)
480     {
481         this.size = size;
482     }
483 
484     /**
485      * @return background
486      */
487     public final Color getBackground()
488     {
489         return this.background;
490     }
491 
492     /**
493      * @param background Color; set background
494      */
495     public final void setBackground(final Color background)
496     {
497         this.background = background;
498     }
499 
500     /**
501      * @return width
502      */
503     public final int getWidth()
504     {
505         return this.size.width;
506     }
507 
508     /**
509      * @return height
510      */
511     public final int getHeight()
512     {
513         return this.size.height;
514     }
515 
516     /**
517      * @return preferredSize
518      */
519     public final Dimension getPreferredSize()
520     {
521         return this.preferredSize;
522     }
523 
524     /**
525      * @param preferredSize Dimension; set preferredSize
526      */
527     public final void setPreferredSize(final Dimension preferredSize)
528     {
529         this.preferredSize = preferredSize;
530     }
531 
532     /**
533      * @return toolTipText
534      */
535     public final String getToolTipText()
536     {
537         return this.toolTipText;
538     }
539 
540     /**
541      * @param toolTipText String; set toolTipText
542      */
543     public final void setToolTipText(final String toolTipText)
544     {
545         this.toolTipText = toolTipText;
546     }
547 
548     /**
549      * @return showing
550      */
551     public final boolean isShowing()
552     {
553         return this.showing;
554     }
555 
556     /**
557      * @param showing boolean; set showing
558      */
559     public final void setShowing(final boolean showing)
560     {
561         this.showing = showing;
562     }
563 
564     /**
565      * @return font
566      */
567     public final Font getFont()
568     {
569         return this.currentFont;
570     }
571 
572     /**
573      * @param font Font; set font
574      */
575     public final void setFont(final Font font)
576     {
577         this.currentFont = font;
578     }
579 
580     /**
581      * @param font Font; the font to calculate the fontmetrics for
582      * @return fontMetrics
583      */
584     public final FontMetrics getFontMetrics(final Font font)
585     {
586         return this.canvas.getFontMetrics(font);
587     }
588 
589     /**
590      * @return dirty
591      */
592     public final boolean isDirty()
593     {
594         return this.dirty;
595     }
596 
597     /** {@inheritDoc} */
598     @Override
599     public boolean imageUpdate(final Image img, final int infoflags, final int x, final int y, final int width, final int height)
600     {
601         return false;
602     }
603 
604 }