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