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