View Javadoc
1   package org.opentrafficsim.base.geometry;
2   
3   import java.util.ArrayList;
4   import java.util.Collections;
5   import java.util.List;
6   
7   import org.djutils.draw.line.Polygon2d;
8   import org.djutils.draw.point.Point2d;
9   import org.djutils.exceptions.Throw;
10  
11  /**
12   * Bounds defined by a rounded rectangle.
13   * <p>
14   * Copyright (c) 2024-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
15   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
16   * </p>
17   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
18   */
19  public class BoundingBoxRounded implements OtsBounds2d
20  {
21  
22      /** Number of line segments in polygon representation for the curves if they are 4 full quarter circles. */
23      private final static int POLYGON_STEPS = 128;
24  
25      /** Half length along y dimension. */
26      private final double dx;
27  
28      /** Half length along y dimension. */
29      private final double dy;
30  
31      /** Polygon representation. */
32      private Polygon2d polygon;
33  
34      /** Rounding radius. */
35      private final double r;
36  
37      /** Max x coordinate, can be lower than dx due to large r. */
38      private final double maxX;
39  
40      /** Max y coordinate, can be lower than dy due to large r. */
41      private final double maxY;
42  
43      /**
44       * Constructor.
45       * @param dx double; complete length along x dimension.
46       * @param dy double; complete length along y dimension.
47       * @param r double; radius of rounding, must be positive.
48       */
49      public BoundingBoxRounded(final double dx, final double dy, final double r)
50      {
51          /*-
52           * Equation derived from r^2 = (r-dx)^2 + (r-dy^2)  [note: dx and dy here as half of input values, i.e. this.dx/this.dy]
53           * 
54           *                dx
55           *    ___       ______
56           * ^ |   ''--_ |      | dy
57           * | |---------o------
58           * | | r-dx  / |'.
59           * r |      /  |  '.
60           * | |   r/    |    \
61           * | |   /     |     \
62           * | | /   r-dy|      |
63           * v |/________|______|
64           *    <-------r------>
65           */
66          this.dx = Math.abs(dx) / 2.0;
67          this.dy = Math.abs(dy) / 2.0;
68          Throw.when(r >= this.dx + this.dy + Math.sqrt(2.0 * this.dx * this.dy), IllegalArgumentException.class,
69                  "Radius makes rounded rectangle non-existent.");
70          Throw.when(r < 0.0, IllegalArgumentException.class, "Radius must be positive.");
71          this.r = r;
72          this.maxX = this.dx - signedDistance(new Point2d(this.dx, 0.0));
73          this.maxY = this.dy - signedDistance(new Point2d(0.0, this.dy));
74      }
75  
76      /** {@inheritDoc} */
77      @Override
78      public double getMinX()
79      {
80          return -this.maxX;
81      }
82  
83      /** {@inheritDoc} */
84      @Override
85      public double getMaxX()
86      {
87          return this.maxX;
88      }
89  
90      /** {@inheritDoc} */
91      @Override
92      public double getMinY()
93      {
94          return -this.maxY;
95      }
96  
97      /** {@inheritDoc} */
98      @Override
99      public double getMaxY()
100     {
101         return this.maxY;
102     }
103 
104     /** {@inheritDoc} */
105     @Override
106     public boolean contains(final Point2d point) throws NullPointerException
107     {
108         return signedDistance(point) < 0.0;
109     }
110 
111     /** {@inheritDoc} */
112     @Override
113     public boolean covers(final Point2d point) throws NullPointerException
114     {
115         return signedDistance(point) <= 0.0;
116     }
117 
118     /**
119      * {@inheritDoc}
120      * @see <a href="https://iquilezles.org/articles/distfunctions/">Signed distance functions by Inigo Quilez</a>
121      */
122     @Override
123     public double signedDistance(final Point2d point)
124     {
125         double qx = Math.abs(point.x) - this.dx + this.r;
126         double qy = Math.abs(point.y) - this.dy + this.r;
127         return Math.hypot(Math.max(qx, 0.0), Math.max(qy, 0.0)) + Math.min(Math.max(qx, qy), 0.0) - this.r;
128     }
129 
130     /** {@inheritDoc} */
131     @Override
132     public Polygon2d asPolygon()
133     {
134         if (this.polygon == null)
135         {
136             // calculate for top right quadrant only, others are negative or reversed copies
137             int n = POLYGON_STEPS / 4;
138             List<Point2d> pq = new ArrayList<>();
139             for (int i = 0; i <= n; i++)
140             {
141                 double ang = (0.5 * Math.PI * i) / n;
142                 double x = this.dx + Math.cos(ang) * this.r - this.r;
143                 double y = this.dy + Math.sin(ang) * this.r - this.r;
144                 if (x >= 0.0 && y >= 0.0) // else, radius larger than at least one of this.dx and this.dy, i.e. no full quarters
145                 {
146                     pq.add(new Point2d(x, y));
147                 }
148             }
149 
150             List<Point2d> pqReversed = new ArrayList<>(pq);
151             Collections.reverse(pqReversed);
152             List<Point2d> points = new ArrayList<>(pq); // top right quadrant (y = up)
153             pqReversed.forEach((p) -> points.add(new Point2d(-p.x, p.y))); // top left quadrant
154             pq.forEach((p) -> points.add(p.neg())); // bottom left quadrant
155             pqReversed.forEach((p) -> points.add(new Point2d(p.x, -p.y))); // bottom right quadrant
156             this.polygon = new Polygon2d(true, points);
157         }
158         return this.polygon;
159     }
160 
161 }