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