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