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