import base64
import json
import logging
import tempfile
import xml.etree.ElementTree as ET
import zipfile
from pathlib import Path
from typing import Any
import requests
from cyber_valley.geodata.models import GeodataLayer
log = logging.getLogger(__name__)
KML_NAMESPACE = "http://www.opengis.net/kml/2.2"
ET.register_namespace("", KML_NAMESPACE)
def sync_geodata(url: str) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
tmpdir_path = Path(tmpdir)
kmz_file = tmpdir_path / "map.kmz"
log.info("Downloading KMZ from %s...", url)
response = requests.get(url, timeout=30)
response.raise_for_status()
with kmz_file.open("wb") as f:
f.write(response.content)
log.info("Downloaded %d bytes", len(response.content))
log.info("Extracting KMZ...")
with zipfile.ZipFile(kmz_file, "r") as zip_ref:
zip_ref.extractall(tmpdir_path)
kml_files = list(tmpdir_path.glob("*.kml"))
if not kml_files:
log.error("No KML file found in KMZ")
return
log.info("Found %d KML files", len(kml_files))
for kml_file in kml_files:
log.info("Found KML: %s", kml_file.name)
layers = process_kml_by_folders(kml_file)
for folder_name, geodata in layers.items():
layer_name = folder_name.lower().replace(" ", "_")
GeodataLayer.objects.update_or_create(
name=layer_name,
defaults={
"data": geodata,
"source_file": url,
"is_active": True,
},
)
log.info("Saved geodata layer: %s", layer_name)
def get_style_element_by_url(
style_url: str | None, global_styles: dict[str, ET.Element]
) -> ET.Element | None:
if style_url and style_url.startswith("#"):
style_id = style_url.lstrip("#")
return global_styles.get(style_id)
return None
def resolve_normal_style(
style_element: ET.Element | None, global_styles: dict[str, ET.Element]
) -> ET.Element | None:
if (
style_element is not None
and style_element.tag == f"{{{KML_NAMESPACE}}}StyleMap"
):
for pair in style_element.findall(f"./{{{KML_NAMESPACE}}}Pair"):
key_tag = pair.find(f"./{{{KML_NAMESPACE}}}key")
if key_tag is not None and key_tag.text == "normal":
style_url_tag = pair.find(f"./{{{KML_NAMESPACE}}}styleUrl")
if style_url_tag is not None and style_url_tag.text:
return get_style_element_by_url(style_url_tag.text, global_styles)
return None
return style_element
def get_coordinates(geometry_tag: ET.Element) -> str:
coords_tag = geometry_tag.find(f".//{{{KML_NAMESPACE}}}coordinates")
return (
coords_tag.text.strip()
if coords_tag is not None and coords_tag.text is not None
else ""
)
def parse_coordinates(coord_string: str) -> list[dict[str, float]]:
if not coord_string:
return []
coord_sets = coord_string.strip().split()
transformed_coords = []
for coord_set in coord_sets:
try:
lon, lat, *_ = map(float, coord_set.split(","))
transformed_coords.append({"lat": lat, "lng": lon})
except ValueError:
continue
return transformed_coords
def resolve_icon_url(icon_url: str, kml_path: Path) -> str:
if not icon_url:
return ""
if icon_url.startswith(("http://", "https://", "data:")):
return icon_url
kml_dir = Path(kml_path).parent
icon_path = kml_dir / icon_url
if icon_path.exists():
try:
with icon_path.open("rb") as f:
image_data = f.read()
suffix = icon_path.suffix.lower()
mime_type = {
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".svg": "image/svg+xml",
}.get(suffix, "image/png")
except Exception:
log.exception("Failed to encode icon %s", icon_url)
return ""
else:
base64_data = base64.b64encode(image_data).decode("utf-8")
return f"data:{mime_type};base64,{base64_data}"
log.warning("Icon file not found: %s", icon_path)
return ""
def placemark_to_json(
placemark: ET.Element,
global_styles: dict[str, ET.Element],
kml_path: Path | None = None,
) -> dict[str, Any] | None:
placemark_name_tag = placemark.find(f"./{{{KML_NAMESPACE}}}name")
if placemark_name_tag is not None:
placemark_name = (placemark_name_tag.text or "").strip()
if not placemark_name:
placemark_name = "Unnamed Placemark"
else:
placemark_name = "Unnamed Placemark"
attributes: dict[str, str] = {}
extended_data_tag = placemark.find(f"./{{{KML_NAMESPACE}}}ExtendedData")
if extended_data_tag is not None:
for data_tag in extended_data_tag.findall(f"./{{{KML_NAMESPACE}}}Data"):
key = data_tag.get("name", "")
value_tag = data_tag.find(f"./{{{KML_NAMESPACE}}}value")
if key and value_tag is not None and value_tag.text is not None:
attributes[key] = value_tag.text.strip()
style_url_tag = placemark.find(f"./{{{KML_NAMESPACE}}}styleUrl")
initial_style_element = None
if style_url_tag is not None and style_url_tag.text:
initial_style_element = get_style_element_by_url(
style_url_tag.text, global_styles
)
final_style = resolve_normal_style(initial_style_element, global_styles)
polygon_tag = placemark.find(f"./{{{KML_NAMESPACE}}}Polygon")
if polygon_tag is not None:
coordinates = get_coordinates(polygon_tag)
line_color = ""
poly_color = ""
if final_style is not None:
line_style_tag = final_style.find(f"./{{{KML_NAMESPACE}}}LineStyle")
if line_style_tag is not None:
color_tag = line_style_tag.find(f"./{{{KML_NAMESPACE}}}color")
line_color = (
color_tag.text.strip()
if color_tag is not None and color_tag.text is not None
else line_color
)
poly_style_tag = final_style.find(f"./{{{KML_NAMESPACE}}}PolyStyle")
if poly_style_tag is not None:
color_tag = poly_style_tag.find(f"./{{{KML_NAMESPACE}}}color")
poly_color = (
color_tag.text.strip()
if color_tag is not None and color_tag.text is not None
else poly_color
)
result: dict[str, Any] = {
"name": placemark_name,
"type": "polygon",
"coordinates": parse_coordinates(coordinates),
"polygon_color": poly_color,
"line_color": line_color,
}
if attributes:
result["attributes"] = attributes
return result
line_string_tag = placemark.find(f"./{{{KML_NAMESPACE}}}LineString")
if line_string_tag is not None:
coordinates = get_coordinates(line_string_tag)
line_color = ""
if final_style is not None:
line_style_tag = final_style.find(f"./{{{KML_NAMESPACE}}}LineStyle")
if line_style_tag is not None:
color_tag = line_style_tag.find(f"./{{{KML_NAMESPACE}}}color")
line_color = (
color_tag.text.strip()
if color_tag is not None and color_tag.text is not None
else line_color
)
line_result: dict[str, Any] = {
"name": placemark_name,
"type": "line",
"coordinates": parse_coordinates(coordinates),
"line_color": line_color,
}
if attributes:
line_result["attributes"] = attributes
return line_result
point_tag = placemark.find(f"./{{{KML_NAMESPACE}}}Point")
if point_tag is not None:
coordinates = get_coordinates(point_tag)
icon_url = ""
if final_style is not None:
icon_style_tag = final_style.find(f"./{{{KML_NAMESPACE}}}IconStyle")
if icon_style_tag is not None:
icon_tag = icon_style_tag.find(f"./{{{KML_NAMESPACE}}}Icon")
if icon_tag is not None:
href_tag = icon_tag.find(f"./{{{KML_NAMESPACE}}}href")
raw_icon_url = (
href_tag.text.strip()
if href_tag is not None and href_tag.text is not None
else ""
)
if raw_icon_url and kml_path:
icon_url = resolve_icon_url(raw_icon_url, kml_path)
else:
icon_url = raw_icon_url
point_result: dict[str, Any] = {
"name": placemark_name,
"type": "point",
"coordinates": parse_coordinates(coordinates),
"iconUrl": icon_url,
}
if attributes:
point_result["attributes"] = attributes
return point_result
return None
def process_kml_file(kml_path: Path | str) -> list[dict[str, Any]]:
kml_path = Path(kml_path)
if not kml_path.exists():
raise FileNotFoundError(f"KML file not found: {kml_path}")
tree = ET.parse(kml_path) root = tree.getroot()
global_styles = {}
document_element = root.find(f".//{{{KML_NAMESPACE}}}Document")
style_container = document_element if document_element is not None else root
for style_type in ["Style", "StyleMap"]:
for style_element in style_container.findall(
f".//{{{KML_NAMESPACE}}}{style_type}"
):
style_id = style_element.get("id")
if style_id:
global_styles[style_id] = style_element
all_placemarks_json = []
placemark_elements = root.findall(f".//{{{KML_NAMESPACE}}}Placemark")
for placemark in placemark_elements:
result = placemark_to_json(placemark, global_styles, kml_path)
if result:
all_placemarks_json.append(result)
return all_placemarks_json
def deduplicate_placemarks(
placemarks: list[dict[str, Any]],
) -> list[dict[str, Any]]:
seen: dict[str, dict[str, Any]] = {}
for placemark in placemarks:
name = placemark.get("name", "")
has_attributes = bool(placemark.get("attributes"))
if name not in seen:
seen[name] = placemark
else:
existing = seen[name]
existing_has_attrs = bool(existing.get("attributes"))
if has_attributes and not existing_has_attrs:
seen[name] = placemark
return list(seen.values())
def process_kml_by_folders(kml_path: Path | str) -> dict[str, list[dict[str, Any]]]:
kml_path = Path(kml_path)
if not kml_path.exists():
raise FileNotFoundError(f"KML file not found: {kml_path}")
tree = ET.parse(kml_path) root = tree.getroot()
global_styles = {}
document_element = root.find(f".//{{{KML_NAMESPACE}}}Document")
style_container = document_element if document_element is not None else root
for style_type in ["Style", "StyleMap"]:
for style_element in style_container.findall(
f".//{{{KML_NAMESPACE}}}{style_type}"
):
style_id = style_element.get("id")
if style_id:
global_styles[style_id] = style_element
layers = {}
folders = root.findall(f".//{{{KML_NAMESPACE}}}Folder")
for folder in folders:
folder_name_tag = folder.find(f"./{{{KML_NAMESPACE}}}name")
folder_name = (
folder_name_tag.text.strip()
if folder_name_tag is not None and folder_name_tag.text
else "Unnamed"
)
placemarks = []
for placemark in folder.findall(f"./{{{KML_NAMESPACE}}}Placemark"):
result = placemark_to_json(placemark, global_styles, kml_path)
if result:
placemarks.append(result)
if placemarks:
layers[folder_name] = deduplicate_placemarks(placemarks)
return layers
def load_json_file(json_path: Path | str) -> Any:
json_path = Path(json_path)
if not json_path.exists():
raise FileNotFoundError(f"JSON file not found: {json_path}")
with json_path.open(encoding="utf-8") as f:
return json.load(f)