TrajectoryPlot.java

  1. package org.opentrafficsim.draw.graphs;

  2. import java.awt.BasicStroke;
  3. import java.awt.Color;
  4. import java.awt.Paint;
  5. import java.awt.Shape;
  6. import java.awt.Stroke;
  7. import java.awt.geom.CubicCurve2D;
  8. import java.awt.geom.Line2D;
  9. import java.util.ArrayList;
  10. import java.util.LinkedHashMap;
  11. import java.util.List;
  12. import java.util.Map;

  13. import org.djunits.value.vdouble.scalar.Duration;
  14. import org.djunits.value.vdouble.scalar.Length;
  15. import org.djunits.value.vdouble.scalar.Time;
  16. import org.djutils.exceptions.Try;
  17. import org.jfree.chart.JFreeChart;
  18. import org.jfree.chart.LegendItem;
  19. import org.jfree.chart.LegendItemCollection;
  20. import org.jfree.chart.axis.NumberAxis;
  21. import org.jfree.chart.entity.EntityCollection;
  22. import org.jfree.chart.entity.XYItemEntity;
  23. import org.jfree.chart.labels.XYToolTipGenerator;
  24. import org.jfree.chart.plot.XYPlot;
  25. import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
  26. import org.jfree.data.DomainOrder;
  27. import org.jfree.data.xy.XYDataset;
  28. import org.opentrafficsim.core.animation.gtu.colorer.IDGTUColorer;
  29. import org.opentrafficsim.core.dsol.OTSSimulatorInterface;
  30. import org.opentrafficsim.draw.core.BoundsPaintScale;
  31. import org.opentrafficsim.draw.graphs.GraphPath.Section;
  32. import org.opentrafficsim.kpi.sampling.KpiLaneDirection;
  33. import org.opentrafficsim.kpi.sampling.Sampler;
  34. import org.opentrafficsim.kpi.sampling.SamplingException;
  35. import org.opentrafficsim.kpi.sampling.Trajectory;
  36. import org.opentrafficsim.kpi.sampling.TrajectoryGroup;

  37. /**
  38.  * Plot of trajectories along a path.
  39.  * <p>
  40.  * Copyright (c) 2013-2020 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
  41.  * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
  42.  * <p>
  43.  * @version $Revision$, $LastChangedDate$, by $Author$, initial version 13 okt. 2018 <br>
  44.  * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
  45.  * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
  46.  * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
  47.  */
  48. public class TrajectoryPlot extends AbstractSamplerPlot implements XYDataset
  49. {
  50.     /** Single shape to provide due to non-null requirement, but actually not used. */
  51.     private static final Shape NO_SHAPE = new Line2D.Float(0, 0, 0, 0);

  52.     /** Color map. */
  53.     private static final Color[] COLORMAP;

  54.     /** Strokes. */
  55.     private static final BasicStroke[] STROKES;

  56.     /** Shape for the legend entries to draw the line over. */
  57.     private static final Shape LEGEND_LINE = new CubicCurve2D.Float(-20, 7, -10, -7, 0, 7, 20, -7);

  58.     /** Updater for update times. */
  59.     private final GraphUpdater<Time> graphUpdater;

  60.     /** Counter of the number of trajectories imported per lane. */
  61.     private final Map<KpiLaneDirection, Integer> knownTrajectories = new LinkedHashMap<>();

  62.     /** Per lane, mapping from series rank number to trajectory. */
  63.     private List<List<OffsetTrajectory>> curves = new ArrayList<>();

  64.     /** Stroke per series. */
  65.     private List<List<Stroke>> strokes = new ArrayList<>();

  66.     /** Number of curves per lane. This may be less than the length of {@code List<OffsetTrajectory>} due to concurrency. */
  67.     private List<Integer> curvesPerLane = new ArrayList<>();

  68.     /** Legend to change text color to indicate visibility. */
  69.     private LegendItemCollection legend;

  70.     /** Whether each lane is visible or not. */
  71.     private final List<Boolean> laneVisible = new ArrayList<>();

  72.     static
  73.     {
  74.         Color[] c = BoundsPaintScale.hue(6);
  75.         COLORMAP = new Color[] {c[0], c[4], c[2], c[1], c[3], c[5]};
  76.         float lw = 0.4f;
  77.         STROKES = new BasicStroke[] {new BasicStroke(lw, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER, 10.0f, null, 0.0f),
  78.                 new BasicStroke(lw, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER, 1.0f, new float[] {13f, 4f}, 0.0f),
  79.                 new BasicStroke(lw, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER, 1.0f, new float[] {11f, 3f, 2f, 3f}, 0.0f)};
  80.     }

  81.     /**
  82.      * Constructor.
  83.      * @param caption String; caption
  84.      * @param updateInterval Duration; regular update interval (simulation time)
  85.      * @param simulator OTSSimulatorInterface; simulator
  86.      * @param sampler Sampler&lt;?&gt;; road sampler
  87.      * @param path GraphPath&lt;KpiLaneDirection&gt;; path
  88.      */
  89.     public TrajectoryPlot(final String caption, final Duration updateInterval, final OTSSimulatorInterface simulator,
  90.             final Sampler<?> sampler, final GraphPath<KpiLaneDirection> path)
  91.     {
  92.         super(caption, updateInterval, simulator, sampler, path, Duration.ZERO);
  93.         for (int i = 0; i < path.getNumberOfSeries(); i++)
  94.         {
  95.             this.curves.add(new ArrayList<>());
  96.             this.strokes.add(new ArrayList<>());
  97.             this.curvesPerLane.add(0);
  98.             this.laneVisible.add(true);
  99.         }
  100.         setChart(createChart());

  101.         // setup updater to do the actual work in another thread
  102.         this.graphUpdater = new GraphUpdater<>("Trajectories worker", Thread.currentThread(), (t) ->
  103.         {
  104.             for (Section<KpiLaneDirection> section : path.getSections())
  105.             {
  106.                 Length startDistance = path.getStartDistance(section);
  107.                 for (int i = 0; i < path.getNumberOfSeries(); i++)
  108.                 {
  109.                     KpiLaneDirection lane = section.getSource(i);
  110.                     TrajectoryGroup<?> trajectoryGroup = getSampler().getTrajectoryGroup(lane);
  111.                     int from = this.knownTrajectories.getOrDefault(lane, 0);
  112.                     int to = trajectoryGroup.size();
  113.                     double scaleFactor = section.getLength().si / lane.getLaneData().getLength().si;
  114.                     for (Trajectory<?> trajectory : trajectoryGroup.getTrajectories().subList(from, to))
  115.                     {
  116.                         if (getPath().getNumberOfSeries() > 1)
  117.                         {
  118.                             // assign a stroke with random offset, otherwise it will look artificial
  119.                             BasicStroke stroke = STROKES[i % STROKES.length];
  120.                             if (stroke.getDashArray() != null)
  121.                             {
  122.                                 float dashLength = 0.0f;
  123.                                 for (float d : stroke.getDashArray())
  124.                                 {
  125.                                     dashLength += d;
  126.                                 }
  127.                                 stroke = new BasicStroke(stroke.getLineWidth(), stroke.getEndCap(), stroke.getLineJoin(),
  128.                                         stroke.getMiterLimit(), stroke.getDashArray(), (float) (Math.random() * dashLength));
  129.                             }
  130.                             this.strokes.get(i).add(stroke);
  131.                         }
  132.                         this.curves.get(i).add(
  133.                                 new OffsetTrajectory(trajectory, startDistance, scaleFactor, lane.getLaneData().getLength()));
  134.                     }
  135.                     this.knownTrajectories.put(lane, to);
  136.                 }
  137.             }
  138.         });
  139.     }

  140.     /**
  141.      * Create a chart.
  142.      * @return JFreeChart; chart
  143.      */
  144.     private JFreeChart createChart()
  145.     {
  146.         NumberAxis xAxis = new NumberAxis("Time [s] \u2192");
  147.         NumberAxis yAxis = new NumberAxis("Distance [m] \u2192");
  148.         XYLineAndShapeRendererID renderer = new XYLineAndShapeRendererID();
  149.         XYPlot plot = new XYPlot(this, xAxis, yAxis, renderer);
  150.         boolean showLegend;
  151.         if (getPath().getNumberOfSeries() < 2)
  152.         {
  153.             plot.setFixedLegendItems(null);
  154.             showLegend = false;
  155.         }
  156.         else
  157.         {
  158.             this.legend = new LegendItemCollection();
  159.             for (int i = 0; i < getPath().getNumberOfSeries(); i++)
  160.             {
  161.                 LegendItem li = new LegendItem(getPath().getName(i));
  162.                 li.setSeriesKey(i); // lane series, not curve series
  163.                 li.setShape(STROKES[i & STROKES.length].createStrokedShape(LEGEND_LINE));
  164.                 li.setFillPaint(COLORMAP[i % COLORMAP.length]);
  165.                 this.legend.add(li);
  166.             }
  167.             plot.setFixedLegendItems(this.legend);
  168.             showLegend = true;
  169.         }
  170.         return new JFreeChart(getCaption(), JFreeChart.DEFAULT_TITLE_FONT, plot, showLegend);
  171.     }

  172.     /** {@inheritDoc} */
  173.     @Override
  174.     public GraphType getGraphType()
  175.     {
  176.         return GraphType.TRAJECTORY;
  177.     }

  178.     /** {@inheritDoc} */
  179.     @Override
  180.     public String getStatusLabel(final double domainValue, final double rangeValue)
  181.     {
  182.         return String.format("time %.0fs, distance %.0fm", domainValue, rangeValue);
  183.     }

  184.     /** {@inheritDoc} */
  185.     @Override
  186.     protected void increaseTime(final Time time)
  187.     {
  188.         if (this.graphUpdater != null) // null during construction
  189.         {
  190.             this.graphUpdater.offer(time);
  191.         }
  192.     }

  193.     /** {@inheritDoc} */
  194.     @Override
  195.     public int getSeriesCount()
  196.     {
  197.         int n = 0;
  198.         for (int i = 0; i < this.curves.size(); i++)
  199.         {
  200.             List<OffsetTrajectory> list = this.curves.get(i);
  201.             int m = list.size();
  202.             this.curvesPerLane.set(i, m);
  203.             n += m;
  204.         }
  205.         return n;
  206.     }

  207.     /** {@inheritDoc} */
  208.     @Override
  209.     public Comparable<Integer> getSeriesKey(final int series)
  210.     {
  211.         return series;
  212.     }

  213.     /** {@inheritDoc} */
  214.     @SuppressWarnings("rawtypes")
  215.     @Override
  216.     public int indexOf(final Comparable seriesKey)
  217.     {
  218.         return 0;
  219.     }

  220.     /** {@inheritDoc} */
  221.     @Override
  222.     public DomainOrder getDomainOrder()
  223.     {
  224.         return DomainOrder.ASCENDING;
  225.     }

  226.     /** {@inheritDoc} */
  227.     @Override
  228.     public int getItemCount(final int series)
  229.     {
  230.         OffsetTrajectory trajectory = getTrajectory(series);
  231.         return trajectory == null ? 0 : trajectory.size();
  232.     }

  233.     /** {@inheritDoc} */
  234.     @Override
  235.     public Number getX(final int series, final int item)
  236.     {
  237.         return getXValue(series, item);
  238.     }

  239.     /** {@inheritDoc} */
  240.     @Override
  241.     public double getXValue(final int series, final int item)
  242.     {
  243.         return getTrajectory(series).getT(item);
  244.     }

  245.     /** {@inheritDoc} */
  246.     @Override
  247.     public Number getY(final int series, final int item)
  248.     {
  249.         return getYValue(series, item);
  250.     }

  251.     /** {@inheritDoc} */
  252.     @Override
  253.     public double getYValue(final int series, final int item)
  254.     {
  255.         return getTrajectory(series).getX(item);
  256.     }

  257.     /**
  258.      * Get the trajectory of the series number.
  259.      * @param series int; series
  260.      * @return OffsetTrajectory; trajectory of the series number
  261.      */
  262.     private OffsetTrajectory getTrajectory(final int series)
  263.     {
  264.         int[] n = getLaneAndSeriesNumber(series);
  265.         return this.curves.get(n[0]).get(n[1]);
  266.     }

  267.     /**
  268.      * Returns the lane number, and series number within the lane data.
  269.      * @param series int; overall series number
  270.      * @return int[]; lane number, and series number within the lane data
  271.      */
  272.     private int[] getLaneAndSeriesNumber(final int series)
  273.     {
  274.         int n = series;
  275.         for (int i = 0; i < this.curves.size(); i++)
  276.         {
  277.             int m = this.curvesPerLane.get(i);
  278.             if (n < m)
  279.             {
  280.                 return new int[] {i, n};
  281.             }
  282.             n -= m;
  283.         }
  284.         throw new RuntimeException("Discrepancy between series number and available data.");
  285.     }

  286.     /**
  287.      * Extension of a line renderer to select a color based on GTU ID, and to overrule an unused shape to save memory.
  288.      * <p>
  289.      * Copyright (c) 2013-2020 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
  290.      * <br>
  291.      * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
  292.      * <p>
  293.      * @version $Revision$, $LastChangedDate$, by $Author$, initial version 14 okt. 2018 <br>
  294.      * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
  295.      * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
  296.      * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
  297.      */
  298.     private final class XYLineAndShapeRendererID extends XYLineAndShapeRenderer
  299.     {
  300.         /** */
  301.         private static final long serialVersionUID = 20181014L;

  302.         /**
  303.          * Constructor.
  304.          */
  305.         XYLineAndShapeRendererID()
  306.         {
  307.             super(false, true);
  308.             setDefaultLinesVisible(true);
  309.             setDefaultShapesVisible(false);
  310.             setDrawSeriesLineAsPath(true);
  311.         }

  312.         /** {@inheritDoc} */
  313.         @SuppressWarnings("synthetic-access")
  314.         @Override
  315.         public boolean isSeriesVisible(final int series)
  316.         {
  317.             int[] n = getLaneAndSeriesNumber(series);
  318.             return TrajectoryPlot.this.laneVisible.get(n[0]);
  319.         }

  320.         /** {@inheritDoc} */
  321.         @SuppressWarnings("synthetic-access")
  322.         @Override
  323.         public Stroke getSeriesStroke(final int series)
  324.         {
  325.             if (TrajectoryPlot.this.curves.size() == 1)
  326.             {
  327.                 return STROKES[0];
  328.             }
  329.             int[] n = getLaneAndSeriesNumber(series);
  330.             return TrajectoryPlot.this.strokes.get(n[0]).get(n[1]);
  331.         }

  332.         /** {@inheritDoc} */
  333.         @SuppressWarnings("synthetic-access")
  334.         @Override
  335.         public Paint getSeriesPaint(final int series)
  336.         {
  337.             if (TrajectoryPlot.this.curves.size() == 1)
  338.             {
  339.                 String gtuId = getTrajectory(series).getGtuId();
  340.                 for (int pos = gtuId.length(); --pos >= 0;)
  341.                 {
  342.                     Character c = gtuId.charAt(pos);
  343.                     if (Character.isDigit(c))
  344.                     {
  345.                         return IDGTUColorer.LEGEND.get(c - '0').getColor();
  346.                     }
  347.                 }
  348.             }
  349.             int[] n = getLaneAndSeriesNumber(series);
  350.             return COLORMAP[n[0] % COLORMAP.length];
  351.         }

  352.         /**
  353.          * {@inheritDoc} Largely based on the super implementation, but returns a dummy shape for markers to save memory and as
  354.          * markers are not used.
  355.          */
  356.         @SuppressWarnings("synthetic-access")
  357.         @Override
  358.         protected void addEntity(final EntityCollection entities, final Shape hotspot, final XYDataset dataset,
  359.                 final int series, final int item, final double entityX, final double entityY)
  360.         {

  361.             if (!getItemCreateEntity(series, item))
  362.             {
  363.                 return;
  364.             }

  365.             // if not hotspot is provided, we create a default based on the
  366.             // provided data coordinates (which are already in Java2D space)
  367.             Shape hotspot2 = hotspot == null ? NO_SHAPE : hotspot;
  368.             String tip = null;
  369.             XYToolTipGenerator generator = getToolTipGenerator(series, item);
  370.             if (generator != null)
  371.             {
  372.                 tip = generator.generateToolTip(dataset, series, item);
  373.             }
  374.             String url = null;
  375.             if (getURLGenerator() != null)
  376.             {
  377.                 url = getURLGenerator().generateURL(dataset, series, item);
  378.             }
  379.             XYItemEntity entity = new XYItemEntity(hotspot2, dataset, series, item, tip, url);
  380.             entities.add(entity);
  381.         }

  382.         /** {@inheritDoc} */
  383.         @Override
  384.         public String toString()
  385.         {
  386.             return "XYLineAndShapeRendererID []";
  387.         }

  388.     }

  389.     /**
  390.      * Class containing a trajectory with an offset. Takes care of bits that are before and beyond the lane without affecting
  391.      * the trajectory itself.
  392.      * <p>
  393.      * Copyright (c) 2013-2020 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
  394.      * <br>
  395.      * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
  396.      * <p>
  397.      * @version $Revision$, $LastChangedDate$, by $Author$, initial version 14 okt. 2018 <br>
  398.      * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
  399.      * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
  400.      * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
  401.      */
  402.     private class OffsetTrajectory
  403.     {
  404.         /** The trajectory. */
  405.         private final Trajectory<?> trajectory;

  406.         /** The offset. */
  407.         private final double offset;

  408.         /** Scale factor for space dimension. */
  409.         private final double scaleFactor;

  410.         /** First index of the trajectory to include, possibly cutting some measurements before the lane. */
  411.         private int first;

  412.         /** Size of the trajectory to consider starting at first, possibly cutting some measurements beyond the lane. */
  413.         private int size;

  414.         /** Length of the lane to determine {@code size}. */
  415.         private final Length laneLength;

  416.         /**
  417.          * Construct a new TrajectoryAndLengthOffset object.
  418.          * @param trajectory Trajectory&lt;?&gt;; the trajectory
  419.          * @param offset Length; the length from the beginning of the sampled path to the start of the lane to which the
  420.          *            trajectory belongs
  421.          * @param scaleFactor double; scale factor for space dimension
  422.          * @param laneLength Length; length of the lane
  423.          */
  424.         OffsetTrajectory(final Trajectory<?> trajectory, final Length offset, final double scaleFactor, final Length laneLength)
  425.         {
  426.             this.trajectory = trajectory;
  427.             this.offset = offset.si;
  428.             this.scaleFactor = scaleFactor;
  429.             this.laneLength = laneLength;
  430.         }

  431.         /**
  432.          * Returns the number of measurements in the trajectory.
  433.          * @return int; number of measurements in the trajectory
  434.          */
  435.         public final int size()
  436.         {
  437.             // as trajectories grow, this calculation needs to be done on each request
  438.             try
  439.             {
  440.                 /*
  441.                  * Note on overlap:
  442.                  *
  443.                  * Suppose a GTU crosses a lane boundary producing the following events, where distance e->| is the front, and
  444.                  * |->l is the tail, relative to the reference point of the GTU.
  445.                  * @formatter:off
  446.                  * -------------------------------------------  o) regular move event
  447.                  *  o     e   o         o |  l    o         o   e) lane enter event on next lane
  448.                  * -------------------------------------------  l) lane leave event on previous lane
  449.                  *  o         o         o   (l)                 measurements on previous lane
  450.                  *       (e) (o)       (o)        o         o   measurements on next lane
  451.                  * @formatter:on
  452.                  * Trajectories of a particular GTU are not explicitly tied together. Not only would this involve quite some
  453.                  * work, it is also impossible to distinguish a lane change near the start or end of a lane, from moving
  454.                  * longitudinally on to the next lane. The basic idea to minimize overlap is to remove all positions on the
  455.                  * previous lane beyond the lane length, and all negative positions on the next lane, i.e. all between ( ). This
  456.                  * would however create a gap at the lane boundary '|'. Allowing one event beyond the lane length may still
  457.                  * result in a gap, l->o in this case. Allowing one event before the lane would work in this case, but 'e' could
  458.                  * also fall between an 'o' and '|'. At one trajectory it is thus not known whether the other trajectory
  459.                  * continues from, or is continued from, the extra point. Hence we require an extra point before the lane and
  460.                  * one beyond the lane to assure there is no gap. The resulting overlap can be as large as a move, but this is
  461.                  * better than occasional gaps.
  462.                  */
  463.                 int f = 0;
  464.                 while (f < this.trajectory.size() - 1 && this.trajectory.getX(f + 1) < 0.0)
  465.                 {
  466.                     f++;
  467.                 }
  468.                 this.first = f;
  469.                 int s = this.trajectory.size() - 1;
  470.                 while (s > 1 && this.trajectory.getX(s - 1) > this.laneLength.si)
  471.                 {
  472.                     s--;
  473.                 }
  474.                 this.size = s - f + 1;
  475.             }
  476.             catch (SamplingException exception)
  477.             {
  478.                 throw new RuntimeException("Unexpected exception while obtaining location value from trajectory for plotting.",
  479.                         exception);
  480.             }
  481.             return this.size;
  482.         }

  483.         /**
  484.          * Returns the location, including offset, of an item.
  485.          * @param item int; item (sample) number
  486.          * @return double; location, including offset, of an item
  487.          */
  488.         public final double getX(final int item)
  489.         {
  490.             return Try.assign(() -> this.offset + this.trajectory.getX(this.first + item) * this.scaleFactor,
  491.                     "Unexpected exception while obtaining location value from trajectory for plotting.");
  492.         }

  493.         /**
  494.          * Returns the time of an item.
  495.          * @param item int; item (sample) number
  496.          * @return double; time of an item
  497.          */
  498.         public final double getT(final int item)
  499.         {
  500.             return Try.assign(() -> (double) this.trajectory.getT(this.first + item),
  501.                     "Unexpected exception while obtaining time value from trajectory for plotting.");
  502.         }

  503.         /**
  504.          * Returns the ID of the GTU of this trajectory.
  505.          * @return String; the ID of the GTU of this trajectory
  506.          */
  507.         public final String getGtuId()
  508.         {
  509.             return this.trajectory.getGtuId();
  510.         }

  511.         /** {@inheritDoc} */
  512.         @Override
  513.         public final String toString()
  514.         {
  515.             return "OffsetTrajectory [trajectory=" + this.trajectory + ", offset=" + this.offset + "]";
  516.         }

  517.     }

  518.     /** {@inheritDoc} */
  519.     @Override
  520.     public String toString()
  521.     {
  522.         return "TrajectoryPlot [graphUpdater=" + this.graphUpdater + ", knownTrajectories=" + this.knownTrajectories
  523.                 + ", curves=" + this.curves + ", strokes=" + this.strokes + ", curvesPerLane=" + this.curvesPerLane
  524.                 + ", legend=" + this.legend + ", laneVisible=" + this.laneVisible + "]";
  525.     }

  526.     /**
  527.      * Retrieve the legend.
  528.      * @return LegendItemCollection; the legend
  529.      */
  530.     public LegendItemCollection getLegend()
  531.     {
  532.         return legend;
  533.     }

  534.     /**
  535.      * Retrieve the lane visibility flags.
  536.      * @return List&lt;Boolean&gt;; the lane visibility flags
  537.      */
  538.     public List<Boolean> getLaneVisible()
  539.     {
  540.         return laneVisible;
  541.     }

  542. }