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