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