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