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