package org.opentrafficsim.draw.graphs; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.djunits.unit.SpeedUnit; import org.djunits.value.vdouble.scalar.Duration; import org.djunits.value.vdouble.scalar.Length; import org.djunits.value.vdouble.scalar.Speed; import org.djunits.value.vdouble.scalar.Time; import org.djutils.exceptions.Throw; import org.djutils.logger.CategoryLogger; import org.opentrafficsim.core.egtf.Converter; import org.opentrafficsim.core.egtf.DataSource; import org.opentrafficsim.core.egtf.DataStream; import org.opentrafficsim.core.egtf.EGTF; import org.opentrafficsim.core.egtf.EgtfEvent; import org.opentrafficsim.core.egtf.EgtfListener; import org.opentrafficsim.core.egtf.Filter; import org.opentrafficsim.core.egtf.Quantity; import org.opentrafficsim.core.egtf.typed.TypedQuantity; import org.opentrafficsim.draw.graphs.GraphPath.Section; import org.opentrafficsim.kpi.interfaces.GtuDataInterface; import org.opentrafficsim.kpi.sampling.KpiLaneDirection; import org.opentrafficsim.kpi.sampling.SamplerData; import org.opentrafficsim.kpi.sampling.Trajectory; import org.opentrafficsim.kpi.sampling.Trajectory.SpaceTimeView; import org.opentrafficsim.kpi.sampling.TrajectoryGroup; /** * Class that contains data for contour plots. One data source can be shared between contour plots, in which case the * granularity, path, sampler, update interval, and whether the data is smoothed (EGTF) are equal between the plots. *

* By default the source contains traveled time and traveled distance per cell. *

* Copyright (c) 2013-2020 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
* BSD-style license. See OpenTrafficSim License. *

* @version $Revision$, $LastChangedDate$, by $Author$, initial version 5 okt. 2018
* @author Alexander Verbraeck * @author Peter Knoppers * @author Wouter Schakel * @param gtu type data */ public class ContourDataSource { // ************************* // *** GLOBAL PROPERTIES *** // ************************* /** Space granularity values. */ protected static final double[] DEFAULT_SPACE_GRANULARITIES = { 10, 20, 50, 100, 200, 500, 1000 }; /** Index of the initial space granularity. */ protected static final int DEFAULT_SPACE_GRANULARITY_INDEX = 3; /** Time granularity values. */ protected static final double[] DEFAULT_TIME_GRANULARITIES = { 1, 2, 5, 10, 20, 30, 60, 120, 300, 600 }; /** Index of the initial time granularity. */ protected static final int DEFAULT_TIME_GRANULARITY_INDEX = 3; /** Initial lower bound for the time scale. */ protected static final Time DEFAULT_LOWER_TIME_BOUND = Time.ZERO; /** * Total kernel size relative to sigma and tau. This factor is determined through -log(1 - p) with p ~= 99%. This means that * the cumulative exponential distribution has 99% at 5 times sigma or tau. Note that due to a coordinate change in the * Adaptive Smoothing Method, the actual cumulative distribution is slightly different. Hence, this is just a heuristic. */ private static final int KERNEL_FACTOR = 5; /** Spatial kernel size. Larger value may be used when using a large granularity. */ private static final Length SIGMA = Length.instantiateSI(300); /** Temporal kernel size. Larger value may be used when using a large granularity. */ private static final Duration TAU = Duration.instantiateSI(30); /** Maximum free flow propagation speed. */ private static final Speed MAX_C_FREE = new Speed(80.0, SpeedUnit.KM_PER_HOUR); /** Factor on speed limit to determine vc, the flip over speed between congestion and free flow. */ private static final double VC_FACRTOR = 0.8; /** Congestion propagation speed. */ private static final Speed C_CONG = new Speed(-18.0, SpeedUnit.KM_PER_HOUR); /** Delta v, speed transition region around threshold. */ private static final Speed DELTA_V = new Speed(10.0, SpeedUnit.KM_PER_HOUR); // ***************************** // *** CONTEXTUAL PROPERTIES *** // ***************************** /** Sampler data. */ private final SamplerData samplerData; /** Update interval. */ private final Duration updateInterval; /** Delay so critical future events have occurred, e.g. GTU's next move's to extend trajectories. */ private final Duration delay; /** Path. */ private final GraphPath path; /** Space axis. */ final Axis spaceAxis; /** Time axis. */ final Axis timeAxis; /** Registered plots. */ private Set> plots = new LinkedHashSet<>(); // ***************** // *** PLOT DATA *** // ***************** /** Total distance traveled per cell. */ private float[][] distance; /** Total time traveled per cell. */ private float[][] time; /** Data of other types. */ private final Map, float[][]> additionalData = new LinkedHashMap<>(); // **************************** // *** SMOOTHING PROPERTIES *** // **************************** /** Free flow propagation speed. */ private Speed cFree; /** Flip-over speed between congestion and free flow. */ private Speed vc; /** Smoothing filter. */ private EGTF egtf; /** Data stream for speed. */ private DataStream speedStream; /** Data stream for travel time. */ private DataStream travelTimeStream; /** Data stream for travel distance. */ private DataStream travelDistanceStream; /** Quantity for travel time. */ private final Quantity travelTimeQuantity = new Quantity<>("travel time", Converter.SI); /** Quantity for travel distance. */ private final Quantity travelDistanceQuantity = new Quantity<>("travel distance", Converter.SI); /** Data streams for any additional data. */ private Map, DataStream> additionalStreams = new LinkedHashMap<>(); // ***************************** // *** CONTINUITY PROPERTIES *** // ***************************** /** Updater for update times. */ private final GraphUpdater

* Every method calling this method should be {@code synchronized}, at least for the part where the setting is changed and * this method is called. This method will in all cases add an update request to the updater, working in another thread. It * will invoke method {@code update()}. That method utilizes a synchronized block to obtain all synchronization sensitive * data, before starting the actual work. * @param t Time; time up to which to show data */ private synchronized void invalidate(final Time t) { if (t != null) { this.toTime = t; } else { this.redo = true; } if (this.toTime != null) // null at initialization { // either a later time was set, or time was null and a redo is required (will be picked up through the redo field) // note that we cannot set {@code null}, hence we set the current to time, which may or may not have just changed this.graphUpdater.offer(this.toTime); } } /** * Heart of the data pool. This method is invoked regularly by the "DataPool worker" thread, as scheduled in a queue through * planned updates at an interval, or by user action changing the plot appearance. No two invocations can happen at the same * time, as the "DataPool worker" thread executes this method before the next update request from the queue is considered. *

* This method regularly checks conditions that indicate the update should be interrupted as for example a setting has * changed and appearance should change. Whenever a new invalidation causes {@code redo = true}, this method can stop as the * full data needs to be recalculated. This can be set by any change of e.g. granularity or smoothing, during the update. *

* During the data recalculation, a later update time may also trigger this method to stop, while the next update will pick * up where this update left off. During the smoothing this method doesn't stop for an increased update time, as that will * leave a gap in the smoothed data. Note that smoothing either smoothes all data (when {@code redo = true}), or only the * last part that falls within the kernel. * @param t Time; time up to which to show data */ @SuppressWarnings({ "synthetic-access", "methodlength" }) private void update(final Time t) { Throw.when(this.plots.isEmpty(), IllegalStateException.class, "ContourDataSource is used, but not by a contour plot!"); if (t.si < this.toTime.si) { // skip this update as new updates were commanded, while this update was in the queue, and a previous was running return; } /** * This method is executed once at a time by the worker Thread. Many properties, such as the data, are maintained by * this method. Other properties, which other methods can change, are read first in a synchronized block, while those * methods are also synchronized. */ boolean redo0; boolean smooth0; boolean interpolate0; double timeGranularity; double spaceGranularity; double[] spaceTicks; double[] timeTicks; int fromSpaceIndex = 0; int fromTimeIndex = 0; int toTimeIndex; double tFromEgtf = 0; int nFromEgtf = 0; synchronized (this) { // save local copies so commands given during this execution can change it for the next execution redo0 = this.redo; smooth0 = this.smooth; interpolate0 = this.timeAxis.interpolate; // timeTicks may be longer than the simulation time, so we use the time bin for the required time of data if (this.desiredTimeGranularity != null) { this.timeAxis.setGranularity(this.desiredTimeGranularity); this.desiredTimeGranularity = null; } if (this.desiredSpaceGranularity != null) { this.spaceAxis.setGranularity(this.desiredSpaceGranularity); this.desiredSpaceGranularity = null; } timeGranularity = this.timeAxis.granularity; spaceGranularity = this.spaceAxis.granularity; spaceTicks = this.spaceAxis.getTicks(); timeTicks = this.timeAxis.getTicks(); if (!redo0) { // remember where we started, readyItems will be updated but we need to know where we started during the update fromSpaceIndex = getSpaceBin(this.readyItems + 1); fromTimeIndex = getTimeBin(this.readyItems + 1); } toTimeIndex = ((int) (t.si / timeGranularity)) - (interpolate0 ? 0 : 1); if (smooth0) { // time of current bin - kernel size, get bin of that time, get time (middle) of that bin tFromEgtf = this.timeAxis.getBinValue(redo0 ? 0 : this.timeAxis.getValueBin( this.timeAxis.getBinValue(fromTimeIndex) - Math.max(TAU.si, timeGranularity / 2) * KERNEL_FACTOR)); nFromEgtf = this.timeAxis.getValueBin(tFromEgtf); } // starting execution, so reset redo trigger which any next command may set to true if needed this.redo = false; } // reset upon a redo if (redo0) { this.readyItems = -1; // init all data arrays int nSpace = spaceTicks.length - 1; int nTime = timeTicks.length - 1; this.distance = new float[nSpace][nTime]; this.time = new float[nSpace][nTime]; for (ContourDataType contourDataType : this.additionalData.keySet()) { this.additionalData.put(contourDataType, new float[nSpace][nTime]); } // setup the smoothing filter if (smooth0) { // create the filter this.egtf = new EGTF(C_CONG.si, this.cFree.si, DELTA_V.si, this.vc.si); // create data source and its data streams for speed, distance traveled, time traveled, and additional DataSource generic = this.egtf.getDataSource("generic"); generic.addStream(TypedQuantity.SPEED, Speed.instantiateSI(1.0), Speed.instantiateSI(1.0)); generic.addStreamSI(this.travelTimeQuantity, 1.0, 1.0); generic.addStreamSI(this.travelDistanceQuantity, 1.0, 1.0); this.speedStream = generic.getStream(TypedQuantity.SPEED); this.travelTimeStream = generic.getStream(this.travelTimeQuantity); this.travelDistanceStream = generic.getStream(this.travelDistanceQuantity); for (ContourDataType contourDataType : this.additionalData.keySet()) { this.additionalStreams.put(contourDataType, generic.addStreamSI(contourDataType.getQuantity(), 1.0, 1.0)); } // in principle we use sigma and tau, unless the data is so rough, we need more (granularity / 2). double tau2 = Math.max(TAU.si, timeGranularity / 2); double sigma2 = Math.max(SIGMA.si, spaceGranularity / 2); // for maximum space and time range, increase sigma and tau by KERNEL_FACTOR, beyond which both kernels diminish this.egtf.setGaussKernelSI(sigma2 * KERNEL_FACTOR, tau2 * KERNEL_FACTOR, sigma2, tau2); // add listener to provide a filter status update and to possibly stop the filter when the plot is invalidated this.egtf.addListener(new EgtfListener() { /** {@inheritDoc} */ @Override public void notifyProgress(final EgtfEvent event) { // check stop (explicit use of property, not locally stored value) if (ContourDataSource.this.redo) { // plots need to be redone event.interrupt(); // stop the EGTF setStatusLabel(" "); // reset status label so no "ASM at 12.6%" remains there return; } String status = event.getProgress() >= 1.0 ? " " : String.format("ASM at %.2f%%", event.getProgress() * 100); setStatusLabel(status); } }); } } // discard any data from smoothing if we are not smoothing if (!smooth0) { // free for garbage collector to remove the data this.egtf = null; this.speedStream = null; this.travelTimeStream = null; this.travelDistanceStream = null; this.additionalStreams.clear(); } // ensure capacity for (int i = 0; i < this.distance.length; i++) { this.distance[i] = GraphUtil.ensureCapacity(this.distance[i], toTimeIndex + 1); this.time[i] = GraphUtil.ensureCapacity(this.time[i], toTimeIndex + 1); for (float[][] additional : this.additionalData.values()) { additional[i] = GraphUtil.ensureCapacity(additional[i], toTimeIndex + 1); } } // loop cells to update data for (int j = fromTimeIndex; j <= toTimeIndex; j++) { Time tFrom = Time.instantiateSI(timeTicks[j]); Time tTo = Time.instantiateSI(timeTicks[j + 1]); // we never filter time, time always spans the entire simulation, it will contain tFrom till tTo for (int i = fromSpaceIndex; i < spaceTicks.length - 1; i++) { // when interpolating, set the first row and column to NaN so colors representing 0 do not mess up the edges if ((j == 0 || i == 0) && interpolate0) { this.distance[i][j] = Float.NaN; this.time[i][j] = Float.NaN; this.readyItems++; continue; } // only first loop with offset, later in time, none of the space was done in the previous update fromSpaceIndex = 0; Length xFrom = Length.instantiateSI(spaceTicks[i]); Length xTo = Length.instantiateSI(Math.min(spaceTicks[i + 1], this.path.getTotalLength().si)); // init cell data double totalDistance = 0.0; double totalTime = 0.0; Map, Object> additionalIntermediate = new LinkedHashMap<>(); for (ContourDataType contourDataType : this.additionalData.keySet()) { additionalIntermediate.put(contourDataType, contourDataType.identity()); } // aggregate series in cell for (int series = 0; series < this.path.getNumberOfSeries(); series++) { // obtain trajectories List> trajectories = new ArrayList<>(); for (Section section : getPath().getSections()) { TrajectoryGroup trajectoryGroup = this.samplerData.getTrajectoryGroup(section.getSource(series)); if (null == trajectoryGroup) { CategoryLogger.always().error("trajectoryGroup {} is null", series); } trajectories.add(trajectoryGroup); } // filter groups (lanes) that overlap with section i List> included = new ArrayList<>(); List xStart = new ArrayList<>(); List xEnd = new ArrayList<>(); for (int k = 0; k < trajectories.size(); k++) { TrajectoryGroup trajectoryGroup = trajectories.get(k); KpiLaneDirection lane = trajectoryGroup.getLaneDirection(); Length startDistance = this.path.getStartDistance(this.path.get(k)); if (startDistance.si + this.path.get(k).getLength().si > spaceTicks[i] && startDistance.si < spaceTicks[i + 1]) { included.add(trajectoryGroup); double scale = this.path.get(k).getLength().si / lane.getLaneData().getLength().si; // divide by scale, so we go from base length to section length xStart.add(Length.max(xFrom.minus(startDistance).divide(scale), Length.ZERO)); xEnd.add(Length.min(xTo.minus(startDistance).divide(scale), trajectoryGroup.getLaneDirection().getLaneData().getLength())); } } // accumulate distance and time of trajectories for (int k = 0; k < included.size(); k++) { TrajectoryGroup trajectoryGroup = included.get(k); for (Trajectory trajectory : trajectoryGroup.getTrajectories()) { // for optimal operations, we first do quick-reject based on time, as by far most trajectories // during the entire time span of simulation will not apply to a particular cell in space-time if (GraphUtil.considerTrajectory(trajectory, tFrom, tTo)) { // again for optimal operations, we use a space-time view only (we don't need more) SpaceTimeView spaceTimeView; try { spaceTimeView = trajectory.getSpaceTimeView(xStart.get(k), xEnd.get(k), tFrom, tTo); } catch (IllegalArgumentException exception) { CategoryLogger.always().debug(exception, "Unable to generate space-time view from x = {} to {} and t = {} to {}.", xStart.get(k), xEnd.get(k), tFrom, tTo); continue; } totalDistance += spaceTimeView.getDistance().si; totalTime += spaceTimeView.getTime().si; } } } // loop and set any additional data for (ContourDataType contourDataType : this.additionalData.keySet()) { addAdditional(additionalIntermediate, contourDataType, included, xStart, xEnd, tFrom, tTo); } } // scale values to the full size of a cell on a single lane, so the EGTF is interpolating comparable values double norm = spaceGranularity / (xTo.si - xFrom.si) / this.path.getNumberOfSeries(); totalDistance *= norm; totalTime *= norm; this.distance[i][j] = (float) totalDistance; this.time[i][j] = (float) totalTime; for (ContourDataType contourDataType : this.additionalData.keySet()) { this.additionalData.get(contourDataType)[i][j] = finalizeAdditional(additionalIntermediate, contourDataType); } // add data to EGTF (yes it's a copy, but our local data will be overwritten with smoothed data later) if (smooth0) { // center of cell double xDat = (xFrom.si + xTo.si) / 2.0; double tDat = (tFrom.si + tTo.si) / 2.0; // speed data is implicit as totalDistance/totalTime, but the EGTF needs it explicitly this.egtf.addPointDataSI(this.speedStream, xDat, tDat, totalDistance / totalTime); this.egtf.addPointDataSI(this.travelDistanceStream, xDat, tDat, totalDistance); this.egtf.addPointDataSI(this.travelTimeStream, xDat, tDat, totalTime); for (ContourDataType contourDataType : this.additionalStreams.keySet()) { ContourDataSource.this.egtf.addPointDataSI( ContourDataSource.this.additionalStreams.get(contourDataType), xDat, tDat, this.additionalData.get(contourDataType)[i][j]); } } // check stop (explicit use of properties, not locally stored values) if (this.redo) { // plots need to be redone, or time has increased meaning that a next call may continue further just as well return; } // one more item is ready for plotting this.readyItems++; } // notify changes for every time slice this.plots.forEach((plot) -> plot.notifyPlotChange()); } // smooth all data that is as old as our kernel includes (or all data on a redo) if (smooth0) { Set> quantities = new LinkedHashSet<>(); quantities.add(this.travelDistanceQuantity); quantities.add(this.travelTimeQuantity); for (ContourDataType contourDataType : this.additionalData.keySet()) { quantities.add(contourDataType.getQuantity()); } Filter filter = this.egtf.filterFastSI(spaceTicks[0] + 0.5 * spaceGranularity, spaceGranularity, spaceTicks[0] + (-1.5 + spaceTicks.length) * spaceGranularity, tFromEgtf, timeGranularity, t.si, quantities.toArray(new Quantity[quantities.size()])); if (filter != null) // null if interrupted { overwriteSmoothed(this.distance, nFromEgtf, filter.getSI(this.travelDistanceQuantity)); overwriteSmoothed(this.time, nFromEgtf, filter.getSI(this.travelTimeQuantity)); for (ContourDataType contourDataType : this.additionalData.keySet()) { overwriteSmoothed(this.additionalData.get(contourDataType), nFromEgtf, filter.getSI(contourDataType.getQuantity())); } this.plots.forEach((plot) -> plot.notifyPlotChange()); } } } /** * Add additional data to stored intermediate result. * @param additionalIntermediate Map<ContourDataType<?, ?>, Object>; intermediate storage map * @param contourDataType ContourDataType<?, ?>; additional data type * @param included List<TrajectoryGroup<?>>; trajectories * @param xStart List<Length>; start distance per trajectory group * @param xEnd List<Length>; end distance per trajectory group * @param tFrom Time; start time * @param tTo Time; end time * @param intermediate data type */ @SuppressWarnings("unchecked") private void addAdditional(final Map, Object> additionalIntermediate, final ContourDataType contourDataType, final List> included, final List xStart, final List xEnd, final Time tFrom, final Time tTo) { additionalIntermediate.put(contourDataType, ((ContourDataType) contourDataType) .processSeries((I) additionalIntermediate.get(contourDataType), included, xStart, xEnd, tFrom, tTo)); } /** * Stores a finalized result for additional data. * @param additionalIntermediate Map<ContourDataType<?, ?>, Object>; intermediate storage map * @param contourDataType ContourDataType<?, ?>; additional data type * @return float; finalized results for a cell * @param intermediate data type */ @SuppressWarnings("unchecked") private float finalizeAdditional(final Map, Object> additionalIntermediate, final ContourDataType contourDataType) { return ((ContourDataType) contourDataType).finalize((I) additionalIntermediate.get(contourDataType)).floatValue(); } /** * Helper method to fill smoothed data in to raw data. * @param raw float[][]; the raw unsmoothed data * @param rawCol int; column from which onward to fill smoothed data in to the raw data which is used for plotting * @param smoothed double[][]; smoothed data returned by {@code EGTF} */ private void overwriteSmoothed(final float[][] raw, final int rawCol, final double[][] smoothed) { for (int i = 0; i < raw.length; i++) { // can't use System.arraycopy due to float vs double for (int j = 0; j < smoothed[i].length; j++) { raw[i][j + rawCol] = (float) smoothed[i][j]; } } } /** * Helper method used by an {@code EgtfListener} to present the filter progress. * @param status String; progress report */ private void setStatusLabel(final String status) { for (AbstractContourPlot plot : ContourDataSource.this.plots) { // TODO what shall we do this this? plot.setStatusLabel(status); } } // ****************************** // *** DATA RETRIEVAL METHODS *** // ****************************** /** * Returns the speed of the cell pertaining to plot item. * @param item int; plot item * @return double; speed of the cell, calculated as 'total distance' / 'total space'. */ public double getSpeed(final int item) { if (item > this.readyItems) { return Double.NaN; } return getTotalDistance(item) / getTotalTime(item); } /** * Returns the total distance traveled in the cell pertaining to plot item. * @param item int; plot item * @return double; total distance traveled in the cell */ public double getTotalDistance(final int item) { if (item > this.readyItems) { return Double.NaN; } return this.distance[getSpaceBin(item)][getTimeBin(item)]; } /** * Returns the total time traveled in the cell pertaining to plot item. * @param item int; plot item * @return double; total time traveled in the cell */ public double getTotalTime(final int item) { if (item > this.readyItems) { return Double.NaN; } return this.time[getSpaceBin(item)][getTimeBin(item)]; } /** * Returns data of the given {@code ContourDataType} for a specific item. * @param item int; plot item * @param contourDataType ContourDataType<?, ?>; contour data type * @return data of the given {@code ContourDataType} for a specific item */ public double get(final int item, final ContourDataType contourDataType) { if (item > this.readyItems) { return Double.NaN; } return this.additionalData.get(contourDataType)[getSpaceBin(item)][getTimeBin(item)]; } /** * Returns the time bin number of the item. * @param item int; item number * @return int; time bin number of the item */ private int getTimeBin(final int item) { Throw.when(item < 0 || item >= this.spaceAxis.getBinCount() * this.timeAxis.getBinCount(), IndexOutOfBoundsException.class, "Item out of range"); return item / this.spaceAxis.getBinCount(); } /** * Returns the space bin number of the item. * @param item int; item number * @return int; space bin number of the item */ private int getSpaceBin(final int item) { return item % this.spaceAxis.getBinCount(); } // ********************** // *** HELPER CLASSES *** // ********************** /** * Enum to refer to either the distance or time axis. *

* Copyright (c) 2013-2020 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. *
* BSD-style license. See OpenTrafficSim License. *

* @version $Revision$, $LastChangedDate$, by $Author$, initial version 10 okt. 2018
* @author Alexander Verbraeck * @author Peter Knoppers * @author Wouter Schakel */ public enum Dimension { /** Distance axis. */ DISTANCE { /** {@inheritDoc} */ @Override protected Axis getAxis(final ContourDataSource dataPool) { return dataPool.spaceAxis; } }, /** Time axis. */ TIME { /** {@inheritDoc} */ @Override protected Axis getAxis(final ContourDataSource dataPool) { return dataPool.timeAxis; } }; /** * Returns the {@code Axis} object. * @param dataPool ContourDataSource<?>; data pool * @return Axis; axis */ protected abstract Axis getAxis(ContourDataSource dataPool); } /** * Class to store and determine axis information such as granularity, ticks, and range. *

* Copyright (c) 2013-2020 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. *
* BSD-style license. See OpenTrafficSim License. *

* @version $Revision$, $LastChangedDate$, by $Author$, initial version 10 okt. 2018
* @author Alexander Verbraeck * @author Peter Knoppers * @author Wouter Schakel */ static class Axis { /** Minimum value. */ private final double minValue; /** Maximum value. */ private double maxValue; /** Selected granularity. */ private double granularity; /** Possible granularities. */ private final double[] granularities; /** Whether the data pool is set to interpolate. */ private boolean interpolate = true; /** Tick values. */ private double[] ticks; /** * Constructor. * @param minValue double; minimum value * @param maxValue double; maximum value * @param granularity double; initial granularity * @param granularities double[]; possible granularities */ Axis(final double minValue, final double maxValue, final double granularity, final double[] granularities) { this.minValue = minValue; this.maxValue = maxValue; this.granularity = granularity; this.granularities = granularities; } /** * Sets the maximum value. * @param maxValue double; maximum value */ void setMaxValue(final double maxValue) { if (this.maxValue != maxValue) { this.maxValue = maxValue; this.ticks = null; } } /** * Sets the granularity. * @param granularity double; granularity */ void setGranularity(final double granularity) { if (this.granularity != granularity) { this.granularity = granularity; this.ticks = null; } } /** * Returns the ticks, which are calculated if needed. * @return double[]; ticks */ double[] getTicks() { if (this.ticks == null) { int n = getBinCount() + 1; this.ticks = new double[n]; int di = this.interpolate ? 1 : 0; for (int i = 0; i < n; i++) { if (i == n - 1) { this.ticks[i] = Math.min((i - di) * this.granularity, this.maxValue); } else { this.ticks[i] = (i - di) * this.granularity; } } } return this.ticks; } /** * Calculates the number of bins. * @return int; number of bins */ int getBinCount() { return (int) Math.ceil((this.maxValue - this.minValue) / this.granularity) + (this.interpolate ? 1 : 0); } /** * Calculates the center value of a bin. * @param bin int; bin number * @return double; center value of the bin */ double getBinValue(final int bin) { return this.minValue + (0.5 + bin - (this.interpolate ? 1 : 0)) * this.granularity; } /** * Looks up the bin number of the value. * @param value double; value * @return int; bin number */ int getValueBin(final double value) { getTicks(); if (value > this.ticks[this.ticks.length - 1]) { return this.ticks.length - 1; } int i = 0; while (i < this.ticks.length - 1 && this.ticks[i + 1] < value + 1e-9) { i++; } return i; } /** * Sets interpolation, important is it required the data to have an additional row or column. * @param interpolate boolean; interpolation */ void setInterpolate(final boolean interpolate) { if (this.interpolate != interpolate) { this.interpolate = interpolate; this.ticks = null; } } /** * Retrieve the interpolate flag. * @return boolean; true if interpolation is on; false if interpolation is off */ public boolean isInterpolate() { return this.interpolate; } /** {@inheritDoc} */ @Override public String toString() { return "Axis [minValue=" + this.minValue + ", maxValue=" + this.maxValue + ", granularity=" + this.granularity + ", granularities=" + Arrays.toString(this.granularities) + ", interpolate=" + this.interpolate + ", ticks=" + Arrays.toString(this.ticks) + "]"; } } /** * Interface for data types of which a contour plot can be made. Using this class, the data pool can determine and store * cell values for a variable set of additional data types (besides total distance, total time and speed). *

* Copyright (c) 2013-2020 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. *
* BSD-style license. See OpenTrafficSim License. *

* @version $Revision$, $LastChangedDate$, by $Author$, initial version 10 okt. 2018
* @author Alexander Verbraeck * @author Peter Knoppers * @author Wouter Schakel * @param value type * @param intermediate data type */ public interface ContourDataType { /** * Returns the initial value for intermediate result. * @return I, initial intermediate value */ I identity(); /** * Calculate value from provided trajectories that apply to a single grid cell on a single series (lane). * @param intermediate I; intermediate value of previous series, starts as the identity * @param trajectories List<TrajectoryGroup<?>>; trajectories, all groups overlap the requested space-time * @param xFrom List<Length>; start location of cell on the section * @param xTo List<Length>; end location of cell on the section. * @param tFrom Time; start time of cell * @param tTo Time; end time of cell * @return I; intermediate value */ I processSeries(I intermediate, List> trajectories, List xFrom, List xTo, Time tFrom, Time tTo); /** * Returns the final value of the intermediate result after all lanes. * @param intermediate I; intermediate result after all lanes * @return Z; final value */ Z finalize(I intermediate); /** * Returns the quantity that is being plotted on the z-axis for the EGTF filter. * @return Quantity<Z, ?>; quantity that is being plotted on the z-axis for the EGTF filter */ Quantity getQuantity(); } /** {@inheritDoc} */ @Override public String toString() { return "ContourDataSource [samplerData=" + this.samplerData + ", updateInterval=" + this.updateInterval + ", delay=" + this.delay + ", path=" + this.path + ", spaceAxis=" + this.spaceAxis + ", timeAxis=" + this.timeAxis + ", plots=" + this.plots + ", distance=" + Arrays.toString(this.distance) + ", time=" + Arrays.toString(this.time) + ", additionalData=" + this.additionalData + ", smooth=" + this.smooth + ", cFree=" + this.cFree + ", vc=" + this.vc + ", egtf=" + this.egtf + ", speedStream=" + this.speedStream + ", travelTimeStream=" + this.travelTimeStream + ", travelDistanceStream=" + this.travelDistanceStream + ", travelTimeQuantity=" + this.travelTimeQuantity + ", travelDistanceQuantity=" + this.travelDistanceQuantity + ", additionalStreams=" + this.additionalStreams + ", graphUpdater=" + this.graphUpdater + ", redo=" + this.redo + ", toTime=" + this.toTime + ", readyItems=" + this.readyItems + ", desiredSpaceGranularity=" + this.desiredSpaceGranularity + ", desiredTimeGranularity=" + this.desiredTimeGranularity + "]"; } }