View Javadoc
1   package org.opentrafficsim.editor.extensions.map;
2   
3   import java.awt.Color;
4   import java.awt.Graphics;
5   import java.awt.geom.Point2D;
6   import java.awt.geom.RectangularShape;
7   import java.rmi.RemoteException;
8   import java.util.List;
9   
10  import javax.naming.NamingException;
11  
12  import org.djutils.draw.bounds.Bounds2d;
13  import org.djutils.draw.point.Point2d;
14  import org.djutils.event.EventProducer;
15  import org.djutils.event.EventType;
16  import org.djutils.metadata.MetaData;
17  
18  import nl.tudelft.simulation.dsol.animation.Locatable;
19  import nl.tudelft.simulation.dsol.swing.animation.d2.VisualizationPanel;
20  import nl.tudelft.simulation.naming.context.ContextInterface;
21  
22  /**
23   * Visualization panel to show the map. This implementation has an adaptive grid color, fixed grid drawing imprecision, and
24   * makes certain methods accessible.
25   * <p>
26   * Copyright (c) 2026-2026 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.<br>
27   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
28   * @author Wouter Schakel
29   */
30  public class MapVisualizationPanel extends VisualizationPanel
31  {
32  
33      /** Serialization version UID. */
34      private static final long serialVersionUID = 20260212L;
35  
36      /** Event when extent changed. */
37      public static final EventType EXTENT_CHANGED = new EventType(new MetaData("EXTENT_CHANGED", "EXTENT_CHANGED"));
38  
39      /**
40       * Constructor.
41       * @param homeExtent the initial extent
42       * @param producer the object firing animation update events
43       * @param context the context that contains the drawing objects
44       * @throws RemoteException on error when remote panel and producer cannot connect
45       * @throws NamingException on context error
46       */
47      public MapVisualizationPanel(final Bounds2d homeExtent, final EventProducer producer, final ContextInterface context)
48              throws RemoteException, NamingException
49      {
50          super(homeExtent, producer, context);
51      }
52  
53      @Override
54      public void setBackground(final Color bg)
55      {
56          int threshold = 64;
57          int alternative = 96;
58          if (bg.getRed() <= threshold && bg.getGreen() <= threshold && bg.getBlue() <= threshold)
59          {
60              setGridColor(new Color(alternative, alternative, alternative));
61          }
62          else
63          {
64              setGridColor(Color.BLACK);
65          }
66          super.setBackground(bg);
67      }
68  
69      // Overridden because there are rounding and vertical mod errors in the super implementation.
70      // See https://github.com/averbraeck/dsol/issues/116.
71      @Override
72      protected synchronized void drawGrid(final Graphics g)
73      {
74          // we prepare the graphics object for the grid
75          g.setFont(g.getFont().deriveFont(11.0f));
76          g.setColor(this.getGridColor());
77          double scaleX = this.getRenderableScale().getXScale(this.getExtent(), this.getSize());
78          double scaleY = this.getRenderableScale().getYScale(this.getExtent(), this.getSize());
79  
80          int count = 0;
81          double gridSizePixelsX = this.gridSizeX / scaleX;
82          while (gridSizePixelsX < 40)
83          {
84              this.gridSizeX = 10 * this.gridSizeX;
85              int maximumNumberOfDigits = (int) Math.max(0, 1 + Math.ceil(Math.log(1 / this.gridSizeX) / Math.log(10)));
86              this.formatter.setMaximumFractionDigits(maximumNumberOfDigits);
87              gridSizePixelsX = (int) Math.round(this.gridSizeX / scaleX);
88              if (count++ > 10)
89              {
90                  break;
91              }
92          }
93  
94          count = 0;
95          while (gridSizePixelsX > 10 * 40)
96          {
97              int maximumNumberOfDigits = (int) Math.max(0, 2 + Math.ceil(Math.log(1 / this.gridSizeX) / Math.log(10)));
98              this.formatter.setMaximumFractionDigits(maximumNumberOfDigits);
99              this.gridSizeX = this.gridSizeX / 10;
100             gridSizePixelsX = (int) Math.round(this.gridSizeX / scaleX);
101             if (count++ > 10)
102             {
103                 break;
104             }
105         }
106 
107         double gridSizePixelsY = this.gridSizeY / scaleY;
108         while (gridSizePixelsY < 40)
109         {
110             this.gridSizeY = 10 * this.gridSizeY;
111             int maximumNumberOfDigits = (int) Math.max(0, 1 + Math.ceil(Math.log(1 / this.gridSizeY) / Math.log(10)));
112             this.formatter.setMaximumFractionDigits(maximumNumberOfDigits);
113             gridSizePixelsY = (int) Math.round(this.gridSizeY / scaleY);
114             if (count++ > 10)
115             {
116                 break;
117             }
118         }
119 
120         count = 0;
121         while (gridSizePixelsY > 10 * 40)
122         {
123             int maximumNumberOfDigits = (int) Math.max(0, 2 + Math.ceil(Math.log(1 / this.gridSizeY) / Math.log(10)));
124             this.formatter.setMaximumFractionDigits(maximumNumberOfDigits);
125             this.gridSizeY = this.gridSizeY / 10;
126             gridSizePixelsY = (int) Math.round(this.gridSizeY / scaleY);
127             if (count++ > 10)
128             {
129                 break;
130             }
131         }
132 
133         // Let's draw the vertical lines
134         double mod = this.getExtent().getMinX() % this.gridSizeX;
135         double x = -mod / scaleX;
136         while (x < this.getWidth())
137         {
138             Point2d point =
139                     this.getRenderableScale().getWorldCoordinates(new Point2D.Double(x, 0), this.getExtent(), this.getSize());
140             if (point != null)
141             {
142                 String label = this.formatter.format(Math.round(point.getX() / this.gridSizeX) * this.gridSizeX);
143                 double labelWidth = this.getFontMetrics(this.getFont()).getStringBounds(label, g).getWidth();
144                 if (x > labelWidth + 4)
145                 {
146                     int xInt = (int) Math.round(x);
147                     g.drawLine(xInt, 15, xInt, this.getHeight());
148                     g.drawString(label, (int) Math.round(x - 0.5 * labelWidth), 11);
149                 }
150             }
151             x = x + gridSizePixelsX;
152         }
153 
154         // Let's draw the horizontal lines
155         mod = this.getExtent().getMinY() % this.gridSizeY;
156         double y = this.getSize().getHeight() + (mod / scaleY);
157         while (y > 15)
158         {
159             Point2d point =
160                     this.getRenderableScale().getWorldCoordinates(new Point2D.Double(0, y), this.getExtent(), this.getSize());
161             if (point != null)
162             {
163                 String label = this.formatter.format(Math.round(point.getY() / this.gridSizeY) * this.gridSizeY);
164                 RectangularShape labelBounds = this.getFontMetrics(this.getFont()).getStringBounds(label, g);
165                 int yInt = (int) Math.round(y);
166                 g.drawLine((int) Math.round(labelBounds.getWidth() + 4), yInt, this.getWidth(), yInt);
167                 g.drawString(label, 2, (int) Math.round(y + labelBounds.getHeight() * 0.3));
168             }
169             y = y - gridSizePixelsY;
170         }
171     }
172 
173     // convenience method
174 
175     /**
176      * Returns the current world size of 1px.
177      * @return current world size of 1px
178      */
179     public double pxScale()
180     {
181         double x = getRenderableScale().getXScale(getExtent(), getSize());
182         double y = x / getRenderableScale().getYScaleRatio();
183         return Math.min(x, y);
184     }
185 
186     // throw event
187 
188     @Override
189     public void setExtent(final Bounds2d extent)
190     {
191         super.setExtent(extent);
192         fireEvent(EXTENT_CHANGED);
193     }
194 
195     // make public for MapInputListener
196 
197     @Override
198     public void pan(final Point2D mouseClickedPoint, final Point2D mouseReleasedPoint)
199     {
200         super.pan(mouseClickedPoint, mouseReleasedPoint);
201     }
202 
203     @Override
204     public List<Locatable> getSelectedObjects(final Point2D mousePoint)
205     {
206         return super.getSelectedObjects(mousePoint);
207     }
208 
209     @Override
210     public Object getSelectedObject(final List<Locatable> targets)
211     {
212         return super.getSelectedObject(targets);
213     }
214 
215 }