"""
Placemark model for KML point locations.
This module implements the Placemark class for representing point locations
with coordinates and extended attributes from KML files.
"""
# pylint: disable=too-many-arguments, too-many-positional-arguments
from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Union
from kmlorm.core.managers import PlacemarkManager
from .base import KMLElement
from .point import Coordinate, Point
if TYPE_CHECKING:
from .multigeometry import MultiGeometry
from ..spatial.calculations import DistanceUnit
[docs]
class Placemark(KMLElement):
"""
Represents a KML Placemark (point location).
Placemarks are the most common KML elements, representing specific
geographic locations with coordinates and optional metadata like
addresses, phone numbers, and extended data.
"""
objects: PlacemarkManager = PlacemarkManager()
point: Optional["Point"]
multigeometry: Optional["MultiGeometry"]
address: Optional[str]
phone_number: Optional[str]
snippet: Optional[str]
style_url: Optional[str]
extended_data: Optional[Dict[str, Any]]
[docs]
def __init__(
self,
point: Optional["Point"] = None,
multigeometry: Optional["MultiGeometry"] = None,
address: Optional[str] = None,
phone_number: Optional[str] = None,
snippet: Optional[str] = None,
style_url: Optional[str] = None,
extended_data: Optional[Dict[str, Any]] = None,
**kwargs: Any,
) -> None:
"""
Initialize a Placemark with location and metadata.
Args:
point: Point object containing geometry (for direct Point Placemarks)
multigeometry: MultiGeometry object (for MultiGeometry Placemarks)
address: Street address or location description
phone_number: Contact phone number
snippet: Short description snippet
style_url: Reference to KML style definition
extended_data: Dictionary of additional key-value data
**kwargs: Additional base element attributes (id, name, etc.)
"""
# Initialize point first to avoid AttributeError in coordinates setter
self.point = point
self.multigeometry = multigeometry
self.address = address
self.phone_number = phone_number
self.snippet = snippet
self.style_url = style_url
self.extended_data = extended_data or {}
# Call super after initializing our attributes
super().__init__(**kwargs)
[docs]
def __str__(self) -> str:
"""
String representation of the Placemark.
Returns name if available, otherwise address, otherwise coordinates.
"""
if self.name:
return self.name
if self.address:
return f"Placemark at {self.address}"
if self.point and self.point.coordinates:
lon, lat = self.point.coordinates.latitude, self.point.coordinates.longitude
return f"Placemark({lon:.4f}, {lat:.4f})"
return "Placemark(no location)"
@property
def coordinates(self) -> "Optional[Coordinate]":
"""Get coordinates from the point."""
return self.point.coordinates if self.point else None
@coordinates.setter
def coordinates(self, value: "Union[Tuple[float, ...], str, None]") -> None:
"""Set coordinates by creating or updating the point."""
if self.point is None:
self.point = Point(coordinates=value)
else:
self.point.coordinates = value
@property
def longitude(self) -> Optional[float]:
"""Get the longitude coordinate."""
return self.point.longitude if self.point else None
@property
def latitude(self) -> Optional[float]:
"""Get the latitude coordinate."""
return self.point.latitude if self.point else None
@property
def altitude(self) -> Optional[float]:
"""Get the altitude coordinate."""
return self.point.altitude if self.point else None
@property
def has_coordinates(self) -> bool:
"""Check if placemark has valid coordinates."""
return self.point.has_coordinates() if self.point else False
[docs]
def to_dict(self) -> Dict[str, Any]:
"""
Convert placemark to dictionary representation.
Returns:
Dictionary with all placemark attributes
"""
base_dict = super().to_dict()
base_dict.update(
{
"point": self.point.to_dict() if self.point else None,
"multigeometry": (self.multigeometry.to_dict() if self.multigeometry else None),
"coordinates": self.coordinates,
"longitude": self.longitude,
"latitude": self.latitude,
"altitude": self.altitude,
"address": self.address,
"phone_number": self.phone_number,
"snippet": self.snippet,
"style_url": self.style_url,
"extended_data": self.extended_data,
}
)
return base_dict
[docs]
def get_coordinates(self) -> Optional["Coordinate"]:
"""
Return the coordinate representation of this placemark.
This method satisfies the HasCoordinates protocol, allowing Placemark
objects to be used directly in spatial calculations.
Returns:
The Coordinate object from the point, or None if no point/coordinates exist
"""
if self.point and self.point.coordinates:
return self.point.coordinates
return None
[docs]
def distance_to(
self,
other: Union["Coordinate", "Point", "Placemark", Tuple[float, float], list],
unit: Optional["DistanceUnit"] = None,
) -> Optional[float]:
"""
Calculate distance to another spatial object.
Args:
other: Target object with coordinates (Coordinate, Point, Placemark, or tuple/list)
unit: Distance unit (defaults to kilometers)
Returns:
Distance in specified units, or None if this placemark or target has no coordinates
Examples:
>>> placemark1 = Placemark(name="NYC", coordinates=(-74.006, 40.7128))
>>> placemark2 = Placemark(name="London", coordinates=(-0.1276, 51.5074))
>>> distance = placemark1.distance_to(placemark2)
>>> print(f"Distance: {distance:.1f} km")
>>> # Different units
>>> from kmlorm.spatial import DistanceUnit
>>> distance_miles = placemark1.distance_to(placemark2, unit=DistanceUnit.MILES)
>>> # Works with Point and Coordinate objects too
>>> from kmlorm.models.point import Point, Coordinate
>>> point = Point(coordinates=(0, 0))
>>> coord = Coordinate(longitude=1, latitude=1)
>>> distance_to_point = placemark1.distance_to(point)
>>> distance_to_coord = placemark1.distance_to(coord)
"""
if not self.has_coordinates:
return None
# Use the point's spatial functionality if available
if self.point:
return self.point.distance_to(other, unit)
return None
[docs]
def bearing_to(
self, other: Union["Coordinate", "Point", "Placemark", Tuple[float, float], list]
) -> Optional[float]:
"""
Calculate bearing to another spatial object.
Args:
other: Target object with coordinates
Returns:
Initial bearing in degrees (0-360), or None if this placemark or target has
no coordinates 0° = North, 90° = East, 180° = South, 270° = West
Examples:
>>> placemark1 = Placemark(name="Start", coordinates=(0, 0))
>>> placemark2 = Placemark(name="East", coordinates=(1, 0))
>>> bearing = placemark1.bearing_to(placemark2) # Should be ~90°
"""
if not self.has_coordinates:
return None
# Use the point's spatial functionality if available
if self.point:
return self.point.bearing_to(other)
return None
[docs]
def midpoint_to(
self, other: Union["Coordinate", "Point", "Placemark", Tuple[float, float], list]
) -> Optional["Coordinate"]:
"""
Find geographic midpoint to another spatial object.
Args:
other: Target object with coordinates
Returns:
Coordinate at the midpoint, or None if this placemark or target has no coordinates
Examples:
>>> placemark1 = Placemark(name="Start", coordinates=(0, 0))
>>> placemark2 = Placemark(name="End", coordinates=(2, 2))
>>> midpoint = placemark1.midpoint_to(placemark2)
"""
if not self.has_coordinates:
return None
# Use the point's spatial functionality if available
if self.point:
return self.point.midpoint_to(other)
return None
[docs]
def validate(self) -> bool:
"""
Validate the placemark's data.
Returns:
True if validation passes
Raises:
KMLValidationError: If validation fails
"""
# Call parent validation first
super().validate()
# Import here to avoid circular imports
from ..core.exceptions import KMLValidationError # pylint: disable=import-outside-toplevel
# Validate point if present
if self.point is not None:
self.point.validate()
# Validate extended_data is a dictionary
if not isinstance(self.extended_data, dict):
raise KMLValidationError(
"Extended data must be a dictionary",
field="extended_data",
value=self.extended_data,
)
return True