# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 Rickard Lindberg, Roger Lindberg
#
# This file is part of Timeline.
#
# Timeline is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Timeline is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Timeline. If not, see <http://www.gnu.org/licenses/>.
[docs]class TimePeriod:
"""
Represents a period in time using a start and end time.
This is used both to store the time period for an event and for storing the
currently displayed time period in the GUI.
"""
[docs] def __init__(self, start_time, end_time):
self._start_time, self._end_time = self._update(start_time, end_time)
@property
def start_time(self):
return self._start_time
@property
def end_time(self):
return self._end_time
@property
def start_and_end_time(self):
return self._start_time, self._end_time
[docs] def __eq__(self, other):
return (isinstance(other, TimePeriod) and
self.start_time == other.start_time and
self.end_time == other.end_time)
[docs] def __ne__(self, other):
return not (self == other)
[docs] def __repr__(self):
return "TimePeriod<%s, %s>" % (self.start_time, self.end_time)
[docs] def get_time_at_percent(self, percent):
return self.start_time + self.delta() * percent
[docs] def get_start_time(self):
return self.start_time
[docs] def get_end_time(self):
return self.end_time
[docs] def set_start_time(self, time):
return self.update(time, self.end_time)
[docs] def set_end_time(self, time):
return self.update(self.start_time, time)
[docs] def start_to_start(self, time_period):
return TimePeriod(self.start_time, time_period.get_start_time())
[docs] def start_to_end(self, time_period):
return TimePeriod(self.start_time, time_period.get_end_time())
[docs] def end_to_start(self, time_period):
return TimePeriod(self.end_time, time_period.get_start_time())
[docs] def end_to_end(self, time_period):
return TimePeriod(self.end_time, time_period.get_end_time())
[docs] def update(self, start_time, end_time,
start_delta=None, end_delta=None):
new_start, new_end = self._update(start_time, end_time, start_delta, end_delta)
return TimePeriod(new_start, new_end)
def _update(self, start_time, end_time, start_delta=None, end_delta=None):
"""
Change the time period data.
Optionally add the deltas to the times like this: time + delta.
If data is invalid, it will not be set, and a ValueError will be raised
instead. Data is invalid if or if the start time is larger than the end
time.
"""
new_start = self._calc_new_time(start_time, start_delta)
new_end = self._calc_new_time(end_time, end_delta)
self._assert_period_is_valid(new_start, new_end)
return (new_start, new_end)
def _assert_period_is_valid(self, new_start, new_end):
if new_start is None:
raise ValueError(_("Invalid start time"))
if new_end is None:
raise ValueError(_("Invalid end time"))
if new_start > new_end:
raise ValueError(_("Start time can't be after end time. Start:%s End:%s" %
(new_start.to_str(), new_end.to_str())))
[docs] def inside(self, time):
"""
Return True if the given time is inside this period or on the border,
otherwise False.
"""
return time >= self.start_time and time <= self.end_time
[docs] def distance_to(self, time_period):
if time_period.starts_after(self.end_time):
return self.end_to_start(time_period).delta()
elif time_period.ends_before(self.start_time):
return time_period.end_to_start(self).delta()
else:
return None
[docs] def overlaps(self, time_period):
return (time_period.ends_after(self.start_time) and
time_period.starts_before(self.end_time))
[docs] def outside_period(self, time_period):
return (time_period.ends_before(self.start_time) or
time_period.starts_after(self.end_time))
[docs] def inside_period(self, time_period):
return not self.outside_period(time_period)
[docs] def starts_after(self, time):
return self.start_time > time
[docs] def starts_before(self, time):
return self.start_time < time
[docs] def ends_before(self, time):
return self.end_time < time
[docs] def ends_after(self, time):
return self.end_time > time
[docs] def ends_at(self, time):
return self.end_time == time
[docs] def is_period(self):
"""
Return True if this time period is longer than just a point in time,
otherwise False.
"""
return self.start_time != self.end_time
[docs] def mean_time(self):
"""
Return the time in the middle if this time period is longer than just a
point in time, otherwise the point in time for this time period.
"""
return self.start_time + (self.delta() / 2)
[docs] def duration(self):
return self.end_time - self.start_time
[docs] def zoom(self, times, ratio=0.5):
try:
start_delta = self.delta() * (times * ratio / 5.0)
end_delta = self.delta() * (-times * (1.0 - ratio) / 5.0)
# The Numeric timeline get's stuck when fully zoomed in
# start- and end.delta becoms both Delta<0>
# The while statement below is made to get out of this stuck situation.
while start_delta == end_delta:
ratio += 0.1
start_delta = self.delta() * (times * ratio / 5.0)
end_delta = self.delta() * (-times * (1.0 - ratio) / 5.0)
return self.update(self.start_time, self.end_time, start_delta, end_delta)
except ValueError:
return self.update(self.start_time, self.end_time)
[docs] def move(self, direction):
"""
Move this time period one 10th to the given direction.
Direction should be -1 for moving to the left or 1 for moving to the
right.
"""
delta = self.delta() * (direction / 10.0)
return self.move_delta(delta)
[docs] def move_delta(self, delta):
return self.update(self.start_time, self.end_time, delta, delta)
[docs] def delta(self):
"""Return the length of this time period as a timedelta object."""
return self.end_time - self.start_time
[docs] def center(self, time):
return self.move_delta(time - self.mean_time())
[docs] def has_nonzero_time(self):
return self.start_time.seconds != 0 or self.end_time.seconds != 0
def _calc_new_time(self, time, delta):
if delta is None:
return time
return time + delta
[docs]class TimeOutOfRangeLeftError(ValueError):
pass
[docs]class TimeOutOfRangeRightError(ValueError):
pass
[docs]def time_period_center(time, length):
"""
TimePeriod factory method.
Return a time period with the given length (represented as a timedelta)
centered around `time`.
"""
half_length = length * 0.5
start_time = time - half_length
end_time = time + half_length
return TimePeriod(start_time, end_time)