diff --git a/fastkml/containers.py b/fastkml/containers.py new file mode 100644 index 00000000..b0285a6c --- /dev/null +++ b/fastkml/containers.py @@ -0,0 +1,202 @@ +""" +Container classes for KML elements. +""" +import logging +import urllib.parse as urlparse +from typing import Dict +from typing import Iterator +from typing import List +from typing import Optional +from typing import Union + +from fastkml import gx +from fastkml.data import Schema +from fastkml.enums import Verbosity +from fastkml.features import Placemark +from fastkml.features import _Feature +from fastkml.geometry import LinearRing +from fastkml.geometry import LineString +from fastkml.geometry import MultiGeometry +from fastkml.geometry import Point +from fastkml.geometry import Polygon +from fastkml.overlays import _Overlay +from fastkml.styles import Style +from fastkml.styles import StyleMap +from fastkml.types import Element + +logger = logging.getLogger(__name__) + +KmlGeometry = Union[ + Point, + LineString, + LinearRing, + Polygon, + MultiGeometry, + gx.MultiTrack, + gx.Track, +] + + +class _Container(_Feature): + """ + abstract element; do not create + A Container element holds one or more Features and allows the + creation of nested hierarchies. + subclasses are: + Document, + Folder. + """ + + _features = [] + + def __init__( + self, + ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, + id: Optional[str] = None, + target_id: Optional[str] = None, + name: Optional[str] = None, + description: Optional[str] = None, + styles: Optional[List[Style]] = None, + style_url: Optional[str] = None, + features: None = None, + ) -> None: + super().__init__( + ns=ns, + name_spaces=name_spaces, + id=id, + target_id=target_id, + name=name, + description=description, + styles=styles, + style_url=style_url, + ) + self._features = features or [] + + def features(self) -> Iterator[_Feature]: + """Iterate over features.""" + for feature in self._features: + if isinstance(feature, (Folder, Placemark, Document, _Overlay)): + yield feature + else: + msg = ( + "Features must be instances of " + "(Folder, Placemark, Document, Overlay)" + ) + raise TypeError( + msg, + ) + + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> Element: + element = super().etree_element(precision=precision, verbosity=verbosity) + for feature in self.features(): + element.append(feature.etree_element()) + return element + + def append(self, kmlobj: _Feature) -> None: + """Append a feature.""" + if id(kmlobj) == id(self): + msg = "Cannot append self" + raise ValueError(msg) + if isinstance(kmlobj, (Folder, Placemark, Document, _Overlay)): + self._features.append(kmlobj) + else: + msg = "Features must be instances of (Folder, Placemark, Document, Overlay)" + raise TypeError( + msg, + ) + + +class Folder(_Container): + """ + A Folder is used to arrange other Features hierarchically + (Folders, Placemarks, #NetworkLinks, or #Overlays). + """ + + __name__ = "Folder" + + def from_element(self, element: Element) -> None: + super().from_element(element) + folders = element.findall(f"{self.ns}Folder") + for folder in folders: + feature = Folder(self.ns) + feature.from_element(folder) + self.append(feature) + placemarks = element.findall(f"{self.ns}Placemark") + for placemark in placemarks: + feature = Placemark(self.ns) + feature.from_element(placemark) + self.append(feature) + documents = element.findall(f"{self.ns}Document") + for document in documents: + feature = Document(self.ns) + feature.from_element(document) + self.append(feature) + + +class Document(_Container): + """ + A Document is a container for features and styles. This element is + required if your KML file uses shared styles or schemata for typed + extended data. + """ + + __name__ = "Document" + _schemata = None + + def schemata(self) -> Iterator["Schema"]: + if self._schemata: + yield from self._schemata + + def append_schema(self, schema: "Schema") -> None: + if self._schemata is None: + self._schemata = [] + if isinstance(schema, Schema): + self._schemata.append(schema) + else: + s = Schema(schema) + self._schemata.append(s) + + def from_element(self, element: Element, strict: bool = False) -> None: + super().from_element(element) + documents = element.findall(f"{self.ns}Document") + for document in documents: + feature = Document(self.ns) + feature.from_element(document) + self.append(feature) + folders = element.findall(f"{self.ns}Folder") + for folder in folders: + feature = Folder(self.ns) + feature.from_element(folder) + self.append(feature) + placemarks = element.findall(f"{self.ns}Placemark") + for placemark in placemarks: + feature = Placemark(self.ns) + feature.from_element(placemark) + self.append(feature) + schemata = element.findall(f"{self.ns}Schema") + for schema in schemata: + s = Schema.class_from_element(ns=self.ns, element=schema, strict=strict) + self.append_schema(s) + + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> Element: + element = super().etree_element(precision=precision, verbosity=verbosity) + if self._schemata is not None: + for schema in self._schemata: + element.append(schema.etree_element()) + return element + + def get_style_by_url(self, style_url: str) -> Optional[Union[Style, StyleMap]]: + id = urlparse.urlparse(style_url).fragment + for style in self.styles(): + if style.id == id: + return style + return None diff --git a/fastkml/features.py b/fastkml/features.py new file mode 100644 index 00000000..b911f359 --- /dev/null +++ b/fastkml/features.py @@ -0,0 +1,634 @@ +""" +Feature base, Placemark and NetworkLink. + +These are the objects that can be added to a KML file. +""" + +import logging +from datetime import datetime +from typing import Any +from typing import Dict +from typing import Iterator +from typing import List +from typing import Optional +from typing import Union + +from fastkml import atom +from fastkml import config +from fastkml import gx +from fastkml.base import _BaseObject +from fastkml.data import ExtendedData +from fastkml.enums import Verbosity +from fastkml.geometry import AnyGeometryType +from fastkml.geometry import LinearRing +from fastkml.geometry import LineString +from fastkml.geometry import MultiGeometry +from fastkml.geometry import Point +from fastkml.geometry import Polygon +from fastkml.mixins import TimeMixin +from fastkml.styles import Style +from fastkml.styles import StyleMap +from fastkml.styles import StyleUrl +from fastkml.styles import _StyleSelector +from fastkml.times import TimeSpan +from fastkml.times import TimeStamp +from fastkml.types import Element +from fastkml.views import Camera +from fastkml.views import LookAt + +logger = logging.getLogger(__name__) + +KmlGeometry = Union[ + Point, + LineString, + LinearRing, + Polygon, + MultiGeometry, + gx.MultiTrack, + gx.Track, +] + + +class _Feature(TimeMixin, _BaseObject): + """ + abstract element; do not create + subclasses are: + * Container (Document, Folder) + * Placemark + * Overlay + Not Implemented Yet: + * NetworkLink. + """ + + name: Optional[str] + # User-defined text displayed in the 3D viewer as the label for the + # object (for example, for a Placemark, Folder, or NetworkLink). + + visibility: Optional[bool] + # Boolean value. Specifies whether the feature is drawn in the 3D + # viewer when it is initially loaded. In order for a feature to be + # visible, the tag of all its ancestors must also be + # set to 1. + + isopen: Optional[bool] + # Boolean value. Specifies whether a Document or Folder appears + # closed or open when first loaded into the Places panel. + # 0=collapsed (the default), 1=expanded. + + _atom_author: Optional[atom.Author] + # KML 2.2 supports new elements for including data about the author + # and related website in your KML file. This information is displayed + # in geo search results, both in Earth browsers such as Google Earth, + # and in other applications such as Google Maps. + + _atom_link: Optional[atom.Link] + # Specifies the URL of the website containing this KML or KMZ file. + + _address: Optional[str] + # A string value representing an unstructured address written as a + # standard street, city, state address, and/or as a postal code. + # You can use the
tag to specify the location of a point + # instead of using latitude and longitude coordinates. + + _phone_number: Optional[str] + # A string value representing a telephone number. + # This element is used by Google Maps Mobile only. + + _snippet: Optional[Union[str, Dict[str, Any]]] + # _snippet is either a tuple of a string Snippet.text and an integer + # Snippet.maxLines or a string + # + # A short description of the feature. In Google Earth, this + # description is displayed in the Places panel under the name of the + # feature. If a Snippet is not supplied, the first two lines of + # the are used. In Google Earth, if a Placemark + # contains both a description and a Snippet, the appears + # beneath the Placemark in the Places panel, and the + # appears in the Placemark's description balloon. This tag does not + # support HTML markup. has a maxLines attribute, an integer + # that specifies the maximum number of lines to display. + + description: Optional[str] + # User-supplied content that appears in the description balloon. + + _style_url: Optional[Union[Style, StyleMap]] + # URL of a