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