View Javadoc
1   package org.opentrafficsim.kpi.sampling;
2   
3   import java.util.ArrayList;
4   import java.util.Iterator;
5   import java.util.LinkedHashMap;
6   import java.util.LinkedHashSet;
7   import java.util.List;
8   import java.util.Map;
9   import java.util.Map.Entry;
10  import java.util.Set;
11  import java.util.UUID;
12  
13  import org.djunits.value.vdouble.scalar.Duration;
14  import org.djunits.value.vdouble.scalar.Frequency;
15  import org.djunits.value.vdouble.scalar.Length;
16  import org.djutils.base.Identifiable;
17  import org.djutils.exceptions.Throw;
18  import org.djutils.immutablecollections.ImmutableIterator;
19  import org.opentrafficsim.kpi.interfaces.GtuData;
20  import org.opentrafficsim.kpi.interfaces.LaneData;
21  import org.opentrafficsim.kpi.interfaces.LinkData;
22  import org.opentrafficsim.kpi.sampling.filter.FilterDataSet;
23  import org.opentrafficsim.kpi.sampling.filter.FilterDataType;
24  
25  /**
26   * A query defines which subset of trajectory information should be included. This is in terms of space-time regions, and in
27   * terms of filter data of trajectories, e.g. only include trajectories of trucks.
28   * <p>
29   * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
30   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
31   * </p>
32   * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
33   * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
34   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
35   * @param <G> GTU data type
36   * @param <L> lane data type
37   */
38  public final class Query<G extends GtuData, L extends LaneData<L>> implements Identifiable
39  {
40  
41      /** unique id. */
42      private final String id;
43  
44      /** Sampling. */
45      private final Sampler<G, L> sampler;
46  
47      /** Description. */
48      private final String description;
49  
50      /** Filter data set. */
51      private final FilterDataSet filterDataSet;
52  
53      /** Update frequency. */
54      private final Frequency updateFrequency;
55  
56      /** Interval to gather statistics over. */
57      private final Duration interval;
58  
59      /** List of space-time regions of this query. */
60      private final List<SpaceTimeRegion<? extends L>> spaceTimeRegions = new ArrayList<>();
61  
62      /**
63       * Constructor.
64       * @param sampler sampler
65       * @param id id
66       * @param description description
67       * @param filterDataSet filter data
68       * @throws NullPointerException if sampling, description or filterDataSet is null
69       */
70      public Query(final Sampler<G, L> sampler, final String id, final String description, final FilterDataSet filterDataSet)
71      {
72          this(sampler, id, description, filterDataSet, null, null);
73      }
74  
75      /**
76       * Constructor with time interval and update frequency.
77       * @param sampler sampler
78       * @param id id, may be {@code null}
79       * @param description description
80       * @param filterDataSet filter data
81       * @param updateFrequency update frequency, used by external controller, may be {@code null}
82       * @param interval interval to gather statistics over, used by external controller, may be {@code null}
83       * @throws NullPointerException if sampling, description or filterDataSet is {@code null}
84       */
85      public Query(final Sampler<G, L> sampler, final String id, final String description, final FilterDataSet filterDataSet,
86              final Frequency updateFrequency, final Duration interval)
87      {
88          Throw.whenNull(sampler, "Sampling may not be null.");
89          Throw.whenNull(description, "Description may not be null.");
90          Throw.whenNull(filterDataSet, "Meta data may not be null.");
91          this.sampler = sampler;
92          this.filterDataSet = new FilterDataSet(filterDataSet);
93          this.id = id == null ? UUID.randomUUID().toString() : id;
94          this.description = description;
95          this.updateFrequency = updateFrequency;
96          this.interval = interval;
97      }
98  
99      @Override
100     public String getId()
101     {
102         return this.id.toString();
103     }
104 
105     /**
106      * Returns the description.
107      * @return description
108      */
109     public String getDescription()
110     {
111         return this.description;
112     }
113 
114     /**
115      * Returns the update frequency.
116      * @return updateFrequency.
117      */
118     public Frequency getUpdateFrequency()
119     {
120         return this.updateFrequency;
121     }
122 
123     /**
124      * Returns the time interval.
125      * @return interval.
126      */
127     public Duration getInterval()
128     {
129         return this.interval;
130     }
131 
132     /**
133      * Returns the number of filter datas.
134      * @return number of filter data entries
135      */
136     public int filterSize()
137     {
138         return this.filterDataSet.size();
139     }
140 
141     /**
142      * Returns an iterator over the filter datas and the related data sets.
143      * @return iterator over filter data entries, removal is not allowed
144      */
145     public Iterator<Entry<FilterDataType<?, ?>, Set<?>>> getFilterDataSetIterator()
146     {
147         return this.filterDataSet.getFilterDataSetIterator();
148     }
149 
150     /**
151      * Defines a region in space and time for which this query is valid. All lanes in the link are included.
152      * @param link link
153      * @param startPosition start position
154      * @param endPosition end position
155      * @param startTime start time
156      * @param endTime end time
157      */
158     public void addSpaceTimeRegionLink(final LinkData<? extends L> link, final Length startPosition, final Length endPosition,
159             final Duration startTime, final Duration endTime)
160     {
161         Throw.whenNull(link, "Link may not be null.");
162         Throw.whenNull(startPosition, "Start position may not be null.");
163         Throw.whenNull(endPosition, "End position may not be null.");
164         Throw.whenNull(startTime, "Start time may not be null.");
165         Throw.whenNull(endTime, "End time may not be null.");
166         Throw.when(endPosition.lt(startPosition), IllegalArgumentException.class,
167                 "End position should be greater than start position.");
168         Throw.when(endTime.lt(startTime), IllegalArgumentException.class, "End time should be greater than start time.");
169         double fStart = startPosition.si / link.getLength().si;
170         double fEnd = endPosition.si / link.getLength().si;
171         for (L lane : link.getLanes())
172         {
173             Length x0 = lane.getLength().times(fStart);
174             Length x1 = lane.getLength().times(fEnd);
175             addSpaceTimeRegion(lane, x0, x1, startTime, endTime);
176         }
177     }
178 
179     /**
180      * Defines a region in space and time for which this query is valid.
181      * @param lane lane
182      * @param startPosition start position
183      * @param endPosition end position
184      * @param startTime start time
185      * @param endTime end time
186      */
187     public void addSpaceTimeRegion(final L lane, final Length startPosition, final Length endPosition, final Duration startTime,
188             final Duration endTime)
189     {
190         Throw.whenNull(lane, "Lane direction may not be null.");
191         Throw.whenNull(startPosition, "Start position may not be null.");
192         Throw.whenNull(endPosition, "End position may not be null.");
193         Throw.whenNull(startTime, "Start time may not be null.");
194         Throw.whenNull(endTime, "End time may not be null.");
195         Throw.when(endPosition.lt(startPosition), IllegalArgumentException.class,
196                 "End position should be greater than start position.");
197         Throw.when(endTime.lt(startTime), IllegalArgumentException.class, "End time should be greater than start time.");
198         SpaceTimeRegion<L> spaceTimeRegion = new SpaceTimeRegion<>(lane, startPosition, endPosition, startTime, endTime);
199         this.sampler.registerSpaceTimeRegion(spaceTimeRegion);
200         this.spaceTimeRegions.add(spaceTimeRegion);
201     }
202 
203     /**
204      * Returns the number of space-time regions.
205      * @return number of space-time regions
206      */
207     public int spaceTimeRegionSize()
208     {
209         return this.spaceTimeRegions.size();
210     }
211 
212     /**
213      * Returns an iterator over the space-time regions.
214      * @return iterator over space-time regions, removal is not allowed
215      */
216     public Iterator<SpaceTimeRegion<? extends L>> getSpaceTimeIterator()
217     {
218         return new ImmutableIterator<>(this.spaceTimeRegions.iterator());
219     }
220 
221     /**
222      * Returns a list of TrajectoryGroups in accordance with the query. Each {@code TrajectoryGroup} contains {@code Trajectory}
223      * objects pertaining to a {@code SpaceTimeRegion} from the query. A {@code Trajectory} is only included if all the filter
224      * data of this query accepts the trajectory. This method uses {@code Time.ZERO} as start.
225      * @param endTime end time of interval to get trajectory groups for
226      * @param <T> underlying class of filter data type and its value
227      * @return list of trajectory groups in accordance with the query
228      */
229     public <T> List<TrajectoryGroup<G>> getTrajectoryGroups(final Duration endTime)
230     {
231         return getTrajectoryGroups(Duration.ZERO, endTime);
232     }
233 
234     /**
235      * Returns a list of TrajectoryGroups in accordance with the query. Each {@code TrajectoryGroup} contains {@code Trajectory}
236      * objects pertaining to a {@code SpaceTimeRegion} from the query. A {@code Trajectory} is only included if all the filter
237      * data of this query accepts the trajectory.
238      * @param startTime start time of interval to get trajectory groups for
239      * @param endTime start time of interval to get trajectory groups for
240      * @param <T> underlying class of filter data type and its value
241      * @return list of trajectory groups in accordance with the query
242      */
243     @SuppressWarnings("unchecked")
244     public <T> List<TrajectoryGroup<G>> getTrajectoryGroups(final Duration startTime, final Duration endTime)
245     {
246         Throw.whenNull(startTime, "Start t may not be null.");
247         Throw.whenNull(endTime, "End t may not be null.");
248         // Step 1) gather trajectories per GTU, truncated over space and time
249         Map<String, TrajectoryAcceptList> trajectoryAcceptLists = new LinkedHashMap<>();
250         List<TrajectoryGroup<G>> trajectoryGroupList = new ArrayList<>();
251         for (SpaceTimeRegion<? extends L> spaceTimeRegion : this.spaceTimeRegions)
252         {
253             Duration start = startTime.gt(spaceTimeRegion.startTime()) ? startTime : spaceTimeRegion.startTime();
254             Duration end = endTime.lt(spaceTimeRegion.endTime()) ? endTime : spaceTimeRegion.endTime();
255             TrajectoryGroup<G> trajectoryGroup;
256             if (this.sampler.getSamplerData().getTrajectoryGroup(spaceTimeRegion.lane()).isEmpty())
257             {
258                 trajectoryGroup = new TrajectoryGroup<>(start, spaceTimeRegion.lane());
259             }
260             else
261             {
262                 trajectoryGroup = this.sampler.getSamplerData().getTrajectoryGroup(spaceTimeRegion.lane()).get()
263                         .getTrajectoryGroup(spaceTimeRegion.startPosition(), spaceTimeRegion.endPosition(), start, end);
264             }
265             for (Trajectory<G> trajectory : trajectoryGroup.getTrajectories())
266             {
267                 if (!trajectoryAcceptLists.containsKey(trajectory.getGtuId()))
268                 {
269                     trajectoryAcceptLists.put(trajectory.getGtuId(), new TrajectoryAcceptList());
270                 }
271                 trajectoryAcceptLists.get(trajectory.getGtuId()).addTrajectory(trajectory, trajectoryGroup);
272             }
273             trajectoryGroupList.add(trajectoryGroup);
274         }
275         // Step 2) accept per GTU
276         Iterator<String> iterator = trajectoryAcceptLists.keySet().iterator();
277         while (iterator.hasNext())
278         {
279             String gtuId = iterator.next();
280             TrajectoryAcceptList trajectoryAcceptListCombined = trajectoryAcceptLists.get(gtuId);
281             trajectoryAcceptListCombined.acceptAll(); // refuse only if any filter data type refuses
282             for (FilterDataType<?, ?> filterDataType : this.filterDataSet.getFilterDataTypes())
283             {
284                 // create safe copy per filter data type, with defaults accepts = false
285                 TrajectoryAcceptList trajectoryAcceptListCopy = copyTrajectoryAcceptList(trajectoryAcceptLists.get(gtuId));
286                 // request filter data type to accept or reject
287                 ((FilterDataType<T, ?>) filterDataType).accept(trajectoryAcceptListCopy,
288                         (Set<T>) new LinkedHashSet<>(this.filterDataSet.get(filterDataType)));
289                 // combine acceptance/rejection of filter data types so far
290                 for (int i = 0; i < trajectoryAcceptListCopy.size(); i++)
291                 {
292                     Trajectory<?> trajectory = trajectoryAcceptListCopy.getTrajectory(i);
293                     trajectoryAcceptListCombined.acceptTrajectory(trajectory,
294                             trajectoryAcceptListCombined.isAccepted(trajectory)
295                                     && trajectoryAcceptListCopy.isAccepted(trajectory));
296                 }
297             }
298         }
299         // Step 3) filter trajectories
300         List<TrajectoryGroup<G>> out = new ArrayList<>();
301         for (TrajectoryGroup<G> full : trajectoryGroupList)
302         {
303             TrajectoryGroup<G> filtered = new TrajectoryGroup<>(full.getStartTime(), full.getLane());
304             for (Trajectory<G> trajectory : full.getTrajectories())
305             {
306                 String gtuId = trajectory.getGtuId();
307                 if (trajectory.size() > 0 && trajectoryAcceptLists.get(gtuId).isAccepted(trajectory))
308                 {
309                     filtered.addTrajectory(trajectory);
310                 }
311             }
312             out.add(filtered);
313         }
314         return out;
315     }
316 
317     /**
318      * Returns a copy of the trajectory accept list, with all assumed not accepted.
319      * @param trajectoryAcceptList trajectory accept list to copy
320      * @return copy of the trajectory accept list, with all assumed not accepted
321      */
322     private TrajectoryAcceptList copyTrajectoryAcceptList(final TrajectoryAcceptList trajectoryAcceptList)
323     {
324         TrajectoryAcceptList trajectoryAcceptListCopy = new TrajectoryAcceptList();
325         for (int i = 0; i < trajectoryAcceptList.size(); i++)
326         {
327             trajectoryAcceptListCopy.addTrajectory(trajectoryAcceptList.getTrajectory(i),
328                     trajectoryAcceptList.getTrajectoryGroup(i));
329         }
330         return trajectoryAcceptListCopy;
331     }
332 
333     /**
334      * Returns the sampler.
335      * @return sampler.
336      */
337     public Sampler<G, L> getSampler()
338     {
339         return this.sampler;
340     }
341 
342     @Override
343     public int hashCode()
344     {
345         final int prime = 31;
346         int result = 1;
347         result = prime * result + this.description.hashCode();
348         result = prime * result + ((this.interval == null) ? 0 : this.interval.hashCode());
349         result = prime * result + this.filterDataSet.hashCode();
350         result = prime * result + this.sampler.hashCode();
351         result = prime * result + this.spaceTimeRegions.hashCode();
352         result = prime * result + this.id.hashCode();
353         result = prime * result + ((this.updateFrequency == null) ? 0 : this.updateFrequency.hashCode());
354         return result;
355     }
356 
357     @Override
358     public boolean equals(final Object obj)
359     {
360         if (this == obj)
361         {
362             return true;
363         }
364         if (obj == null)
365         {
366             return false;
367         }
368         if (getClass() != obj.getClass())
369         {
370             return false;
371         }
372         Query<?, ?> other = (Query<?, ?>) obj;
373         if (!this.description.equals(other.description))
374         {
375             return false;
376         }
377         if (this.interval == null)
378         {
379             if (other.interval != null)
380             {
381                 return false;
382             }
383         }
384         else if (!this.interval.equals(other.interval))
385         {
386             return false;
387         }
388         if (!this.filterDataSet.equals(other.filterDataSet))
389         {
390             return false;
391         }
392         if (!this.sampler.equals(other.sampler))
393         {
394             return false;
395         }
396         if (!this.spaceTimeRegions.equals(other.spaceTimeRegions))
397         {
398             return false;
399         }
400         if (!this.id.equals(other.id))
401         {
402             return false;
403         }
404         if (this.updateFrequency == null)
405         {
406             if (other.updateFrequency != null)
407             {
408                 return false;
409             }
410         }
411         else if (!this.updateFrequency.equals(other.updateFrequency))
412         {
413             return false;
414         }
415         return true;
416     }
417 
418     @Override
419     public String toString()
420     {
421         return "Query (" + this.description + ")";
422     }
423 
424 }