ODMatrix.java

  1. package org.opentrafficsim.road.gtu.strategical.od;

  2. import java.io.Serializable;
  3. import java.util.ArrayList;
  4. import java.util.Collections;
  5. import java.util.Comparator;
  6. import java.util.LinkedHashMap;
  7. import java.util.LinkedHashSet;
  8. import java.util.List;
  9. import java.util.Map;
  10. import java.util.Set;
  11. import java.util.TreeMap;

  12. import org.djunits.unit.FrequencyUnit;
  13. import org.djunits.unit.TimeUnit;
  14. import org.djunits.value.StorageType;
  15. import org.djunits.value.ValueException;
  16. import org.djunits.value.vdouble.scalar.Frequency;
  17. import org.djunits.value.vdouble.scalar.Time;
  18. import org.djunits.value.vdouble.vector.FrequencyVector;
  19. import org.djunits.value.vdouble.vector.TimeVector;
  20. import org.djutils.exceptions.Throw;
  21. import org.opentrafficsim.base.Identifiable;
  22. import org.opentrafficsim.core.network.NetworkException;
  23. import org.opentrafficsim.core.network.Node;
  24. import org.opentrafficsim.core.network.route.Route;
  25. import org.opentrafficsim.road.gtu.generator.headway.DemandPattern;

  26. /**
  27.  * The minimal OD matrix has 1 origin, 1 destination and 1 time period. More of each can be used. Further categorization of data
  28.  * is possible, i.e. for origin O to destination D, <i>for lane L, for route R and for vehicle class C</i>, the demand at time T
  29.  * is D. The further categorization is defined by an array of {@code Class}'s that define the categorization.
  30.  * <p>
  31.  * Copyright (c) 2013-2019 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
  32.  * BSD-style license. See <a href="http://opentrafficsim.org/docs/current/license.html">OpenTrafficSim License</a>.
  33.  * <p>
  34.  * @version $Revision$, $LastChangedDate$, by $Author$, initial version Sep 15, 2016 <br>
  35.  * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
  36.  * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
  37.  * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
  38.  */
  39. public class ODMatrix implements Serializable, Identifiable
  40. {

  41.     /** */
  42.     private static final long serialVersionUID = 20160921L;

  43.     /** Id. */
  44.     private final String id;

  45.     /** Origin nodes. */
  46.     private final List<Node> origins;

  47.     /** Destination nodes. */
  48.     private final List<Node> destinations;

  49.     /** Categorization of demand data. */
  50.     private final Categorization categorization;

  51.     /** Global time vector. */
  52.     private final TimeVector globalTimeVector;

  53.     /** Global interpolation of the data. */
  54.     private final Interpolation globalInterpolation;

  55.     /** Demand data per origin and destination, and possibly further categorization. */
  56.     private final Map<Node, Map<Node, Map<Category, DemandPattern>>> demandData = new LinkedHashMap<>();

  57.     /** Node comparator. */
  58.     private static final Comparator<Node> COMPARATOR = new Comparator<Node>()
  59.     {
  60.         /** {@inheritDoc} */
  61.         @Override
  62.         public int compare(final Node o1, final Node o2)
  63.         {
  64.             return o1.getId().compareTo(o2.getId());
  65.         }
  66.     };

  67.     /**
  68.      * Constructs an OD matrix.
  69.      * @param id String; id
  70.      * @param origins List&lt;? extends Node&gt;; origin nodes
  71.      * @param destinations List&lt;? extends Node&gt;; destination nodes
  72.      * @param categorization Categorization; categorization of data
  73.      * @param globalTimeVector TimeVector; default time
  74.      * @param globalInterpolation Interpolation; interpolation of demand data
  75.      * @throws NullPointerException if any input is null
  76.      */
  77.     public ODMatrix(final String id, final List<? extends Node> origins, final List<? extends Node> destinations,
  78.             final Categorization categorization, final TimeVector globalTimeVector, final Interpolation globalInterpolation)
  79.     {
  80.         Throw.whenNull(id, "Id may not be null.");
  81.         Throw.when(origins == null || origins.contains(null), NullPointerException.class, "Origin may not be or contain null.");
  82.         Throw.when(destinations == null || destinations.contains(null), NullPointerException.class,
  83.                 "Destination may not be or contain null.");
  84.         Throw.whenNull(categorization, "Categorization may not be null.");
  85.         Throw.whenNull(globalTimeVector, "Global time vector may not be null.");
  86.         Throw.whenNull(globalInterpolation, "Global interpolation may not be null.");
  87.         this.id = id;
  88.         this.origins = new ArrayList<>(origins);
  89.         this.destinations = new ArrayList<>(destinations);
  90.         Collections.sort(this.origins, COMPARATOR);
  91.         Collections.sort(this.destinations, COMPARATOR);
  92.         this.categorization = categorization;
  93.         this.globalTimeVector = globalTimeVector;
  94.         this.globalInterpolation = globalInterpolation;
  95.         // build empty OD
  96.         for (Node origin : origins)
  97.         {
  98.             Map<Node, Map<Category, DemandPattern>> map = new LinkedHashMap<>();
  99.             for (Node destination : destinations)
  100.             {
  101.                 map.put(destination, new TreeMap<>(new Comparator<Category>()
  102.                 {
  103.                     /** {@inheritDoc} */
  104.                     @Override
  105.                     public int compare(final Category o1, final Category o2)
  106.                     {
  107.                         for (int i = 0; i < o1.getCategorization().size(); i++)
  108.                         {
  109.                             int order = Integer.compare(o1.get(i).hashCode(), o2.get(i).hashCode());
  110.                             if (order != 0)
  111.                             {
  112.                                 return order;
  113.                             }
  114.                         }
  115.                         return 0;
  116.                     }
  117.                 }));
  118.             }
  119.             this.demandData.put(origin, map);
  120.         }
  121.     }

  122.     /**
  123.      * @return id.
  124.      */
  125.     @Override
  126.     public final String getId()
  127.     {
  128.         return this.id;
  129.     }

  130.     /**
  131.      * @return origins.
  132.      */
  133.     public final List<Node> getOrigins()
  134.     {
  135.         return new ArrayList<>(this.origins);
  136.     }

  137.     /**
  138.      * @return destinations.
  139.      */
  140.     public final List<Node> getDestinations()
  141.     {
  142.         return new ArrayList<>(this.destinations);
  143.     }

  144.     /**
  145.      * @return categorization.
  146.      */
  147.     public final Categorization getCategorization()
  148.     {
  149.         return this.categorization;
  150.     }

  151.     /**
  152.      * @return globalTimeVector.
  153.      */
  154.     public final TimeVector getGlobalTimeVector()
  155.     {
  156.         return this.globalTimeVector;
  157.     }

  158.     /**
  159.      * @return globalInterpolation.
  160.      */
  161.     public final Interpolation getGlobalInterpolation()
  162.     {
  163.         return this.globalInterpolation;
  164.     }

  165.     /**
  166.      * Add a demand vector to OD.
  167.      * @param origin Node; origin
  168.      * @param destination Node; destination
  169.      * @param category Category; category
  170.      * @param demand FrequencyVector; demand data, length has to be equal to the global time vector
  171.      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
  172.      * @throws IllegalArgumentException if the category does not belong to the categorization
  173.      * @throws IllegalArgumentException if the demand data has a different length than time data, or is less than 2
  174.      * @throws IllegalArgumentException if demand is negative or time not strictly increasing
  175.      * @throws IllegalArgumentException if the route (if in the category) is not from the origin to the destination
  176.      * @throws NullPointerException if an input is null
  177.      */
  178.     public final void putDemandVector(final Node origin, final Node destination, final Category category,
  179.             final FrequencyVector demand)
  180.     {
  181.         putDemandVector(origin, destination, category, demand, this.globalTimeVector, this.globalInterpolation);
  182.     }

  183.     /**
  184.      * Add a demand vector to OD. In this method, which all other methods that add or put demand indirectly refer to, many
  185.      * consistency and validity checks are performed. These do not include checks on network connectivity, since the network may
  186.      * be subject to change during simulation.
  187.      * @param origin Node; origin
  188.      * @param destination Node; destination
  189.      * @param category Category; category
  190.      * @param demand FrequencyVector; demand data, length has to be equal to the time vector
  191.      * @param timeVector TimeVector; time vector
  192.      * @param interpolation Interpolation; interpolation
  193.      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
  194.      * @throws IllegalArgumentException if the category does not belong to the categorization
  195.      * @throws IllegalArgumentException if the demand data has a different length than time data, or is less than 2
  196.      * @throws IllegalArgumentException if demand is negative or time not strictly increasing
  197.      * @throws IllegalArgumentException if the route (if in the category) is not from the origin to the destination
  198.      * @throws NullPointerException if an input is null
  199.      */
  200.     public final void putDemandVector(final Node origin, final Node destination, final Category category,
  201.             final FrequencyVector demand, final TimeVector timeVector, final Interpolation interpolation)
  202.     {
  203.         Throw.whenNull(origin, "Origin may not be null.");
  204.         Throw.whenNull(destination, "Destination may not be null.");
  205.         Throw.whenNull(category, "Category may not be null.");
  206.         Throw.whenNull(demand, "Demand data may not be null.");
  207.         Throw.whenNull(timeVector, "Time vector may not be null.");
  208.         Throw.whenNull(interpolation, "Interpolation may not be null.");
  209.         Throw.when(!this.origins.contains(origin), IllegalArgumentException.class, "Origin '%s' is not part of the OD matrix.",
  210.                 origin);
  211.         Throw.when(!this.destinations.contains(destination), IllegalArgumentException.class,
  212.                 "Destination '%s' is not part of the OD matrix.", destination);
  213.         Throw.when(!this.categorization.equals(category.getCategorization()), IllegalArgumentException.class,
  214.                 "Provided category %s does not belong to the categorization %s.", category, this.categorization);
  215.         Throw.when(demand.size() != timeVector.size() || demand.size() < 2, IllegalArgumentException.class,
  216.                 "Demand data has different length than time vector, or has less than 2 values.");
  217.         for (Frequency q : demand)
  218.         {
  219.             Throw.when(q.lt0(), IllegalArgumentException.class, "Demand contains negative value(s).");
  220.         }
  221.         Time prevTime;
  222.         try
  223.         {
  224.             prevTime = timeVector.get(0).eq0() ? Time.createSI(-1.0) : Time.ZERO;
  225.         }
  226.         catch (ValueException exception)
  227.         {
  228.             // verified to be > 1, so no empty vector
  229.             throw new RuntimeException("Unexpected exception while checking time vector.", exception);
  230.         }
  231.         for (Time time : timeVector)
  232.         {
  233.             Throw.when(prevTime.ge(time), IllegalArgumentException.class,
  234.                     "Time vector is not strictly increasing, or contains negative time.");
  235.             prevTime = time;
  236.         }
  237.         if (this.categorization.entails(Route.class))
  238.         {
  239.             Route route = category.get(Route.class);
  240.             try
  241.             {
  242.                 Throw.when(!route.originNode().equals(origin) || !route.destinationNode().equals(destination),
  243.                         IllegalArgumentException.class,
  244.                         "Route from %s to %s does not comply with origin %s and destination %s.", route.originNode(),
  245.                         route.destinationNode(), origin, destination);
  246.             }
  247.             catch (NetworkException exception)
  248.             {
  249.                 throw new IllegalArgumentException("Route in OD has no nodes.", exception);
  250.             }
  251.         }
  252.         DemandPattern demandPattern = new DemandPattern(demand, timeVector, interpolation);
  253.         this.demandData.get(origin).get(destination).put(category, demandPattern);
  254.     }

  255.     /**
  256.      * Add a demand vector to OD, by a fraction of total demand.
  257.      * @param origin Node; origin
  258.      * @param destination Node; destination
  259.      * @param category Category; category
  260.      * @param demand FrequencyVector; demand data, length has to be equal to the time vector
  261.      * @param timeVector TimeVector; time vector
  262.      * @param interpolation Interpolation; interpolation
  263.      * @param fraction double; fraction of demand for this category
  264.      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
  265.      * @throws IllegalArgumentException if the category does not belong to the categorization
  266.      * @throws IllegalArgumentException if the demand data has a different length than time data, or is less than 2
  267.      * @throws IllegalArgumentException if demand is negative or time not strictly increasing
  268.      * @throws IllegalArgumentException if the route (if in the category) is not from the origin to the destination
  269.      * @throws NullPointerException if an input is null
  270.      */
  271.     public final void putDemandVector(final Node origin, final Node destination, final Category category,
  272.             final FrequencyVector demand, final TimeVector timeVector, final Interpolation interpolation, final double fraction)
  273.     {
  274.         Throw.whenNull(demand, "Demand data may not be null.");
  275.         FrequencyVector demandScaled;
  276.         if (fraction == 1.0)
  277.         {
  278.             demandScaled = demand;
  279.         }
  280.         else
  281.         {
  282.             double[] in = demand.getValuesInUnit();
  283.             double[] scaled = new double[in.length];
  284.             for (int i = 0; i < in.length; i++)
  285.             {
  286.                 scaled[i] = in[i] * fraction;
  287.             }
  288.             try
  289.             {
  290.                 demandScaled = new FrequencyVector(scaled, demand.getUnit(), demand.getStorageType());
  291.             }
  292.             catch (ValueException exception)
  293.             {
  294.                 // cannot happen, we use an existing vector
  295.                 throw new RuntimeException("An object was null.", exception);
  296.             }
  297.         }
  298.         putDemandVector(origin, destination, category, demandScaled, timeVector, interpolation);
  299.     }

  300.     /**
  301.      * Add a demand vector to OD, by a fraction per time period of total demand.
  302.      * @param origin Node; origin
  303.      * @param destination Node; destination
  304.      * @param category Category; category
  305.      * @param demand FrequencyVector; demand data, length has to be equal to the time vector
  306.      * @param timeVector TimeVector; time vector
  307.      * @param interpolation Interpolation; interpolation
  308.      * @param fraction double[]; fraction of demand for this category
  309.      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
  310.      * @throws IllegalArgumentException if the category does not belong to the categorization
  311.      * @throws IllegalArgumentException if the demand data has a different length than time data, or is less than 2
  312.      * @throws IllegalArgumentException if demand is negative or time not strictly increasing
  313.      * @throws IllegalArgumentException if the route (if in the category) is not from the origin to the destination
  314.      * @throws NullPointerException if an input is null
  315.      */
  316.     public final void putDemandVector(final Node origin, final Node destination, final Category category,
  317.             final FrequencyVector demand, final TimeVector timeVector, final Interpolation interpolation,
  318.             final double[] fraction)
  319.     {
  320.         Throw.whenNull(demand, "Demand data may not be null.");
  321.         Throw.whenNull(fraction, "Fraction data may not be null.");
  322.         Throw.whenNull(timeVector, "Time vector may not be null.");
  323.         Throw.when(demand.size() != timeVector.size() || timeVector.size() != fraction.length, IllegalArgumentException.class,
  324.                 "Arrays are of unequal length: demand=%d, timeVector=%d, fraction=%d", demand.size(), timeVector.size(),
  325.                 fraction.length);
  326.         double[] in = demand.getValuesInUnit();
  327.         double[] scaled = new double[in.length];
  328.         for (int i = 0; i < in.length; i++)
  329.         {
  330.             scaled[i] = in[i] * fraction[i];
  331.         }
  332.         FrequencyVector demandScaled;
  333.         try
  334.         {
  335.             demandScaled = new FrequencyVector(scaled, demand.getUnit(), demand.getStorageType());
  336.         }
  337.         catch (ValueException exception)
  338.         {
  339.             // cannot happen, we use an existing vector
  340.             throw new RuntimeException("An object was null.", exception);
  341.         }
  342.         putDemandVector(origin, destination, category, demandScaled, timeVector, interpolation);
  343.     }

  344.     /**
  345.      * @param origin Node; origin
  346.      * @param destination Node; destination
  347.      * @param category Category; category
  348.      * @return demand data for given origin, destination and categorization, {@code null} if no data is given
  349.      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
  350.      * @throws IllegalArgumentException if the category does not belong to the categorization
  351.      * @throws NullPointerException if an input is null
  352.      */
  353.     public final FrequencyVector getDemandVector(final Node origin, final Node destination, final Category category)
  354.     {
  355.         DemandPattern demandPattern = getDemandPattern(origin, destination, category);
  356.         if (demandPattern == null)
  357.         {
  358.             return null;
  359.         }
  360.         return demandPattern.getDemandVector();
  361.     }

  362.     /**
  363.      * @param origin Node; origin
  364.      * @param destination Node; destination
  365.      * @param category Category; category
  366.      * @return interpolation for given origin, destination and categorization, {@code null} if no data is given
  367.      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
  368.      * @throws IllegalArgumentException if the category does not belong to the categorization
  369.      * @throws NullPointerException if an input is null
  370.      */
  371.     public final TimeVector getTimeVector(final Node origin, final Node destination, final Category category)
  372.     {
  373.         DemandPattern demandPattern = getDemandPattern(origin, destination, category);
  374.         if (demandPattern == null)
  375.         {
  376.             return null;
  377.         }
  378.         return demandPattern.getTimeVector();
  379.     }

  380.     /**
  381.      * @param origin Node; origin
  382.      * @param destination Node; destination
  383.      * @param category Category; category
  384.      * @return interpolation for given origin, destination and categorization, {@code null} if no data is given
  385.      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
  386.      * @throws IllegalArgumentException if the category does not belong to the categorization
  387.      * @throws NullPointerException if an input is null
  388.      */
  389.     public final Interpolation getInterpolation(final Node origin, final Node destination, final Category category)
  390.     {
  391.         DemandPattern demandPattern = getDemandPattern(origin, destination, category);
  392.         if (demandPattern == null)
  393.         {
  394.             return null;
  395.         }
  396.         return demandPattern.getInterpolation();
  397.     }

  398.     /**
  399.      * Returns the demand at given time. If given time is before the first time slice or after the last time slice, 0 demand is
  400.      * returned.
  401.      * @param origin Node; origin
  402.      * @param destination Node; destination
  403.      * @param category Category; category
  404.      * @param time Time; time
  405.      * @param sliceStart boolean; whether the time is at the start of an arbitrary time slice
  406.      * @return demand for given origin, destination and categorization, at given time
  407.      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
  408.      * @throws IllegalArgumentException if the category does not belong to the categorization
  409.      * @throws NullPointerException if an input is null
  410.      */
  411.     public final Frequency getDemand(final Node origin, final Node destination, final Category category, final Time time,
  412.             final boolean sliceStart)
  413.     {
  414.         Throw.whenNull(time, "Time may not be null.");
  415.         DemandPattern demandPattern = getDemandPattern(origin, destination, category);
  416.         if (demandPattern == null)
  417.         {
  418.             return new Frequency(0.0, FrequencyUnit.PER_HOUR); // Frequency.ZERO gives "Hz" which is not nice for flow
  419.         }
  420.         return demandPattern.getFrequency(time, sliceStart);
  421.     }

  422.     /**
  423.      * @param origin Node; origin
  424.      * @param destination Node; destination
  425.      * @param category Category; category
  426.      * @return OD entry for given origin, destination and categorization.
  427.      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
  428.      * @throws IllegalArgumentException if the category does not belong to the categorization
  429.      * @throws NullPointerException if an input is null
  430.      */
  431.     public DemandPattern getDemandPattern(final Node origin, final Node destination, final Category category)
  432.     {
  433.         Throw.whenNull(origin, "Origin may not be null.");
  434.         Throw.whenNull(destination, "Destination may not be null.");
  435.         Throw.whenNull(category, "Category may not be null.");
  436.         Throw.when(!this.origins.contains(origin), IllegalArgumentException.class, "Origin '%s' is not part of the OD matrix",
  437.                 origin);
  438.         Throw.when(!this.destinations.contains(destination), IllegalArgumentException.class,
  439.                 "Destination '%s' is not part of the OD matrix.", destination);
  440.         Throw.when(!this.categorization.equals(category.getCategorization()), IllegalArgumentException.class,
  441.                 "Provided category %s does not belong to the categorization %s.", category, this.categorization);
  442.         return this.demandData.get(origin).get(destination).get(category);
  443.     }

  444.     /**
  445.      * @param origin Node; origin
  446.      * @param destination Node; destination
  447.      * @param category Category; category
  448.      * @return whether there is data for the specified origin, destination and category
  449.      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
  450.      * @throws IllegalArgumentException if the category does not belong to the categorization
  451.      * @throws NullPointerException if an input is null
  452.      */
  453.     public final boolean contains(final Node origin, final Node destination, final Category category)
  454.     {
  455.         return getDemandPattern(origin, destination, category) != null;
  456.     }

  457.     /**
  458.      * Returns the categories specified for given origin-destination combination.
  459.      * @param origin Node; origin
  460.      * @param destination Node; destination
  461.      * @return categories specified for given origin-destination combination
  462.      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
  463.      * @throws NullPointerException if an input is null
  464.      */
  465.     public final Set<Category> getCategories(final Node origin, final Node destination)
  466.     {
  467.         Throw.whenNull(origin, "Origin may not be null.");
  468.         Throw.whenNull(destination, "Destination may not be null.");
  469.         Throw.when(!this.origins.contains(origin), IllegalArgumentException.class, "Origin '%s' is not part of the OD matrix",
  470.                 origin);
  471.         Throw.when(!this.destinations.contains(destination), IllegalArgumentException.class,
  472.                 "Destination '%s' is not part of the OD matrix.", destination);
  473.         return new LinkedHashSet<>(this.demandData.get(origin).get(destination).keySet());
  474.     }

  475.     /******************************************************************************************************/
  476.     /****************************************** TRIP METHODS **********************************************/
  477.     /******************************************************************************************************/

  478.     /**
  479.      * @param origin Node; origin
  480.      * @param destination Node; destination
  481.      * @param category Category; category
  482.      * @param trips int[]; trip data, length has to be equal to the global time vector - 1, each value is the number of trips
  483.      *            during a period
  484.      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
  485.      * @throws IllegalArgumentException if the category does not belong to the categorization
  486.      * @throws IllegalArgumentException if the demand data has a different length than time data, or is less than 2
  487.      * @throws IllegalArgumentException if demand is negative or time not strictly increasing
  488.      * @throws IllegalArgumentException if the route (if in the category) is not from the origin to the destination
  489.      * @throws NullPointerException if an input is null
  490.      */
  491.     public final void putTripsVector(final Node origin, final Node destination, final Category category, final int[] trips)
  492.     {
  493.         putTripsVector(origin, destination, category, trips, getGlobalTimeVector());
  494.     }

  495.     /**
  496.      * Sets demand data by number of trips. Interpolation over time is stepwise.
  497.      * @param origin Node; origin
  498.      * @param destination Node; destination
  499.      * @param category Category; category
  500.      * @param trips int[]; trip data, length has to be equal to the time vector - 1, each value is the number of trips during a
  501.      *            period
  502.      * @param timeVector TimeVector; time vector
  503.      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
  504.      * @throws IllegalArgumentException if the category does not belong to the categorization
  505.      * @throws IllegalArgumentException if the demand data has a different length than time data, or is less than 2
  506.      * @throws IllegalArgumentException if demand is negative or time not strictly increasing
  507.      * @throws IllegalArgumentException if the route (if in the category) is not from the origin to the destination
  508.      * @throws NullPointerException if an input is null
  509.      */
  510.     public final void putTripsVector(final Node origin, final Node destination, final Category category, final int[] trips,
  511.             final TimeVector timeVector)
  512.     {
  513.         // this is what we need here, other checks in putDemandVector
  514.         Throw.whenNull(trips, "Demand data may not be null.");
  515.         Throw.whenNull(timeVector, "Time vector may not be null.");
  516.         Throw.when(trips.length != timeVector.size() - 1, IllegalArgumentException.class,
  517.                 "Trip data and time data have wrong lengths. Trip data should be 1 shorter than time data.");
  518.         // convert to flow
  519.         double[] flow = new double[timeVector.size()];
  520.         try
  521.         {
  522.             for (int i = 0; i < trips.length; i++)
  523.             {
  524.                 flow[i] = trips[i] / (timeVector.get(i + 1).getInUnit(TimeUnit.BASE_HOUR)
  525.                         - timeVector.get(i).getInUnit(TimeUnit.BASE_HOUR));
  526.             }
  527.             // last value can remain zero as initialized
  528.             putDemandVector(origin, destination, category, new FrequencyVector(flow, FrequencyUnit.PER_HOUR, StorageType.DENSE),
  529.                     timeVector, Interpolation.STEPWISE);
  530.         }
  531.         catch (ValueException exception)
  532.         {
  533.             // should not happen as we check and then loop over the array length
  534.             throw new RuntimeException("Could not translate trip vector into demand vector.", exception);
  535.         }
  536.     }

  537.     /**
  538.      * @param origin Node; origin
  539.      * @param destination Node; destination
  540.      * @param category Category; category
  541.      * @return trip data for given origin, destination and categorization, {@code null} if no data is given
  542.      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
  543.      * @throws IllegalArgumentException if the category does not belong to the categorization
  544.      * @throws NullPointerException if an input is null
  545.      */
  546.     public final int[] getTripsVector(final Node origin, final Node destination, final Category category)
  547.     {
  548.         FrequencyVector demand = getDemandVector(origin, destination, category);
  549.         if (demand == null)
  550.         {
  551.             return null;
  552.         }
  553.         int[] trips = new int[demand.size() - 1];
  554.         TimeVector time = getTimeVector(origin, destination, category);
  555.         Interpolation interpolation = getInterpolation(origin, destination, category);
  556.         for (int i = 0; i < trips.length; i++)
  557.         {
  558.             try
  559.             {
  560.                 trips[i] = interpolation.integrate(demand.get(i), time.get(i), demand.get(i + 1), time.get(i + 1));
  561.             }
  562.             catch (ValueException exception)
  563.             {
  564.                 // should not happen as we loop over the array length
  565.                 throw new RuntimeException("Could not translate demand vector into trip vector.", exception);
  566.             }
  567.         }
  568.         return trips;
  569.     }

  570.     /**
  571.      * Returns the number of trips in the given time period.
  572.      * @param origin Node; origin
  573.      * @param destination Node; destination
  574.      * @param category Category; category
  575.      * @param periodIndex int; index of time period
  576.      * @return demand for given origin, destination and categorization, at given time
  577.      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
  578.      * @throws IllegalArgumentException if the category does not belong to the categorization
  579.      * @throws IndexOutOfBoundsException if the period is outside of the specified range
  580.      * @throws NullPointerException if an input is null
  581.      */
  582.     public final int getTrips(final Node origin, final Node destination, final Category category, final int periodIndex)
  583.     {
  584.         TimeVector time = getTimeVector(origin, destination, category);
  585.         if (time == null)
  586.         {
  587.             return 0;
  588.         }
  589.         Throw.when(periodIndex < 0 || periodIndex >= time.size() - 1, IndexOutOfBoundsException.class,
  590.                 "Period index out of range.");
  591.         FrequencyVector demand = getDemandVector(origin, destination, category);
  592.         Interpolation interpolation = getInterpolation(origin, destination, category);
  593.         try
  594.         {
  595.             return interpolation.integrate(demand.get(periodIndex), time.get(periodIndex), demand.get(periodIndex + 1),
  596.                     time.get(periodIndex + 1));
  597.         }
  598.         catch (ValueException exception)
  599.         {
  600.             // should not happen as the index was checked
  601.             throw new RuntimeException("Could not get number of trips.", exception);
  602.         }
  603.     }

  604.     /**
  605.      * Adds a number of trips to given origin-destination combination, category and time period. This can only be done for data
  606.      * with stepwise interpolation.
  607.      * @param origin Node; origin
  608.      * @param destination Node; destination
  609.      * @param category Category; category
  610.      * @param periodIndex int; index of time period
  611.      * @param trips int; trips to add (may be negative)
  612.      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
  613.      * @throws IllegalArgumentException if the category does not belong to the categorization
  614.      * @throws IndexOutOfBoundsException if the period is outside of the specified range
  615.      * @throws UnsupportedOperationException if the interpolation of the data is not stepwise, or demand becomes negtive
  616.      * @throws NullPointerException if an input is null
  617.      */
  618.     public final void increaseTrips(final Node origin, final Node destination, final Category category, final int periodIndex,
  619.             final int trips)
  620.     {
  621.         Interpolation interpolation = getInterpolation(origin, destination, category);
  622.         Throw.when(!interpolation.equals(Interpolation.STEPWISE), UnsupportedOperationException.class,
  623.                 "Can only increase the number of trips for data with stepwise interpolation.");
  624.         TimeVector time = getTimeVector(origin, destination, category);
  625.         Throw.when(periodIndex < 0 || periodIndex >= time.size() - 1, IndexOutOfBoundsException.class,
  626.                 "Period index out of range.");
  627.         FrequencyVector demand = getDemandVector(origin, destination, category);
  628.         try
  629.         {
  630.             double additionalDemand = trips / (time.get(periodIndex + 1).getInUnit(TimeUnit.BASE_HOUR)
  631.                     - time.get(periodIndex).getInUnit(TimeUnit.BASE_HOUR));
  632.             double[] dem = demand.getValuesInUnit(FrequencyUnit.PER_HOUR);
  633.             Throw.when(dem[periodIndex] < -additionalDemand, UnsupportedOperationException.class,
  634.                     "Demand may not become negative.");
  635.             dem[periodIndex] += additionalDemand;
  636.             putDemandVector(origin, destination, category, new FrequencyVector(dem, FrequencyUnit.PER_HOUR, StorageType.DENSE),
  637.                     time, Interpolation.STEPWISE);
  638.         }
  639.         catch (ValueException exception)
  640.         {
  641.             // should not happen as the index was checked
  642.             throw new RuntimeException("Unexpected exception while getting number of trips.", exception);
  643.         }
  644.     }

  645.     /**
  646.      * Calculates total number of trips over time for given origin.
  647.      * @param origin Node; origin
  648.      * @return total number of trips over time for given origin
  649.      * @throws IllegalArgumentException if origin is not part of the OD matrix
  650.      * @throws NullPointerException if origin is null
  651.      */
  652.     public final int originTotal(final Node origin)
  653.     {
  654.         int sum = 0;
  655.         for (Node destination : getDestinations())
  656.         {
  657.             sum += originDestinationTotal(origin, destination);
  658.         }
  659.         return sum;
  660.     }

  661.     /**
  662.      * Calculates total number of trips over time for given destination.
  663.      * @param destination Node; destination
  664.      * @return total number of trips over time for given destination
  665.      * @throws IllegalArgumentException if destination is not part of the OD matrix
  666.      * @throws NullPointerException if destination is null
  667.      */
  668.     public final int destinationTotal(final Node destination)
  669.     {
  670.         int sum = 0;
  671.         for (Node origin : getOrigins())
  672.         {
  673.             sum += originDestinationTotal(origin, destination);
  674.         }
  675.         return sum;
  676.     }

  677.     /**
  678.      * Calculates total number of trips over time for the complete matrix.
  679.      * @return total number of trips over time for the complete matrix
  680.      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
  681.      * @throws NullPointerException if an input is null
  682.      */
  683.     public final int matrixTotal()
  684.     {
  685.         int sum = 0;
  686.         for (Node origin : getOrigins())
  687.         {
  688.             for (Node destination : getDestinations())
  689.             {
  690.                 sum += originDestinationTotal(origin, destination);
  691.             }
  692.         }
  693.         return sum;
  694.     }

  695.     /**
  696.      * Calculates total number of trips over time for given origin-destination combination.
  697.      * @param origin Node; origin
  698.      * @param destination Node; destination
  699.      * @return total number of trips over time for given origin-destination combination
  700.      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
  701.      * @throws NullPointerException if an input is null
  702.      */
  703.     public final int originDestinationTotal(final Node origin, final Node destination)
  704.     {
  705.         int sum = 0;
  706.         for (Category category : getCategories(origin, destination))
  707.         {
  708.             TimeVector time = getTimeVector(origin, destination, category);
  709.             FrequencyVector demand = getDemandVector(origin, destination, category);
  710.             Interpolation interpolation = getInterpolation(origin, destination, category);
  711.             for (int i = 0; i < time.size() - 1; i++)
  712.             {
  713.                 try
  714.                 {
  715.                     sum += interpolation.integrate(demand.get(i), time.get(i), demand.get(i + 1), time.get(i + 1));
  716.                 }
  717.                 catch (ValueException exception)
  718.                 {
  719.                     // should not happen as we loop over the array length
  720.                     throw new RuntimeException("Unexcepted exception while determining total trips over time.", exception);
  721.                 }
  722.             }
  723.         }
  724.         return sum;
  725.     }

  726.     /******************************************************************************************************/
  727.     /****************************************** OTHER METHODS *********************************************/
  728.     /******************************************************************************************************/

  729.     /** {@inheritDoc} */
  730.     @Override
  731.     @SuppressWarnings("checkstyle:designforextension")
  732.     public String toString()
  733.     {
  734.         return "ODMatrix [" + this.id + ", " + this.origins.size() + " origins, " + this.destinations.size() + " destinations, "
  735.                 + this.categorization + " ]";
  736.     }

  737.     /**
  738.      * Prints the complete OD matrix with each demand data on a single line.
  739.      */
  740.     public final void print()
  741.     {
  742.         int originLength = 0;
  743.         for (Node origin : this.origins)
  744.         {
  745.             originLength = originLength >= origin.getId().length() ? originLength : origin.getId().length();
  746.         }
  747.         int destinLength = 0;
  748.         for (Node destination : this.destinations)
  749.         {
  750.             destinLength = destinLength >= destination.getId().length() ? destinLength : destination.getId().length();
  751.         }
  752.         String format = "%-" + Math.max(originLength, 1) + "s -> %-" + Math.max(destinLength, 1) + "s | ";
  753.         for (Node origin : this.origins)
  754.         {
  755.             Map<Node, Map<Category, DemandPattern>> destinationMap = this.demandData.get(origin);
  756.             for (Node destination : this.destinations)
  757.             {
  758.                 Map<Category, DemandPattern> categoryMap = destinationMap.get(destination);
  759.                 if (categoryMap.isEmpty())
  760.                 {
  761.                     System.out.println(String.format(format, origin.getId(), destination.getId()) + "-no data-");
  762.                 }
  763.                 else
  764.                 {
  765.                     for (Category category : categoryMap.keySet())
  766.                     {
  767.                         StringBuilder catStr = new StringBuilder("[");
  768.                         String sep = "";
  769.                         for (int i = 0; i < category.getCategorization().size(); i++)
  770.                         {
  771.                             catStr.append(sep);
  772.                             Object obj = category.get(i);
  773.                             if (obj instanceof Route)
  774.                             {
  775.                                 catStr.append("Route: " + ((Route) obj).getId());
  776.                             }
  777.                             else
  778.                             {
  779.                                 catStr.append(obj);
  780.                             }
  781.                             sep = ", ";
  782.                         }
  783.                         catStr.append("]");
  784.                         // System.out.println("DEBUG format is \"" + format + "\"");
  785.                         System.out.println(String.format(format, origin.getId(), destination.getId()) + catStr + " | "
  786.                                 + categoryMap.get(category).getDemandVector());
  787.                     }
  788.                 }
  789.             }
  790.         }
  791.     }

  792.     /** {@inheritDoc} */
  793.     @Override
  794.     public final int hashCode()
  795.     {
  796.         final int prime = 31;
  797.         int result = 1;
  798.         result = prime * result + ((this.categorization == null) ? 0 : this.categorization.hashCode());
  799.         result = prime * result + ((this.demandData == null) ? 0 : this.demandData.hashCode());
  800.         result = prime * result + ((this.destinations == null) ? 0 : this.destinations.hashCode());
  801.         result = prime * result + ((this.globalInterpolation == null) ? 0 : this.globalInterpolation.hashCode());
  802.         result = prime * result + ((this.globalTimeVector == null) ? 0 : this.globalTimeVector.hashCode());
  803.         result = prime * result + ((this.id == null) ? 0 : this.id.hashCode());
  804.         result = prime * result + ((this.origins == null) ? 0 : this.origins.hashCode());
  805.         return result;
  806.     }

  807.     /** {@inheritDoc} */
  808.     @Override
  809.     public final boolean equals(final Object obj)
  810.     {
  811.         if (this == obj)
  812.         {
  813.             return true;
  814.         }
  815.         if (obj == null)
  816.         {
  817.             return false;
  818.         }
  819.         if (getClass() != obj.getClass())
  820.         {
  821.             return false;
  822.         }
  823.         ODMatrix other = (ODMatrix) obj;
  824.         if (this.categorization == null)
  825.         {
  826.             if (other.categorization != null)
  827.             {
  828.                 return false;
  829.             }
  830.         }
  831.         else if (!this.categorization.equals(other.categorization))
  832.         {
  833.             return false;
  834.         }
  835.         if (this.demandData == null)
  836.         {
  837.             if (other.demandData != null)
  838.             {
  839.                 return false;
  840.             }
  841.         }
  842.         else if (!this.demandData.equals(other.demandData))
  843.         {
  844.             return false;
  845.         }
  846.         if (this.destinations == null)
  847.         {
  848.             if (other.destinations != null)
  849.             {
  850.                 return false;
  851.             }
  852.         }
  853.         else if (!this.destinations.equals(other.destinations))
  854.         {
  855.             return false;
  856.         }
  857.         if (this.globalInterpolation != other.globalInterpolation)
  858.         {
  859.             return false;
  860.         }
  861.         if (this.globalTimeVector == null)
  862.         {
  863.             if (other.globalTimeVector != null)
  864.             {
  865.                 return false;
  866.             }
  867.         }
  868.         else if (!this.globalTimeVector.equals(other.globalTimeVector))
  869.         {
  870.             return false;
  871.         }
  872.         if (this.id == null)
  873.         {
  874.             if (other.id != null)
  875.             {
  876.                 return false;
  877.             }
  878.         }
  879.         else if (!this.id.equals(other.id))
  880.         {
  881.             return false;
  882.         }
  883.         if (this.origins == null)
  884.         {
  885.             if (other.origins != null)
  886.             {
  887.                 return false;
  888.             }
  889.         }
  890.         else if (!this.origins.equals(other.origins))
  891.         {
  892.             return false;
  893.         }
  894.         return true;
  895.     }

  896. }