Source code for czml3.types

from __future__ import annotations

import datetime as dt
import re
from typing import Any

from pydantic import (
    AliasChoices,
    Field,
    field_validator,
    model_serializer,
    model_validator,
)

from ._compat import Self
from ._datetime import NaiveDatetimeWarning as NaiveDatetimeWarning
from ._datetime import format_datetime_like as format_datetime_like
from .base import BaseCZMLObject
from .common import Deletable, Interpolatable

# Not used directly: pydantic resolves the string annotations that NumberValue
# inherits from Interpolatable in this module's namespace, so they must be here.
from .enums import ExtrapolationTypes, InterpolationAlgorithms  # noqa: F401

TYPE_MAPPING = {
    bool: "boolean",
    float: "number",
    int: "number",
    list: "number",
    set: "number",
    str: "string",
    tuple: "number",
}


def _value_key(value: Any) -> str | None:
    """Return the CZML key for a primitive interval value, or None if unsupported.

    ``isinstance`` rather than an exact type lookup, so subclasses such as
    ``numpy.float64`` or a ``StrEnum`` member are accepted. ``bool`` is checked
    before ``int`` by the ordering of ``TYPE_MAPPING``.
    """
    for value_type, key in TYPE_MAPPING.items():
        if isinstance(value, value_type):
            return key
    return None


def _split_interval(interval: str) -> dict[str, str]:
    """Split a serialized ``start/end`` ISO 8601 interval into its two times."""
    start, separator, end = interval.partition("/")
    if not separator:
        raise ValueError(
            f"{interval!r} is not an ISO 8601 interval of the form 'start/end'"
        )
    return {"start": start, "end": end}


def _is_czml_object_list(value: Any) -> bool:
    return isinstance(value, list) and all(isinstance(v, BaseCZMLObject) for v in value)


[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 ValueError("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 ValueError( 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 ValueError( 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 ValueError( f"Input values must have either {num_points} or N * {num_points + 1} values, where N is the number of time-tagged samples." ) times = values[:: num_points + 1] if len(values) % (num_points + 1) == 0 and any( b <= a for a, b in zip(times, times[1:], strict=False) ): raise ValueError("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 ValueError( "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) -> str: 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: list[int | float]) -> list[int | float] | None: return get_color(r, 1.0)
[docs] @model_serializer def custom_serializer(self) -> list[float]: 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: list[int | float]) -> list[int | float] | None: return get_color(r, 255)
[docs] @model_serializer def custom_serializer(self) -> list[int] | list[float] | list[int | float]: 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: str) -> str: check_reference(v) return v
[docs] @model_serializer def custom_serializer(self) -> str: 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: list[str]) -> list[str]: for v in vs: check_reference(v) return vs
[docs] @model_serializer def custom_serializer(self) -> list[str]: 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: list[list[str]]) -> list[list[str]]: for vs in vss: for v in vs: check_reference(v) return vss
[docs] @model_serializer def custom_serializer(self) -> list[list[str]]: 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) -> list[list[float]]: 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]
[docs] @model_validator(mode="before") @classmethod def parse_serialized_form(cls, data: Any) -> Any: """Accept the serialized ``{"cartesian2": [...]}`` form.""" if isinstance(data, dict) and set(data) == {"cartesian2"}: return {"values": data["cartesian2"]} return data
@model_validator(mode="after") def _check_values(self) -> Self: check_values(2, self.values) return self
[docs] @model_serializer def custom_serializer(self) -> dict[str, list[float]]: 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) -> list[float]: 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) -> list[float]: 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) -> list[list[float]]: 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) -> list[float]: 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) -> list[list[float]]: 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) -> list[float]: 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) -> list[float]: 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_validator(mode="before") @classmethod def parse_interval_string(cls, data: Any) -> Any: """Accept the serialized ``start/end`` form.""" return _split_interval(data) if isinstance(data, str) else data
[docs] @field_validator("start", "end") @classmethod def format_time(cls, time: str | dt.datetime) -> str | None: return format_datetime_like(time)
[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 = None
[docs] @model_validator(mode="before") @classmethod def parse_serialized_form(cls, data: Any) -> Any: """Accept the serialized ``{"interval": "start/end", <key>: value}`` form.""" if not (isinstance(data, dict) and "interval" in data): return data rest = {key: value for key, value in data.items() if key != "interval"} parsed: dict[str, Any] = _split_interval(data["interval"]) if len(rest) == 1 and next(iter(rest)) in set(TYPE_MAPPING.values()): parsed["value"] = next(iter(rest.values())) elif rest: parsed["value"] = rest return parsed
[docs] @field_validator("start", "end") @classmethod def format_time(cls, time: str | dt.datetime) -> str | None: return format_datetime_like(time)
@model_validator(mode="after") def _check_value(self) -> Self: value = self.value if ( value is None or isinstance(value, BaseCZMLObject) or _is_czml_object_list(value) or isinstance(value, dict) or _value_key(value) is not None ): return self accepted = ", ".join(t.__name__ for t in TYPE_MAPPING) raise ValueError( f"Unsupported interval value of type {type(value).__name__}; expected a CZML object, a list of CZML objects, a dict of CZML properties, or one of: {accepted}" )
[docs] @model_serializer def custom_serializer(self) -> dict[str, Any]: obj_dict: dict[str, Any] = {"interval": f"{self.start}/{self.end}"} if isinstance(self.value, BaseCZMLObject): obj_dict.update(self.value.to_dict()) elif _is_czml_object_list(self.value): for value in self.value: obj_dict.update(value.to_dict()) elif isinstance(self.value, dict): obj_dict.update(self.value) elif self.value is not None: key = _value_key(self.value) assert key is not None # guaranteed by _check_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_validator(mode="before") @classmethod def parse_interval_list(cls, data: Any) -> Any: """Accept the serialized form, a bare list of intervals.""" return {"values": data} if isinstance(data, list) else data
[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) -> list[float]: 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(2, 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: str) -> str: check_reference(v) return v
[docs] @model_serializer def custom_serializer(self) -> str: return self.value
[docs] class EpochValue(BaseCZMLObject): """A value representing a time epoch.""" value: str | dt.datetime
[docs] @model_validator(mode="before") @classmethod def parse_serialized_form(cls, data: Any) -> Any: """Accept the serialized ``{"epoch": ...}`` form.""" if isinstance(data, dict) and set(data) == {"epoch"}: return {"value": data["epoch"]} return data
[docs] @field_validator("value") @classmethod def format_time(cls, time: str | dt.datetime) -> str | None: return format_datetime_like(time)
[docs] @model_serializer def custom_serializer(self) -> dict[str, str | dt.datetime]: return {"epoch": 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", validation_alias=AliasChoices("values", "number"), serialization_alias="number", ) """The numerical value or values."""
__all__ = [ "Cartesian2Value", "Cartesian3ListOfListsValue", "Cartesian3ListValue", "Cartesian3Value", "Cartesian3VelocityValue", "CartographicDegreesListOfListsValue", "CartographicDegreesListValue", "CartographicDegreesValue", "CartographicRadiansListOfListsValue", "CartographicRadiansListValue", "CartographicRadiansValue", "DistanceDisplayConditionValue", "EpochValue", "FontValue", "IntervalValue", "NaiveDatetimeWarning", "NearFarScalarValue", "NumberValue", "ReferenceListOfListsValue", "ReferenceListValue", "ReferenceValue", "RgbaValue", "RgbafValue", "StringValue", "TimeInterval", "TimeIntervalCollection", "UnitCartesian3Value", "UnitQuaternionValue", "UnitSphericalValue", "VelocityReferenceValue", "check_list_of_list_values", "check_list_of_values", "check_reference", "check_values", "format_datetime_like", "get_color", ]