120 lines
3.8 KiB
Python
120 lines
3.8 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
combine_kmz_to_kml.py
|
||
|
||
Combine all KMZ files in a folder into a single KML file.
|
||
|
||
Usage:
|
||
python combine_kmz_to_kml.py <input_folder> <output_kml>
|
||
|
||
Example:
|
||
python combine_kmz_to_kml.py ./kmz_files all_points.kml
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import zipfile
|
||
import xml.etree.ElementTree as ET
|
||
from pathlib import Path
|
||
from typing import List
|
||
|
||
# ------------------------------------------------------------
|
||
# Helper functions
|
||
# ------------------------------------------------------------
|
||
|
||
def find_kmz_files(folder: Path) -> List[Path]:
|
||
"""Return a list of .kmz files in the given folder (non‑recursive)."""
|
||
return list(folder.rglob("*.kmz"))
|
||
|
||
def extract_kml_from_kmz(kmz_path: Path) -> ET.ElementTree:
|
||
"""
|
||
Extract the first KML file found inside a KMZ archive.
|
||
Returns an ElementTree of the extracted KML.
|
||
"""
|
||
with zipfile.ZipFile(kmz_path, "r") as z:
|
||
# Find the first file ending with .kml (case‑insensitive)
|
||
kml_name = next((n for n in z.namelist()
|
||
if n.lower().endswith(".kml")), None)
|
||
if not kml_name:
|
||
raise ValueError(f"No KML file found inside {kmz_path}")
|
||
with z.open(kml_name) as kml_file:
|
||
return ET.parse(kml_file)
|
||
|
||
def get_top_level_elements(kml_tree: ET.ElementTree) -> List[ET.Element]:
|
||
"""
|
||
Return a list of the top‑level elements that contain placemarks.
|
||
Common patterns are:
|
||
<Document>
|
||
<Folder>
|
||
<Placemark>
|
||
"""
|
||
root = kml_tree.getroot()
|
||
# KML namespace handling (most KMLs use the same namespace)
|
||
ns = {"kml": "http://www.opengis.net/kml/2.2"}
|
||
# Find <Document> or <Folder> or <Placemark> elements directly under root
|
||
elements = []
|
||
for tag in ["Document", "Folder", "Placemark"]:
|
||
elements.extend(root.findall(f"kml:{tag}", ns))
|
||
return elements
|
||
|
||
def build_merged_kml(elements_by_file: List[List[ET.Element]]) -> ET.ElementTree:
|
||
"""
|
||
Build a new KML ElementTree that contains a single <Document>
|
||
wrapping all collected elements.
|
||
"""
|
||
kml_ns = "http://www.opengis.net/kml/2.2"
|
||
ET.register_namespace("", kml_ns) # ensure no prefix in output
|
||
|
||
kml_root = ET.Element(f"{{{kml_ns}}}kml")
|
||
doc = ET.SubElement(kml_root, "Document")
|
||
|
||
for elems in elements_by_file:
|
||
for elem in elems:
|
||
doc.append(elem)
|
||
|
||
return ET.ElementTree(kml_root)
|
||
|
||
# ------------------------------------------------------------
|
||
# Main logic
|
||
# ------------------------------------------------------------
|
||
|
||
def main(input_folder: Path, output_file: Path):
|
||
kmz_files = find_kmz_files(input_folder)
|
||
if not kmz_files:
|
||
print(f"No .kmz files found in {input_folder}")
|
||
sys.exit(1)
|
||
|
||
all_elements = []
|
||
|
||
for kmz in kmz_files:
|
||
try:
|
||
kml_tree = extract_kml_from_kmz(kmz)
|
||
elems = get_top_level_elements(kml_tree)
|
||
if not elems:
|
||
print(f"WARNING: No placemark elements found in {kmz}")
|
||
else:
|
||
all_elements.append(elems)
|
||
except Exception as e:
|
||
print(f"ERROR processing {kmz}: {e}")
|
||
|
||
if not any(all_elements):
|
||
print("No placemark data to write.")
|
||
sys.exit(1)
|
||
|
||
merged_tree = build_merged_kml(all_elements)
|
||
merged_tree.write(output_file, encoding="utf-8", xml_declaration=True)
|
||
print(f"Combined KML written to {output_file}")
|
||
|
||
if __name__ == "__main__":
|
||
if len(sys.argv) != 3:
|
||
print("Usage: python combine_kmz_to_kml.py <input_folder> <output_kml>")
|
||
sys.exit(1)
|
||
|
||
input_dir = Path(sys.argv[1]).expanduser().resolve()
|
||
output_kml = Path(sys.argv[2]).expanduser().resolve()
|
||
|
||
if not input_dir.is_dir():
|
||
print(f"Input path {input_dir} is not a directory.")
|
||
sys.exit(1)
|
||
|
||
main(input_dir, output_kml) |