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   * Shape 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 RoundedRectangleShape implements OtsShape
20  {
21  
22      /** Half length along y dimension. */
23      private final double dx;
24  
25      /** Half length along y dimension. */
26      private final double dy;
27  
28      /** Rounding radius. */
29      private final double r;
30  
31      /** Max x coordinate, can be lower than dx due to large r. */
32      private final double maxX;
33  
34      /** Max y coordinate, can be lower than dy due to large r. */
35      private final double maxY;
36  
37      /** Number of line segments in polygon representation. */
38      private final int polygonSegments;
39  
40      /** Polygon representation. */
41      private Polygon2d polygon;
42  
43      /**
44       * Constructor.
45       * @param dx complete length along x dimension.
46       * @param dy complete length along y dimension.
47       * @param r radius of rounding, must be positive.
48       * @throws IllegalArgumentException when r is negative, or so large no net shape remains
49       */
50      public RoundedRectangleShape(final double dx, final double dy, final double r)
51      {
52          this(dx, dy, r, DEFAULT_POLYGON_SEGMENTS);
53      }
54  
55      /**
56       * Constructor.
57       * @param dx complete length along x dimension.
58       * @param dy complete length along y dimension.
59       * @param r radius of rounding, must be positive.
60       * @param polygonSegments number of segments in polygon representation.
61       * @throws IllegalArgumentException when r is negative, or so large no net shape remains
62       */
63      public RoundedRectangleShape(final double dx, final double dy, final double r, final int polygonSegments)
64      {
65          /*-
66           * 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]
67           * 
68           *                dx
69           *    ___       ______
70           * ^ |   ''--_ |      | dy
71           * | |---------o------
72           * | | r-dx  / |'.
73           * r |      /  |  '.
74           * | |   r/    |    \
75           * | |   /     |     \
76           * | | /   r-dy|      |
77           * v |/________|______|
78           *    <-------r------>
79           */
80          this.dx = Math.abs(dx) / 2.0;
81          this.dy = Math.abs(dy) / 2.0;
82          Throw.when(r >= this.dx + this.dy + Math.sqrt(2.0 * this.dx * this.dy), IllegalArgumentException.class,
83                  "Radius makes rounded rectangle non-existent.");
84          Throw.when(r < 0.0, IllegalArgumentException.class, "Radius must be positive.");
85          this.r = r;
86          this.maxX = this.dx - signedDistance(new Point2d(this.dx, 0.0));
87          this.maxY = this.dy - signedDistance(new Point2d(0.0, this.dy));
88          this.polygonSegments = polygonSegments;
89      }
90  
91      @Override
92      public double getMinX()
93      {
94          return -this.maxX;
95      }
96  
97      @Override
98      public double getMaxX()
99      {
100         return this.maxX;
101     }
102 
103     @Override
104     public double getMinY()
105     {
106         return -this.maxY;
107     }
108 
109     @Override
110     public double getMaxY()
111     {
112         return this.maxY;
113     }
114 
115     @Override
116     public boolean contains(final Point2d point) throws NullPointerException
117     {
118         return signedDistance(point) < 0.0;
119     }
120 
121     @Override
122     public boolean contains(final double x, final double y) throws NullPointerException
123     {
124         return contains(new Point2d(x, y));
125     }
126 
127     /**
128      * {@inheritDoc}
129      * @see <a href="https://iquilezles.org/articles/distfunctions/">Signed distance functions by Inigo Quilez</a>
130      */
131     @Override
132     public double signedDistance(final Point2d point)
133     {
134         double qx = Math.abs(point.x) - this.dx + this.r;
135         double qy = Math.abs(point.y) - this.dy + this.r;
136         return Math.hypot(Math.max(qx, 0.0), Math.max(qy, 0.0)) + Math.min(Math.max(qx, qy), 0.0) - this.r;
137     }
138 
139     @Override
140     public Polygon2d asPolygon()
141     {
142         if (this.polygon == null)
143         {
144             // calculate for top right quadrant only, others are negative or reversed copies
145             int n = this.polygonSegments / 4;
146             List<Point2d> pq = new ArrayList<>();
147             for (int i = 0; i <= n; i++)
148             {
149                 double ang = (0.5 * Math.PI * i) / n;
150                 double x = this.dx + Math.cos(ang) * this.r - this.r;
151                 double y = this.dy + Math.sin(ang) * this.r - this.r;
152                 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
153                 {
154                     pq.add(new Point2d(x, y));
155                 }
156             }
157 
158             List<Point2d> pqReversed = new ArrayList<>(pq);
159             Collections.reverse(pqReversed);
160             List<Point2d> points = new ArrayList<>(pq); // top right quadrant (y = up)
161             pqReversed.forEach((p) -> points.add(new Point2d(-p.x, p.y))); // top left quadrant
162             pq.forEach((p) -> points.add(p.neg())); // bottom left quadrant
163             pqReversed.forEach((p) -> points.add(new Point2d(p.x, -p.y))); // bottom right quadrant
164             this.polygon = new Polygon2d(true, points);
165         }
166         return this.polygon;
167     }
168 
169     @Override
170     public String toString()
171     {
172         return "RoundedRectangleShape [dx=" + this.dx + ", dy=" + this.dy + ", r=" + this.r + "]";
173     }
174 
175 }