View Javadoc
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   * The VisualizationPanel introduces the gridPanel.
24   * <p>
25   * Copyright (c) 2003-2024 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   * @author <a href="mailto:nlang@fbk.eur.nl">Niels Lang </a>, <a href="https://www.peter-jacobs.com">Peter Jacobs </a>
29   */
30  public class HtmlGridPanel implements ImageObserver
31  {
32      /** the UP directions for moving/zooming. */
33      public static final int UP = 1;
34  
35      /** the DOWN directions for moving/zooming. */
36      public static final int DOWN = 2;
37  
38      /** the LEFT directions for moving/zooming. */
39      public static final int LEFT = 3;
40  
41      /** the RIGHT directions for moving/zooming. */
42      public static final int RIGHT = 4;
43  
44      /** the ZOOM factor. */
45      public static final double ZOOMFACTOR = 1.2;
46  
47      /** gridColor. */
48      protected static final Color GRIDCOLOR = Color.BLACK;
49  
50      /** the extent of this panel. */
51      @SuppressWarnings("checkstyle:visibilitymodifier")
52      protected Bounds2d extent = null;
53  
54      /** the extent of this panel. */
55      @SuppressWarnings("checkstyle:visibilitymodifier")
56      protected Bounds2d homeExtent = null;
57  
58      /** show the grid. */
59      @SuppressWarnings("checkstyle:visibilitymodifier")
60      protected boolean showGrid = true;
61  
62      /** the gridSize for the X-direction in world Units. */
63      @SuppressWarnings("checkstyle:visibilitymodifier")
64      protected double gridSizeX = 100.0;
65  
66      /** the gridSize for the Y-direction in world Units. */
67      @SuppressWarnings("checkstyle:visibilitymodifier")
68      protected double gridSizeY = 100.0;
69  
70      /** the formatter to use. */
71      @SuppressWarnings("checkstyle:visibilitymodifier")
72      protected NumberFormat formatter = NumberFormat.getInstance();
73  
74      /** the last computed Dimension. */
75      @SuppressWarnings("checkstyle:visibilitymodifier")
76      protected Dimension lastDimension = null;
77  
78      /** the last stored screen dimensions for zoom-in, zoom-out. */
79      @SuppressWarnings("checkstyle:visibilitymodifier")
80      protected Dimension lastScreen = null;
81  
82      /** the last stored x-scale for zoom-in, zoom-out. */
83      @SuppressWarnings("checkstyle:visibilitymodifier")
84      protected Double lastXScale = null;
85  
86      /** the last stored y-scale for zoom-in, zoom-out. */
87      @SuppressWarnings("checkstyle:visibilitymodifier")
88      protected Double lastYScale = null;
89  
90      /** the last computed Dimension. */
91      @SuppressWarnings("checkstyle:visibilitymodifier")
92      protected Dimension size = null;
93  
94      /** the last computed Dimension. */
95      @SuppressWarnings("checkstyle:visibilitymodifier")
96      protected Dimension preferredSize = null;
97  
98      /** the last known world coordinate of the mouse. */
99      @SuppressWarnings("checkstyle:visibilitymodifier")
100     protected Point2d worldCoordinate = new Point2d(0, 0);
101 
102     /** whether to show a tooltip with the coordinates or not. */
103     @SuppressWarnings("checkstyle:visibilitymodifier")
104     protected boolean showToolTip = true;
105 
106     /** the background color. */
107     private Color background;
108 
109     /** The tooltip text which shows the coordinates. */
110     private String toolTipText = "";
111 
112     /** Whether the panel is showing or not. */
113     private boolean showing = true;
114 
115     /** the current font. */
116     private Font currentFont = new Font(Font.SANS_SERIF, Font.PLAIN, 10);
117 
118     /** the canvas to determine the font metrics. */
119     private Canvas canvas = new Canvas();
120 
121     /** the HTMLGraphics2D 'shadow' canvas. */
122     @SuppressWarnings("checkstyle:visibilitymodifier")
123     protected HtmlGraphics2d htmlGraphics2D;
124 
125     /** the renderable scale (X/Y ratio) to use. */
126     @SuppressWarnings("checkstyle:visibilitymodifier")
127     protected RenderableScale renderableScale;
128 
129     /** dirty flag. */
130     private boolean dirty = false;
131 
132     /**
133      * constructs a new VisualizationPanel.
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 VisualizationPanel.
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.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      * 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             setExtent(computeVisibleExtent(this.extent).get());
181         }
182         if (this.showGrid)
183         {
184             this.drawGrid(g);
185         }
186     }
187 
188     /**
189      * show the grid?
190      * @param bool true/false
191      */
192     public final 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 final 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 the x,y world coordinates
226      */
227     public final synchronized void setWorldCoordinate(final Point2d point)
228     {
229         this.worldCoordinate = point;
230     }
231 
232     /**
233      * Returns world coordinates.
234      * @return worldCoordinate
235      */
236     public final synchronized Point2d getWorldCoordinate()
237     {
238         return this.worldCoordinate;
239     }
240 
241     /**
242      * Display a tooltip with the last known world coordinates of the mouse, in case the tooltip should be displayed.
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      * Returns whether to show tooltip.
256      * @return showToolTip
257      */
258     public final synchronized boolean isShowToolTip()
259     {
260         return this.showToolTip;
261     }
262 
263     /**
264      * Sets whether to show tooltip.
265      * @param showToolTip set showToolTip
266      */
267     public final synchronized void setShowToolTip(final boolean showToolTip)
268     {
269         this.showToolTip = showToolTip;
270     }
271 
272     /**
273      * pans the panel in a specified direction.
274      * @param direction the direction
275      * @param percentage the percentage
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      * resets the panel to its original extent.
313      */
314     public final synchronized void home()
315     {
316         setExtent(computeVisibleExtent(this.homeExtent).get());
317         this.repaint();
318     }
319 
320     /**
321      * Returns show grid.
322      * @return Returns the showGrid.
323      */
324     public final boolean isShowGrid()
325     {
326         return this.showGrid;
327     }
328 
329     /**
330      * Sets show grid.
331      * @param showGrid The showGrid to set.
332      */
333     public final void setShowGrid(final boolean showGrid)
334     {
335         this.showGrid = showGrid;
336     }
337 
338     /**
339      * zooms in/out.
340      * @param factor The zoom factor
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      * zooms in/out.
349      * @param factor The zoom factor
350      * @param mouseX x-position of the mouse around which we zoom
351      * @param mouseY y-position of the mouse around which we zoom
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      * Added to make sure the recursive render-call calls THIS render method instead of a potential super-class defined
367      * 'paintComponent' render method.
368      * @param g the graphics object
369      */
370     protected synchronized void drawGrid(final Graphics g)
371     {
372         // we prepare the graphics object for the grid
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         // Let's draw the vertical lines
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         // Let's draw the horizontal lines
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      * Returns renderable scale.
469      * @return renderableScale
470      */
471     public final RenderableScale getRenderableScale()
472     {
473         return this.renderableScale;
474     }
475 
476     /**
477      * Sets renderable scale.
478      * @param renderableScale set renderableScale
479      */
480     public final void setRenderableScale(final RenderableScale renderableScale)
481     {
482         this.renderableScale = renderableScale;
483     }
484 
485     /**
486      * Repaint the shadow canvas.
487      */
488     public void repaint()
489     {
490         // repaint does not do any painting -- information is pulled from the browser
491         this.dirty = true;
492     }
493 
494     /**
495      * Returns size.
496      * @return size
497      */
498     public final Dimension getSize()
499     {
500         return this.size;
501     }
502 
503     /**
504      * Sets size.
505      * @param size set size
506      */
507     public final void setSize(final Dimension size)
508     {
509         this.size = size;
510     }
511 
512     /**
513      * Returns background.
514      * @return background
515      */
516     public final Color getBackground()
517     {
518         return this.background;
519     }
520 
521     /**
522      * Sets background.
523      * @param background set background
524      */
525     public final void setBackground(final Color background)
526     {
527         this.background = background;
528     }
529 
530     /**
531      * Returns width.
532      * @return width
533      */
534     public final int getWidth()
535     {
536         return this.size.width;
537     }
538 
539     /**
540      * Returns height.
541      * @return height
542      */
543     public final int getHeight()
544     {
545         return this.size.height;
546     }
547 
548     /**
549      * Returns preferred size.
550      * @return preferredSize
551      */
552     public final Dimension getPreferredSize()
553     {
554         return this.preferredSize;
555     }
556 
557     /**
558      * Sets preferred size.
559      * @param preferredSize set preferredSize
560      */
561     public final void setPreferredSize(final Dimension preferredSize)
562     {
563         this.preferredSize = preferredSize;
564     }
565 
566     /**
567      * Returns tooltip.
568      * @return toolTipText
569      */
570     public final String getToolTipText()
571     {
572         return this.toolTipText;
573     }
574 
575     /**
576      * Sets tooltip.
577      * @param toolTipText set toolTipText
578      */
579     public final void setToolTipText(final String toolTipText)
580     {
581         this.toolTipText = toolTipText;
582     }
583 
584     /**
585      * Returns whether panel is showing.
586      * @return showing
587      */
588     public final boolean isShowing()
589     {
590         return this.showing;
591     }
592 
593     /**
594      * Sets whether panel is showing.
595      * @param showing set showing
596      */
597     public final void setShowing(final boolean showing)
598     {
599         this.showing = showing;
600     }
601 
602     /**
603      * Returns font.
604      * @return font
605      */
606     public final Font getFont()
607     {
608         return this.currentFont;
609     }
610 
611     /**
612      * Sets font.
613      * @param font set font
614      */
615     public final void setFont(final Font font)
616     {
617         this.currentFont = font;
618     }
619 
620     /**
621      * Returns font metrics.
622      * @param font the font to calculate the fontmetrics for
623      * @return fontMetrics
624      */
625     public final FontMetrics getFontMetrics(final Font font)
626     {
627         return this.canvas.getFontMetrics(font);
628     }
629 
630     /**
631      * Return whether the panel is dirty.
632      * @return dirty
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      * Computes the visible extent, while preserving zoom scale, otherwise dragging the split screen may pump up the zoom
648      * factor.
649      * @param extent the extent to use
650      * @return a new extent or empty if parameters are null or screen is invalid (width / height &lt;= 0)
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 }