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