Source code for fastkml.gx

# 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

"""
KML Extension Namespace and the gx prefix.

With the launch of Google Earth 5.0, Google has provided extensions to KML
to support a number of new features. These extensions use the gx prefix
and the following namespace URI::

    xmlns:gx="http://www.google.com/kml/ext/2.2"

This namespace URI must be added to the <kml> element in any KML file
using gx-prefixed elements::

    <kml
        xmlns="http://www.opengis.net/kml/2.2"
        xmlns:gx="http://www.google.com/kml/ext/2.2"
    >

Extensions to KML may not be supported in all geo-browsers. If your
browser doesn't support particular extensions, the data in those
extensions should be silently ignored, and the rest of the KML file
should load without errors.

Elements that currently use the gx prefix are:

* gx:altitudeMode
* gx:altitudeOffset
* gx:angles
* gx:AnimatedUpdate
* gx:balloonVisibility
* gx:coord
* gx:delayedStart
* gx:drawOrder
* gx:duration
* gx:FlyTo
* gx:flyToMode
* gx:h
* gx:horizFov
* gx:interpolate
* gx:labelVisibility
* gx:LatLonQuad
* gx:MultiTrack
* gx:vieweroptions
* gx:outerColor
* gx:outerWidth
* gx:physicalWidth
* gx:Playlist
* gx:playMode
* gx:SoundCue
* gx:TimeSpan
* gx:TimeStamp
* gx:Tour
* gx:TourControl
* gx:TourPrimitive
* gx:Track
* gx:ViewerOptions
* gx:w
* gx:Wait
* gx:x
* gx:y

The complete XML schema for elements in this extension namespace is
located at http://developers.google.com/kml/schema/kml22gx.xsd.
"""

import logging
from dataclasses import dataclass
from itertools import zip_longest
from typing import Any
from typing import Dict
from typing import Iterable
from typing import List
from typing import Optional
from typing import Tuple
from typing import cast

import pygeoif.geometry as geo
from pygeoif.types import PointType

from fastkml import config
from fastkml.data import ExtendedData
from fastkml.enums import AltitudeMode
from fastkml.geometry import _Geometry
from fastkml.helpers import bool_subelement
from fastkml.helpers import coords_subelement_list
from fastkml.helpers import coords_subelement_list_kwarg
from fastkml.helpers import datetime_subelement_list
from fastkml.helpers import datetime_subelement_list_kwarg
from fastkml.helpers import enum_subelement
from fastkml.helpers import subelement_bool_kwarg
from fastkml.helpers import subelement_enum_kwarg
from fastkml.helpers import xml_subelement
from fastkml.helpers import xml_subelement_kwarg
from fastkml.helpers import xml_subelement_list
from fastkml.helpers import xml_subelement_list_kwarg
from fastkml.registry import RegistryItem
from fastkml.registry import registry
from fastkml.times import KmlDateTime

__all__ = [
    "Angle",
    "MultiTrack",
    "Track",
    "TrackItem",
    "track_items_to_geometry",
    "tracks_to_geometry",
]

logger = logging.getLogger(__name__)


[docs] @dataclass(frozen=True) class Angle: """ The gx:angles element specifies the heading, tilt, and roll. The angles are specified in degrees, and the default values are 0 (heading and tilt) and 0 (roll). The angles are specified in the following order: heading, tilt, roll. """ heading: float = 0.0 tilt: float = 0.0 roll: float = 0.0 @property def coords(self) -> PointType: """ Get the coordinates of the angle. Returns ------- PointType The coordinates of the angle. """ return (self.heading, self.tilt, self.roll)
[docs] @dataclass(frozen=True) class TrackItem: """A track item describes an objects position and heading at a specific time.""" when: KmlDateTime coord: geo.Point angle: Optional[Angle] = None
[docs] def track_items_to_geometry(track_items: Iterable[TrackItem]) -> geo.LineString: """ Convert a sequence of TrackItems to a LineString geometry. Args: ---- track_items : Iterable[TrackItem] A sequence of TrackItems. Returns: ------- geo.LineString A LineString geometry representing the track. """ return geo.LineString.from_points( *[item.coord for item in track_items if item.coord is not None], )
[docs] class Track(_Geometry): """ A track describes how an object moves through the world over a given time period. This feature allows you to create one visible object in Google Earth (either a Point icon or a Model) that encodes multiple positions for the same object for multiple times. In Google Earth, the time slider allows the user to move the view through time, which animates the position of the object. Tracks are a more efficient mechanism for associating time data with visible Features, since you create only one Feature, which can be associated with multiple time elements as the object moves through space. https://developers.google.com/kml/documentation/kmlreference#gxtrack """ _default_nsid = config.GX track_items: List[TrackItem] extended_data: Optional[ExtendedData] def __init__( self, *, ns: Optional[str] = None, name_spaces: Optional[Dict[str, str]] = None, id: Optional[str] = None, target_id: Optional[str] = None, altitude_mode: Optional[AltitudeMode] = None, track_items: Optional[Iterable[TrackItem]] = None, whens: Optional[Iterable[KmlDateTime]] = None, coords: Optional[Iterable[PointType]] = None, angles: Optional[Iterable[PointType]] = None, extended_data: Optional[ExtendedData] = None, **kwargs: Any, ) -> None: """ Initialize a GX object. Parameters ---------- ns : Optional[str], optional The namespace for the GX object, by default None name_spaces : Optional[Dict[str, str]], optional A dictionary of namespace prefixes and URIs, by default None id : Optional[str], optional The ID of the GX object, by default None target_id : Optional[str], optional The target ID of the GX object, by default None altitude_mode : Optional[AltitudeMode], optional The altitude mode of the GX object, by default None track_items : Optional[Iterable[TrackItem]], optional The track items of the GX object, by default None whens : Optional[Iterable[KmlDateTime]], optional The timestamps of the track items, by default None coords : Optional[Iterable[PointType]], optional The coordinates of the track items, by default None angles : Optional[Iterable[PointType]], optional The angles of the track items, by default None extended_data : Optional[ExtendedData], optional The extended data of the GX object, by default None **kwargs : Any, optional Additional keyword arguments. Raises ------ ValueError If both `geometry` and `track_items` are specified. """ angles = list(angles) if angles else [] if (whens or coords) and track_items: msg = "Cannot specify both geometry and track_items" raise ValueError(msg) if not track_items and whens and coords: track_items = [ TrackItem( when=cast("KmlDateTime", when), coord=geo.Point(*coord), angle=Angle(*angle), ) for when, coord, angle in zip_longest( whens, coords, angles, fillvalue=(), ) ] self.track_items = list(track_items) if track_items else [] self.extended_data = extended_data super().__init__( ns=ns, name_spaces=name_spaces, id=id, target_id=target_id, altitude_mode=altitude_mode, **kwargs, ) def __repr__(self) -> str: """ Create a string representation for Track. Returns ------- str The string representation of the Track object. """ 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"altitude_mode={self.altitude_mode}, " f"track_items={self.track_items!r}, " f"extended_data={self.extended_data!r}, " f"**{self._get_splat()!r}," ")" ) @property def geometry(self) -> Optional[geo.LineString]: """ Get the geometry of the track. Returns ------- Optional[geo.LineString] The geometry of the track. """ return track_items_to_geometry(self.track_items) @property def whens(self) -> Tuple[KmlDateTime, ...]: """ Get the timestamps of the track items. Returns ------- Tuple[KmlDateTime] The timestamps of the track items. """ return tuple(item.when for item in self.track_items) @property def coords(self) -> Tuple[PointType, ...]: """ Get the coordinates of the track items. Returns ------- Tuple[PointType] The coordinates of the track items. """ return tuple( item.coord.coords[0] # type: ignore[misc] for item in self.track_items if item.coord ) @property def angles(self) -> Tuple[PointType, ...]: """ Get the angles of the track items. Returns ------- Tuple[Angle] The angles of the track items. """ return tuple(item.angle.coords for item in self.track_items if item.angle) def __bool__(self) -> bool: """ Check if the track has any track items. Returns ------- bool True if the track has track items, False otherwise. """ return bool(self.track_items)
registry.register( Track, item=RegistryItem( ns_ids=("gx", "kml", ""), classes=(AltitudeMode,), attr_name="altitude_mode", node_name="altitudeMode", get_kwarg=subelement_enum_kwarg, set_element=enum_subelement, default=AltitudeMode.clamp_to_ground, ), ) registry.register( Track, item=RegistryItem( ns_ids=("kml", "gx", ""), classes=(KmlDateTime,), attr_name="whens", node_name="when", get_kwarg=datetime_subelement_list_kwarg, set_element=datetime_subelement_list, ), ) registry.register( Track, item=RegistryItem( ns_ids=("gx", ""), classes=(tuple,), attr_name="coords", node_name="coord", get_kwarg=coords_subelement_list_kwarg, set_element=coords_subelement_list, ), ) registry.register( Track, item=RegistryItem( ns_ids=("gx", ""), classes=(tuple,), attr_name="angles", node_name="angles", get_kwarg=coords_subelement_list_kwarg, set_element=coords_subelement_list, default=(0.0, 0.0, 0.0), ), ) registry.register( Track, RegistryItem( ns_ids=("kml", ""), attr_name="extended_data", node_name="ExtendedData", classes=(ExtendedData,), get_kwarg=xml_subelement_kwarg, set_element=xml_subelement, ), )
[docs] def tracks_to_geometry(tracks: Iterable[Track]) -> geo.MultiLineString: """ Convert a collection of tracks to a MultiLineString geometry. Args: ---- tracks : Iterable[Track] A collection of tracks. Returns: ------- geo.MultiLineString A MultiLineString geometry representing the tracks. """ return geo.MultiLineString.from_linestrings( *[track.geometry for track in tracks if track.geometry], )
[docs] class MultiTrack(_Geometry): """ A MultiTrack is a collection of tracks. A multi-track element is used to combine multiple track elements into a single conceptual unit. For example, suppose you collect GPS data for a day's bike ride that includes several rest stops and a stop for lunch. Because of the interruptions in time, one bike ride might appear as four different tracks when the times and positions are plotted. Grouping these <gx:Track> elements into one <gx:MultiTrack> container causes them to be displayed in Google Earth as sections of a single path. When the icon reaches the end of one segment, it moves to the beginning of the next segment. The <gx:interpolate> element specifies whether to stop at the end of one track and jump immediately to the start of the next one, or to interpolate the missing values between the two tracks. """ _default_nsid = config.GX tracks: List[Track] def __init__( self, *, ns: Optional[str] = None, name_spaces: Optional[Dict[str, str]] = None, id: Optional[str] = None, target_id: Optional[str] = None, altitude_mode: Optional[AltitudeMode] = None, tracks: Optional[Iterable[Track]] = None, interpolate: Optional[bool] = None, **kwargs: Any, ) -> None: """ Initialize a GX object. Args: ---- ns (Optional[str]): The namespace for the GX object. name_spaces (Optional[Dict[str, str]]): The dictionary of namespace prefixes and URIs. id (Optional[str]): The ID of the GX object. target_id (Optional[str]): The target ID of the GX object. altitude_mode (Optional[AltitudeMode]): The altitude mode of the GX object. geometry (Optional[geo.MultiLineString]): The geometry of the GX object. tracks (Optional[Iterable[Track]]): The tracks of the GX object. interpolate (Optional[bool]): The interpolate flag of the GX object. **kwargs (Any): Additional keyword arguments. Raises: ------ ValueError: If both geometry and tracks are specified. """ self.tracks = [t for t in tracks if t] if tracks else [] self.interpolate = interpolate super().__init__( ns=ns, name_spaces=name_spaces, id=id, target_id=target_id, altitude_mode=altitude_mode, **kwargs, ) def __repr__(self) -> str: """Create a string (c)representation for MultiTrack.""" 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"altitude_mode={self.altitude_mode}, " f"tracks={self.tracks!r}, " f"interpolate={self.interpolate!r}, " f"**{self._get_splat()!r}," ")" ) @property def geometry(self) -> Optional[geo.MultiLineString]: """ Get the geometry of the gx object. Returns ------- Optional[geo.MultiLineString]: The geometry of the gx object. """ return tracks_to_geometry(self.tracks) def __bool__(self) -> bool: """ Check if the object has any tracks. Returns ------- bool True if the object has tracks, False otherwise. """ return bool(self.tracks)
registry.register( MultiTrack, item=RegistryItem( ns_ids=("gx", "kml"), classes=(AltitudeMode,), attr_name="altitude_mode", node_name="altitudeMode", get_kwarg=subelement_enum_kwarg, set_element=enum_subelement, default=AltitudeMode.clamp_to_ground, ), ) registry.register( MultiTrack, item=RegistryItem( ns_ids=("gx",), classes=(bool,), attr_name="interpolate", node_name="interpolate", get_kwarg=subelement_bool_kwarg, set_element=bool_subelement, ), ) registry.register( MultiTrack, item=RegistryItem( ns_ids=("gx",), classes=(Track,), attr_name="tracks", node_name="gx:Track", get_kwarg=xml_subelement_list_kwarg, set_element=xml_subelement_list, ), )