import datetime as dt
import re
import sys
from typing import Any
import numpy as np
from dateutil.parser import isoparse as parse_iso_date
from pydantic import (
Field,
field_validator,
model_serializer,
model_validator,
)
from .base import BaseCZMLObject
from .common import Deletable, Interpolatable
from .constants import ISO8601_FORMAT_Z
from .enums import ExtrapolationTypes, InterpolationAlgorithms # noqa
if sys.version_info[1] >= 11:
from typing import Self
else:
from typing_extensions import Self # pragma: no cover
TYPE_MAPPING = {
bool: "boolean",
float: "number",
int: "number",
list: "number",
set: "number",
str: "string",
tuple: "number",
}
[docs]
def get_color(
color: None | list[int | float], max_val: int | float
) -> list[int | float] | None:
"""Determines if the input is a valid color"""
if isinstance(color, list) and len(color) == 0:
raise ValueError("Length of colours must be non-zero")
if color is None or (
isinstance(color, list)
and len(color) == 4
and all(0 <= v <= max_val for v in color)
): # [r, g, b, a]
return color
if (
isinstance(color, list)
and len(color) % 5 == 0
and all(0 <= v <= max_val for v in color[1::5])
and all(0 <= v <= max_val for v in color[2::5])
and all(0 <= v <= max_val for v in color[3::5])
and all(0 <= v <= max_val for v in color[4::5])
): # [time, r, g, b, a]
return color
elif (
isinstance(color, list)
and len(color) == 3
and all(0 <= v <= max_val for v in color)
): # [r, g, b]
return color + [max_val]
raise TypeError("Colour type not supported")
[docs]
def check_list_of_list_values(num_points: int, values: list[list[Any]]) -> None:
"""Values that support `[X, Y, Z, X, Y, Z, ...]`"""
if len(values) <= 0:
raise ValueError("No values present")
for value in values:
if len(value) <= 0:
raise ValueError("No values present in a list")
if len(value) % num_points != 0:
raise TypeError(
f"Input values of each list must have either {num_points} or N * {num_points} values, where N is the number of samples."
)
[docs]
def check_list_of_values(num_points: int, values: list[Any]) -> None:
"""Values that support `[X, Y, Z, X, Y, Z, ...]`"""
if len(values) <= 0:
raise ValueError("No values present")
if len(values) % num_points != 0:
raise TypeError(
f"Input values must have either {num_points} or N * {num_points} values, where N is the number of samples."
)
[docs]
def check_values(num_points: int, values: list[Any]) -> None:
"""Values that support `[X, Y, Z]` or `[Time, X, Y, Z, Time, X, Y, Z, ...]`"""
if len(values) <= 0:
raise ValueError("No values present")
if not (len(values) % (num_points) == 0 or len(values) % (num_points + 1) == 0):
raise TypeError(
f"Input values must have either {num_points} or N * {num_points + 1} values, where N is the number of time-tagged samples."
)
if len(values) % (num_points + 1) == 0 and np.any(
np.diff(values[:: num_points + 1]) <= 0
):
raise TypeError("Time values must be increasing.")
[docs]
def check_reference(r: str | None) -> None:
if r is None:
return
elif re.search(r"^.+#.+$", r) is None:
raise TypeError(
"Invalid reference string format. Input must be of the form id#property"
)
[docs]
class FontValue(BaseCZMLObject):
"""A font, specified using the same syntax as the CSS "font" property.
See `here <https://github.com/AnalyticalGraphicsInc/czml-writer/wiki/FontValue>`__ for it's definition.
"""
font: str
[docs]
@model_serializer
def custom_serializer(self):
return self.font
[docs]
class RgbafValue(BaseCZMLObject):
"""A color specified as an array of color components `[Red, Green, Blue, Alpha]` where each component is in the range 0.0-1.0. If the array has four elements, the color is constant. If it has five or more elements, they are time-tagged samples arranged as `[Time, Red, Green, Blue, Alpha, Time, Red, Green, Blue, Alpha, ...]`, where Time is an ISO 8601 date and time string or seconds since epoch.
See `here <https://github.com/AnalyticalGraphicsInc/czml-writer/wiki/RgbafValue>`__ for it's definition.
"""
values: list[float]
[docs]
@field_validator("values")
@classmethod
def get_color_from_values(cls, r):
return get_color(r, 1.0)
[docs]
@model_serializer
def custom_serializer(self):
return self.values
[docs]
class RgbaValue(BaseCZMLObject):
"""A color specified as an array of color components `[Red, Green, Blue, Alpha]` where each component is in the range 0-255. If the array has four elements, the color is constant. If it has five or more elements, they are time-tagged samples arranged as `[Time, Red, Green, Blue, Alpha, Time, Red, Green, Blue, Alpha, ...]`, where Time is an ISO 8601 date and time string or seconds since epoch.
See `here <https://github.com/AnalyticalGraphicsInc/czml-writer/wiki/RgbaValue>`__ for it's definition.
"""
values: list[int] | list[float] | list[int | float]
[docs]
@field_validator("values")
@classmethod
def get_color_from_values(cls, r):
return get_color(r, 255)
[docs]
@model_serializer
def custom_serializer(self):
return self.values
[docs]
class ReferenceValue(BaseCZMLObject):
"""Represents a reference to another property. References can be used to specify that two properties on different objects are in fact, the same property.
See `here <https://github.com/AnalyticalGraphicsInc/czml-writer/wiki/ReferenceValue>`__ for it's definition.
"""
value: str
@field_validator("value")
@classmethod
def _check_string(cls, v):
check_reference(v)
return v
[docs]
@model_serializer
def custom_serializer(self):
return self.value
[docs]
class ReferenceListValue(BaseCZMLObject):
"""Represents a reference to another property. References can be used to specify that two properties on different objects are in fact, the same property.
See `here <https://github.com/AnalyticalGraphicsInc/czml-writer/wiki/ReferenceListValue>`__ for it's definition.
"""
values: list[str]
@field_validator("values")
@classmethod
def _check_string(cls, vs):
for v in vs:
check_reference(v)
return vs
[docs]
@model_serializer
def custom_serializer(self):
return self.values
[docs]
class ReferenceListOfListsValue(BaseCZMLObject):
"""Represents a reference to another property. References can be used to specify that two properties on different objects are in fact, the same property.
See `here <https://github.com/AnalyticalGraphicsInc/czml-writer/wiki/ReferenceListOfListsValue>`__ for it's definition.
"""
values: list[list[str]]
@field_validator("values")
@classmethod
def _check_string(cls, vss):
for vs in vss:
for v in vs:
check_reference(v)
return vss
[docs]
@model_serializer
def custom_serializer(self):
return self.values
[docs]
class Cartesian3Value(BaseCZMLObject):
"""A three-dimensional Cartesian value specified as `[X, Y, Z]`. If the values has three elements, the value is constant. If it has four or more elements, they are time-tagged samples arranged as `[Time, X, Y, Z, Time, X, Y, Z, ...]`, where Time is an ISO 8601 date and time string or seconds since epoch.
See `here <https://github.com/AnalyticalGraphicsInc/czml-writer/wiki/Cartesian3Value>`__ for it's definition.
"""
values: list[float]
@model_validator(mode="after")
def _check_values(self) -> Self:
check_values(3, self.values)
return self
[docs]
@model_serializer
def custom_serializer(self) -> list[float]:
return self.values
[docs]
class Cartesian3ListValue(BaseCZMLObject):
"""A list of three-dimensional Cartesian values specified as `[X, Y, Z, X, Y, Z, ...]`
See `here <https://github.com/AnalyticalGraphicsInc/czml-writer/wiki/Cartesian3ListValue>`__ for it's definition.
"""
values: list[float]
@model_validator(mode="after")
def _check_values(self) -> Self:
check_list_of_values(3, self.values)
return self
[docs]
@model_serializer
def custom_serializer(self) -> list[float]:
return self.values
[docs]
class Cartesian3ListOfListsValue(BaseCZMLObject):
"""A list of lists of three-dimensional Cartesian values specified as `[X, Y, Z, X, Y, Z, ...]`
See `here <https://github.com/AnalyticalGraphicsInc/czml-writer/wiki/Cartesian3ListOfListsValue>`__ for it's definition.
"""
values: list[list[float]]
@model_validator(mode="after")
def _check_values(self) -> Self:
check_list_of_list_values(3, self.values)
return self
[docs]
@model_serializer
def custom_serializer(self):
return self.values
[docs]
class Cartesian2Value(BaseCZMLObject):
"""A two-dimensional Cartesian value specified as `[X, Y]`. If the values has two elements, the value is constant. If it has three or more elements, they are time-tagged samples arranged as `[Time, X, Y, Time, X, Y, ...]`, where Time is an ISO 8601 date and time string or seconds since epoch.
See `here <https://github.com/AnalyticalGraphicsInc/czml-writer/wiki/Cartesian2Value>`__ for it's definition.
"""
values: list[float]
@model_validator(mode="after")
def _check_values(self) -> Self:
check_values(2, self.values)
return self
[docs]
@model_serializer
def custom_serializer(self):
return {"cartesian2": list(self.values)}
[docs]
class CartographicRadiansValue(BaseCZMLObject):
"""A geodetic, WGS84 position specified as `[Longitude, Latitude, Height]`, where Longitude and Latitude are in radians and Height is in meters. If the array has three elements, the value is constant. If it has four or more elements, they are time-tagged samples arranged as `[Time, Longitude, Latitude, Height, Time, Longitude, Latitude, Height, ...]`, where Time is an ISO 8601 date and time string or seconds since epoch.
See `here <https://github.com/AnalyticalGraphicsInc/czml-writer/wiki/CartographicRadiansValue>`__ for it's definition.
"""
values: list[float]
@model_validator(mode="after")
def _check_values(self) -> Self:
check_values(3, self.values)
return self
[docs]
@model_serializer
def custom_serializer(self):
return self.values
[docs]
class CartographicDegreesValue(BaseCZMLObject):
"""A geodetic, WGS84 position specified as `[Longitude, Latitude, Height]`, where Longitude and Latitude are in degrees and Height is in meters. If the array has three elements, the value is constant. If it has four or more elements, they are time-tagged samples arranged as `[Time, Longitude, Latitude, Height, Time, Longitude, Latitude, Height, ...]`, where Time is an ISO 8601 date and time string or seconds since epoch.
See `here <https://github.com/AnalyticalGraphicsInc/czml-writer/wiki/CartographicDegreesValue>`__ for it's definition.
"""
values: list[float]
@model_validator(mode="after")
def _check_values(self) -> Self:
check_values(3, self.values)
return self
[docs]
@model_serializer
def custom_serializer(self) -> list[float]:
return self.values
[docs]
class Cartesian3VelocityValue(BaseCZMLObject):
"""A three-dimensional Cartesian value and its derivative specified as `[X, Y, Z, dX, dY, dZ]`. If the array has six elements, the value is constant. If it has seven or more elements, they are time-tagged samples arranged as `[Time, X, Y, Z, dX, dY, dZ, Time, X, Y, Z, dX, dY, dZ, ...]`, where Time is an ISO 8601 date and time string or seconds since epoch.
See `here <https://github.com/AnalyticalGraphicsInc/czml-writer/wiki/Cartesian3VelocityValue>`__ for it's definition.
"""
values: list[float]
@model_validator(mode="after")
def _check_values(self) -> Self:
check_values(6, self.values)
return self
[docs]
@model_serializer
def custom_serializer(self) -> list[float]:
return self.values
[docs]
class StringValue(BaseCZMLObject):
"""A string value.
See `here <https://github.com/AnalyticalGraphicsInc/czml-writer/wiki/StringValue>`__ for it's definition.
"""
string: str
[docs]
@model_serializer
def custom_serializer(self) -> str:
return self.string
[docs]
class CartographicRadiansListValue(BaseCZMLObject):
"""A list of geodetic, WGS84 positions specified as `[Longitude, Latitude, Height, Longitude, Latitude, Height, ...]`, where Longitude and Latitude are in radians and Height is in meters.
See `here <https://github.com/AnalyticalGraphicsInc/czml-writer/wiki/CartographicRadiansListValue>`__ for it's definition.
"""
values: list[float]
@model_validator(mode="after")
def _check_values(self) -> Self:
check_list_of_values(3, self.values)
return self
[docs]
@model_serializer
def custom_serializer(self):
return self.values
[docs]
class CartographicRadiansListOfListsValue(BaseCZMLObject):
"""A list of lists of geodetic, WGS84 positions specified as `[Longitude, Latitude, Height, Longitude, Latitude, Height, ...]`, where Longitude and Latitude are in radians and Height is in meters
See `here <https://github.com/AnalyticalGraphicsInc/czml-writer/wiki/CartographicRadiansListOfListsValue>`__ for it's definition.
"""
values: list[list[float]]
@model_validator(mode="after")
def _check_values(self) -> Self:
check_list_of_list_values(3, self.values)
return self
[docs]
@model_serializer
def custom_serializer(self):
return self.values
[docs]
class CartographicDegreesListValue(BaseCZMLObject):
"""A list of geodetic, WGS84 positions specified as `[Longitude, Latitude, Height, Longitude, Latitude, Height, ...]`, where Longitude and Latitude are in degrees and Height is in meters.
See `here <https://github.com/AnalyticalGraphicsInc/czml-writer/wiki/CartographicDegreesListValue>`__ for it's definition.
"""
values: list[float]
@model_validator(mode="after")
def _check_values(self) -> Self:
check_list_of_values(3, self.values)
return self
[docs]
@model_serializer
def custom_serializer(self):
return self.values
[docs]
class CartographicDegreesListOfListsValue(BaseCZMLObject):
"""A list of lists of geodetic, WGS84 positions specified as `[Longitude, Latitude, Height, Longitude, Latitude, Height, ...]`, where Longitude and Latitude are in degrees and Height is in meters
See `here <https://github.com/AnalyticalGraphicsInc/czml-writer/wiki/CartographicDegreesListOfListsValue>`__ for it's definition.
"""
values: list[list[float]]
@model_validator(mode="after")
def _check_values(self) -> Self:
check_list_of_list_values(3, self.values)
return self
[docs]
@model_serializer
def custom_serializer(self):
return self.values
[docs]
class DistanceDisplayConditionValue(BaseCZMLObject):
"""A value indicating the visibility of an object based on the distance to the camera, specified as two values `[NearDistance, FarDistance]`. If the array has two elements, the value is constant. If it has three or more elements, they are time-tagged samples arranged as `[Time, NearDistance, FarDistance, Time, NearDistance, FarDistance, ...]`, where Time is an ISO 8601 date and time string or seconds since epoch.
See `here <https://github.com/AnalyticalGraphicsInc/czml-writer/wiki/DistanceDisplayConditionValue>`__ for it's definition.
"""
values: list[float]
@model_validator(mode="after")
def _check_values(self) -> Self:
check_values(2, self.values)
return self
[docs]
@model_serializer
def custom_serializer(self):
return self.values
[docs]
class NearFarScalarValue(BaseCZMLObject):
"""A near-far scalar value specified as four values `[NearDistance, NearValue, FarDistance, FarValue]`. If the array has four elements, the value is constant. If it has five or more elements, they are time-tagged samples arranged as `[Time, NearDistance, NearValue, FarDistance, FarValue, Time, NearDistance, NearValue, FarDistance, FarValue, ...]`, where Time is an ISO 8601 date and time string or seconds since epoch.
See `here <https://github.com/AnalyticalGraphicsInc/czml-writer/wiki/NearFarScalarValue>`__ for it's definition.
"""
values: list[float]
@model_validator(mode="after")
def _check_values(self) -> Self:
check_values(4, self.values)
return self
[docs]
@model_serializer
def custom_serializer(self):
return self.values
[docs]
class TimeInterval(BaseCZMLObject):
"""A time interval, specified in ISO8601 interval format."""
start: str | dt.datetime
end: str | dt.datetime
[docs]
@model_serializer
def custom_serializer(self) -> str:
return f"{self.start}/{self.end}"
[docs]
class IntervalValue(BaseCZMLObject):
"""Value over some interval."""
start: str | dt.datetime
end: str | dt.datetime
value: Any = Field(default=None)
[docs]
@model_serializer
def custom_serializer(self) -> dict[str, Any]:
obj_dict = {"interval": TimeInterval(start=self.start, end=self.end).to_dict()}
if isinstance(self.value, BaseCZMLObject):
obj_dict.update(self.value.to_dict())
elif isinstance(self.value, list) and all(
isinstance(v, BaseCZMLObject) for v in self.value
):
for value in self.value:
obj_dict.update(value.to_dict())
else:
key = TYPE_MAPPING[type(self.value)]
obj_dict[key] = self.value
return obj_dict
[docs]
class TimeIntervalCollection(BaseCZMLObject):
"""A collection of time intervals, specified in ISO8601 interval format.
See `here <https://github.com/AnalyticalGraphicsInc/czml-writer/wiki/TimeIntervalCollection>`__ for it's definition.
"""
values: list[TimeInterval] | list[IntervalValue]
[docs]
@model_serializer
def custom_serializer(self) -> list[Any]:
return self.values
[docs]
class UnitCartesian3Value(BaseCZMLObject):
"""A three-dimensional unit magnitude Cartesian value specified as [X, Y, Z]. If the array has three elements, the value is constant. If it has four or more elements, they are time-tagged samples arranged as [Time, X, Y, Z, Time, X, Y, Z, ...], where Time is an ISO 8601 date and time string or seconds since epoch.
See `here <https://github.com/AnalyticalGraphicsInc/czml-writer/wiki/UnitCartesian3Value>`__ for it's definition.
"""
values: list[float]
@model_validator(mode="after")
def _check_values(self) -> Self:
check_values(3, self.values)
return self
[docs]
@model_serializer
def custom_serializer(self) -> list[float]:
return self.values
[docs]
class UnitQuaternionValue(BaseCZMLObject):
"""A set of 4-dimensional coordinates used to represent rotation in 3-dimensional space. It's specified as `[X, Y, Z, W]`. If the array has four elements, the value is constant. If it has five or more elements, they are time-tagged samples arranged as `[Time, X, Y, Z, W, Time, X, Y, Z, W, ...]`, where Time is an ISO 8601 date and time string or seconds since epoch.
See `here <https://github.com/AnalyticalGraphicsInc/czml-writer/wiki/UnitQuaternionValue>`__ for it's definition.
"""
values: list[float]
@model_validator(mode="after")
def _check_values(self) -> Self:
check_values(4, self.values)
return self
[docs]
@model_serializer
def custom_serializer(self):
return self.values
[docs]
class UnitSphericalValue(BaseCZMLObject):
"""A unit spherical value specified as [Clock, Cone] angles. The clock angle is measured in the XY plane from the positive X axis toward the positive Y axis. The cone angle is the angle from the positive Z axis toward the negative Z axis. If the array has two elements, the value is constant. If it has three or more elements, they are time-tagged samples arranged as [Time, Clock, Cone, Time, Clock, Cone, ...], where Time is an ISO 8601 date and time string or seconds since epoch.
See `here <https://github.com/AnalyticalGraphicsInc/czml-writer/wiki/UnitSphericalValue>`__ for it's definition.
"""
values: list[float]
@model_validator(mode="after")
def _check_values(self) -> Self:
check_values(3, self.values)
return self
[docs]
@model_serializer
def custom_serializer(self) -> list[float]:
return self.values
[docs]
class VelocityReferenceValue(BaseCZMLObject):
"""Represents the normalized velocity vector of a position property. The reference must be to a position property.
See `here <https://github.com/AnalyticalGraphicsInc/czml-writer/wiki/VelocityReferenceValue>`__ for it's definition.
"""
value: str
@field_validator("value")
@classmethod
def _check_string(cls, v):
check_reference(v)
return v
[docs]
@model_serializer
def custom_serializer(self):
return self.value
[docs]
class EpochValue(BaseCZMLObject):
"""A value representing a time epoch."""
value: str | dt.datetime
[docs]
@model_serializer
def custom_serializer(self):
return {"epoch": format_datetime_like(self.value)}
[docs]
class NumberValue(BaseCZMLObject, Interpolatable, Deletable):
"""A single number, or a list of number pairs signifying the time and representative value."""
number: int | float | list[int] | list[float] | list[int | float] = Field(
alias="values"
)
"""The numerical value or values."""