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 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 the initial extent.
144      * @param size 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 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         { this.drawGrid(g); }
184     }
185 
186     /**
187      * show the grid?
188      * @param bool true/false
189      */
190     public synchronized void showGrid(final boolean bool)
191     {
192         this.showGrid = bool;
193         this.repaint();
194     }
195 
196     /**
197      * returns the extent of this panel.
198      * @return Bounds2d
199      */
200     public Bounds2d getExtent()
201     {
202         return this.extent;
203     }
204 
205     /**
206      * returns the extent of this panel.
207      * @param extent the new extent
208      */
209     public void setExtent(final Bounds2d extent)
210     {
211         if (this.lastScreen != null)
212         {
213             // this prevents zoom being undone when resizing the screen afterwards
214             this.lastXScale = this.getRenderableScale().getXScale(extent, this.lastScreen);
215             this.lastYScale = this.getRenderableScale().getYScale(extent, this.lastScreen);
216         }
217         this.extent = extent;
218         this.repaint();
219     }
220 
221     /**
222      * Set the world coordinates based on a mouse move.
223      * @param point the x,y world coordinates
224      */
225     public synchronized void setWorldCoordinate(final Point2d point)
226     {
227         this.worldCoordinate = point;
228     }
229 
230     /**
231      * @return worldCoordinate
232      */
233     public synchronized Point2d getWorldCoordinate()
234     {
235         return this.worldCoordinate;
236     }
237 
238     /**
239      * Display a tooltip with the last known world coordinates of the mouse, in case the tooltip should be displayed.
240      */
241     public synchronized void displayWorldCoordinateToolTip()
242     {
243         if (this.showToolTip)
244         {
245             String worldPoint = "(x=" + this.formatter.format(this.worldCoordinate.getX()) + " ; y="
246                     + this.formatter.format(this.worldCoordinate.getY()) + ")";
247             setToolTipText(worldPoint);
248         }
249     }
250 
251     /**
252      * @return showToolTip
253      */
254     public synchronized boolean isShowToolTip()
255     {
256         return this.showToolTip;
257     }
258 
259     /**
260      * @param showToolTip set showToolTip
261      */
262     public synchronized void setShowToolTip(final boolean showToolTip)
263     {
264         this.showToolTip = showToolTip;
265     }
266 
267     /**
268      * pans the panel in a specified direction.
269      * @param direction the direction
270      * @param percentage the percentage
271      */
272     public synchronized void pan(final int direction, final double percentage)
273     {
274         if (percentage <= 0 || percentage > 1.0)
275         { throw new IllegalArgumentException("percentage<=0 || >1.0"); }
276         switch (direction)
277         {
278             case LEFT:
279                 this.extent = new Bounds2d(this.extent.getMinX() - percentage * this.extent.getDeltaX(),
280                         this.extent.getMaxX() - percentage * this.extent.getDeltaX(), this.extent.getMinY(),
281                         this.extent.getMaxY());
282                 break;
283             case RIGHT:
284                 this.extent = new Bounds2d(this.extent.getMinX() + percentage * this.extent.getDeltaX(),
285                         this.extent.getMaxX() + percentage * this.extent.getDeltaX(), this.extent.getMinY(),
286                         this.extent.getMaxY());
287                 break;
288             case UP:
289                 this.extent = new Bounds2d(this.extent.getMinX(), this.extent.getMaxX(),
290                         this.extent.getMinY() + percentage * this.extent.getDeltaY(),
291                         this.extent.getMaxY() + percentage * this.extent.getDeltaY());
292                 break;
293             case DOWN:
294                 this.extent = new Bounds2d(this.extent.getMinX(), this.extent.getMaxX(),
295                         this.extent.getMinY() - percentage * this.extent.getDeltaY(),
296                         this.extent.getMaxY() - percentage * this.extent.getDeltaY());
297                 break;
298             default:
299                 throw new IllegalArgumentException("direction unkown");
300         }
301         this.repaint();
302     }
303 
304     /**
305      * resets the panel to its original extent.
306      */
307     public synchronized void home()
308     {
309         this.extent = computeVisibleExtent(this.homeExtent);
310         this.repaint();
311     }
312 
313     /**
314      * @return Returns the showGrid.
315      */
316     public boolean isShowGrid()
317     {
318         return this.showGrid;
319     }
320 
321     /**
322      * @param showGrid The showGrid to set.
323      */
324     public void setShowGrid(final boolean showGrid)
325     {
326         this.showGrid = showGrid;
327     }
328 
329     /**
330      * Return the current grid color for this VisualizationPanel.
331      * @return the current grid color
332      */
333     public Color getGridColor()
334     {
335         return this.gridColor;
336     }
337 
338     /**
339      * Set a new grid color for this VisualizationPanel
340      * @param gridColor the new grid color
341      */
342     public void setGridColor(final Color gridColor)
343     {
344         this.gridColor = gridColor;
345     }
346 
347     /**
348      * zooms in/out.
349      * @param factor The zoom factor
350      */
351     public synchronized void zoom(final double factor)
352     {
353         zoom(factor, (int) (this.getWidth() / 2.0), (int) (this.getHeight() / 2.0));
354     }
355 
356     /**
357      * zooms in/out.
358      * @param factor The zoom factor
359      * @param mouseX x-position of the mouse around which we zoom
360      * @param mouseY y-position of the mouse around which we zoom
361      */
362     public synchronized void zoom(final double factor, final int mouseX, final int mouseY)
363     {
364         Point2d mwc = this.renderableScale.getWorldCoordinates(new Point2D.Double(mouseX, mouseY), this.extent, this.getSize());
365         double minX = mwc.getX() - (mwc.getX() - this.extent.getMinX()) * factor;
366         double minY = mwc.getY() - (mwc.getY() - this.extent.getMinY()) * factor;
367         double w = this.extent.getDeltaX() * factor;
368         double h = this.extent.getDeltaY() * factor;
369 
370         this.extent = new Bounds2d(minX, minX + w, minY, minY + h);
371         this.repaint();
372     }
373 
374     /**
375      * Added to make sure the recursive render-call calls THIS render method instead of a potential super-class defined
376      * 'paintComponent' render method.
377      * @param g the graphics object
378      */
379     protected synchronized void drawGrid(final Graphics g)
380     {
381         // we prepare the graphics object for the grid
382         g.setFont(g.getFont().deriveFont(11.0f));
383         g.setColor(this.gridColor);
384         double scaleX = this.renderableScale.getXScale(this.extent, this.getSize());
385         double scaleY = this.renderableScale.getYScale(this.extent, this.getSize());
386 
387         int count = 0;
388         int gridSizePixelsX = (int) Math.round(this.gridSizeX / scaleX);
389         while (gridSizePixelsX < 40)
390         {
391             this.gridSizeX = 10 * this.gridSizeX;
392             int maximumNumberOfDigits = (int) Math.max(0, 1 + Math.ceil(Math.log(1 / this.gridSizeX) / Math.log(10)));
393             this.formatter.setMaximumFractionDigits(maximumNumberOfDigits);
394             gridSizePixelsX = (int) Math.round(this.gridSizeX / scaleX);
395             if (count++ > 10)
396             { break; }
397         }
398 
399         count = 0;
400         while (gridSizePixelsX > 10 * 40)
401         {
402             int maximumNumberOfDigits = (int) Math.max(0, 2 + Math.ceil(Math.log(1 / this.gridSizeX) / Math.log(10)));
403             this.formatter.setMaximumFractionDigits(maximumNumberOfDigits);
404             this.gridSizeX = this.gridSizeX / 10;
405             gridSizePixelsX = (int) Math.round(this.gridSizeX / scaleX);
406             if (count++ > 10)
407             { break; }
408         }
409 
410         int gridSizePixelsY = (int) Math.round(this.gridSizeY / scaleY);
411         while (gridSizePixelsY < 40)
412         {
413             this.gridSizeY = 10 * this.gridSizeY;
414             int maximumNumberOfDigits = (int) Math.max(0, 1 + Math.ceil(Math.log(1 / this.gridSizeY) / Math.log(10)));
415             this.formatter.setMaximumFractionDigits(maximumNumberOfDigits);
416             gridSizePixelsY = (int) Math.round(this.gridSizeY / scaleY);
417             if (count++ > 10)
418             { break; }
419         }
420 
421         count = 0;
422         while (gridSizePixelsY > 10 * 40)
423         {
424             int maximumNumberOfDigits = (int) Math.max(0, 2 + Math.ceil(Math.log(1 / this.gridSizeY) / Math.log(10)));
425             this.formatter.setMaximumFractionDigits(maximumNumberOfDigits);
426             this.gridSizeY = this.gridSizeY / 10;
427             gridSizePixelsY = (int) Math.round(this.gridSizeY / scaleY);
428             if (count++ > 10)
429             { break; }
430         }
431 
432         // Let's draw the vertical lines
433         double mod = this.extent.getMinX() % this.gridSizeX;
434         int x = (int) -Math.round(mod / scaleX);
435         while (x < this.getWidth())
436         {
437             Point2d point = this.renderableScale.getWorldCoordinates(new Point2D.Double(x, 0), this.extent, this.getSize());
438             if (point != null)
439             {
440                 String label = this.formatter.format(Math.round(point.getX() / this.gridSizeX) * this.gridSizeX);
441                 double labelWidth = this.getFontMetrics(this.getFont()).getStringBounds(label, g).getWidth();
442                 if (x > labelWidth + 4)
443                 {
444                     g.drawLine(x, 15, x, this.getHeight());
445                     g.drawString(label, (int) Math.round(x - 0.5 * labelWidth), 11);
446                 }
447             }
448             x = x + gridSizePixelsX;
449         }
450 
451         // Let's draw the horizontal lines
452         mod = Math.abs(this.extent.getMinY()) % this.gridSizeY;
453         int y = (int) Math.round(this.getSize().getHeight() - (mod / scaleY));
454         while (y > 15)
455         {
456             Point2d point = this.renderableScale.getWorldCoordinates(new Point2D.Double(0, y), this.extent, this.getSize());
457             if (point != null)
458             {
459                 String label = this.formatter.format(Math.round(point.getY() / this.gridSizeY) * this.gridSizeY);
460                 RectangularShape labelBounds = this.getFontMetrics(this.getFont()).getStringBounds(label, g);
461                 g.drawLine((int) Math.round(labelBounds.getWidth() + 4), y, this.getWidth(), y);
462                 g.drawString(label, 2, (int) Math.round(y + labelBounds.getHeight() * 0.3));
463             }
464             y = y - gridSizePixelsY;
465         }
466     }
467 
468     /**
469      * Repaint the shadow canvas.
470      */
471     public void repaint()
472     {
473         // repaint does not do any painting -- information is pulled from the browser
474         this.dirty = true;
475     }
476 
477     /**
478      * @return size
479      */
480     public Dimension getSize()
481     {
482         return this.size;
483     }
484 
485     /**
486      * @param size set size
487      */
488     public void setSize(final Dimension size)
489     {
490         this.size = size;
491     }
492 
493     /**
494      * @return background
495      */
496     public Color getBackground()
497     {
498         return this.background;
499     }
500 
501     /**
502      * @param background set background
503      */
504     public void setBackground(final Color background)
505     {
506         this.background = background;
507     }
508 
509     /**
510      * @return width
511      */
512     public int getWidth()
513     {
514         return this.size.width;
515     }
516 
517     /**
518      * @return height
519      */
520     public int getHeight()
521     {
522         return this.size.height;
523     }
524 
525     /**
526      * @return preferredSize
527      */
528     public Dimension getPreferredSize()
529     {
530         return this.preferredSize;
531     }
532 
533     /**
534      * @param preferredSize set preferredSize
535      */
536     public void setPreferredSize(final Dimension preferredSize)
537     {
538         this.preferredSize = preferredSize;
539     }
540 
541     /**
542      * @return toolTipText
543      */
544     public String getToolTipText()
545     {
546         return this.toolTipText;
547     }
548 
549     /**
550      * @param toolTipText set toolTipText
551      */
552     public void setToolTipText(final String toolTipText)
553     {
554         this.toolTipText = toolTipText;
555     }
556 
557     /**
558      * @return showing
559      */
560     public boolean isShowing()
561     {
562         return this.showing;
563     }
564 
565     /**
566      * @param showing set showing
567      */
568     public void setShowing(final boolean showing)
569     {
570         this.showing = showing;
571     }
572 
573     /**
574      * @return font
575      */
576     public Font getFont()
577     {
578         return this.currentFont;
579     }
580 
581     /**
582      * @param font set font
583      */
584     public void setFont(final Font font)
585     {
586         this.currentFont = font;
587     }
588 
589     /**
590      * @param font the font to calculate the fontmetrics for
591      * @return fontMetrics
592      */
593     public FontMetrics getFontMetrics(final Font font)
594     {
595         return this.canvas.getFontMetrics(font);
596     }
597 
598     /**
599      * @return dirty
600      */
601     public boolean isDirty()
602     {
603         return this.dirty;
604     }
605 
606     @Override
607     public boolean imageUpdate(final Image img, final int infoflags, final int x, final int y, final int width,
608             final int height)
609     {
610         return false;
611     }
612 
613     /**
614      * @return renderableScale
615      */
616     public RenderableScale getRenderableScale()
617     {
618         return this.renderableScale;
619     }
620 
621     /**
622      * @param renderableScale set renderableScale
623      */
624     public void setRenderableScale(final RenderableScale renderableScale)
625     {
626         this.renderableScale = renderableScale;
627     }
628 
629     /**
630      * Computes the visible extent, while preserving zoom scale, otherwise dragging the split screen may pump up the zoom factor
631      * @param extent the extent to use
632      * @return a new extent or null if parameters are null or screen is invalid (width / height &lt;= 0)
633      */
634     public Bounds2d computeVisibleExtent(final Bounds2d extent)
635     {
636         Dimension screen = getSize();
637         double xScale = this.renderableScale.getXScale(extent, screen);
638         double yScale = this.renderableScale.getYScale(extent, screen);
639         Bounds2d result;
640         if (this.lastYScale != null && yScale == this.lastYScale)
641         {
642             result = new Bounds2d(extent.midPoint().getX() - 0.5 * screen.getWidth() * yScale,
643                     extent.midPoint().getX() + 0.5 * screen.getWidth() * yScale, extent.getMinY(), extent.getMaxY());
644             xScale = yScale;
645         }
646         else if (this.lastXScale != null && xScale == this.lastXScale)
647         {
648             result = new Bounds2d(extent.getMinX(), extent.getMaxX(),
649                     extent.midPoint().getY() - 0.5 * screen.getHeight() * xScale * this.renderableScale.getYScaleRatio(),
650                     extent.midPoint().getY() + 0.5 * screen.getHeight() * xScale * this.renderableScale.getYScaleRatio());
651             yScale = xScale;
652         }
653         else
654         {
655             double scale = this.lastXScale == null ? Math.min(xScale, yScale)
656                     : this.lastXScale * this.lastScreen.getWidth() / screen.getWidth();
657             result = new Bounds2d(extent.midPoint().getX() - 0.5 * screen.getWidth() * scale,
658                     extent.midPoint().getX() + 0.5 * screen.getWidth() * scale,
659                     extent.midPoint().getY() - 0.5 * screen.getHeight() * scale * this.renderableScale.getYScaleRatio(),
660                     extent.midPoint().getY() + 0.5 * screen.getHeight() * scale * this.renderableScale.getYScaleRatio());
661             yScale = scale;
662             xScale = scale;
663         }
664         this.lastXScale = xScale;
665         this.lastYScale = yScale;
666         this.lastScreen = screen;
667         return result;
668     }
669 
670 }