# Copyright (C) 2012 - 2023 Christian Ledermann
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation; either version 2.1 of the License, or (at your option)
# any later version.
#
# This library 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this library; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
"""
Date and time handling in KML.
Any Feature in KML can have time data associated with it.
This time data has the effect of restricting the visibility of the data set to a given
time period or point in time.
https://developers.google.com/kml/documentation/time
"""
import re
from datetime import date
from datetime import datetime
from typing import Any
from typing import Dict
from typing import Optional
from typing import Union
import arrow
from fastkml import config
from fastkml.enums import DateTimeResolution
from fastkml.helpers import datetime_subelement
from fastkml.helpers import datetime_subelement_kwarg
from fastkml.kml_base import _BaseObject
from fastkml.registry import RegistryItem
from fastkml.registry import registry
__all__ = [
"KmlDateTime",
"TimeSpan",
"TimeStamp",
]
# regular expression to parse a gYearMonth string
# year and month may be separated by an optional dash
# year is always 4 digits, month, day is always 2 digits
year_month_day = re.compile(
r"^(?P<year>\d{4})(?:-)?(?P<month>\d{2})?(?:-)?(?P<day>\d{2})?$",
)
def adjust_date_to_resolution(
dt: Union[date, datetime],
resolution: Optional[DateTimeResolution] = None,
) -> Union[date, datetime]:
"""
Adjust the date or datetime to the specified resolution.
This function adjusts the date or datetime to the specified resolution.
If the resolution is not specified, the function will return the date or
datetime as is.
The function will return the date if the resolution is set to year,
year_month, or date. If the resolution is set to datetime, the function
will return the datetime as is.
Args:
----
dt : Union[date, datetime]
The date or datetime to adjust.
resolution : Optional[DateTimeResolution], optional
The resolution to adjust the date or datetime to, by default None.
Returns:
-------
Union[date, datetime]
The adjusted date or datetime.
"""
if resolution == DateTimeResolution.year:
return date(dt.year, 1, 1)
if resolution == DateTimeResolution.year_month:
return date(dt.year, dt.month, 1)
return (
dt.date()
if isinstance(dt, datetime) and resolution != DateTimeResolution.datetime
else dt
)
[docs]
class KmlDateTime:
"""
A KML DateTime object.
This class is used to parse and format KML DateTime objects.
A KML DateTime object is a string that conforms to the ISO 8601 standard
for date and time representation. The following formats are supported:
- yyyy-mm-ddThh:mm:sszzzzzz
- yyyy-mm-ddThh:mm:ss
- yyyy-mm-dd
- yyyy-mm
- yyyy
The T is the separator between the date and the time, and the time zone
is either Z (for UTC) or zzzzzz, which represents ±hh:mm in relation to
UTC. Additionally, the value can be expressed as a date only.
The precision of the DateTime is dictated by the DateTime value
which can be one of the following:
- dateTime gives second resolution
- date gives day resolution
- gYearMonth gives month resolution
- gYear gives year resolution
The KmlDateTime class can be used to parse a KML DateTime string into a
Python datetime object, or to format a Python datetime object into a
KML DateTime string.
The KmlDateTime class is used by the TimeStamp and TimeSpan classes.
"""
def __init__(
self,
dt: Union[date, datetime],
resolution: Optional[DateTimeResolution] = None,
) -> None:
"""
Initialize a KmlDateTime object.
Args:
----
dt : Union[date, datetime]
The date or datetime to adjust.
resolution : Optional[DateTimeResolution], optional
The resolution to adjust the date or datetime to, by default None.
"""
if resolution is None:
# sourcery skip: swap-if-expression
resolution = (
DateTimeResolution.date
if not isinstance(dt, datetime)
else DateTimeResolution.datetime
)
self.dt = adjust_date_to_resolution(dt, resolution)
self.resolution = (
DateTimeResolution.date
if not isinstance(self.dt, datetime)
and resolution == DateTimeResolution.datetime
else resolution
)
def __repr__(self) -> str:
"""Create a string (c)representation for KmlDateTime."""
return (
f"{self.__class__.__module__}.{self.__class__.__name__}("
f"dt={self.dt!r}, "
f"resolution={self.resolution}, "
")"
)
def __bool__(self) -> bool:
"""Return True if the date or datetime is valid."""
return isinstance(self.dt, date)
def __eq__(self, other: object) -> bool:
"""Return True if the two objects are equal."""
return (
self.dt == other.dt and self.resolution == other.resolution
if isinstance(other, KmlDateTime)
else False
)
def __str__(self) -> str:
"""Return the KML DateTime string representation of the object."""
if self.resolution == DateTimeResolution.year:
return self.dt.strftime("%Y")
if self.resolution == DateTimeResolution.year_month:
return self.dt.strftime("%Y-%m")
if self.resolution == DateTimeResolution.date:
return (
self.dt.date().isoformat()
if isinstance(self.dt, datetime)
else self.dt.isoformat()
)
return self.dt.isoformat()
[docs]
@classmethod
def parse(cls, datestr: str) -> Optional["KmlDateTime"]:
"""Parse a KML DateTime string into a KmlDateTime object."""
resolution = None
dt = None
if year_month_day_match := year_month_day.match(datestr):
year = int(year_month_day_match.group("year"))
month = int(year_month_day_match.group("month") or 1)
day = int(year_month_day_match.group("day") or 1)
dt = date(year, month, day)
resolution = DateTimeResolution.date
if year_month_day_match.group("day") is None:
resolution = DateTimeResolution.year_month
if year_month_day_match.group("month") is None:
resolution = DateTimeResolution.year
return cls(dt, resolution)
return cls(arrow.get(datestr).datetime, DateTimeResolution.datetime)
[docs]
@classmethod
def get_ns_id(cls) -> str:
"""Return the namespace ID."""
return config.KML
class _TimePrimitive(_BaseObject):
"""
Abstract element that cannot be used directly in a KML file.
This element is extended by the <TimeSpan> and <TimeStamp> elements.
https://developers.google.com/kml/documentation/kmlreference#timeprimitive
"""
[docs]
class TimeStamp(_TimePrimitive):
"""
Represents a single moment in time.
https://developers.google.com/kml/documentation/kmlreference#timestamp
"""
def __init__(
self,
ns: Optional[str] = None,
name_spaces: Optional[Dict[str, str]] = None,
id: Optional[str] = None,
target_id: Optional[str] = None,
timestamp: Optional[KmlDateTime] = None,
**kwargs: Any,
) -> None:
"""
Initialize a new instance of the Times class.
Args:
----
ns : str, optional
The namespace for the element, by default None.
name_spaces : dict[str, str], optional
The dictionary of namespace prefixes and URIs, by default None.
id : str, optional
The ID of the element, by default None.
target_id : str, optional
The target ID of the element, by default None.
timestamp : KmlDateTime, optional
The timestamp of the element, by default None.
**kwargs : Any
Additional keyword arguments.
Returns:
-------
None
"""
super().__init__(
ns=ns,
name_spaces=name_spaces,
id=id,
target_id=target_id,
**kwargs,
)
self.timestamp = timestamp
def __repr__(self) -> str:
"""Create a string (c)representation for TimeStamp."""
return (
f"{self.__class__.__module__}.{self.__class__.__name__}("
f"ns={self.ns!r}, "
f"name_spaces={self.name_spaces!r}, "
f"id={self.id!r}, "
f"target_id={self.target_id!r}, "
f"timestamp={self.timestamp!r}, "
f"**{self._get_splat()!r},"
")"
)
def __bool__(self) -> bool:
"""Return True if the timestamp is valid."""
return bool(self.timestamp)
registry.register(
TimeStamp,
item=RegistryItem(
ns_ids=("kml", "gx", ""),
classes=(KmlDateTime,),
attr_name="timestamp",
node_name="when",
get_kwarg=datetime_subelement_kwarg,
set_element=datetime_subelement,
),
)
[docs]
class TimeSpan(_TimePrimitive):
"""
Represents an extent in time bounded by begin and end dateTimes.
https://developers.google.com/kml/documentation/kmlreference#timespan
"""
def __init__(
self,
ns: Optional[str] = None,
name_spaces: Optional[Dict[str, str]] = None,
id: Optional[str] = None,
target_id: Optional[str] = None,
begin: Optional[KmlDateTime] = None,
end: Optional[KmlDateTime] = None,
**kwargs: Any,
) -> None:
"""
Initialize a new instance of the Times class.
Args:
----
ns (Optional[str]): The namespace for the element.
name_spaces (Optional[Dict[str, str]]): The dictionary of namespace prefixes
and URIs.
id (Optional[str]): The ID of the element.
target_id (Optional[str]): The target ID of the element.
begin (Optional[KmlDateTime]): The begin time.
end (Optional[KmlDateTime]): The end time.
**kwargs (Any): Additional keyword arguments.
Returns:
-------
None
"""
super().__init__(
ns=ns,
name_spaces=name_spaces,
id=id,
target_id=target_id,
**kwargs,
)
self.begin = begin
self.end = end
def __repr__(self) -> str:
"""Create a string (c)representation for TimeSpan."""
return (
f"{self.__class__.__module__}.{self.__class__.__name__}("
f"ns={self.ns!r}, "
f"name_spaces={self.name_spaces!r}, "
f"id={self.id!r}, "
f"target_id={self.target_id!r}, "
f"begin={self.begin!r}, "
f"end={self.end!r}, "
f"**{self._get_splat()!r},"
")"
)
def __bool__(self) -> bool:
"""Return True if the begin or end date is valid."""
return bool(self.begin) or bool(self.end)
registry.register(
TimeSpan,
item=RegistryItem(
ns_ids=("kml", "gx", ""),
classes=(KmlDateTime,),
attr_name="begin",
node_name="begin",
get_kwarg=datetime_subelement_kwarg,
set_element=datetime_subelement,
),
)
registry.register(
TimeSpan,
item=RegistryItem(
ns_ids=("kml", "gx", ""),
classes=(KmlDateTime,),
attr_name="end",
node_name="end",
get_kwarg=datetime_subelement_kwarg,
set_element=datetime_subelement,
),
)