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