View Javadoc
1   package org.opentrafficsim.road.gtu.strategical.od;
2   
3   import java.io.Serializable;
4   import java.util.ArrayList;
5   import java.util.Collections;
6   import java.util.Comparator;
7   import java.util.LinkedHashMap;
8   import java.util.LinkedHashSet;
9   import java.util.List;
10  import java.util.Map;
11  import java.util.Set;
12  import java.util.TreeMap;
13  
14  import org.djunits.unit.FrequencyUnit;
15  import org.djunits.unit.TimeUnit;
16  import org.djunits.value.StorageType;
17  import org.djunits.value.ValueException;
18  import org.djunits.value.vdouble.scalar.Frequency;
19  import org.djunits.value.vdouble.scalar.Time;
20  import org.djunits.value.vdouble.vector.FrequencyVector;
21  import org.djunits.value.vdouble.vector.TimeVector;
22  import org.djutils.exceptions.Throw;
23  import org.opentrafficsim.base.Identifiable;
24  import org.opentrafficsim.core.network.NetworkException;
25  import org.opentrafficsim.core.network.Node;
26  import org.opentrafficsim.core.network.route.Route;
27  import org.opentrafficsim.road.gtu.generator.headway.DemandPattern;
28  
29  /**
30   * The minimal OD matrix has 1 origin, 1 destination and 1 time period. More of each can be used. Further categorization of data
31   * 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
32   * is D. The further categorization is defined by an array of {@code Class}'s that define the categorization.
33   * <p>
34   * Copyright (c) 2013-2019 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
35   * BSD-style license. See <a href="http://opentrafficsim.org/docs/current/license.html">OpenTrafficSim License</a>.
36   * <p>
37   * @version $Revision$, $LastChangedDate$, by $Author$, initial version Sep 15, 2016 <br>
38   * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
39   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
40   * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
41   */
42  public class ODMatrix implements Serializable, Identifiable
43  {
44  
45      /** */
46      private static final long serialVersionUID = 20160921L;
47  
48      /** Id. */
49      private final String id;
50  
51      /** Origin nodes. */
52      private final List<Node> origins;
53  
54      /** Destination nodes. */
55      private final List<Node> destinations;
56  
57      /** Categorization of demand data. */
58      private final Categorization categorization;
59  
60      /** Global time vector. */
61      private final TimeVector globalTimeVector;
62  
63      /** Global interpolation of the data. */
64      private final Interpolation globalInterpolation;
65  
66      /** Demand data per origin and destination, and possibly further categorization. */
67      private final Map<Node, Map<Node, Map<Category, DemandPattern>>> demandData = new LinkedHashMap<>();
68  
69      /** Node comparator. */
70      private static final Comparator<Node> COMPARATOR = new Comparator<Node>()
71      {
72          /** {@inheritDoc} */
73          @Override
74          public int compare(final Node o1, final Node o2)
75          {
76              return o1.getId().compareTo(o2.getId());
77          }
78      };
79  
80      /**
81       * Constructs an OD matrix.
82       * @param id String; id
83       * @param origins List&lt;? extends Node&gt;; origin nodes
84       * @param destinations List&lt;? extends Node&gt;; destination nodes
85       * @param categorization Categorization; categorization of data
86       * @param globalTimeVector TimeVector; default time
87       * @param globalInterpolation Interpolation; interpolation of demand data
88       * @throws NullPointerException if any input is null
89       */
90      public ODMatrix(final String id, final List<? extends Node> origins, final List<? extends Node> destinations,
91              final Categorization categorization, final TimeVector globalTimeVector, final Interpolation globalInterpolation)
92      {
93          Throw.whenNull(id, "Id may not be null.");
94          Throw.when(origins == null || origins.contains(null), NullPointerException.class, "Origin may not be or contain null.");
95          Throw.when(destinations == null || destinations.contains(null), NullPointerException.class,
96                  "Destination may not be or contain null.");
97          Throw.whenNull(categorization, "Categorization may not be null.");
98          // Throw.whenNull(globalTimeVector, "Global time vector may not be null.");
99          // Throw.whenNull(globalInterpolation, "Global interpolation may not be null.");
100         this.id = id;
101         this.origins = new ArrayList<>(origins);
102         this.destinations = new ArrayList<>(destinations);
103         Collections.sort(this.origins, COMPARATOR);
104         Collections.sort(this.destinations, COMPARATOR);
105         this.categorization = categorization;
106         this.globalTimeVector = globalTimeVector;
107         this.globalInterpolation = globalInterpolation;
108         // build empty OD
109         for (Node origin : origins)
110         {
111             Map<Node, Map<Category, DemandPattern>> map = new LinkedHashMap<>();
112             for (Node destination : destinations)
113             {
114                 map.put(destination, new TreeMap<>(new Comparator<Category>()
115                 {
116                     /** {@inheritDoc} */
117                     @Override
118                     public int compare(final Category o1, final Category o2)
119                     {
120                         for (int i = 0; i < o1.getCategorization().size(); i++)
121                         {
122                             int order = Integer.compare(o1.get(i).hashCode(), o2.get(i).hashCode());
123                             if (order != 0)
124                             {
125                                 return order;
126                             }
127                         }
128                         return 0;
129                     }
130                 }));
131             }
132             this.demandData.put(origin, map);
133         }
134     }
135 
136     /**
137      * @return id.
138      */
139     @Override
140     public final String getId()
141     {
142         return this.id;
143     }
144 
145     /**
146      * @return origins.
147      */
148     public final List<Node> getOrigins()
149     {
150         return new ArrayList<>(this.origins);
151     }
152 
153     /**
154      * @return destinations.
155      */
156     public final List<Node> getDestinations()
157     {
158         return new ArrayList<>(this.destinations);
159     }
160 
161     /**
162      * @return categorization.
163      */
164     public final Categorization getCategorization()
165     {
166         return this.categorization;
167     }
168 
169     /**
170      * @return globalTimeVector.
171      */
172     public final TimeVector getGlobalTimeVector()
173     {
174         return this.globalTimeVector;
175     }
176 
177     /**
178      * @return globalInterpolation.
179      */
180     public final Interpolation getGlobalInterpolation()
181     {
182         return this.globalInterpolation;
183     }
184 
185     /**
186      * Add a demand vector to OD.
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 global time vector
191      * @param fraction double; fraction of demand for this category
192      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
193      * @throws IllegalArgumentException if the category does not belong to the categorization
194      * @throws IllegalArgumentException if the demand data has a different length than time data, or is less than 2
195      * @throws IllegalArgumentException if demand is negative or time not strictly increasing
196      * @throws IllegalArgumentException if the route (if in the category) is not from the origin to the destination
197      * @throws NullPointerException if an input is null
198      */
199     public final void putDemandVector(final Node origin, final Node destination, final Category category,
200             final FrequencyVector demand, final double fraction)
201     {
202         putDemandVector(origin, destination, category, demand, this.globalTimeVector, this.globalInterpolation, fraction);
203     }
204 
205     /**
206      * Add a demand vector to OD.
207      * @param origin Node; origin
208      * @param destination Node; destination
209      * @param category Category; category
210      * @param demand FrequencyVector; demand data, length has to be equal to the global time vector
211      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
212      * @throws IllegalArgumentException if the category does not belong to the categorization
213      * @throws IllegalArgumentException if the demand data has a different length than time data, or is less than 2
214      * @throws IllegalArgumentException if demand is negative or time not strictly increasing
215      * @throws IllegalArgumentException if the route (if in the category) is not from the origin to the destination
216      * @throws NullPointerException if an input is null
217      */
218     public final void putDemandVector(final Node origin, final Node destination, final Category category,
219             final FrequencyVector demand)
220     {
221         putDemandVector(origin, destination, category, demand, this.globalTimeVector, this.globalInterpolation);
222     }
223 
224     /**
225      * Add a demand vector to OD. In this method, which all other methods that add or put demand indirectly refer to, many
226      * consistency and validity checks are performed. These do not include checks on network connectivity, since the network may
227      * be subject to change during simulation.
228      * @param origin Node; origin
229      * @param destination Node; destination
230      * @param category Category; category
231      * @param demand FrequencyVector; demand data, length has to be equal to the time vector
232      * @param timeVector TimeVector; time vector
233      * @param interpolation Interpolation; interpolation
234      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
235      * @throws IllegalArgumentException if the category does not belong to the categorization
236      * @throws IllegalArgumentException if the demand data has a different length than time data, or is less than 2
237      * @throws IllegalArgumentException if demand is negative or time not strictly increasing
238      * @throws IllegalArgumentException if the route (if in the category) is not from the origin to the destination
239      * @throws NullPointerException if an input is null
240      */
241     public final void putDemandVector(final Node origin, final Node destination, final Category category,
242             final FrequencyVector demand, final TimeVector timeVector, final Interpolation interpolation)
243     {
244         Throw.whenNull(origin, "Origin may not be null.");
245         Throw.whenNull(destination, "Destination may not be null.");
246         Throw.whenNull(category, "Category may not be null.");
247         Throw.whenNull(demand, "Demand data may not be null.");
248         Throw.whenNull(timeVector, "Time vector may not be null.");
249         Throw.whenNull(interpolation, "Interpolation may not be null.");
250         Throw.when(!this.origins.contains(origin), IllegalArgumentException.class, "Origin '%s' is not part of the OD matrix.",
251                 origin);
252         Throw.when(!this.destinations.contains(destination), IllegalArgumentException.class,
253                 "Destination '%s' is not part of the OD matrix.", destination);
254         Throw.when(!this.categorization.equals(category.getCategorization()), IllegalArgumentException.class,
255                 "Provided category %s does not belong to the categorization %s.", category, this.categorization);
256         Throw.when(demand.size() != timeVector.size() || demand.size() < 2, IllegalArgumentException.class,
257                 "Demand data has different length than time vector, or has less than 2 values.");
258         for (Frequency q : demand)
259         {
260             Throw.when(q.lt0(), IllegalArgumentException.class, "Demand contains negative value(s).");
261         }
262         Time prevTime;
263         try
264         {
265             prevTime = timeVector.get(0).eq0() ? Time.createSI(-1.0) : Time.ZERO;
266         }
267         catch (ValueException exception)
268         {
269             // verified to be > 1, so no empty vector
270             throw new RuntimeException("Unexpected exception while checking time vector.", exception);
271         }
272         for (Time time : timeVector)
273         {
274             Throw.when(prevTime.ge(time), IllegalArgumentException.class,
275                     "Time vector is not strictly increasing, or contains negative time.");
276             prevTime = time;
277         }
278         if (this.categorization.entails(Route.class))
279         {
280             Route route = category.get(Route.class);
281             try
282             {
283                 Throw.when(!route.originNode().equals(origin) || !route.destinationNode().equals(destination),
284                         IllegalArgumentException.class,
285                         "Route from %s to %s does not comply with origin %s and destination %s.", route.originNode(),
286                         route.destinationNode(), origin, destination);
287             }
288             catch (NetworkException exception)
289             {
290                 throw new IllegalArgumentException("Route in OD has no nodes.", exception);
291             }
292         }
293         DemandPattern demandPattern = new DemandPattern(demand, timeVector, interpolation);
294         this.demandData.get(origin).get(destination).put(category, demandPattern);
295     }
296 
297     /**
298      * Add a demand vector to OD, by a fraction of total demand.
299      * @param origin Node; origin
300      * @param destination Node; destination
301      * @param category Category; category
302      * @param demand FrequencyVector; demand data, length has to be equal to the time vector
303      * @param timeVector TimeVector; time vector
304      * @param interpolation Interpolation; interpolation
305      * @param fraction double; fraction of demand for this category
306      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
307      * @throws IllegalArgumentException if the category does not belong to the categorization
308      * @throws IllegalArgumentException if the demand data has a different length than time data, or is less than 2
309      * @throws IllegalArgumentException if demand is negative or time not strictly increasing
310      * @throws IllegalArgumentException if the route (if in the category) is not from the origin to the destination
311      * @throws NullPointerException if an input is null
312      */
313     public final void putDemandVector(final Node origin, final Node destination, final Category category,
314             final FrequencyVector demand, final TimeVector timeVector, final Interpolation interpolation, final double fraction)
315     {
316         Throw.whenNull(demand, "Demand data may not be null.");
317         FrequencyVector demandScaled;
318         if (fraction == 1.0)
319         {
320             demandScaled = demand;
321         }
322         else
323         {
324             double[] in = demand.getValuesInUnit();
325             double[] scaled = new double[in.length];
326             for (int i = 0; i < in.length; i++)
327             {
328                 scaled[i] = in[i] * fraction;
329             }
330             try
331             {
332                 demandScaled = new FrequencyVector(scaled, demand.getUnit(), demand.getStorageType());
333             }
334             catch (ValueException exception)
335             {
336                 // cannot happen, we use an existing vector
337                 throw new RuntimeException("An object was null.", exception);
338             }
339         }
340         putDemandVector(origin, destination, category, demandScaled, timeVector, interpolation);
341     }
342 
343     /**
344      * Add a demand vector to OD, by a fraction per time period of total demand.
345      * @param origin Node; origin
346      * @param destination Node; destination
347      * @param category Category; category
348      * @param demand FrequencyVector; demand data, length has to be equal to the time vector
349      * @param timeVector TimeVector; time vector
350      * @param interpolation Interpolation; interpolation
351      * @param fraction double[]; fraction of demand for this category
352      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
353      * @throws IllegalArgumentException if the category does not belong to the categorization
354      * @throws IllegalArgumentException if the demand data has a different length than time data, or is less than 2
355      * @throws IllegalArgumentException if demand is negative or time not strictly increasing
356      * @throws IllegalArgumentException if the route (if in the category) is not from the origin to the destination
357      * @throws NullPointerException if an input is null
358      */
359     public final void putDemandVector(final Node origin, final Node destination, final Category category,
360             final FrequencyVector demand, final TimeVector timeVector, final Interpolation interpolation,
361             final double[] fraction)
362     {
363         Throw.whenNull(demand, "Demand data may not be null.");
364         Throw.whenNull(fraction, "Fraction data may not be null.");
365         Throw.whenNull(timeVector, "Time vector may not be null.");
366         Throw.when(demand.size() != timeVector.size() || timeVector.size() != fraction.length, IllegalArgumentException.class,
367                 "Arrays are of unequal length: demand=%d, timeVector=%d, fraction=%d", demand.size(), timeVector.size(),
368                 fraction.length);
369         double[] in = demand.getValuesInUnit();
370         double[] scaled = new double[in.length];
371         for (int i = 0; i < in.length; i++)
372         {
373             scaled[i] = in[i] * fraction[i];
374         }
375         FrequencyVector demandScaled;
376         try
377         {
378             demandScaled = new FrequencyVector(scaled, demand.getUnit(), demand.getStorageType());
379         }
380         catch (ValueException exception)
381         {
382             // cannot happen, we use an existing vector
383             throw new RuntimeException("An object was null.", exception);
384         }
385         putDemandVector(origin, destination, category, demandScaled, timeVector, interpolation);
386     }
387 
388     /**
389      * @param origin Node; origin
390      * @param destination Node; destination
391      * @param category Category; category
392      * @return demand data for given origin, destination and categorization, {@code null} if no data is given
393      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
394      * @throws IllegalArgumentException if the category does not belong to the categorization
395      * @throws NullPointerException if an input is null
396      */
397     public final FrequencyVector getDemandVector(final Node origin, final Node destination, final Category category)
398     {
399         DemandPattern demandPattern = getDemandPattern(origin, destination, category);
400         if (demandPattern == null)
401         {
402             return null;
403         }
404         return demandPattern.getDemandVector();
405     }
406 
407     /**
408      * @param origin Node; origin
409      * @param destination Node; destination
410      * @param category Category; category
411      * @return interpolation for given origin, destination and categorization, {@code null} if no data is given
412      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
413      * @throws IllegalArgumentException if the category does not belong to the categorization
414      * @throws NullPointerException if an input is null
415      */
416     public final TimeVector getTimeVector(final Node origin, final Node destination, final Category category)
417     {
418         DemandPattern demandPattern = getDemandPattern(origin, destination, category);
419         if (demandPattern == null)
420         {
421             return null;
422         }
423         return demandPattern.getTimeVector();
424     }
425 
426     /**
427      * @param origin Node; origin
428      * @param destination Node; destination
429      * @param category Category; category
430      * @return interpolation for given origin, destination and categorization, {@code null} if no data is given
431      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
432      * @throws IllegalArgumentException if the category does not belong to the categorization
433      * @throws NullPointerException if an input is null
434      */
435     public final Interpolation getInterpolation(final Node origin, final Node destination, final Category category)
436     {
437         DemandPattern demandPattern = getDemandPattern(origin, destination, category);
438         if (demandPattern == null)
439         {
440             return null;
441         }
442         return demandPattern.getInterpolation();
443     }
444 
445     /**
446      * Returns the demand at given time. If given time is before the first time slice or after the last time slice, 0 demand is
447      * returned.
448      * @param origin Node; origin
449      * @param destination Node; destination
450      * @param category Category; category
451      * @param time Time; time
452      * @param sliceStart boolean; whether the time is at the start of an arbitrary time slice
453      * @return demand for given origin, destination and categorization, at given time
454      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
455      * @throws IllegalArgumentException if the category does not belong to the categorization
456      * @throws NullPointerException if an input is null
457      */
458     public final Frequency getDemand(final Node origin, final Node destination, final Category category, final Time time,
459             final boolean sliceStart)
460     {
461         Throw.whenNull(time, "Time may not be null.");
462         DemandPattern demandPattern = getDemandPattern(origin, destination, category);
463         if (demandPattern == null)
464         {
465             return new Frequency(0.0, FrequencyUnit.PER_HOUR); // Frequency.ZERO gives "Hz" which is not nice for flow
466         }
467         return demandPattern.getFrequency(time, sliceStart);
468     }
469 
470     /**
471      * @param origin Node; origin
472      * @param destination Node; destination
473      * @param category Category; category
474      * @return OD entry for given origin, destination and categorization.
475      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
476      * @throws IllegalArgumentException if the category does not belong to the categorization
477      * @throws NullPointerException if an input is null
478      */
479     public DemandPattern getDemandPattern(final Node origin, final Node destination, final Category category)
480     {
481         Throw.whenNull(origin, "Origin may not be null.");
482         Throw.whenNull(destination, "Destination may not be null.");
483         Throw.whenNull(category, "Category may not be null.");
484         Throw.when(!this.origins.contains(origin), IllegalArgumentException.class, "Origin '%s' is not part of the OD matrix",
485                 origin);
486         Throw.when(!this.destinations.contains(destination), IllegalArgumentException.class,
487                 "Destination '%s' is not part of the OD matrix.", destination);
488         Throw.when(!this.categorization.equals(category.getCategorization()), IllegalArgumentException.class,
489                 "Provided category %s does not belong to the categorization %s.", category, this.categorization);
490         return this.demandData.get(origin).get(destination).get(category);
491     }
492 
493     /**
494      * @param origin Node; origin
495      * @param destination Node; destination
496      * @param category Category; category
497      * @return whether there is data for the specified origin, destination and category
498      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
499      * @throws IllegalArgumentException if the category does not belong to the categorization
500      * @throws NullPointerException if an input is null
501      */
502     public final boolean contains(final Node origin, final Node destination, final Category category)
503     {
504         return getDemandPattern(origin, destination, category) != null;
505     }
506 
507     /**
508      * Returns the categories specified for given origin-destination combination.
509      * @param origin Node; origin
510      * @param destination Node; destination
511      * @return categories specified for given origin-destination combination
512      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
513      * @throws NullPointerException if an input is null
514      */
515     public final Set<Category> getCategories(final Node origin, final Node destination)
516     {
517         Throw.whenNull(origin, "Origin may not be null.");
518         Throw.whenNull(destination, "Destination may not be null.");
519         Throw.when(!this.origins.contains(origin), IllegalArgumentException.class, "Origin '%s' is not part of the OD matrix",
520                 origin);
521         Throw.when(!this.destinations.contains(destination), IllegalArgumentException.class,
522                 "Destination '%s' is not part of the OD matrix.", destination);
523         return new LinkedHashSet<>(this.demandData.get(origin).get(destination).keySet());
524     }
525 
526     /******************************************************************************************************/
527     /****************************************** TRIP METHODS **********************************************/
528     /******************************************************************************************************/
529 
530     /**
531      * @param origin Node; origin
532      * @param destination Node; destination
533      * @param category Category; category
534      * @param trips int[]; trip data, length has to be equal to the global time vector - 1, each value is the number of trips
535      *            during a period
536      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
537      * @throws IllegalArgumentException if the category does not belong to the categorization
538      * @throws IllegalArgumentException if the demand data has a different length than time data, or is less than 2
539      * @throws IllegalArgumentException if demand is negative or time not strictly increasing
540      * @throws IllegalArgumentException if the route (if in the category) is not from the origin to the destination
541      * @throws NullPointerException if an input is null
542      */
543     public final void putTripsVector(final Node origin, final Node destination, final Category category, final int[] trips)
544     {
545         putTripsVector(origin, destination, category, trips, getGlobalTimeVector());
546     }
547 
548     /**
549      * Sets demand data by number of trips. Interpolation over time is stepwise.
550      * @param origin Node; origin
551      * @param destination Node; destination
552      * @param category Category; category
553      * @param trips int[]; trip data, length has to be equal to the time vector - 1, each value is the number of trips during a
554      *            period
555      * @param timeVector TimeVector; time vector
556      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
557      * @throws IllegalArgumentException if the category does not belong to the categorization
558      * @throws IllegalArgumentException if the demand data has a different length than time data, or is less than 2
559      * @throws IllegalArgumentException if demand is negative or time not strictly increasing
560      * @throws IllegalArgumentException if the route (if in the category) is not from the origin to the destination
561      * @throws NullPointerException if an input is null
562      */
563     public final void putTripsVector(final Node origin, final Node destination, final Category category, final int[] trips,
564             final TimeVector timeVector)
565     {
566         // this is what we need here, other checks in putDemandVector
567         Throw.whenNull(trips, "Demand data may not be null.");
568         Throw.whenNull(timeVector, "Time vector may not be null.");
569         Throw.when(trips.length != timeVector.size() - 1, IllegalArgumentException.class,
570                 "Trip data and time data have wrong lengths. Trip data should be 1 shorter than time data.");
571         // convert to flow
572         double[] flow = new double[timeVector.size()];
573         try
574         {
575             for (int i = 0; i < trips.length; i++)
576             {
577                 flow[i] = trips[i] / (timeVector.get(i + 1).getInUnit(TimeUnit.BASE_HOUR)
578                         - timeVector.get(i).getInUnit(TimeUnit.BASE_HOUR));
579             }
580             // last value can remain zero as initialized
581             putDemandVector(origin, destination, category, new FrequencyVector(flow, FrequencyUnit.PER_HOUR, StorageType.DENSE),
582                     timeVector, Interpolation.STEPWISE);
583         }
584         catch (ValueException exception)
585         {
586             // should not happen as we check and then loop over the array length
587             throw new RuntimeException("Could not translate trip vector into demand vector.", exception);
588         }
589     }
590 
591     /**
592      * @param origin Node; origin
593      * @param destination Node; destination
594      * @param category Category; category
595      * @return trip data for given origin, destination and categorization, {@code null} if no data is given
596      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
597      * @throws IllegalArgumentException if the category does not belong to the categorization
598      * @throws NullPointerException if an input is null
599      */
600     public final int[] getTripsVector(final Node origin, final Node destination, final Category category)
601     {
602         FrequencyVector demand = getDemandVector(origin, destination, category);
603         if (demand == null)
604         {
605             return null;
606         }
607         int[] trips = new int[demand.size() - 1];
608         TimeVector time = getTimeVector(origin, destination, category);
609         Interpolation interpolation = getInterpolation(origin, destination, category);
610         for (int i = 0; i < trips.length; i++)
611         {
612             try
613             {
614                 trips[i] = interpolation.integrate(demand.get(i), time.get(i), demand.get(i + 1), time.get(i + 1));
615             }
616             catch (ValueException exception)
617             {
618                 // should not happen as we loop over the array length
619                 throw new RuntimeException("Could not translate demand vector into trip vector.", exception);
620             }
621         }
622         return trips;
623     }
624 
625     /**
626      * Returns the number of trips in the given time period.
627      * @param origin Node; origin
628      * @param destination Node; destination
629      * @param category Category; category
630      * @param periodIndex int; index of time period
631      * @return demand for given origin, destination and categorization, at given time
632      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
633      * @throws IllegalArgumentException if the category does not belong to the categorization
634      * @throws IndexOutOfBoundsException if the period is outside of the specified range
635      * @throws NullPointerException if an input is null
636      */
637     public final int getTrips(final Node origin, final Node destination, final Category category, final int periodIndex)
638     {
639         TimeVector time = getTimeVector(origin, destination, category);
640         if (time == null)
641         {
642             return 0;
643         }
644         Throw.when(periodIndex < 0 || periodIndex >= time.size() - 1, IndexOutOfBoundsException.class,
645                 "Period index out of range.");
646         FrequencyVector demand = getDemandVector(origin, destination, category);
647         Interpolation interpolation = getInterpolation(origin, destination, category);
648         try
649         {
650             return interpolation.integrate(demand.get(periodIndex), time.get(periodIndex), demand.get(periodIndex + 1),
651                     time.get(periodIndex + 1));
652         }
653         catch (ValueException exception)
654         {
655             // should not happen as the index was checked
656             throw new RuntimeException("Could not get number of trips.", exception);
657         }
658     }
659 
660     /**
661      * Adds a number of trips to given origin-destination combination, category and time period. This can only be done for data
662      * with stepwise interpolation.
663      * @param origin Node; origin
664      * @param destination Node; destination
665      * @param category Category; category
666      * @param periodIndex int; index of time period
667      * @param trips int; trips to add (may be negative)
668      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
669      * @throws IllegalArgumentException if the category does not belong to the categorization
670      * @throws IndexOutOfBoundsException if the period is outside of the specified range
671      * @throws UnsupportedOperationException if the interpolation of the data is not stepwise, or demand becomes negtive
672      * @throws NullPointerException if an input is null
673      */
674     public final void increaseTrips(final Node origin, final Node destination, final Category category, final int periodIndex,
675             final int trips)
676     {
677         Interpolation interpolation = getInterpolation(origin, destination, category);
678         Throw.when(!interpolation.equals(Interpolation.STEPWISE), UnsupportedOperationException.class,
679                 "Can only increase the number of trips for data with stepwise interpolation.");
680         TimeVector time = getTimeVector(origin, destination, category);
681         Throw.when(periodIndex < 0 || periodIndex >= time.size() - 1, IndexOutOfBoundsException.class,
682                 "Period index out of range.");
683         FrequencyVector demand = getDemandVector(origin, destination, category);
684         try
685         {
686             double additionalDemand = trips / (time.get(periodIndex + 1).getInUnit(TimeUnit.BASE_HOUR)
687                     - time.get(periodIndex).getInUnit(TimeUnit.BASE_HOUR));
688             double[] dem = demand.getValuesInUnit(FrequencyUnit.PER_HOUR);
689             Throw.when(dem[periodIndex] < -additionalDemand, UnsupportedOperationException.class,
690                     "Demand may not become negative.");
691             dem[periodIndex] += additionalDemand;
692             putDemandVector(origin, destination, category, new FrequencyVector(dem, FrequencyUnit.PER_HOUR, StorageType.DENSE),
693                     time, Interpolation.STEPWISE);
694         }
695         catch (ValueException exception)
696         {
697             // should not happen as the index was checked
698             throw new RuntimeException("Unexpected exception while getting number of trips.", exception);
699         }
700     }
701 
702     /**
703      * Calculates total number of trips over time for given origin.
704      * @param origin Node; origin
705      * @return total number of trips over time for given origin
706      * @throws IllegalArgumentException if origin is not part of the OD matrix
707      * @throws NullPointerException if origin is null
708      */
709     public final int originTotal(final Node origin)
710     {
711         int sum = 0;
712         for (Node destination : getDestinations())
713         {
714             sum += originDestinationTotal(origin, destination);
715         }
716         return sum;
717     }
718 
719     /**
720      * Calculates total number of trips over time for given destination.
721      * @param destination Node; destination
722      * @return total number of trips over time for given destination
723      * @throws IllegalArgumentException if destination is not part of the OD matrix
724      * @throws NullPointerException if destination is null
725      */
726     public final int destinationTotal(final Node destination)
727     {
728         int sum = 0;
729         for (Node origin : getOrigins())
730         {
731             sum += originDestinationTotal(origin, destination);
732         }
733         return sum;
734     }
735 
736     /**
737      * Calculates total number of trips over time for the complete matrix.
738      * @return total number of trips over time for the complete matrix
739      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
740      * @throws NullPointerException if an input is null
741      */
742     public final int matrixTotal()
743     {
744         int sum = 0;
745         for (Node origin : getOrigins())
746         {
747             for (Node destination : getDestinations())
748             {
749                 sum += originDestinationTotal(origin, destination);
750             }
751         }
752         return sum;
753     }
754 
755     /**
756      * Calculates total number of trips over time for given origin-destination combination.
757      * @param origin Node; origin
758      * @param destination Node; destination
759      * @return total number of trips over time for given origin-destination combination
760      * @throws IllegalArgumentException if origin or destination is not part of the OD matrix
761      * @throws NullPointerException if an input is null
762      */
763     public final int originDestinationTotal(final Node origin, final Node destination)
764     {
765         int sum = 0;
766         for (Category category : getCategories(origin, destination))
767         {
768             TimeVector time = getTimeVector(origin, destination, category);
769             FrequencyVector demand = getDemandVector(origin, destination, category);
770             Interpolation interpolation = getInterpolation(origin, destination, category);
771             for (int i = 0; i < time.size() - 1; i++)
772             {
773                 try
774                 {
775                     sum += interpolation.integrate(demand.get(i), time.get(i), demand.get(i + 1), time.get(i + 1));
776                 }
777                 catch (ValueException exception)
778                 {
779                     // should not happen as we loop over the array length
780                     throw new RuntimeException("Unexcepted exception while determining total trips over time.", exception);
781                 }
782             }
783         }
784         return sum;
785     }
786 
787     /******************************************************************************************************/
788     /****************************************** OTHER METHODS *********************************************/
789     /******************************************************************************************************/
790 
791     /** {@inheritDoc} */
792     @Override
793     @SuppressWarnings("checkstyle:designforextension")
794     public String toString()
795     {
796         return "ODMatrix [" + this.id + ", " + this.origins.size() + " origins, " + this.destinations.size() + " destinations, "
797                 + this.categorization + " ]";
798     }
799 
800     /**
801      * Prints the complete OD matrix with each demand data on a single line.
802      */
803     public final void print()
804     {
805         int originLength = 0;
806         for (Node origin : this.origins)
807         {
808             originLength = originLength >= origin.getId().length() ? originLength : origin.getId().length();
809         }
810         int destinLength = 0;
811         for (Node destination : this.destinations)
812         {
813             destinLength = destinLength >= destination.getId().length() ? destinLength : destination.getId().length();
814         }
815         String format = "%-" + Math.max(originLength, 1) + "s -> %-" + Math.max(destinLength, 1) + "s | ";
816         for (Node origin : this.origins)
817         {
818             Map<Node, Map<Category, DemandPattern>> destinationMap = this.demandData.get(origin);
819             for (Node destination : this.destinations)
820             {
821                 Map<Category, DemandPattern> categoryMap = destinationMap.get(destination);
822                 if (categoryMap.isEmpty())
823                 {
824                     System.out.println(String.format(format, origin.getId(), destination.getId()) + "-no data-");
825                 }
826                 else
827                 {
828                     for (Category category : categoryMap.keySet())
829                     {
830                         StringBuilder catStr = new StringBuilder("[");
831                         String sep = "";
832                         for (int i = 0; i < category.getCategorization().size(); i++)
833                         {
834                             catStr.append(sep);
835                             Object obj = category.get(i);
836                             if (obj instanceof Route)
837                             {
838                                 catStr.append("Route: " + ((Route) obj).getId());
839                             }
840                             else
841                             {
842                                 catStr.append(obj);
843                             }
844                             sep = ", ";
845                         }
846                         catStr.append("]");
847                         // System.out.println("DEBUG format is \"" + format + "\"");
848                         System.out.println(String.format(format, origin.getId(), destination.getId()) + catStr + " | "
849                                 + categoryMap.get(category).getDemandVector());
850                     }
851                 }
852             }
853         }
854     }
855 
856     /** {@inheritDoc} */
857     @Override
858     public final int hashCode()
859     {
860         final int prime = 31;
861         int result = 1;
862         result = prime * result + ((this.categorization == null) ? 0 : this.categorization.hashCode());
863         result = prime * result + ((this.demandData == null) ? 0 : this.demandData.hashCode());
864         result = prime * result + ((this.destinations == null) ? 0 : this.destinations.hashCode());
865         result = prime * result + ((this.globalInterpolation == null) ? 0 : this.globalInterpolation.hashCode());
866         result = prime * result + ((this.globalTimeVector == null) ? 0 : this.globalTimeVector.hashCode());
867         result = prime * result + ((this.id == null) ? 0 : this.id.hashCode());
868         result = prime * result + ((this.origins == null) ? 0 : this.origins.hashCode());
869         return result;
870     }
871 
872     /** {@inheritDoc} */
873     @Override
874     public final boolean equals(final Object obj)
875     {
876         if (this == obj)
877         {
878             return true;
879         }
880         if (obj == null)
881         {
882             return false;
883         }
884         if (getClass() != obj.getClass())
885         {
886             return false;
887         }
888         ODMatrix other = (ODMatrix) obj;
889         if (this.categorization == null)
890         {
891             if (other.categorization != null)
892             {
893                 return false;
894             }
895         }
896         else if (!this.categorization.equals(other.categorization))
897         {
898             return false;
899         }
900         if (this.demandData == null)
901         {
902             if (other.demandData != null)
903             {
904                 return false;
905             }
906         }
907         else if (!this.demandData.equals(other.demandData))
908         {
909             return false;
910         }
911         if (this.destinations == null)
912         {
913             if (other.destinations != null)
914             {
915                 return false;
916             }
917         }
918         else if (!this.destinations.equals(other.destinations))
919         {
920             return false;
921         }
922         if (this.globalInterpolation != other.globalInterpolation)
923         {
924             return false;
925         }
926         if (this.globalTimeVector == null)
927         {
928             if (other.globalTimeVector != null)
929             {
930                 return false;
931             }
932         }
933         else if (!this.globalTimeVector.equals(other.globalTimeVector))
934         {
935             return false;
936         }
937         if (this.id == null)
938         {
939             if (other.id != null)
940             {
941                 return false;
942             }
943         }
944         else if (!this.id.equals(other.id))
945         {
946             return false;
947         }
948         if (this.origins == null)
949         {
950             if (other.origins != null)
951             {
952                 return false;
953             }
954         }
955         else if (!this.origins.equals(other.origins))
956         {
957             return false;
958         }
959         return true;
960     }
961 
962 }