1 package org.opentrafficsim.draw.graphs;
2
3 import java.util.ArrayList;
4 import java.util.Arrays;
5 import java.util.LinkedHashMap;
6 import java.util.LinkedHashSet;
7 import java.util.List;
8 import java.util.Map;
9 import java.util.Set;
10
11 import org.djunits.unit.SpeedUnit;
12 import org.djunits.value.vdouble.scalar.Duration;
13 import org.djunits.value.vdouble.scalar.Length;
14 import org.djunits.value.vdouble.scalar.Speed;
15 import org.djunits.value.vdouble.scalar.Time;
16 import org.djutils.exceptions.Throw;
17 import org.djutils.logger.CategoryLogger;
18 import org.opentrafficsim.core.egtf.Converter;
19 import org.opentrafficsim.core.egtf.DataSource;
20 import org.opentrafficsim.core.egtf.DataStream;
21 import org.opentrafficsim.core.egtf.EGTF;
22 import org.opentrafficsim.core.egtf.EgtfEvent;
23 import org.opentrafficsim.core.egtf.EgtfListener;
24 import org.opentrafficsim.core.egtf.Filter;
25 import org.opentrafficsim.core.egtf.Quantity;
26 import org.opentrafficsim.core.egtf.typed.TypedQuantity;
27 import org.opentrafficsim.draw.graphs.GraphPath.Section;
28 import org.opentrafficsim.kpi.interfaces.GtuDataInterface;
29 import org.opentrafficsim.kpi.sampling.KpiLaneDirection;
30 import org.opentrafficsim.kpi.sampling.Sampler;
31 import org.opentrafficsim.kpi.sampling.Trajectory;
32 import org.opentrafficsim.kpi.sampling.Trajectory.SpaceTimeView;
33 import org.opentrafficsim.kpi.sampling.TrajectoryGroup;
34
35 /**
36 * Class that contains data for contour plots. One data source can be shared between contour plots, in which case the
37 * granularity, path, sampler, update interval, and whether the data is smoothed (EGTF) are equal between the plots.
38 * <p>
39 * By default the source contains traveled time and traveled distance per cell.
40 * <p>
41 * Copyright (c) 2013-2020 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
42 * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
43 * <p>
44 * @version $Revision$, $LastChangedDate$, by $Author$, initial version 5 okt. 2018 <br>
45 * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
46 * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
47 * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
48 * @param <G> gtu type data
49 */
50 public class ContourDataSource<G extends GtuDataInterface>
51 {
52
53 // *************************
54 // *** GLOBAL PROPERTIES ***
55 // *************************
56
57 /** Space granularity values. */
58 protected static final double[] DEFAULT_SPACE_GRANULARITIES = {10, 20, 50, 100, 200, 500, 1000};
59
60 /** Index of the initial space granularity. */
61 protected static final int DEFAULT_SPACE_GRANULARITY_INDEX = 3;
62
63 /** Time granularity values. */
64 protected static final double[] DEFAULT_TIME_GRANULARITIES = {1, 2, 5, 10, 20, 30, 60, 120, 300, 600};
65
66 /** Index of the initial time granularity. */
67 protected static final int DEFAULT_TIME_GRANULARITY_INDEX = 3;
68
69 /** Initial lower bound for the time scale. */
70 protected static final Time DEFAULT_LOWER_TIME_BOUND = Time.ZERO;
71
72 /**
73 * Total kernel size relative to sigma and tau. This factor is determined through -log(1 - p) with p ~= 99%. This means that
74 * the cumulative exponential distribution has 99% at 5 times sigma or tau. Note that due to a coordinate change in the
75 * Adaptive Smoothing Method, the actual cumulative distribution is slightly different. Hence, this is just a heuristic.
76 */
77 private static final int KERNEL_FACTOR = 5;
78
79 /** Spatial kernel size. Larger value may be used when using a large granularity. */
80 private static final Length SIGMA = Length.instantiateSI(300);
81
82 /** Temporal kernel size. Larger value may be used when using a large granularity. */
83 private static final Duration TAU = Duration.instantiateSI(30);
84
85 /** Maximum free flow propagation speed. */
86 private static final Speed MAX_C_FREE = new Speed(80.0, SpeedUnit.KM_PER_HOUR);
87
88 /** Factor on speed limit to determine vc, the flip over speed between congestion and free flow. */
89 private static final double VC_FACRTOR = 0.8;
90
91 /** Congestion propagation speed. */
92 private static final Speed C_CONG = new Speed(-18.0, SpeedUnit.KM_PER_HOUR);
93
94 /** Delta v, speed transition region around threshold. */
95 private static final Speed DELTA_V = new Speed(10.0, SpeedUnit.KM_PER_HOUR);
96
97 // *****************************
98 // *** CONTEXTUAL PROPERTIES ***
99 // *****************************
100
101 /** Sampler. */
102 private final Sampler<G> sampler;
103
104 /** Update interval. */
105 private final Duration updateInterval;
106
107 /** Delay so critical future events have occurred, e.g. GTU's next move's to extend trajectories. */
108 private final Duration delay;
109
110 /** Path. */
111 private final GraphPath<KpiLaneDirection> path;
112
113 /** Space axis. */
114 final Axis spaceAxis;
115
116 /** Time axis. */
117 final Axis timeAxis;
118
119 /** Registered plots. */
120 private Set<AbstractContourPlot<?>> plots = new LinkedHashSet<>();
121
122 // *****************
123 // *** PLOT DATA ***
124 // *****************
125
126 /** Total distance traveled per cell. */
127 private float[][] distance;
128
129 /** Total time traveled per cell. */
130 private float[][] time;
131
132 /** Data of other types. */
133 private final Map<ContourDataType<?, ?>, float[][]> additionalData = new LinkedHashMap<>();
134
135 // ****************************
136 // *** SMOOTHING PROPERTIES ***
137 // ****************************
138
139 /** Free flow propagation speed. */
140 private Speed cFree;
141
142 /** Flip-over speed between congestion and free flow. */
143 private Speed vc;
144
145 /** Smoothing filter. */
146 private EGTF egtf;
147
148 /** Data stream for speed. */
149 private DataStream<Speed> speedStream;
150
151 /** Data stream for travel time. */
152 private DataStream<Duration> travelTimeStream;
153
154 /** Data stream for travel distance. */
155 private DataStream<Length> travelDistanceStream;
156
157 /** Quantity for travel time. */
158 private final Quantity<Duration, double[][]> travelTimeQuantity = new Quantity<>("travel time", Converter.SI);
159
160 /** Quantity for travel distance. */
161 private final Quantity<Length, double[][]> travelDistanceQuantity = new Quantity<>("travel distance", Converter.SI);
162
163 /** Data streams for any additional data. */
164 private Map<ContourDataType<?, ?>, DataStream<?>> additionalStreams = new LinkedHashMap<>();
165
166 // *****************************
167 // *** CONTINUITY PROPERTIES ***
168 // *****************************
169
170 /** Updater for update times. */
171 private final GraphUpdater<Time> graphUpdater;
172
173 /** Whether any command since or during the last update asks for a complete redo. */
174 private boolean redo = true;
175
176 /** Time up to which to determine data. This is a multiple of the update interval, which is now, or recent on a redo. */
177 private Time toTime;
178
179 /** Number of items that are ready. To return NaN values if not ready, and for operations between consecutive updates. */
180 private int readyItems = -1;
181
182 /** Selected space granularity, to be set and taken on the next update. */
183 private Double desiredSpaceGranularity = null;
184
185 /** Selected time granularity, to be set and taken on the next update. */
186 private Double desiredTimeGranularity = null;
187
188 /** Whether to smooth data. */
189 private boolean smooth = false;
190
191 // ********************
192 // *** CONSTRUCTORS ***
193 // ********************
194
195 /**
196 * Constructor using default granularities.
197 * @param sampler Sampler<G>; sampler
198 * @param path GraphPath<KpiLaneDirection>; path
199 */
200 public ContourDataSource(final Sampler<G> sampler, final GraphPath<KpiLaneDirection> path)
201 {
202 this(sampler, Duration.instantiateSI(1.0), path, DEFAULT_SPACE_GRANULARITIES, DEFAULT_SPACE_GRANULARITY_INDEX,
203 DEFAULT_TIME_GRANULARITIES, DEFAULT_TIME_GRANULARITY_INDEX, DEFAULT_LOWER_TIME_BOUND,
204 AbstractPlot.DEFAULT_INITIAL_UPPER_TIME_BOUND);
205 }
206
207 /**
208 * Constructor for non-default input.
209 * @param sampler Sampler<G>; sampler
210 * @param delay Duration; delay so critical future events have occurred, e.g. GTU's next move's to extend trajectories
211 * @param path GraphPath<KpiLaneDirection>; path
212 * @param spaceGranularity double[]; granularity options for space dimension
213 * @param initSpaceIndex int; initial selected space granularity
214 * @param timeGranularity double[]; granularity options for time dimension
215 * @param initTimeIndex int; initial selected time granularity
216 * @param start Time; start time
217 * @param initialEnd Time; initial end time of plots, will be expanded if simulation time exceeds it
218 */
219 @SuppressWarnings("parameternumber")
220 public ContourDataSource(final Sampler<G> sampler, final Duration delay, final GraphPath<KpiLaneDirection> path,
221 final double[] spaceGranularity, final int initSpaceIndex, final double[] timeGranularity, final int initTimeIndex,
222 final Time start, final Time initialEnd)
223 {
224 this.sampler = sampler;
225 this.updateInterval = Duration.instantiateSI(timeGranularity[initTimeIndex]);
226 this.delay = delay;
227 this.path = path;
228 this.spaceAxis = new Axis(0.0, path.getTotalLength().si, spaceGranularity[initSpaceIndex], spaceGranularity);
229 this.timeAxis = new Axis(start.si, initialEnd.si, timeGranularity[initTimeIndex], timeGranularity);
230
231 // get length-weighted mean speed limit from path to determine cFree and Vc for smoothing
232 this.cFree = Speed.min(path.getSpeedLimit(), MAX_C_FREE);
233 this.vc = Speed.min(path.getSpeedLimit().times(VC_FACRTOR), MAX_C_FREE);
234
235 // setup updater to do the actual work in another thread
236 this.graphUpdater = new GraphUpdater<>("Contour Data Source worker", Thread.currentThread(), (t) -> update(t));
237 }
238
239 // ************************************
240 // *** PLOT INTERFACING AND GETTERS ***
241 // ************************************
242
243 /**
244 * Returns the sampler for an {@code AbstractContourPlot} using this {@code ContourDataSource}.
245 * @return Sampler<G>; the sampler
246 */
247 public final Sampler<G> getSampler()
248 {
249 return this.sampler;
250 }
251
252 /**
253 * Returns the update interval for an {@code AbstractContourPlot} using this {@code ContourDataSource}.
254 * @return Duration; update interval
255 */
256 final Duration getUpdateInterval()
257 {
258 return this.updateInterval;
259 }
260
261 /**
262 * Returns the delay for an {@code AbstractContourPlot} using this {@code ContourDataSource}.
263 * @return Duration; delay
264 */
265 final Duration getDelay()
266 {
267 return this.delay;
268 }
269
270 /**
271 * Returns the path for an {@code AbstractContourPlot} using this {@code ContourDataSource}.
272 * @return GraphPath<KpiLaneDirection>; the path
273 */
274 final GraphPath<KpiLaneDirection> getPath()
275 {
276 return this.path;
277 }
278
279 /**
280 * Register a contour plot to this data pool. The contour constructor will do this.
281 * @param contourPlot AbstractContourPlot<?>; contour plot
282 */
283 final void registerContourPlot(final AbstractContourPlot<?> contourPlot)
284 {
285 ContourDataType<?, ?> contourDataType = contourPlot.getContourDataType();
286 if (contourDataType != null)
287 {
288 this.additionalData.put(contourDataType, null);
289 }
290 this.plots.add(contourPlot);
291 }
292
293 /**
294 * Returns the bin count.
295 * @param dimension Dimension; space or time
296 * @return int; bin count
297 */
298 final int getBinCount(final Dimension dimension)
299 {
300 return dimension.getAxis(this).getBinCount();
301 }
302
303 /**
304 * Returns the size of a bin. Usually this is equal to the granularity, except for the last which is likely smaller.
305 * @param dimension Dimension; space or time
306 * @param item int; item number (cell number in contour plot)
307 * @return double; the size of a bin
308 */
309 final synchronized double getBinSize(final Dimension dimension, final int item)
310 {
311 int n = dimension.equals(Dimension.DISTANCE) ? getSpaceBin(item) : getTimeBin(item);
312 double[] ticks = dimension.getAxis(this).getTicks();
313 return ticks[n + 1] - ticks[n];
314 }
315
316 /**
317 * Returns the value on the axis of an item.
318 * @param dimension Dimension; space or time
319 * @param item int; item number (cell number in contour plot)
320 * @return double; the value on the axis of this item
321 */
322 final double getAxisValue(final Dimension dimension, final int item)
323 {
324 if (dimension.equals(Dimension.DISTANCE))
325 {
326 return this.spaceAxis.getBinValue(getSpaceBin(item));
327 }
328 return this.timeAxis.getBinValue(getTimeBin(item));
329 }
330
331 /**
332 * Returns the axis bin number of the given value.
333 * @param dimension Dimension; space or time
334 * @param value double; value
335 * @return int; axis bin number of the given value
336 */
337 final int getAxisBin(final Dimension dimension, final double value)
338 {
339 if (dimension.equals(Dimension.DISTANCE))
340 {
341 return this.spaceAxis.getValueBin(value);
342 }
343 return this.timeAxis.getValueBin(value);
344 }
345
346 /**
347 * Returns the available granularities that a linked plot may use.
348 * @param dimension Dimension; space or time
349 * @return double[]; available granularities that a linked plot may use
350 */
351 @SuppressWarnings("synthetic-access")
352 public final double[] getGranularities(final Dimension dimension)
353 {
354 return dimension.getAxis(this).granularities;
355 }
356
357 /**
358 * Returns the selected granularity that a linked plot should use.
359 * @param dimension Dimension; space or time
360 * @return double; granularity that a linked plot should use
361 */
362 @SuppressWarnings("synthetic-access")
363 public final double getGranularity(final Dimension dimension)
364 {
365 return dimension.getAxis(this).granularity;
366 }
367
368 /**
369 * Called by {@code AbstractContourPlot} to update the time. This will invalidate the plot triggering a redraw.
370 * @param updateTime Time; current time
371 */
372 @SuppressWarnings("synthetic-access")
373 final synchronized void increaseTime(final Time updateTime)
374 {
375 if (updateTime.si > this.timeAxis.maxValue)
376 {
377 this.timeAxis.setMaxValue(updateTime.si);
378 for (AbstractContourPlot<?> plot : this.plots)
379 {
380 plot.setUpperDomainBound(updateTime.si);
381 }
382 }
383 if (this.toTime == null || updateTime.si > this.toTime.si) // null at initialization
384 {
385 invalidate(updateTime);
386 }
387 }
388
389 /**
390 * Sets the granularity of the plot. This will invalidate the plot triggering a redraw.
391 * @param dimension Dimension; space or time
392 * @param granularity double; granularity in space or time (SI unit)
393 */
394 public final synchronized void setGranularity(final Dimension dimension, final double granularity)
395 {
396 if (dimension.equals(Dimension.DISTANCE))
397 {
398 this.desiredSpaceGranularity = granularity;
399 for (AbstractContourPlot<?> contourPlot : ContourDataSource.this.plots)
400 {
401 contourPlot.setSpaceGranularity(granularity);
402 }
403 }
404 else
405 {
406 this.desiredTimeGranularity = granularity;
407 for (AbstractContourPlot<?> contourPlot : ContourDataSource.this.plots)
408 {
409 contourPlot.setUpdateInterval(Duration.instantiateSI(granularity));
410 contourPlot.setTimeGranularity(granularity);
411 }
412 }
413 invalidate(null);
414 }
415
416 /**
417 * Sets bi-linear interpolation enabled or disabled. This will invalidate the plot triggering a redraw.
418 * @param interpolate boolean; whether to enable interpolation
419 */
420 public final void setInterpolate(final boolean interpolate)
421 {
422 if (this.timeAxis.interpolate != interpolate)
423 {
424 synchronized (this)
425 {
426 this.timeAxis.setInterpolate(interpolate);
427 this.spaceAxis.setInterpolate(interpolate);
428 for (AbstractContourPlot<?> contourPlot : ContourDataSource.this.plots)
429 {
430 contourPlot.setInterpolation(interpolate);
431 }
432 invalidate(null);
433 }
434 }
435 }
436
437 /**
438 * Sets the adaptive smoothing enabled or disabled. This will invalidate the plot triggering a redraw.
439 * @param smooth boolean; whether to smooth the plor
440 */
441 public final void setSmooth(final boolean smooth)
442 {
443 if (this.smooth != smooth)
444 {
445 synchronized (this)
446 {
447 this.smooth = smooth;
448 for (AbstractContourPlot<?> contourPlot : ContourDataSource.this.plots)
449 {
450 System.out.println("not notifying plot " + contourPlot);
451 // TODO work out what to do with this: contourPlot.setSmoothing(smooth);
452 }
453 invalidate(null);
454 }
455 }
456 }
457
458 // ************************
459 // *** UPDATING METHODS ***
460 // ************************
461
462 /**
463 * Each method that changes a setting such that the plot is no longer valid, should call this method after the setting was
464 * changed. If time is updated (increased), it should be given as input in to this method. The given time <i>should</i> be
465 * {@code null} if the plot is not valid for any other reason. In this case a full redo is initiated.
466 * <p>
467 * Every method calling this method should be {@code synchronized}, at least for the part where the setting is changed and
468 * this method is called. This method will in all cases add an update request to the updater, working in another thread. It
469 * will invoke method {@code update()}. That method utilizes a synchronized block to obtain all synchronization sensitive
470 * data, before starting the actual work.
471 * @param t Time; time up to which to show data
472 */
473 private synchronized void invalidate(final Time t)
474 {
475 if (t != null)
476 {
477 this.toTime = t;
478 }
479 else
480 {
481 this.redo = true;
482 }
483 if (this.toTime != null) // null at initialization
484 {
485 // either a later time was set, or time was null and a redo is required (will be picked up through the redo field)
486 // note that we cannot set {@code null}, hence we set the current to time, which may or may not have just changed
487 this.graphUpdater.offer(this.toTime);
488 }
489 }
490
491 /**
492 * Heart of the data pool. This method is invoked regularly by the "DataPool worker" thread, as scheduled in a queue through
493 * planned updates at an interval, or by user action changing the plot appearance. No two invocations can happen at the same
494 * time, as the "DataPool worker" thread executes this method before the next update request from the queue is considered.
495 * <p>
496 * This method regularly checks conditions that indicate the update should be interrupted as for example a setting has
497 * changed and appearance should change. Whenever a new invalidation causes {@code redo = true}, this method can stop as the
498 * full data needs to be recalculated. This can be set by any change of e.g. granularity or smoothing, during the update.
499 * <p>
500 * During the data recalculation, a later update time may also trigger this method to stop, while the next update will pick
501 * up where this update left off. During the smoothing this method doesn't stop for an increased update time, as that will
502 * leave a gap in the smoothed data. Note that smoothing either smoothes all data (when {@code redo = true}), or only the
503 * last part that falls within the kernel.
504 * @param t Time; time up to which to show data
505 */
506 @SuppressWarnings({"synthetic-access", "methodlength"})
507 private void update(final Time t)
508 {
509 Throw.when(this.plots.isEmpty(), IllegalStateException.class, "ContourDataSource is used, but not by a contour plot!");
510
511 if (t.si < this.toTime.si)
512 {
513 // skip this update as new updates were commanded, while this update was in the queue, and a previous was running
514 return;
515 }
516
517 /**
518 * This method is executed once at a time by the worker Thread. Many properties, such as the data, are maintained by
519 * this method. Other properties, which other methods can change, are read first in a synchronized block, while those
520 * methods are also synchronized.
521 */
522 boolean redo0;
523 boolean smooth0;
524 boolean interpolate0;
525 double timeGranularity;
526 double spaceGranularity;
527 double[] spaceTicks;
528 double[] timeTicks;
529 int fromSpaceIndex = 0;
530 int fromTimeIndex = 0;
531 int toTimeIndex;
532 double tFromEgtf = 0;
533 int nFromEgtf = 0;
534 synchronized (this)
535 {
536 // save local copies so commands given during this execution can change it for the next execution
537 redo0 = this.redo;
538 smooth0 = this.smooth;
539 interpolate0 = this.timeAxis.interpolate;
540 // timeTicks may be longer than the simulation time, so we use the time bin for the required time of data
541 if (this.desiredTimeGranularity != null)
542 {
543 this.timeAxis.setGranularity(this.desiredTimeGranularity);
544 this.desiredTimeGranularity = null;
545 }
546 if (this.desiredSpaceGranularity != null)
547 {
548 this.spaceAxis.setGranularity(this.desiredSpaceGranularity);
549 this.desiredSpaceGranularity = null;
550 }
551 timeGranularity = this.timeAxis.granularity;
552 spaceGranularity = this.spaceAxis.granularity;
553 spaceTicks = this.spaceAxis.getTicks();
554 timeTicks = this.timeAxis.getTicks();
555 if (!redo0)
556 {
557 // remember where we started, readyItems will be updated but we need to know where we started during the update
558 fromSpaceIndex = getSpaceBin(this.readyItems + 1);
559 fromTimeIndex = getTimeBin(this.readyItems + 1);
560 }
561 toTimeIndex = ((int) (t.si / timeGranularity)) - (interpolate0 ? 0 : 1);
562 if (smooth0)
563 {
564 // time of current bin - kernel size, get bin of that time, get time (middle) of that bin
565 tFromEgtf = this.timeAxis.getBinValue(redo0 ? 0 : this.timeAxis.getValueBin(
566 this.timeAxis.getBinValue(fromTimeIndex) - Math.max(TAU.si, timeGranularity / 2) * KERNEL_FACTOR));
567 nFromEgtf = this.timeAxis.getValueBin(tFromEgtf);
568 }
569 // starting execution, so reset redo trigger which any next command may set to true if needed
570 this.redo = false;
571 }
572
573 // reset upon a redo
574 if (redo0)
575 {
576 this.readyItems = -1;
577
578 // init all data arrays
579 int nSpace = spaceTicks.length - 1;
580 int nTime = timeTicks.length - 1;
581 this.distance = new float[nSpace][nTime];
582 this.time = new float[nSpace][nTime];
583 for (ContourDataType<?, ?> contourDataType : this.additionalData.keySet())
584 {
585 this.additionalData.put(contourDataType, new float[nSpace][nTime]);
586 }
587
588 // setup the smoothing filter
589 if (smooth0)
590 {
591 // create the filter
592 this.egtf = new EGTF(C_CONG.si, this.cFree.si, DELTA_V.si, this.vc.si);
593
594 // create data source and its data streams for speed, distance traveled, time traveled, and additional
595 DataSource generic = this.egtf.getDataSource("generic");
596 generic.addStream(TypedQuantity.SPEED, Speed.instantiateSI(1.0), Speed.instantiateSI(1.0));
597 generic.addStreamSI(this.travelTimeQuantity, 1.0, 1.0);
598 generic.addStreamSI(this.travelDistanceQuantity, 1.0, 1.0);
599 this.speedStream = generic.getStream(TypedQuantity.SPEED);
600 this.travelTimeStream = generic.getStream(this.travelTimeQuantity);
601 this.travelDistanceStream = generic.getStream(this.travelDistanceQuantity);
602 for (ContourDataType<?, ?> contourDataType : this.additionalData.keySet())
603 {
604 this.additionalStreams.put(contourDataType, generic.addStreamSI(contourDataType.getQuantity(), 1.0, 1.0));
605 }
606
607 // in principle we use sigma and tau, unless the data is so rough, we need more (granularity / 2).
608 double tau2 = Math.max(TAU.si, timeGranularity / 2);
609 double sigma2 = Math.max(SIGMA.si, spaceGranularity / 2);
610 // for maximum space and time range, increase sigma and tau by KERNEL_FACTOR, beyond which both kernels diminish
611 this.egtf.setGaussKernelSI(sigma2 * KERNEL_FACTOR, tau2 * KERNEL_FACTOR, sigma2, tau2);
612
613 // add listener to provide a filter status update and to possibly stop the filter when the plot is invalidated
614 this.egtf.addListener(new EgtfListener()
615 {
616 /** {@inheritDoc} */
617 @Override
618 public void notifyProgress(final EgtfEvent event)
619 {
620 // check stop (explicit use of property, not locally stored value)
621 if (ContourDataSource.this.redo)
622 {
623 // plots need to be redone
624 event.interrupt(); // stop the EGTF
625 setStatusLabel(" "); // reset status label so no "ASM at 12.6%" remains there
626 return;
627 }
628 String status =
629 event.getProgress() >= 1.0 ? " " : String.format("ASM at %.2f%%", event.getProgress() * 100);
630 setStatusLabel(status);
631 }
632 });
633 }
634 }
635
636 // discard any data from smoothing if we are not smoothing
637 if (!smooth0)
638 {
639 // free for garbage collector to remove the data
640 this.egtf = null;
641 this.speedStream = null;
642 this.travelTimeStream = null;
643 this.travelDistanceStream = null;
644 this.additionalStreams.clear();
645 }
646
647 // ensure capacity
648 for (int i = 0; i < this.distance.length; i++)
649 {
650 this.distance[i] = GraphUtil.ensureCapacity(this.distance[i], toTimeIndex + 1);
651 this.time[i] = GraphUtil.ensureCapacity(this.time[i], toTimeIndex + 1);
652 for (float[][] additional : this.additionalData.values())
653 {
654 additional[i] = GraphUtil.ensureCapacity(additional[i], toTimeIndex + 1);
655 }
656 }
657
658 // loop cells to update data
659 for (int j = fromTimeIndex; j <= toTimeIndex; j++)
660 {
661 Time tFrom = Time.instantiateSI(timeTicks[j]);
662 Time tTo = Time.instantiateSI(timeTicks[j + 1]);
663
664 // we never filter time, time always spans the entire simulation, it will contain tFrom till tTo
665
666 for (int i = fromSpaceIndex; i < spaceTicks.length - 1; i++)
667 {
668 // when interpolating, set the first row and column to NaN so colors representing 0 do not mess up the edges
669 if ((j == 0 || i == 0) && interpolate0)
670 {
671 this.distance[i][j] = Float.NaN;
672 this.time[i][j] = Float.NaN;
673 this.readyItems++;
674 continue;
675 }
676
677 // only first loop with offset, later in time, none of the space was done in the previous update
678 fromSpaceIndex = 0;
679 Length xFrom = Length.instantiateSI(spaceTicks[i]);
680 Length xTo = Length.instantiateSI(Math.min(spaceTicks[i + 1], this.path.getTotalLength().si));
681
682 // init cell data
683 double totalDistance = 0.0;
684 double totalTime = 0.0;
685 Map<ContourDataType<?, ?>, Object> additionalIntermediate = new LinkedHashMap<>();
686 for (ContourDataType<?, ?> contourDataType : this.additionalData.keySet())
687 {
688 additionalIntermediate.put(contourDataType, contourDataType.identity());
689 }
690
691 // aggregate series in cell
692 for (int series = 0; series < this.path.getNumberOfSeries(); series++)
693 {
694 // obtain trajectories
695 List<TrajectoryGroup<?>> trajectories = new ArrayList<>();
696 for (Section<KpiLaneDirection> section : getPath().getSections())
697 {
698 trajectories.add(this.sampler.getTrajectoryGroup(section.getSource(series)));
699 }
700
701 // filter groups (lanes) that overlap with section i
702 List<TrajectoryGroup<?>> included = new ArrayList<>();
703 List<Length> xStart = new ArrayList<>();
704 List<Length> xEnd = new ArrayList<>();
705 for (int k = 0; k < trajectories.size(); k++)
706 {
707 TrajectoryGroup<?> trajectoryGroup = trajectories.get(k);
708 KpiLaneDirection lane = trajectoryGroup.getLaneDirection();
709 Length startDistance = this.path.getStartDistance(this.path.get(k));
710 if (startDistance.si + this.path.get(k).getLength().si > spaceTicks[i]
711 && startDistance.si < spaceTicks[i + 1])
712 {
713 included.add(trajectoryGroup);
714 double scale = this.path.get(k).getLength().si / lane.getLaneData().getLength().si;
715 // divide by scale, so we go from base length to section length
716 xStart.add(Length.max(xFrom.minus(startDistance).divide(scale), Length.ZERO));
717 xEnd.add(Length.min(xTo.minus(startDistance).divide(scale),
718 trajectoryGroup.getLaneDirection().getLaneData().getLength()));
719 }
720 }
721
722 // accumulate distance and time of trajectories
723 for (int k = 0; k < included.size(); k++)
724 {
725 TrajectoryGroup<?> trajectoryGroup = included.get(k);
726 for (Trajectory<?> trajectory : trajectoryGroup.getTrajectories())
727 {
728 // for optimal operations, we first do quick-reject based on time, as by far most trajectories
729 // during the entire time span of simulation will not apply to a particular cell in space-time
730 if (GraphUtil.considerTrajectory(trajectory, tFrom, tTo))
731 {
732 // again for optimal operations, we use a space-time view only (we don't need more)
733 SpaceTimeView spaceTimeView;
734 try
735 {
736 spaceTimeView = trajectory.getSpaceTimeView(xStart.get(k), xEnd.get(k), tFrom, tTo);
737 }
738 catch (IllegalArgumentException exception)
739 {
740 CategoryLogger.always().debug(exception,
741 "Unable to generate space-time view from x = {} to {} and t = {} to {}.",
742 xStart.get(k), xEnd.get(k), tFrom, tTo);
743 continue;
744 }
745 totalDistance += spaceTimeView.getDistance().si;
746 totalTime += spaceTimeView.getTime().si;
747 }
748 }
749 }
750
751 // loop and set any additional data
752 for (ContourDataType<?, ?> contourDataType : this.additionalData.keySet())
753 {
754 addAdditional(additionalIntermediate, contourDataType, included, xStart, xEnd, tFrom, tTo);
755 }
756
757 }
758
759 // scale values to the full size of a cell on a single lane, so the EGTF is interpolating comparable values
760 double norm = spaceGranularity / (xTo.si - xFrom.si) / this.path.getNumberOfSeries();
761 totalDistance *= norm;
762 totalTime *= norm;
763 this.distance[i][j] = (float) totalDistance;
764 this.time[i][j] = (float) totalTime;
765 for (ContourDataType<?, ?> contourDataType : this.additionalData.keySet())
766 {
767 this.additionalData.get(contourDataType)[i][j] =
768 finalizeAdditional(additionalIntermediate, contourDataType);
769 }
770
771 // add data to EGTF (yes it's a copy, but our local data will be overwritten with smoothed data later)
772 if (smooth0)
773 {
774 // center of cell
775 double xDat = (xFrom.si + xTo.si) / 2.0;
776 double tDat = (tFrom.si + tTo.si) / 2.0;
777 // speed data is implicit as totalDistance/totalTime, but the EGTF needs it explicitly
778 this.egtf.addPointDataSI(this.speedStream, xDat, tDat, totalDistance / totalTime);
779 this.egtf.addPointDataSI(this.travelDistanceStream, xDat, tDat, totalDistance);
780 this.egtf.addPointDataSI(this.travelTimeStream, xDat, tDat, totalTime);
781 for (ContourDataType<?, ?> contourDataType : this.additionalStreams.keySet())
782 {
783 ContourDataSource.this.egtf.addPointDataSI(
784 ContourDataSource.this.additionalStreams.get(contourDataType), xDat, tDat,
785 this.additionalData.get(contourDataType)[i][j]);
786 }
787 }
788
789 // check stop (explicit use of properties, not locally stored values)
790 if (this.redo)
791 {
792 // plots need to be redone, or time has increased meaning that a next call may continue further just as well
793 return;
794 }
795
796 // one more item is ready for plotting
797 this.readyItems++;
798 }
799
800 // notify changes for every time slice
801 this.plots.forEach((plot) -> plot.notifyPlotChange());
802 }
803
804 // smooth all data that is as old as our kernel includes (or all data on a redo)
805 if (smooth0)
806 {
807 Set<Quantity<?, ?>> quantities = new LinkedHashSet<>();
808 quantities.add(this.travelDistanceQuantity);
809 quantities.add(this.travelTimeQuantity);
810 for (ContourDataType<?, ?> contourDataType : this.additionalData.keySet())
811 {
812 quantities.add(contourDataType.getQuantity());
813 }
814 Filter filter = this.egtf.filterFastSI(spaceTicks[0] + 0.5 * spaceGranularity, spaceGranularity,
815 spaceTicks[0] + (-1.5 + spaceTicks.length) * spaceGranularity, tFromEgtf, timeGranularity, t.si,
816 quantities.toArray(new Quantity<?, ?>[quantities.size()]));
817 if (filter != null) // null if interrupted
818 {
819 overwriteSmoothed(this.distance, nFromEgtf, filter.getSI(this.travelDistanceQuantity));
820 overwriteSmoothed(this.time, nFromEgtf, filter.getSI(this.travelTimeQuantity));
821 for (ContourDataType<?, ?> contourDataType : this.additionalData.keySet())
822 {
823 overwriteSmoothed(this.additionalData.get(contourDataType), nFromEgtf,
824 filter.getSI(contourDataType.getQuantity()));
825 }
826 this.plots.forEach((plot) -> plot.notifyPlotChange());
827 }
828 }
829 }
830
831 /**
832 * Add additional data to stored intermediate result.
833 * @param additionalIntermediate Map<ContourDataType<?, ?>, Object>; intermediate storage map
834 * @param contourDataType ContourDataType<?, ?>; additional data type
835 * @param included List<TrajectoryGroup<?>>; trajectories
836 * @param xStart List<Length>; start distance per trajectory group
837 * @param xEnd List<Length>; end distance per trajectory group
838 * @param tFrom Time; start time
839 * @param tTo Time; end time
840 * @param <I> intermediate data type
841 */
842 @SuppressWarnings("unchecked")
843 private <I> void addAdditional(final Map<ContourDataType<?, ?>, Object> additionalIntermediate,
844 final ContourDataType<?, ?> contourDataType, final List<TrajectoryGroup<?>> included, final List<Length> xStart,
845 final List<Length> xEnd, final Time tFrom, final Time tTo)
846 {
847 additionalIntermediate.put(contourDataType, ((ContourDataType<?, I>) contourDataType)
848 .processSeries((I) additionalIntermediate.get(contourDataType), included, xStart, xEnd, tFrom, tTo));
849 }
850
851 /**
852 * Stores a finalized result for additional data.
853 * @param additionalIntermediate Map<ContourDataType<?, ?>, Object>; intermediate storage map
854 * @param contourDataType ContourDataType<?, ?>; additional data type
855 * @return float; finalized results for a cell
856 * @param <I> intermediate data type
857 */
858 @SuppressWarnings("unchecked")
859 private <I> float finalizeAdditional(final Map<ContourDataType<?, ?>, Object> additionalIntermediate,
860 final ContourDataType<?, ?> contourDataType)
861 {
862 return ((ContourDataType<?, I>) contourDataType).finalize((I) additionalIntermediate.get(contourDataType)).floatValue();
863 }
864
865 /**
866 * Helper method to fill smoothed data in to raw data.
867 * @param raw float[][]; the raw unsmoothed data
868 * @param rawCol int; column from which onward to fill smoothed data in to the raw data which is used for plotting
869 * @param smoothed double[][]; smoothed data returned by {@code EGTF}
870 */
871 private void overwriteSmoothed(final float[][] raw, final int rawCol, final double[][] smoothed)
872 {
873 for (int i = 0; i < raw.length; i++)
874 {
875 // can't use System.arraycopy due to float vs double
876 for (int j = 0; j < smoothed[i].length; j++)
877 {
878 raw[i][j + rawCol] = (float) smoothed[i][j];
879 }
880 }
881 }
882
883 /**
884 * Helper method used by an {@code EgtfListener} to present the filter progress.
885 * @param status String; progress report
886 */
887 private void setStatusLabel(final String status)
888 {
889 for (AbstractContourPlot<?> plot : ContourDataSource.this.plots)
890 {
891 // TODO what shall we do this this? plot.setStatusLabel(status);
892 }
893 }
894
895 // ******************************
896 // *** DATA RETRIEVAL METHODS ***
897 // ******************************
898
899 /**
900 * Returns the speed of the cell pertaining to plot item.
901 * @param item int; plot item
902 * @return double; speed of the cell, calculated as 'total distance' / 'total space'.
903 */
904 public double getSpeed(final int item)
905 {
906 if (item > this.readyItems)
907 {
908 return Double.NaN;
909 }
910 return getTotalDistance(item) / getTotalTime(item);
911 }
912
913 /**
914 * Returns the total distance traveled in the cell pertaining to plot item.
915 * @param item int; plot item
916 * @return double; total distance traveled in the cell
917 */
918 public double getTotalDistance(final int item)
919 {
920 if (item > this.readyItems)
921 {
922 return Double.NaN;
923 }
924 return this.distance[getSpaceBin(item)][getTimeBin(item)];
925 }
926
927 /**
928 * Returns the total time traveled in the cell pertaining to plot item.
929 * @param item int; plot item
930 * @return double; total time traveled in the cell
931 */
932 public double getTotalTime(final int item)
933 {
934 if (item > this.readyItems)
935 {
936 return Double.NaN;
937 }
938 return this.time[getSpaceBin(item)][getTimeBin(item)];
939 }
940
941 /**
942 * Returns data of the given {@code ContourDataType} for a specific item.
943 * @param item int; plot item
944 * @param contourDataType ContourDataType<?, ?>; contour data type
945 * @return data of the given {@code ContourDataType} for a specific item
946 */
947 public double get(final int item, final ContourDataType<?, ?> contourDataType)
948 {
949 if (item > this.readyItems)
950 {
951 return Double.NaN;
952 }
953 return this.additionalData.get(contourDataType)[getSpaceBin(item)][getTimeBin(item)];
954 }
955
956 /**
957 * Returns the time bin number of the item.
958 * @param item int; item number
959 * @return int; time bin number of the item
960 */
961 private int getTimeBin(final int item)
962 {
963 return item / this.spaceAxis.getBinCount();
964 }
965
966 /**
967 * Returns the space bin number of the item.
968 * @param item int; item number
969 * @return int; space bin number of the item
970 */
971 private int getSpaceBin(final int item)
972 {
973 return item % this.spaceAxis.getBinCount();
974 }
975
976 // **********************
977 // *** HELPER CLASSES ***
978 // **********************
979
980 /**
981 * Enum to refer to either the distance or time axis.
982 * <p>
983 * Copyright (c) 2013-2020 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
984 * <br>
985 * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
986 * <p>
987 * @version $Revision$, $LastChangedDate$, by $Author$, initial version 10 okt. 2018 <br>
988 * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
989 * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
990 * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
991 */
992 public enum Dimension
993 {
994 /** Distance axis. */
995 DISTANCE
996 {
997 /** {@inheritDoc} */
998 @Override
999 protected Axis getAxis(final ContourDataSource<?> dataPool)
1000 {
1001 return dataPool.spaceAxis;
1002 }
1003 },
1004
1005 /** Time axis. */
1006 TIME
1007 {
1008 /** {@inheritDoc} */
1009 @Override
1010 protected Axis getAxis(final ContourDataSource<?> dataPool)
1011 {
1012 return dataPool.timeAxis;
1013 }
1014 };
1015
1016 /**
1017 * Returns the {@code Axis} object.
1018 * @param dataPool ContourDataSource<?>; data pool
1019 * @return Axis; axis
1020 */
1021 protected abstract Axis getAxis(ContourDataSource<?> dataPool);
1022 }
1023
1024 /**
1025 * Class to store and determine axis information such as granularity, ticks, and range.
1026 * <p>
1027 * Copyright (c) 2013-2020 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
1028 * <br>
1029 * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
1030 * <p>
1031 * @version $Revision$, $LastChangedDate$, by $Author$, initial version 10 okt. 2018 <br>
1032 * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
1033 * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
1034 * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
1035 */
1036 static class Axis
1037 {
1038 /** Minimum value. */
1039 private final double minValue;
1040
1041 /** Maximum value. */
1042 private double maxValue;
1043
1044 /** Selected granularity. */
1045 private double granularity;
1046
1047 /** Possible granularities. */
1048 private final double[] granularities;
1049
1050 /** Whether the data pool is set to interpolate. */
1051 private boolean interpolate = true;
1052
1053 /** Tick values. */
1054 private double[] ticks;
1055
1056 /**
1057 * Constructor.
1058 * @param minValue double; minimum value
1059 * @param maxValue double; maximum value
1060 * @param granularity double; initial granularity
1061 * @param granularities double[]; possible granularities
1062 */
1063 Axis(final double minValue, final double maxValue, final double granularity, final double[] granularities)
1064 {
1065 this.minValue = minValue;
1066 this.maxValue = maxValue;
1067 this.granularity = granularity;
1068 this.granularities = granularities;
1069 }
1070
1071 /**
1072 * Sets the maximum value.
1073 * @param maxValue double; maximum value
1074 */
1075 void setMaxValue(final double maxValue)
1076 {
1077 if (this.maxValue != maxValue)
1078 {
1079 this.maxValue = maxValue;
1080 this.ticks = null;
1081 }
1082 }
1083
1084 /**
1085 * Sets the granularity.
1086 * @param granularity double; granularity
1087 */
1088 void setGranularity(final double granularity)
1089 {
1090 if (this.granularity != granularity)
1091 {
1092 this.granularity = granularity;
1093 this.ticks = null;
1094 }
1095 }
1096
1097 /**
1098 * Returns the ticks, which are calculated if needed.
1099 * @return double[]; ticks
1100 */
1101 double[] getTicks()
1102 {
1103 if (this.ticks == null)
1104 {
1105 int n = getBinCount() + 1;
1106 this.ticks = new double[n];
1107 int di = this.interpolate ? 1 : 0;
1108 for (int i = 0; i < n; i++)
1109 {
1110 if (i == n - 1)
1111 {
1112 this.ticks[i] = Math.min((i - di) * this.granularity, this.maxValue);
1113 }
1114 else
1115 {
1116 this.ticks[i] = (i - di) * this.granularity;
1117 }
1118 }
1119 }
1120 return this.ticks;
1121 }
1122
1123 /**
1124 * Calculates the number of bins.
1125 * @return int; number of bins
1126 */
1127 int getBinCount()
1128 {
1129 return (int) Math.ceil((this.maxValue - this.minValue) / this.granularity) + (this.interpolate ? 1 : 0);
1130 }
1131
1132 /**
1133 * Calculates the center value of a bin.
1134 * @param bin int; bin number
1135 * @return double; center value of the bin
1136 */
1137 double getBinValue(final int bin)
1138 {
1139 return this.minValue + (0.5 + bin - (this.interpolate ? 1 : 0)) * this.granularity;
1140 }
1141
1142 /**
1143 * Looks up the bin number of the value.
1144 * @param value double; value
1145 * @return int; bin number
1146 */
1147 int getValueBin(final double value)
1148 {
1149 getTicks();
1150 if (value > this.ticks[this.ticks.length - 1])
1151 {
1152 return this.ticks.length - 1;
1153 }
1154 int i = 0;
1155 while (i < this.ticks.length - 1 && this.ticks[i + 1] < value + 1e-9)
1156 {
1157 i++;
1158 }
1159 return i;
1160 }
1161
1162 /**
1163 * Sets interpolation, important is it required the data to have an additional row or column.
1164 * @param interpolate boolean; interpolation
1165 */
1166 void setInterpolate(final boolean interpolate)
1167 {
1168 if (this.interpolate != interpolate)
1169 {
1170 this.interpolate = interpolate;
1171 this.ticks = null;
1172 }
1173 }
1174
1175 /**
1176 * Retrieve the interpolate flag.
1177 * @return boolean; true if interpolation is on; false if interpolation is off
1178 */
1179 public boolean isInterpolate()
1180 {
1181 return interpolate;
1182 }
1183
1184 /** {@inheritDoc} */
1185 @Override
1186 public String toString()
1187 {
1188 return "Axis [minValue=" + this.minValue + ", maxValue=" + this.maxValue + ", granularity=" + this.granularity
1189 + ", granularities=" + Arrays.toString(this.granularities) + ", interpolate=" + this.interpolate
1190 + ", ticks=" + Arrays.toString(this.ticks) + "]";
1191 }
1192
1193 }
1194
1195 /**
1196 * Interface for data types of which a contour plot can be made. Using this class, the data pool can determine and store
1197 * cell values for a variable set of additional data types (besides total distance, total time and speed).
1198 * <p>
1199 * Copyright (c) 2013-2020 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
1200 * <br>
1201 * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
1202 * <p>
1203 * @version $Revision$, $LastChangedDate$, by $Author$, initial version 10 okt. 2018 <br>
1204 * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
1205 * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
1206 * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
1207 * @param <Z> value type
1208 * @param <I> intermediate data type
1209 */
1210 public interface ContourDataType<Z extends Number, I>
1211 {
1212 /**
1213 * Returns the initial value for intermediate result.
1214 * @return I, initial intermediate value
1215 */
1216 I identity();
1217
1218 /**
1219 * Calculate value from provided trajectories that apply to a single grid cell on a single series (lane).
1220 * @param intermediate I; intermediate value of previous series, starts as the identity
1221 * @param trajectories List<TrajectoryGroup<?>>; trajectories, all groups overlap the requested space-time
1222 * @param xFrom List<Length>; start location of cell on the section
1223 * @param xTo List<Length>; end location of cell on the section.
1224 * @param tFrom Time; start time of cell
1225 * @param tTo Time; end time of cell
1226 * @return I; intermediate value
1227 */
1228 I processSeries(I intermediate, List<TrajectoryGroup<?>> trajectories, List<Length> xFrom, List<Length> xTo, Time tFrom,
1229 Time tTo);
1230
1231 /**
1232 * Returns the final value of the intermediate result after all lanes.
1233 * @param intermediate I; intermediate result after all lanes
1234 * @return Z; final value
1235 */
1236 Z finalize(I intermediate);
1237
1238 /**
1239 * Returns the quantity that is being plotted on the z-axis for the EGTF filter.
1240 * @return Quantity<Z, ?>; quantity that is being plotted on the z-axis for the EGTF filter
1241 */
1242 Quantity<Z, ?> getQuantity();
1243 }
1244
1245 /** {@inheritDoc} */
1246 @Override
1247 public String toString()
1248 {
1249 return "ContourDataSource [sampler=" + this.sampler + ", updateInterval=" + this.updateInterval + ", delay="
1250 + this.delay + ", path=" + this.path + ", spaceAxis=" + this.spaceAxis + ", timeAxis=" + this.timeAxis
1251 + ", plots=" + this.plots + ", distance=" + Arrays.toString(this.distance) + ", time="
1252 + Arrays.toString(this.time) + ", additionalData=" + this.additionalData + ", smooth=" + this.smooth
1253 + ", cFree=" + this.cFree + ", vc=" + this.vc + ", egtf=" + this.egtf + ", speedStream=" + this.speedStream
1254 + ", travelTimeStream=" + this.travelTimeStream + ", travelDistanceStream=" + this.travelDistanceStream
1255 + ", travelTimeQuantity=" + this.travelTimeQuantity + ", travelDistanceQuantity=" + this.travelDistanceQuantity
1256 + ", additionalStreams=" + this.additionalStreams + ", graphUpdater=" + this.graphUpdater + ", redo="
1257 + this.redo + ", toTime=" + this.toTime + ", readyItems=" + this.readyItems + ", desiredSpaceGranularity="
1258 + this.desiredSpaceGranularity + ", desiredTimeGranularity=" + this.desiredTimeGranularity + "]";
1259 }
1260
1261 }