1 package nl.tudelft.simulation.language.d2;
2
3 import java.awt.geom.Line2D;
4 import java.awt.geom.Point2D;
5
6 /**
7 * A directional line with normal vector. Based on the BSPLine-example from the book Developing games in Java from David
8 * Brackeen.
9 * <p>
10 * Copyright (c) 2003-2025 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
11 * for project information <a href="https://simulation.tudelft.nl/dsol/manual/" target="_blank">DSOL Manual</a>. The DSOL
12 * project is distributed under a three-clause BSD-style license, which can be found at
13 * <a href="https://simulation.tudelft.nl/dsol/docs/latest/license.html" target="_blank">DSOL License</a>.
14 * </p>
15 * @author <a href="mailto:royc@tbm.tudelft.nl">Roy Chin </a>
16 */
17 public class DirectionalLine extends Line2D.Double
18 {
19 /** the default serialVersionUID. */
20 private static final long serialVersionUID = 1L;
21
22 /** different values for the side a point can be at w.r.t. the line. */
23 public enum Side
24 {
25 /** point at the back of the line. */
26 BACKSIDE(-1),
27
28 /** point collinear with the line. */
29 COLLINEAR(0),
30
31 /** point in front of the line. */
32 FRONTSIDE(1),
33
34 /** other line is spanning this line. */
35 SPANNING(2);
36
37 /** the value from DSOL-1 before enum was introduced. */
38 private final int value;
39
40 /**
41 * Create a side; store the value from DSOL-1 as well.
42 * @param value the value from DSOL-1 before enum was introduced
43 */
44 Side(final int value)
45 {
46 this.value = value;
47 }
48
49 /**
50 * Returns the value from DSOL-1 before enum was introduced.
51 * @return the value from DSOL-1 before enum was introduced
52 */
53 public int getValue()
54 {
55 return this.value;
56 }
57 }
58
59 /** the thickness of the line. */
60 private double lineThickness = 1;
61
62 /** x coordinate of the line normal. */
63 private double normalX;
64
65 /** y coordinate of the line normal. */
66 private double normalY;
67
68 /**
69 * Creates a new DirectionalLine based on the specified coordinates.
70 * @param x1 Coordinate x1
71 * @param y1 Coordinate y1
72 * @param x2 Coordinate x2
73 * @param y2 Coordinate y2
74 */
75 public DirectionalLine(final double x1, final double y1, final double x2, final double y2)
76 {
77 this.setLine(x1, y1, x2, y2);
78 }
79
80 /**
81 * Creates a new DirectionalLine based on the specified (float) coordinates.
82 * @param x1 Coordinate x1
83 * @param y1 Coordinate y1
84 * @param x2 Coordinate x2
85 * @param y2 Coordinate y2
86 */
87 public DirectionalLine(final float x1, final float y1, final float x2, final float y2)
88 {
89 this.setLine(x1, y1, x2, y2);
90 }
91
92 /**
93 * Calculates the normal to this line. The normal of (a, b) is (-b, a).
94 */
95 public void calcNormal()
96 {
97 this.normalX = this.y1 - this.y2;
98 this.normalY = this.x2 - this.x1;
99 }
100
101 /**
102 * Normalizes the normal of this line (make the normal's length 1).
103 */
104 public void normalize()
105 {
106 double length = Math.sqrt(this.normalX * this.normalX + this.normalY * this.normalY);
107 this.normalX /= length;
108 this.normalY /= length;
109 }
110
111 /**
112 * Set the line using floats.
113 * @param x1 x1 coordinate
114 * @param y1 y1 coordinate
115 * @param x2 x2 coordinate
116 * @param y2 y2 coordinate
117 */
118 public void setLine(final float x1, final float y1, final float x2, final float y2)
119 {
120 super.setLine(x1, y1, x2, y2);
121 this.calcNormal();
122 }
123
124 @Override
125 public void setLine(final double x1, final double y1, final double x2, final double y2)
126 {
127 super.setLine(x1, y1, x2, y2);
128 this.calcNormal();
129 }
130
131 /**
132 * Flips this line so that the end points are reversed (in other words, (x1,y1) becomes (x2,y2) and vice versa) and the
133 * normal is changed to point the opposite direction.
134 */
135 public void flip()
136 {
137 double tx = this.x1;
138 double ty = this.y1;
139 this.x1 = this.x2;
140 this.y1 = this.y2;
141 this.x2 = tx;
142 this.y2 = ty;
143 this.normalX = -this.normalX;
144 this.normalY = -this.normalY;
145 }
146
147 /**
148 * Returns true if the endpoints of this line match the endpoints of the specified line. Ignores normal and height values.
149 * @param line another line
150 * @return true if this line's coordinates are equal to the other line's coordinates
151 */
152 public boolean equalsCoordinates(final DirectionalLine line)
153 {
154 return (this.x1 == line.x1 && this.x2 == line.x2 && this.y1 == line.y1 && this.y2 == line.y2);
155 }
156
157 /**
158 * Returns true if the endpoints of this line match the endpoints of the specified line, ignoring endpoint order (if the
159 * first point of this line is equal to the second point of the specified line, and vice versa, returns true). Ignores
160 * normal and height values.
161 * @param line another line
162 * @return true if coordinates match independent of the order
163 */
164 public boolean equalsCoordinatesIgnoreOrder(final DirectionalLine line)
165 {
166 return equalsCoordinates(line)
167 || ((this.x1 == line.x2 && this.x2 == line.x1 && this.y1 == line.y2 && this.y2 == line.y1));
168 }
169
170 @Override
171 public String toString()
172 {
173 return "(" + this.x1 + "," + this.y1 + ")->(" + this.x2 + "," + this.y2 + ")";
174 }
175
176 /**
177 * Gets the side of this line the specified point is on. This method treats the line as 1-unit thick, so points within this
178 * 1-unit border are considered collinear. For this to work correctly, the normal of this line must be normalized, either by
179 * setting this line to a polygon or by calling normalize(). Returns either FRONTSIDE, BACKSIDE, or COLLINEAR.
180 * @param x coordinate x
181 * @param y coordinate y
182 * @return the side
183 */
184 public Side getSideThick(final double x, final double y)
185 {
186 double normalX2 = this.normalX * this.lineThickness;
187 double normalY2 = this.normalY * this.lineThickness;
188
189 Side frontSide = getSideThin(x - normalX2 / 2, y - normalY2 / 2);
190 if (frontSide.equals(Side.FRONTSIDE))
191 {
192 return Side.FRONTSIDE;
193 }
194 else if (frontSide.equals(Side.BACKSIDE))
195 {
196 Side backSide = getSideThin(x + normalX2 / 2, y + normalY2 / 2);
197 if (backSide.equals(Side.BACKSIDE))
198 {
199 return Side.BACKSIDE;
200 }
201 }
202 return Side.COLLINEAR;
203 }
204
205 /**
206 * Gets the side of this line the specified point is on. Because of doubling point inaccuracy, a collinear line will be
207 * rare. For this to work correctly, the normal of this line must be normalized, either by setting this line to a polygon or
208 * by calling normalize(). Returns either FRONTSIDE, BACKSIDE, or COLLINEAR.
209 * @param x coordinate x
210 * @param y coordinate y
211 * @return the side
212 */
213 public Side getSideThin(final double x, final double y)
214 {
215 // dot product between vector to the point and the normal
216 double side = (x - this.x1) * this.normalX + (y - this.y1) * this.normalY;
217 if (side < 0)
218 {
219 return Side.BACKSIDE;
220 }
221 else if (side > 0)
222 {
223 return Side.FRONTSIDE;
224 }
225 else
226 {
227 return Side.COLLINEAR;
228 }
229 }
230
231 /**
232 * Gets the side of this line that the specified line segment is on. Returns either FRONT, BACK, COLINEAR, or SPANNING.
233 * @param line Line2D.Double; line segment
234 * @return the side
235 */
236 public Side getSide(final Line2D.Double line)
237 {
238 if (this.x1 == line.x1 && this.x2 == line.x2 && this.y1 == line.y1 && this.y2 == line.y2)
239 {
240 return Side.COLLINEAR;
241 }
242 Side p1Side = getSideThick(line.x1, line.y1);
243 Side p2Side = getSideThick(line.x2, line.y2);
244 if (p1Side == p2Side)
245 {
246 return p1Side;
247 }
248 else if (p1Side == Side.COLLINEAR)
249 {
250 return p2Side;
251 }
252 else if (p2Side == Side.COLLINEAR)
253 {
254 return p1Side;
255 }
256 else
257 {
258 return Side.SPANNING;
259 }
260 }
261
262 /**
263 * Returns the fraction of intersection along this line. Returns a value from 0 to 1 if the segments intersect. For example,
264 * a return value of 0 means the intersection occurs at point (x1, y1), 1 means the intersection occurs at point (x2, y2),
265 * and .5 mean the intersection occurs halfway between the two endpoints of this line. Returns -1 if the lines are parallel.
266 * @param line Line2D.Double; a line
267 * @return the intersection
268 */
269 public double getIntersection(final Line2D.Double line)
270 {
271 // The intersection point I, of two vectors, A1->A2 and
272 // B1->B2, is:
273 // I = A1 + Ua * (A2 - A1)
274 // I = B1 + Ub * (B2 - B1)
275 //
276 // Solving for Ua gives us the following formula.
277 // Ua is returned.
278 double denominator = (line.y2 - line.y1) * (this.x2 - this.x1) - (line.x2 - line.x1) * (this.y2 - this.y1);
279
280 // check if the two lines are parallel
281 if (denominator == 0)
282 {
283 return -1;
284 }
285
286 double numerator = (line.x2 - line.x1) * (this.y1 - line.y1) - (line.y2 - line.y1) * (this.x1 - line.x1);
287
288 return numerator / denominator;
289 }
290
291 /**
292 * Returns the intersection point of this line with the specified line.
293 * @param line Line2D.Double; a line
294 * @return intersection point
295 */
296 public Point2D.Double getIntersectionPoint(final Line2D.Double line)
297 {
298 double fraction = getIntersection(line);
299 Point2D.Double intersection = new Point2D.Double();
300 intersection.setLocation(this.x1 + fraction * (this.x2 - this.x1), this.y1 + fraction * (this.y2 - this.y1));
301 return intersection;
302 }
303
304 /**
305 * Gets the thickness of the line.
306 * @return returns the lineThickness
307 */
308 public double getLineThickness()
309 {
310 return this.lineThickness;
311 }
312
313 /**
314 * Sets the thickness of the line.
315 * @param lineThickness the lineThickness to set
316 */
317 public void setLineThickness(final double lineThickness)
318 {
319 this.lineThickness = lineThickness;
320 }
321
322 /**
323 * @return returns the normalX
324 */
325 public double getNormalx()
326 {
327 return this.normalX;
328 }
329
330 /**
331 * @return returns the normalY
332 */
333 public double getNormaly()
334 {
335 return this.normalY;
336 }
337 }