View Javadoc
1   package org.opentrafficsim.road.network.lane;
2   
3   import static org.junit.jupiter.api.Assertions.assertEquals;
4   import static org.junit.jupiter.api.Assertions.assertFalse;
5   import static org.junit.jupiter.api.Assertions.assertTrue;
6   import static org.junit.jupiter.api.Assertions.fail;
7   
8   import java.awt.geom.Point2D;
9   import java.rmi.RemoteException;
10  import java.util.ArrayList;
11  import java.util.LinkedHashMap;
12  import java.util.List;
13  import java.util.Map;
14  import java.util.SortedMap;
15  
16  import javax.naming.NamingException;
17  
18  import org.djunits.unit.DurationUnit;
19  import org.djunits.unit.util.UNITS;
20  import org.djunits.value.vdouble.scalar.Direction;
21  import org.djunits.value.vdouble.scalar.Duration;
22  import org.djunits.value.vdouble.scalar.Length;
23  import org.djunits.value.vdouble.scalar.Speed;
24  import org.djunits.value.vdouble.scalar.Time;
25  import org.djutils.draw.bounds.Bounds;
26  import org.djutils.draw.line.Polygon2d;
27  import org.djutils.draw.point.OrientedPoint2d;
28  import org.djutils.draw.point.Point2d;
29  import org.djutils.event.Event;
30  import org.djutils.event.EventListener;
31  import org.junit.jupiter.api.Test;
32  import org.mockito.Mockito;
33  import org.opentrafficsim.base.geometry.OtsLine2d;
34  import org.opentrafficsim.core.definitions.DefaultsNl;
35  import org.opentrafficsim.core.dsol.AbstractOtsModel;
36  import org.opentrafficsim.core.dsol.OtsSimulator;
37  import org.opentrafficsim.core.dsol.OtsSimulatorInterface;
38  import org.opentrafficsim.core.geometry.ContinuousLine.ContinuousDoubleFunction;
39  import org.opentrafficsim.core.geometry.FractionalLengthData;
40  import org.opentrafficsim.core.gtu.GtuType;
41  import org.opentrafficsim.core.network.LateralDirectionality;
42  import org.opentrafficsim.core.network.NetworkException;
43  import org.opentrafficsim.core.network.Node;
44  import org.opentrafficsim.core.perception.HistoryManagerDevs;
45  import org.opentrafficsim.road.definitions.DefaultsRoadNl;
46  import org.opentrafficsim.road.mock.MockDevsSimulator;
47  import org.opentrafficsim.road.network.RoadNetwork;
48  import org.opentrafficsim.road.network.lane.changing.LaneKeepingPolicy;
49  import org.opentrafficsim.road.network.lane.object.LaneBasedObject;
50  import org.opentrafficsim.road.network.lane.object.detector.LaneDetector;
51  
52  import nl.tudelft.simulation.dsol.SimRuntimeException;
53  
54  /**
55   * Test the Lane class.
56   * <p>
57   * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
58   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
59   * </p>
60   * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
61   */
62  public class LaneTest implements UNITS
63  {
64      /**
65       * Test the constructor.
66       * @throws Exception when something goes wrong (should not happen)
67       */
68      @Test
69      public void laneConstructorTest() throws Exception
70      {
71          OtsSimulatorInterface simulator = new OtsSimulator("LaneTest");
72          RoadNetwork network = new RoadNetwork("lane test network", simulator);
73          Model model = new Model(simulator);
74          simulator.initialize(Time.ZERO, Duration.ZERO, new Duration(3600.0, DurationUnit.SECOND), model,
75                  HistoryManagerDevs.noHistory(simulator));
76          // First we need two Nodes
77          Node nodeFrom = new Node(network, "A", new Point2d(0, 0), Direction.ZERO);
78          Node nodeTo = new Node(network, "B", new Point2d(1000, 0), Direction.ZERO);
79          // Now we can make a Link
80          Point2d[] coordinates = new Point2d[2];
81          coordinates[0] = nodeFrom.getPoint();
82          coordinates[1] = nodeTo.getPoint();
83          CrossSectionLink link = new CrossSectionLink(network, "A to B", nodeFrom, nodeTo, DefaultsNl.FREEWAY,
84                  new OtsLine2d(coordinates), null, LaneKeepingPolicy.KEEPRIGHT);
85          Length startLateralPos = new Length(2, METER);
86          Length endLateralPos = new Length(5, METER);
87          Length startWidth = new Length(3, METER);
88          Length endWidth = new Length(4, METER);
89          GtuType gtuTypeCar = DefaultsNl.CAR;
90  
91          LaneType laneType = new LaneType("One way", DefaultsRoadNl.FREEWAY);
92          laneType.addCompatibleGtuType(DefaultsNl.VEHICLE);
93          Map<GtuType, Speed> speedMap = new LinkedHashMap<>();
94          speedMap.put(DefaultsNl.VEHICLE, new Speed(100, KM_PER_HOUR));
95          // Now we can construct a Lane
96          // FIXME what overtaking conditions do we want to test in this unit test?
97          Lane lane = LaneGeometryUtil.createStraightLane(link, "lane", startLateralPos, endLateralPos, startWidth, endWidth,
98                  laneType, speedMap);
99          // Verify the easy bits
100         assertEquals(network, link.getNetwork(), "Link returns network");
101         assertEquals(network, lane.getNetwork(), "Lane returns network");
102         assertEquals(0, lane.prevLanes(gtuTypeCar).size(), "PrevLanes should be empty"); // this one caught a bug!
103         assertEquals(0, lane.nextLanes(gtuTypeCar).size(), "NextLanes should be empty");
104         double approximateLengthOfContour =
105                 2 * nodeFrom.getPoint().distance(nodeTo.getPoint()) + startWidth.getSI() + endWidth.getSI();
106         assertEquals(approximateLengthOfContour, lane.getContour().getLength(), 0.1,
107                 "Length of contour is approximately " + approximateLengthOfContour);
108         assertEquals(new Speed(100, KM_PER_HOUR), lane.getSpeedLimit(DefaultsNl.VEHICLE),
109                 "SpeedLimit should be " + (new Speed(100, KM_PER_HOUR)));
110         assertEquals(0, lane.getGtuList().size(), "There should be no GTUs on the lane");
111         assertEquals(laneType, lane.getType(), "LaneType should be " + laneType);
112         // TODO: This test for expectedLateralCenterOffset fails
113         for (int i = 0; i < 10; i++)
114         {
115             double expectedLateralCenterOffset =
116                     startLateralPos.getSI() + (endLateralPos.getSI() - startLateralPos.getSI()) * i / 10;
117             assertEquals(expectedLateralCenterOffset, lane.getLateralCenterPosition(i / 10.0).getSI(), 0.01,
118                     String.format("Lateral offset at %d%% should be %.3fm", 10 * i, expectedLateralCenterOffset));
119             Length longitudinalPosition = new Length(lane.getLength().getSI() * i / 10, METER);
120             assertEquals(expectedLateralCenterOffset, lane.getLateralCenterPosition(longitudinalPosition).getSI(), 0.01,
121                     "Lateral offset at " + longitudinalPosition + " should be " + expectedLateralCenterOffset);
122             double expectedWidth = startWidth.getSI() + (endWidth.getSI() - startWidth.getSI()) * i / 10;
123             assertEquals(expectedWidth, lane.getWidth(i / 10.0).getSI(), 0.0001,
124                     String.format("Width at %d%% should be %.3fm", 10 * i, expectedWidth));
125             assertEquals(expectedWidth, lane.getWidth(longitudinalPosition).getSI(), 0.0001,
126                     "Width at " + longitudinalPosition + " should be " + expectedWidth);
127             double expectedLeftOffset = expectedLateralCenterOffset - expectedWidth / 2;
128             // The next test caught a bug
129             assertEquals(expectedLeftOffset, lane.getLateralBoundaryPosition(LateralDirectionality.LEFT, i / 10.0).getSI(),
130                     0.001, String.format("Left edge at %d%% should be %.3fm", 10 * i, expectedLeftOffset));
131             assertEquals(expectedLeftOffset,
132                     lane.getLateralBoundaryPosition(LateralDirectionality.LEFT, longitudinalPosition).getSI(), 0.001,
133                     "Left edge at " + longitudinalPosition + " should be " + expectedLeftOffset);
134             double expectedRightOffset = expectedLateralCenterOffset + expectedWidth / 2;
135             assertEquals(expectedRightOffset, lane.getLateralBoundaryPosition(LateralDirectionality.RIGHT, i / 10.0).getSI(),
136                     0.001, String.format("Right edge at %d%% should be %.3fm", 10 * i, expectedRightOffset));
137             assertEquals(expectedRightOffset,
138                     lane.getLateralBoundaryPosition(LateralDirectionality.RIGHT, longitudinalPosition).getSI(), 0.001,
139                     "Right edge at " + longitudinalPosition + " should be " + expectedRightOffset);
140         }
141 
142         // Harder case; create a Link with form points along the way
143         // System.out.println("Constructing Link and Lane with one form point");
144         coordinates = new Point2d[3];
145         coordinates[0] = new Point2d(nodeFrom.getPoint().x, nodeFrom.getPoint().y);
146         coordinates[1] = new Point2d(200, 100);
147         coordinates[2] = new Point2d(nodeTo.getPoint().x, nodeTo.getPoint().y);
148         link = new CrossSectionLink(network, "A to B with Kink", nodeFrom, nodeTo, DefaultsNl.FREEWAY,
149                 new OtsLine2d(coordinates), null, LaneKeepingPolicy.KEEPRIGHT);
150         lane = LaneGeometryUtil.createStraightLane(link, "lane.1", startLateralPos, endLateralPos, startWidth, endWidth,
151                 laneType, speedMap);
152         // Verify the easy bits
153 
154         // XXX: This is not correct...
155         /*-
156         assertEquals("PrevLanes should contain one lane from the other link", 1, lane.prevLanes(gtuTypeCar).size());
157         assertEquals("NextLanes should contain one lane from the other link", 1, lane.nextLanes(gtuTypeCar).size());
158         approximateLengthOfContour = 2 * (coordinates[0].distanceSI(coordinates[1]) + coordinates[1].distanceSI(coordinates[2]))
159                 + startWidth.getSI() + endWidth.getSI();
160         // System.out.println("contour of lane is " + lane.getContour());
161         // System.out.println(lane.getContour().toPlot());
162         assertEquals("Length of contour is approximately " + approximateLengthOfContour, approximateLengthOfContour,
163                 lane.getContour().getLengthSI(), 4); // This lane takes a path that is about 3m longer than the design line
164         assertEquals("There should be no GTUs on the lane", 0, lane.getGtuList().size());
165         assertEquals("LaneType should be " + laneType, laneType, lane.getType());
166         // System.out.println("Add another Lane at the inside of the corner in the design line");
167         Length startLateralPos2 = new Length(-8, METER);
168         Length endLateralPos2 = new Length(-5, METER);
169         Lane lane2 =
170                 new Lane(link, "lane.2", startLateralPos2, endLateralPos2, startWidth, endWidth, laneType, speedMap, false);
171         // Verify the easy bits
172         assertEquals("PrevLanes should be empty", 0, lane2.prevLanes(gtuTypeCar).size());
173         assertEquals("NextLanes should be empty", 0, lane2.nextLanes(gtuTypeCar).size());
174         approximateLengthOfContour = 2 * (coordinates[0].distanceSI(coordinates[1]) + coordinates[1].distanceSI(coordinates[2]))
175                 + startWidth.getSI() + endWidth.getSI();
176         assertEquals("Length of contour is approximately " + approximateLengthOfContour, approximateLengthOfContour,
177                 lane2.getContour().getLengthSI(), 12); // This lane takes a path that is about 11 meters shorter
178         assertEquals("There should be no GTUs on the lane", 0, lane2.getGtuList().size());
179         assertEquals("LaneType should be " + laneType, laneType, lane2.getType());
180         */
181 
182         // Construct a lane using CrossSectionSlices
183         OtsLine2d centerLine = new OtsLine2d(new Point2d(0.0, 0.0), new Point2d(100.0, 0.0));
184         Polygon2d contour = new Polygon2d(new Point2d(0.0, -1.75), new Point2d(100.0, -1.75), new Point2d(100.0, 1.75),
185                 new Point2d(0.0, -1.75));
186         ContinuousDoubleFunction offsetFunc = FractionalLengthData.of(0.0, startLateralPos.si);
187         ContinuousDoubleFunction widthFunc = FractionalLengthData.of(0.0, startWidth.si);
188         lane = new Lane(link, "lanex", new CrossSectionGeometry(centerLine, contour, offsetFunc, widthFunc), laneType,
189                 speedMap);
190         sensorTest(lane);
191     }
192 
193     /**
194      * Add/Remove some sensor to/from a lane and see if the expected events occur.
195      * @param lane the lane to manipulate
196      * @throws NetworkException when this happens uncaught; this test has failed
197      */
198     public final void sensorTest(final Lane lane) throws NetworkException
199     {
200         assertEquals(0, lane.getDetectors().size(), "List of sensor is initially empty");
201         Listener listener = new Listener();
202         double length = lane.getLength().si;
203         lane.addListener(listener, Lane.DETECTOR_ADD_EVENT);
204         lane.addListener(listener, Lane.DETECTOR_REMOVE_EVENT);
205         assertEquals(0, listener.events.size(), "event list is initially empty");
206         LaneDetector sensor1 = new MockSensor("sensor1", Length.instantiateSI(length / 4)).getMock();
207         lane.addDetector(sensor1);
208         assertEquals(1, listener.events.size(), "event list now contains one event");
209         assertEquals(listener.events.get(0).getType(), Lane.DETECTOR_ADD_EVENT, "event indicates that a sensor got added");
210         assertEquals(1, lane.getDetectors().size(), "lane now contains one sensor");
211         assertEquals(sensor1, lane.getDetectors().get(0), "sensor on lane is sensor1");
212         LaneDetector sensor2 = new MockSensor("sensor2", Length.instantiateSI(length / 2)).getMock();
213         lane.addDetector(sensor2);
214         assertEquals(2, listener.events.size(), "event list now contains two events");
215         assertEquals(listener.events.get(1).getType(), Lane.DETECTOR_ADD_EVENT, "event indicates that a sensor got added");
216         List<LaneDetector> sensors = lane.getDetectors();
217         assertEquals(2, sensors.size(), "lane now contains two sensors");
218         assertTrue(sensors.contains(sensor1), "sensor list contains sensor1");
219         assertTrue(sensors.contains(sensor2), "sensor list contains sensor2");
220         sensors = lane.getDetectors(Length.ZERO, Length.instantiateSI(length / 3), DefaultsNl.VEHICLE);
221         assertEquals(1, sensors.size(), "first third of lane contains 1 sensor");
222         assertTrue(sensors.contains(sensor1), "sensor list contains sensor1");
223         sensors = lane.getDetectors(Length.instantiateSI(length / 3), Length.instantiateSI(length), DefaultsNl.VEHICLE);
224         assertEquals(1, sensors.size(), "last two-thirds of lane contains 1 sensor");
225         assertTrue(sensors.contains(sensor2), "sensor list contains sensor2");
226         sensors = lane.getDetectors(DefaultsNl.VEHICLE);
227         // NB. The mocked sensor is compatible with all GTU types in all directions.
228         assertEquals(2, sensors.size(), "sensor list contains two sensors");
229         assertTrue(sensors.contains(sensor1), "sensor list contains sensor1");
230         assertTrue(sensors.contains(sensor2), "sensor list contains sensor2");
231         sensors = lane.getDetectors(DefaultsNl.VEHICLE);
232         // NB. The mocked sensor is compatible with all GTU types in all directions.
233         assertEquals(2, sensors.size(), "sensor list contains two sensors");
234         assertTrue(sensors.contains(sensor1), "sensor list contains sensor1");
235         assertTrue(sensors.contains(sensor2), "sensor list contains sensor2");
236         SortedMap<Double, List<LaneDetector>> sensorMap = lane.getDetectorMap(DefaultsNl.VEHICLE);
237         assertEquals(2, sensorMap.size(), "sensor map contains two entries");
238         for (Double d : sensorMap.keySet())
239         {
240             List<LaneDetector> sensorsAtD = sensorMap.get(d);
241             assertEquals(1, sensorsAtD.size(), "There is one sensor at position d");
242             assertEquals(d < length / 3 ? sensor1 : sensor2, sensorsAtD.get(0),
243                     "Sensor map contains the correct sensor at the correct distance");
244         }
245 
246         lane.removeDetector(sensor1);
247         assertEquals(3, listener.events.size(), "event list now contains three events");
248         assertEquals(listener.events.get(2).getType(), Lane.DETECTOR_REMOVE_EVENT, "event indicates that a sensor got removed");
249         sensors = lane.getDetectors();
250         assertEquals(1, sensors.size(), "lane now contains one sensor");
251         assertTrue(sensors.contains(sensor2), "sensor list contains sensor2");
252         try
253         {
254             lane.removeDetector(sensor1);
255             fail("Removing a sensor twice should have thrown a NetworkException");
256         }
257         catch (NetworkException ne)
258         {
259             // Ignore expected exception
260         }
261         try
262         {
263             lane.addDetector(sensor2);
264             fail("Adding a sensor twice should have thrown a NetworkException");
265         }
266         catch (NetworkException ne)
267         {
268             // Ignore expected exception
269         }
270         LaneDetector badSensor = new MockSensor("sensor3", Length.instantiateSI(-0.1)).getMock();
271         try
272         {
273             lane.addDetector(badSensor);
274             fail("Adding a sensor at negative position should have thrown a NetworkException");
275         }
276         catch (NetworkException ne)
277         {
278             // Ignore expected exception
279         }
280         badSensor = new MockSensor("sensor4", Length.instantiateSI(length + 0.1)).getMock();
281         try
282         {
283             lane.addDetector(badSensor);
284             fail("Adding a sensor at position beyond the end of the lane should have thrown a NetworkException");
285         }
286         catch (NetworkException ne)
287         {
288             // Ignore expected exception
289         }
290         lane.removeDetector(sensor2);
291         List<LaneBasedObject> lboList = lane.getLaneBasedObjects();
292         assertEquals(0, lboList.size(), "lane initially contains zero lane based objects");
293         LaneBasedObject lbo1 = new MockLaneBasedObject("lbo1", Length.instantiateSI(length / 4)).getMock();
294         listener.getEvents().clear();
295         lane.addListener(listener, Lane.OBJECT_ADD_EVENT);
296         lane.addListener(listener, Lane.OBJECT_REMOVE_EVENT);
297         lane.addLaneBasedObject(lbo1);
298         assertEquals(1, listener.getEvents().size(), "adding a lane based object cause the lane to emit an event");
299         assertEquals(Lane.OBJECT_ADD_EVENT, listener.getEvents().get(0).getType(), "The emitted event was a OBJECT_ADD_EVENT");
300         LaneBasedObject lbo2 = new MockLaneBasedObject("lbo2", Length.instantiateSI(3 * length / 4)).getMock();
301         lane.addLaneBasedObject(lbo2);
302         lboList = lane.getLaneBasedObjects();
303         assertEquals(2, lboList.size(), "lane based object list now contains two objects");
304         assertTrue(lboList.contains(lbo1), "lane base object list contains lbo1");
305         assertTrue(lboList.contains(lbo2), "lane base object list contains lbo2");
306         lboList = lane.getLaneBasedObjects(Length.ZERO, Length.instantiateSI(length / 2));
307         assertEquals(1, lboList.size(), "first half of lane contains one object");
308         assertEquals(lbo1, lboList.get(0), "object in first haf of lane is lbo1");
309         lboList = lane.getLaneBasedObjects(Length.instantiateSI(length / 2), Length.instantiateSI(length));
310         assertEquals(1, lboList.size(), "second half of lane contains one object");
311         assertEquals(lbo2, lboList.get(0), "object in second haf of lane is lbo2");
312         SortedMap<Double, List<LaneBasedObject>> sortedMap = lane.getLaneBasedObjectMap();
313         assertEquals(2, sortedMap.size(), "sorted map contains two objects");
314         for (Double d : sortedMap.keySet())
315         {
316             List<LaneBasedObject> objectsAtD = sortedMap.get(d);
317             assertEquals(1, objectsAtD.size(), "There is one object at position d");
318             assertEquals(d < length / 2 ? lbo1 : lbo2, objectsAtD.get(0), "Object at position d is the expected one");
319         }
320 
321         for (double fraction : new double[] {-0.5, 0, 0.2, 0.5, 0.9, 1.0, 2})
322         {
323             double positionSI = length * fraction;
324             double fractionSI = lane.fractionSI(positionSI);
325             assertEquals(fraction, fractionSI, 0.0001, "fractionSI matches fraction");
326 
327             LaneBasedObject nextObject = positionSI < lbo1.getLongitudinalPosition().si ? lbo1
328                     : positionSI < lbo2.getLongitudinalPosition().si ? lbo2 : null;
329             List<LaneBasedObject> expected = null;
330             if (null != nextObject)
331             {
332                 expected = new ArrayList<>();
333                 expected.add(nextObject);
334             }
335             List<LaneBasedObject> got = lane.getObjectAhead(Length.instantiateSI(positionSI));
336             assertEquals(expected, got, "First bunch of objects ahead of d");
337 
338             nextObject = positionSI > lbo2.getLongitudinalPosition().si ? lbo2
339                     : positionSI > lbo1.getLongitudinalPosition().si ? lbo1 : null;
340             expected = null;
341             if (null != nextObject)
342             {
343                 expected = new ArrayList<>();
344                 expected.add(nextObject);
345             }
346             got = lane.getObjectBehind(Length.instantiateSI(positionSI));
347             assertEquals(expected, got, "First bunch of objects behind d");
348         }
349 
350         lane.removeLaneBasedObject(lbo1);
351         assertEquals(3, listener.getEvents().size(), "removing a lane based object caused the lane to emit an event");
352         assertEquals(Lane.OBJECT_REMOVE_EVENT, listener.getEvents().get(2).getType(),
353                 "removing a lane based object caused the lane to emit OBJECT_REMOVE_EVENT");
354         try
355         {
356             lane.removeLaneBasedObject(lbo1);
357             fail("Removing a lane bases object that was already removed should have caused a NetworkException");
358         }
359         catch (NetworkException ne)
360         {
361             // Ignore expected exception
362         }
363         try
364         {
365             lane.addLaneBasedObject(lbo2);
366             fail("Adding a lane base object that was already added should have caused a NetworkException");
367         }
368         catch (NetworkException ne)
369         {
370             // Ignore expected exception
371         }
372         LaneBasedObject badLBO = new MockLaneBasedObject("badLBO", Length.instantiateSI(-0.1)).getMock();
373         try
374         {
375             lane.addLaneBasedObject(badLBO);
376             fail("Adding a lane based object at negative position should have thrown a NetworkException");
377         }
378         catch (NetworkException ne)
379         {
380             // Ignore expected exception
381         }
382         badLBO = new MockLaneBasedObject("badLBO", Length.instantiateSI(length + 0.1)).getMock();
383         try
384         {
385             lane.addLaneBasedObject(badLBO);
386             fail("Adding a lane based object at position beyond end of lane should have thrown a NetworkException");
387         }
388         catch (NetworkException ne)
389         {
390             // Ignore expected exception
391         }
392     }
393 
394     /**
395      * Simple event listener that collects events in a list.
396      */
397     class Listener implements EventListener
398     {
399         /** Collect the received events. */
400         private List<Event> events = new ArrayList<>();
401 
402         @Override
403         public void notify(final Event event) throws RemoteException
404         {
405             this.events.add(event);
406         }
407 
408         /**
409          * Retrieve the collected events.
410          * @return the events
411          */
412         public List<Event> getEvents()
413         {
414             return this.events;
415         }
416 
417     }
418 
419     /**
420      * Mock a Detector.
421      */
422     class MockSensor
423     {
424         /** The mocked sensor. */
425         private final LaneDetector mockSensor;
426 
427         /** Id of the mocked sensor. */
428         private final String id;
429 
430         /** The position along the lane of the sensor. */
431         private final Length position;
432 
433         /** Faked simulator. */
434         private final OtsSimulatorInterface simulator = MockDevsSimulator.createMock();
435 
436         /**
437          * Construct a new Mocked Detector.
438          * @param id result of the getId() method of the mocked Detector
439          * @param position result of the getLongitudinalPosition of the mocked Detector
440          */
441         MockSensor(final String id, final Length position)
442         {
443             this.mockSensor = Mockito.mock(LaneDetector.class);
444             this.id = id;
445             this.position = position;
446             Mockito.when(this.mockSensor.getId()).thenReturn(this.id);
447             Mockito.when(this.mockSensor.getLongitudinalPosition()).thenReturn(this.position);
448             Mockito.when(this.mockSensor.getSimulator()).thenReturn(this.simulator);
449             Mockito.when(this.mockSensor.getFullId()).thenReturn(this.id);
450             Mockito.when(this.mockSensor.isCompatible(Mockito.any())).thenReturn(true);
451         }
452 
453         /**
454          * Retrieve the mocked sensor.
455          * @return the mocked sensor
456          */
457         public LaneDetector getMock()
458         {
459             return this.mockSensor;
460         }
461 
462         /**
463          * Retrieve the position of the mocked sensor.
464          * @return the longitudinal position of the mocked sensor
465          */
466         public Length getLongitudinalPosition()
467         {
468             return this.position;
469         }
470 
471         @Override
472         public String toString()
473         {
474             return "MockSensor [mockSensor=" + this.mockSensor + ", id=" + this.id + ", position=" + this.position + "]";
475         }
476 
477     }
478 
479     /**
480      * Mock a LaneBasedObject.
481      */
482     class MockLaneBasedObject
483     {
484         /** The mocked sensor. */
485         private final LaneBasedObject mockLaneBasedObject;
486 
487         /** Id of the mocked sensor. */
488         private final String id;
489 
490         /** The position along the lane of the sensor. */
491         private final Length position;
492 
493         /**
494          * Construct a new Mocked Detector.
495          * @param id result of the getId() method of the mocked Detector
496          * @param position result of the getLongitudinalPosition of the mocked Detector
497          */
498         MockLaneBasedObject(final String id, final Length position)
499         {
500             this.mockLaneBasedObject = Mockito.mock(LaneDetector.class);
501             this.id = id;
502             this.position = position;
503             Mockito.when(this.mockLaneBasedObject.getId()).thenReturn(this.id);
504             Mockito.when(this.mockLaneBasedObject.getLongitudinalPosition()).thenReturn(this.position);
505             Mockito.when(this.mockLaneBasedObject.getFullId()).thenReturn(this.id);
506         }
507 
508         /**
509          * Retrieve the mocked LaneBasedObject.
510          * @return the mocked LaneBasedObject
511          */
512         public LaneBasedObject getMock()
513         {
514             return this.mockLaneBasedObject;
515         }
516 
517         /**
518          * Retrieve the position of the mocked sensor.
519          * @return the longitudinal position of the mocked sensor
520          */
521         public Length getLongitudinalPosition()
522         {
523             return this.position;
524         }
525 
526         @Override
527         public String toString()
528         {
529             return "MockLaneBasedObject [mockLaneBasedObject=" + this.mockLaneBasedObject + ", id=" + this.id + ", position="
530                     + this.position + "]";
531         }
532 
533     }
534 
535     /**
536      * Test that gradually varying lateral offsets have gradually increasing angles (with respect to the design line) in the
537      * first half and gradually decreasing angles in the second half.
538      * @throws NetworkException when that happens uncaught; this test has failed
539      * @throws NamingException when that happens uncaught; this test has failed
540      * @throws SimRuntimeException when that happens uncaught; this test has failed
541      */
542     @Test
543     public final void lateralOffsetTest() throws NetworkException, SimRuntimeException, NamingException
544     {
545         Point2d from = new Point2d(10, 10);
546         Point2d to = new Point2d(1010, 10);
547         OtsSimulatorInterface simulator = new OtsSimulator("LaneTest");
548         Model model = new Model(simulator);
549         simulator.initialize(Time.ZERO, Duration.ZERO, new Duration(3600.0, DurationUnit.SECOND), model,
550                 HistoryManagerDevs.noHistory(simulator));
551         RoadNetwork network = new RoadNetwork("contour test network", simulator);
552         LaneType laneType = DefaultsRoadNl.TWO_WAY_LANE;
553         laneType.addCompatibleGtuType(DefaultsNl.VEHICLE);
554         Map<GtuType, Speed> speedMap = new LinkedHashMap<>();
555         speedMap.put(DefaultsNl.VEHICLE, new Speed(50, KM_PER_HOUR));
556         Node start = new Node(network, "start", from, Direction.ZERO);
557         Node end = new Node(network, "end", to, Direction.ZERO);
558         Point2d[] coordinates = new Point2d[2];
559         coordinates[0] = start.getPoint();
560         coordinates[1] = end.getPoint();
561         OtsLine2d line = new OtsLine2d(coordinates);
562         CrossSectionLink link =
563                 new CrossSectionLink(network, "A to B", start, end, DefaultsNl.ROAD, line, null, LaneKeepingPolicy.KEEPRIGHT);
564         Length offsetAtStart = Length.instantiateSI(5);
565         Length offsetAtEnd = Length.instantiateSI(15);
566         Length width = Length.instantiateSI(4);
567         Lane lane =
568                 LaneGeometryUtil.createStraightLane(link, "lane", offsetAtStart, offsetAtEnd, width, width, laneType, speedMap);
569         OtsLine2d laneCenterLine = lane.getCenterLine();
570         // System.out.println("Center line is " + laneCenterLine);
571         List<Point2d> points = laneCenterLine.getPointList();
572         double prev = offsetAtStart.si + from.y;
573         double prevRatio = 0;
574         double prevDirection = 0;
575         for (int i = 0; i < points.size(); i++)
576         {
577             Point2d p = points.get(i);
578             double relativeLength = p.x - from.x;
579             double ratio = relativeLength / (to.x - from.x);
580             double actualOffset = p.y;
581             if (0 == i)
582             {
583                 assertEquals(offsetAtStart.si + from.y, actualOffset, 0.001, "first point must have offset at start");
584             }
585             if (points.size() - 1 == i)
586             {
587                 assertEquals(offsetAtEnd.si + from.y, actualOffset, 0.001, "last point must have offset at end");
588             }
589             // Other offsets must grow smoothly
590             double delta = actualOffset - prev;
591             assertTrue(delta >= 0, "delta must be nonnegative");
592             if (i > 0)
593             {
594                 Point2d prevPoint = points.get(i - 1);
595                 double direction = Math.atan2(p.y - prevPoint.y, p.x - prevPoint.x);
596                 // System.out.println(String.format("p=%30s: ratio=%7.5f, direction=%10.7f", p, ratio, direction));
597                 assertTrue(direction > 0, "Direction of lane center line is > 0");
598                 if (ratio < 0.5)
599                 {
600                     assertTrue(direction > prevDirection, "in first half direction is increasing");
601                 }
602                 else if (prevRatio > 0.5)
603                 {
604                     assertTrue(direction < prevDirection, "in second half direction is decreasing");
605                 }
606                 prevDirection = direction;
607                 prevRatio = ratio;
608             }
609         }
610     }
611 
612     /**
613      * Test that the contour of a constructed lane covers the expected area. Tests are only performed for straight lanes, but
614      * the orientation of the link and the offset of the lane from the link is varied in many ways.
615      * @throws Exception when something goes wrong (should not happen)
616      */
617     @Test
618     public final void contourTest() throws Exception
619     {
620         final int[] startPositions = {0, 1, -1, 20, -20};
621         final double[] angles = {0, Math.PI * 0.01, Math.PI / 3, Math.PI / 2, Math.PI * 2 / 3, Math.PI * 0.99, Math.PI,
622                 Math.PI * 1.01, Math.PI * 4 / 3, Math.PI * 3 / 2, Math.PI * 1.99, Math.PI * 2, Math.PI * (-0.2)};
623         int laneNum = 0;
624         for (int xStart : startPositions)
625         {
626             for (int yStart : startPositions)
627             {
628                 for (double angle : angles)
629                 {
630                     OtsSimulatorInterface simulator = new OtsSimulator("LaneTest");
631                     Model model = new Model(simulator);
632                     simulator.initialize(Time.ZERO, Duration.ZERO, new Duration(3600.0, DurationUnit.SECOND), model,
633                             HistoryManagerDevs.noHistory(simulator));
634                     RoadNetwork network = new RoadNetwork("contour test network", simulator);
635                     LaneType laneType = DefaultsRoadNl.TWO_WAY_LANE;
636                     laneType.addCompatibleGtuType(DefaultsNl.VEHICLE);
637                     Map<GtuType, Speed> speedMap = new LinkedHashMap<>();
638                     speedMap.put(DefaultsNl.VEHICLE, new Speed(50, KM_PER_HOUR));
639                     Node start = new Node(network, "start", new Point2d(xStart, yStart), Direction.instantiateSI(angle));
640                     double linkLength = 1000;
641                     double xEnd = xStart + linkLength * Math.cos(angle);
642                     double yEnd = yStart + linkLength * Math.sin(angle);
643                     Node end = new Node(network, "end", new Point2d(xEnd, yEnd), Direction.instantiateSI(angle));
644                     Point2d[] coordinates = new Point2d[2];
645                     coordinates[0] = start.getPoint();
646                     coordinates[1] = end.getPoint();
647                     OtsLine2d line = new OtsLine2d(coordinates);
648                     CrossSectionLink link = new CrossSectionLink(network, "A to B", start, end, DefaultsNl.ROAD, line, null,
649                             LaneKeepingPolicy.KEEPRIGHT);
650                     final int[] lateralOffsets = {-10, -3, -1, 0, 1, 3, 10};
651                     for (int startLateralOffset : lateralOffsets)
652                     {
653                         for (int endLateralOffset : lateralOffsets)
654                         {
655                             int startWidth = 4; // This one is not varied
656                             for (int endWidth : new int[] {2, 4, 6})
657                             {
658                                 // Now we can construct a Lane
659                                 // FIXME what overtaking conditions do we want to test in this unit test?
660                                 Lane lane = LaneGeometryUtil.createStraightLane(link, "lane." + ++laneNum,
661                                         new Length(startLateralOffset, METER), new Length(endLateralOffset, METER),
662                                         new Length(startWidth, METER), new Length(endWidth, METER), laneType, speedMap);
663                                 // Verify a couple of points that should be inside the contour of the Lane
664                                 // One meter along the lane design line
665                                 checkInside(lane, 1, startLateralOffset, true);
666                                 // One meter before the end along the lane design line
667                                 checkInside(lane, link.getLength().getSI() - 1, endLateralOffset, true);
668                                 // One meter before the start of the lane along the lane design line
669                                 checkInside(lane, -1, startLateralOffset, false);
670                                 // One meter beyond the end of the lane along the lane design line
671                                 checkInside(lane, link.getLength().getSI() + 1, endLateralOffset, false);
672                                 // One meter along the lane design line, left outside the lane
673                                 checkInside(lane, 1, startLateralOffset - startWidth / 2 - 1, false);
674                                 // One meter along the lane design line, right outside the lane
675                                 checkInside(lane, 1, startLateralOffset + startWidth / 2 + 1, false);
676                                 // One meter before the end, left outside the lane
677                                 checkInside(lane, link.getLength().getSI() - 1, endLateralOffset - endWidth / 2 - 1, false);
678                                 // One meter before the end, right outside the lane
679                                 checkInside(lane, link.getLength().getSI() - 1, endLateralOffset + endWidth / 2 + 1, false);
680                                 // Check the result of getBounds.
681                                 OrientedPoint2d l = lane.getLocation();
682                                 // System.out.println("bb is " + bb);
683                                 // System.out.println("l is " + l.x + "," + l.y + "," + l.z);
684                                 // System.out.println("start is at " + start.getX() + ", " + start.getY());
685                                 // System.out.println(" end is at " + end.getX() + ", " + end.getY());
686                                 Point2D.Double[] cornerPoints = new Point2D.Double[4];
687                                 cornerPoints[0] =
688                                         new Point2D.Double(xStart - (startLateralOffset + startWidth / 2) * Math.sin(angle),
689                                                 yStart + (startLateralOffset + startWidth / 2) * Math.cos(angle));
690                                 cornerPoints[1] =
691                                         new Point2D.Double(xStart - (startLateralOffset - startWidth / 2) * Math.sin(angle),
692                                                 yStart + (startLateralOffset - startWidth / 2) * Math.cos(angle));
693                                 cornerPoints[2] = new Point2D.Double(xEnd - (endLateralOffset + endWidth / 2) * Math.sin(angle),
694                                         yEnd + (endLateralOffset + endWidth / 2) * Math.cos(angle));
695                                 cornerPoints[3] = new Point2D.Double(xEnd - (endLateralOffset - endWidth / 2) * Math.sin(angle),
696                                         yEnd + (endLateralOffset - endWidth / 2) * Math.cos(angle));
697                                 // for (int i = 0; i < cornerPoints.length; i++)
698                                 // {
699                                 // System.out.println("p" + i + ": " + cornerPoints[i].x + "," + cornerPoints[i].y);
700                                 // }
701                                 double minX = cornerPoints[0].getX();
702                                 double maxX = cornerPoints[0].getX();
703                                 double minY = cornerPoints[0].getY();
704                                 double maxY = cornerPoints[0].getY();
705                                 for (int i = 1; i < cornerPoints.length; i++)
706                                 {
707                                     Point2D.Double p = cornerPoints[i];
708                                     minX = Math.min(minX, p.getX());
709                                     minY = Math.min(minY, p.getY());
710                                     maxX = Math.max(maxX, p.getX());
711                                     maxY = Math.max(maxY, p.getY());
712                                 }
713                                 // System.out.println(" my bbox is " + minX + "," + minY + " - " + maxX + "," + maxY);
714                                 // System.out.println("the bbox is " + (bbLow.x + l.x) + "," + (bbLow.y + l.y) + " - "
715                                 // + (bbHigh.x + l.x) + "," + (bbHigh.y + l.y));
716                                 Bounds<?, ?, ?> bb = lane.getContour().getBounds();
717                                 double boundsMinX = bb.getMinX();
718                                 double boundsMinY = bb.getMinY();
719                                 double boundsMaxX = bb.getMaxX();
720                                 double boundsMaxY = bb.getMaxY();
721                                 assertEquals(minX, boundsMinX, 0.1, "low x boundary");
722                                 assertEquals(minY, boundsMinY, 0.1, "low y boundary");
723                                 assertEquals(maxX, boundsMaxX, 0.1, "high x boundary");
724                                 assertEquals(maxY, boundsMaxY, 0.1, "high y boundary");
725                             }
726                         }
727                     }
728                 }
729             }
730         }
731     }
732 
733     /**
734      * Verify that a point at specified distance along and across from the design line of the parent Link of a Lane is inside
735      * c.q. outside the contour of a Lane. The test uses an implementation that is as independent as possible of the Geometry
736      * class methods.
737      * @param lane the lane
738      * @param longitudinal the longitudinal position along the design line of the parent Link of the Lane. This design line is
739      *            expected to be straight and the longitudinal position may be negative (indicating a point before the start of
740      *            the Link) and it may exceed the length of the Link (indicating a point beyond the end of the Link)
741      * @param lateral the lateral offset from the design line of the link (positive is left, negative is right)
742      * @param expectedResult true if the calling method expects the point to be within the contour of the Lane, false if the
743      *            calling method expects the point to be outside the contour of the Lane
744      */
745     private void checkInside(final Lane lane, final double longitudinal, final double lateral, final boolean expectedResult)
746     {
747         CrossSectionLink parentLink = lane.getLink();
748         Node start = parentLink.getStartNode();
749         Node end = parentLink.getEndNode();
750         double startX = start.getPoint().x;
751         double startY = start.getPoint().y;
752         double endX = end.getPoint().x;
753         double endY = end.getPoint().y;
754         double length = Math.sqrt((endX - startX) * (endX - startX) + (endY - startY) * (endY - startY));
755         double ratio = longitudinal / length;
756         double designLineX = startX + (endX - startX) * ratio;
757         double designLineY = startY + (endY - startY) * ratio;
758         double lateralAngle = Math.atan2(endY - startY, endX - startX) + Math.PI / 2;
759         double px = designLineX + lateral * Math.cos(lateralAngle);
760         double py = designLineY + lateral * Math.sin(lateralAngle);
761         Polygon2d contour = lane.getContour();
762         // GeometryFactory factory = new GeometryFactory();
763         // Geometry p = factory.createPoint(new Coordinate(px, py));
764         Point2d p = new Point2d(px, py);
765         // CrossSectionElement.printCoordinates("contour: ", contour);
766         // System.out.println("p: " + p);
767         boolean result2 = contour.contains(p);
768         boolean result = contains(contour, p);
769         if (expectedResult)
770         {
771             assertTrue(result, "Point at " + longitudinal + " along and " + lateral + " lateral is within lane");
772         }
773         else
774         {
775             assertFalse(result, "Point at " + longitudinal + " along and " + lateral + " lateral is outside lane");
776         }
777     }
778 
779     /*
780      * TODO: remove this method Pending djutils issue #15, this method should be removed. Then, 'result2' above should become
781      * 'result' instead.
782      */
783     private boolean contains(final Polygon2d contour, final Point2d p)
784     {
785         if (!contour.getBounds().contains(p.x, p.y))
786         {
787             return false;
788         }
789         int counter = 0;
790         // Unlike Paul Bourke, we initialize prevPoint to the last point of the polygon (so we never have to wrap around)
791         double prevPointX = contour.getX(contour.size() - 1);
792         double prevPointY = contour.getY(contour.size() - 1);
793         for (int i = 0; i < contour.size(); i++)
794         {
795             double curPointX = contour.getX(i);
796             double curPointY = contour.getY(i);
797             // Combined 4 if statements into one; I trust that the java compiler will short-circuit this nicely
798             if (p.y >= Math.min(prevPointY, curPointY) && p.y < Math.max(prevPointY, curPointY)
799                     && p.x <= Math.max(prevPointX, curPointX) && prevPointY != curPointY)
800             {
801                 double xIntersection = (p.y - prevPointY) * (curPointX - prevPointX) / (curPointY - prevPointY) + prevPointX;
802                 if (prevPointX == curPointX || p.x <= xIntersection)
803                 {
804                     counter++;
805                 }
806             }
807             prevPointX = curPointX;
808             prevPointY = curPointY;
809         }
810         return counter % 2 != 0;
811     }
812 
813     /** The helper model. */
814     protected static class Model extends AbstractOtsModel
815     {
816         /** */
817         private static final long serialVersionUID = 20141027L;
818 
819         /**
820          * @param simulator the simulator to use
821          */
822         public Model(final OtsSimulatorInterface simulator)
823         {
824             super(simulator);
825         }
826 
827         @Override
828         public final void constructModel() throws SimRuntimeException
829         {
830             //
831         }
832 
833         @Override
834         public final RoadNetwork getNetwork()
835         {
836             return null;
837         }
838     }
839 
840 }