# This file is part of NIT ( http://www.nitlanguage.org ). # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # Register, update and discard events in a timeline. # # A queue of event is used to register objects associated with a start time and # a duration and to return them when they are active. # # The main class is `EventQueue`. It controls a timeline and register events. # Event can be any kind of object, usually specific time-related domain objects should be used. # # The auxiliary class `EventInfo` registers and control information about each event. # It can also be used for fluent programming and create new events relatively to existing ones. # # ## Basic usage # # The event queue is created empty, and can handle any kind of data. # # ~~~ # var eq = new EventQueue[String] # ~~~ # # To register an event, add it with a start time and a duration. # Note that the time is relative to the current time frame in the queue. # # ~~~ # eq.add("1..2", 1.0, 1.0) # eq.add("2..10", 2.0, 8.0) # ~~~ # # To register instantaneous event, use a 0.0 duration (or no duration) # # ~~~ # eq.add("1.5", 1.5) # ~~~ # # To get active events, use `update` with a time difference. # This returns a sequence of currently active events. # # ~~~ # var es = eq.update(1.0) # What is active after 1 unit of time? # assert es.length == 1 # only "1..2" is active # ~~~ # # Each `update` increments the time frame of the queue and returns an updated information about active events. # # ~~~ # es = eq.update(1.0) # What is active after an other unit of time? # assert eq.time == 2.0 # # assert es.length == 3 # there is now 3 active events # ~~~ # # Results of the `update` method contains a lot of information. # # Obviously the original events: # # ~~~ # assert es[0].event == "1..2" # assert es[1].event == "1.5" # assert es[2].event == "2..10" # ~~~ # # The time since the begin of the event: # # ~~~ # assert es[0].time == 1.0 # Started 1 unit of time ago # assert es[1].time == 0.5 # assert es[2].time == 0.0 # Started just now # ~~~ # # The completion ratio of the event until its termination: # # ~~~ # assert es[0].completion == 1.0 # has just finished # assert es[1].completion == inf # completion ratio does not make sense for instant events # assert es[2].completion == 0.0 # has just started # ~~~ # # And a lot of other things, just look at `EventInfo`. # # ## Expiration and control # # When `update` returns events, `occurrences` count the number of time an # event is returned by update. It can be used for instance to see if it # is the first time that a specific event is active. # # ~~~ # assert es[0].occurrences == 2 # it is the second time we see it # assert es[1].occurrences == 1 # instant one # assert es[2].occurrences == 1 # first time we see it # ~~~ # # The duration is used to automatically manage the expiration of the event. # # Note that if a event expires during an `update`, then it is still returned by the function. # This is the way to handle instant events and newly expired events. # # To distinguish them `has_expired` can be used. # # ~~~ # assert es[0].has_expired == true # just finished # assert es[1].has_expired == true # instant one # assert es[2].has_expired == false # sill active # ~~~ # # On the next `update`, the already expired events are not returned again. # # ~~~ # es = eq.update(1.0) # What is active after an other unit of time? # assert eq.time == 3.0 # assert es.length == 1 # assert es.first.event == "2..10" # No more "1..2" or "1.5" # ~~~ # # The `expire` method can force the expiration of an event. # # ~~~ # es.first.expire # es = eq.update(1.0) # assert es.is_empty # Sorry, "2..10" was expired # ~~~ # # ## Fluent programming # # The `add` method (and its derivates) returns a `EventInfo` object that can be used to have # information about the newly registered event but also to chain the creation of time-related events. # # For instance, the following example registers 4 events, each one relative to the previous one: # # ~~~ # eq = new EventQueue[String] # eq.add("e1", 10.0, 5.0). # add_after("e2", 2.0, 5.0). # add_sync("e3", 2.0, 5.0). # add_before("e4", 2.0, 5.0) # ~~~ # # This can be decomposed as: # # ~~~ # eq = new EventQueue[String] # var e1 = eq.add("e1", 10.0, 5.0) # assert e1.start == 10.0 # First event starts at 10 # # var e2 = e1.add_after("e2", 2.0, 5.0) # starts 2 units of time after the end of e1 # assert e2.start == 17.0 # So starts at 10 + 5 + 2 # # var e3 = e2.add_sync("e3", 2.0, 5.0) # starts 2 units of time after the begin of e2 # assert e3.start == 19.0 # So starts at 17 + 2 # # var e4 = e3.add_before("e4", 2.0, 5.0) # ends 2 units of time before the start of e3 # assert e4.start == 12.0 # So starts at 19 - 2 - 5 # ~~~ module event_queue # Queuing and management of arbitrary events in a timeline class EventQueue[E] # Comparator used by the queue private var event_comparator = new EventComparator # Efficient queue private var queue = new MinHeap[EventInfo[E]](event_comparator) # List of current active event private var actives = new Array[EventInfo[E]] # List of previously active event private var old_actives = new Array[EventInfo[E]] # Global time since the creation of the queue var time = 0.0 # Nearest deadline, used to optimise queue access # # `inf` if no deadline is set private var next: Float = inf # Register an `event` in the queue to be active in `delay` units of time. # # If `duration` is given, this is duration after what the event is automatically discarded. # If `null`, 0.0 or not given, the event will be executed only once # Use `inf` for an event with an infinite duration. fun add(event: E, delay: Float, duration: nullable Float): EventInfo[E] do return add_at(event, time + delay, duration) end # Register an `event` in the queue executed at a specific time. fun add_at(event: E, start: Float, duration: nullable Float): EventInfo[E] do if start < next then next = start var ei = new EventInfo[E](event, self, start, duration or else 0.0) queue.add ei return ei end # Add a given amount of time and return all the events that were active during the delay. # # Note that events that expired during the delay are marked `has_expired` and are still returned. fun update(dt: Float): SequenceRead[EventInfo[E]] do var time = self.time time += dt self.time = time # Switch things var tmp = old_actives old_actives = actives actives = tmp # Discard dead events actives.clear for ei in old_actives do if not ei.has_expired then actives.add ei end # Start new events if time >= next then loop if queue.is_empty then next = inf break end var ei = queue.peek if ei.start > time then next = ei.start break end ei = queue.take if not ei.has_expired then actives.add ei ei.occurrences = 0 end end if actives.is_empty then return actives # Update event information for ei in actives do ei.update(time, dt) return actives end end # Information and management about a registered event # # It allows to retrieve static and dynamic information about an event. # It also allows to register other time-related events with a fluent programming way. class EventInfo[E] # The registered event. var event: E # The associated event queue. # # It is used internally for fluent programming. var queue: EventQueue[E] # Absolute start time for the registered event in the event queue time frame. var start: Float # Registered duration. # # Events with a 0.0 duration are called *instant events*. var duration: Float # Time since the begin of the event, in units of time. var time: Float = nan # Time since the last update (or the begin of the event if smaller) # # Note that if the event has expired during the last time lapse, # then `dt` also counts the *expired* time between the expiration time # and the update time. var dt: Float = nan # Ratio of completion # # Usually between 0.0 and 1. # It might be > 1.0 if the event has expired during the last time lapse. var completion = 0.0 # Number of times that an event was returned by `update`. # # If the event just started during the last update, 1 is returned. # # If the event was not returned by `update` yet, 0 is returned. var occurrences = 0 # Has the event expired? # # Such an event will be not present in the next `update`. # # Note that an event can be `has_expired` and have `occurrences == 0` if it # is entirely included in the last time lapse. # It is especially the case for instant events. var has_expired = false ## Fluent methods # Register an event that starts after the end of the current one. # # `delay` indicates the time between the end of the current event and the begin of the new one. # Use 0.0 if both events should be contiguous and not overlap. # # Returns the new event information that can be used in fluent programming. fun add_after(event: E, delay: Float, duration: nullable Float): EventInfo[E] do return queue.add_at(event, start + self.duration + delay, duration) end # Register an event that starts with the current one. # # `delay` indicates the time between the begin of the current event and the begin of the new one. # Use 0.0 if both events should start at the same time and overlaps. # # Returns the new event information that can be used in fluent programming. fun add_sync(event: E, delay: Float, duration: nullable Float): EventInfo[E] do return queue.add_at(event, start + delay, duration) end # Register an event that finishes before the begin of current one. # # `delay` indicates the time between the end of the new event and the begin of the current one. # Use 0.0 if both event should be contiguous and not overlap. # # Returns the new event information that can be used in fluent programming. fun add_before(event: E, delay: Float, duration: nullable Float): EventInfo[E] do duration = duration or else 0.0 return queue.add_at(event, start - delay - duration, duration) end # Update attributes. Is called by `EventQueue::update` private fun update(queue_time, queue_dt: Float) do time = queue_time - start if time >= duration then expire dt = queue_dt.min(time) completion = time / duration occurrences += 1 end # Force the event to expire. # # The event can be active of not. # # ~~~ # var eq = new EventQueue[String] # var e1 = eq.add("active", 0.0, 10.0) # var e2 = eq.add("not active", 2.0, 10.0) # var es = eq.update(1.0) # assert es == [e1] # e1.expire # e2.expire # es = eq.update(2.0) # assert es.is_empty # ~~~ # # Note that when an event is forced to expire, # it will not appears in the next `update`. fun expire do has_expired = true redef fun to_s do return "{event or else "null"}@{start}+{duration}" end private class EventComparator super Comparator redef type COMPARED: EventInfo[nullable Object] redef fun compare(a, b) do return a.start <=> b.start end