aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/geojson2kml.py56
-rw-r--r--src/kml_utils.py80
-rw-r--r--src/n2000.py174
-rw-r--r--src/rn.py149
4 files changed, 289 insertions, 170 deletions
diff --git a/src/geojson2kml.py b/src/geojson2kml.py
index b923d98..8c116f0 100644
--- a/src/geojson2kml.py
+++ b/src/geojson2kml.py
@@ -1,60 +1,48 @@
+import argparse
import json
import xml.etree.ElementTree as ET
+from kml_utils import add_placemark, add_polygon, new_document, write_kml
-KML_NS = "http://www.opengis.net/kml/2.2"
-ET.register_namespace("", KML_NS)
+def geojson_to_kml(geojson: dict) -> ET.Element:
+ """Convert a GeoJSON FeatureCollection into a KML tree.
-def geojson_to_kml(geojson: dict) -> ET.ElementTree[ET.Element]:
- root = ET.Element(f"kml", xmlns=KML_NS)
- document = ET.SubElement(root, f"Document")
+ Only MultiPolygon features are supported; other geometry types are skipped.
- ET.SubElement(document, f"name").text = geojson.get("name", "Unnamed")
+ Args:
+ geojson (dict): Parsed GeoJSON FeatureCollection.
+ Returns:
+ ET.Element: The root <kml> element.
+ """
+ root, document = new_document(geojson.get("name", "Unnamed"))
for feature in geojson.get("features", []):
- placemark = ET.SubElement(document, f"Placemark")
-
- ET.SubElement(placemark, f"name").text = ""
- ET.SubElement(placemark, f"description").text = ""
-
- # TODO: Handle other geometry types if needed
- if feature["geometry"]["type"] != "MultiPolygon":
- print(f"Skipping feature with unsupported geometry type: {feature['geometry']['type']}")
+ geometry_type = feature["geometry"]["type"]
+ if geometry_type != "MultiPolygon":
+ print(f"Skipping feature with unsupported geometry type: {geometry_type}")
continue
- for polygon in feature["geometry"]["coordinates"][0]:
- polygon_el = ET.SubElement(placemark, f"Polygon")
-
- outer = ET.SubElement(polygon_el, f"outerBoundaryIs")
- ring = ET.SubElement(outer, f"LinearRing")
+ placemark = add_placemark(document)
+ for ring in feature["geometry"]["coordinates"][0]:
+ add_polygon(placemark, ring)
- coords = ET.SubElement(ring, f"coordinates")
- coords.text = "\n".join(
- f"{lon},{lat}"
- for lon, lat in polygon
- )
-
- ET.indent(root)
-
- return ET.ElementTree(root)
+ return root
def main():
- import argparse
-
parser = argparse.ArgumentParser(description="Convert GeoJSON to KML")
parser.add_argument("input_file", help="Path to the input GeoJSON file")
- parser.add_argument("-o", "--output_file", help="Path to the output KML file")
+ parser.add_argument("-o", "--output", help="Path to the output KML file")
args = parser.parse_args()
with open(args.input_file, "r") as geojson_file:
geojson = json.load(geojson_file)
- output_file = args.output_file or args.input_file.rsplit(".", 1)[0] + ".kml"
+ output_file = args.output or args.input_file.rsplit(".", 1)[0] + ".kml"
- kml = geojson_to_kml(geojson)
- kml.write(output_file, encoding="utf-8", xml_declaration=True)
+ root = geojson_to_kml(geojson)
+ write_kml(root, output_file)
if __name__ == "__main__":
diff --git a/src/kml_utils.py b/src/kml_utils.py
new file mode 100644
index 0000000..70319f8
--- /dev/null
+++ b/src/kml_utils.py
@@ -0,0 +1,80 @@
+"""Shared helpers for building simple KML documents from polygon data."""
+
+import xml.etree.ElementTree as ET
+
+KML_NS = "http://www.opengis.net/kml/2.2"
+ET.register_namespace("", KML_NS)
+
+
+def new_document(name: str) -> tuple[ET.Element, ET.Element]:
+ """Create a <kml><Document> pair and return (root, document).
+
+ Args:
+ name (str): Name of the document, shown as the map/layer title.
+ Returns:
+ tuple[ET.Element, ET.Element]: The root <kml> element and its child <Document> element.
+ """
+ root = ET.Element("kml", xmlns=KML_NS)
+ document = ET.SubElement(root, "Document")
+ ET.SubElement(document, "name").text = name
+ return root, document
+
+
+def add_placemark(document: ET.Element, name: str = "", description: str = "") -> ET.Element:
+ """Add a <Placemark> to `document` and return it.
+
+ Args:
+ document (ET.Element): The <Document> element to attach the placemark to.
+ name (str, optional): Placemark name. Defaults to "".
+ description (str, optional): Placemark description. Defaults to "".
+ Returns:
+ ET.Element: The newly created <Placemark> element.
+ """
+ placemark = ET.SubElement(document, "Placemark")
+ ET.SubElement(placemark, "name").text = name
+ ET.SubElement(placemark, "description").text = description
+ return placemark
+
+
+def add_polygon(parent: ET.Element, ring: list[tuple[float, float]]) -> ET.Element:
+ """Add a single-ring <Polygon> to `parent`.
+
+ Args:
+ parent (ET.Element): Element to attach the polygon to (a <Placemark> or <MultiGeometry>).
+ ring (list[tuple[float, float]]): Outer boundary points as (longitude, latitude) pairs.
+ Returns:
+ ET.Element: The newly created <Polygon> element.
+ """
+ polygon = ET.SubElement(parent, "Polygon")
+ outer = ET.SubElement(polygon, "outerBoundaryIs")
+ linear_ring = ET.SubElement(outer, "LinearRing")
+ coordinates = ET.SubElement(linear_ring, "coordinates")
+ coordinates.text = "\n".join(f"{lon},{lat}" for lon, lat in ring)
+ return polygon
+
+
+def add_multi_polygon(placemark: ET.Element, rings: list[list[tuple[float, float]]]) -> ET.Element:
+ """Add a <MultiGeometry> made of one <Polygon> per ring to `placemark`.
+
+ Args:
+ placemark (ET.Element): The <Placemark> element to attach the geometry to.
+ rings (list[list[tuple[float, float]]]): One outer boundary ring per polygon.
+ Returns:
+ ET.Element: The newly created <MultiGeometry> element.
+ """
+ multi_geometry = ET.SubElement(placemark, "MultiGeometry")
+ for ring in rings:
+ add_polygon(multi_geometry, ring)
+ return multi_geometry
+
+
+def write_kml(root: ET.Element, output_file: str) -> None:
+ """Indent and write a KML tree to `output_file`.
+
+ Args:
+ root (ET.Element): The root <kml> element to write.
+ output_file (str): Path to the output KML file.
+ """
+ tree = ET.ElementTree(root)
+ ET.indent(tree)
+ tree.write(output_file, encoding="utf-8", xml_declaration=True)
diff --git a/src/n2000.py b/src/n2000.py
index 357f744..a0741a0 100644
--- a/src/n2000.py
+++ b/src/n2000.py
@@ -1,96 +1,126 @@
+import argparse
+import json
import re
+import xml.etree.ElementTree as ET
+
import requests
+from kml_utils import add_multi_polygon, add_placemark, new_document, write_kml
+
+MAP_URL = "https://www.natura2000.fr/carte-natura2000"
+POPUP_URL_TEMPLATE = (
+ "https://www.natura2000.fr/leaflet-ajax-popup/node/{entity_id}"
+ "/pop_up_n2000_field/und?_wrapper_format=drupal_ajax"
+)
+SITE_URL_TEMPLATE = "https://www.natura2000.fr/site-natura/{slug}"
-reponse = requests.get("https://www.natura2000.fr/carte-natura2000")
-regex = r"\"entity_id\":\"(\d+)\""
-matches = re.findall(regex, reponse.text)
+def get_entity_ids(map_url: str = MAP_URL) -> list[str]:
+ """Fetch the Natura 2000 map page and extract every site's numeric entity id.
-kml_file = open("natura_2000.kml", "w", encoding="utf-8")
-kml_file.write("""<?xml version="1.0" encoding="UTF-8"?>
-<kml xmlns="http://www.opengis.net/kml/2.2">
- <Document>
- <name>Natura 2000</name>
-""")
+ Args:
+ map_url (str, optional): URL of the Natura 2000 map page. Defaults to MAP_URL.
+ Returns:
+ list[str]: Entity ids found on the page.
+ """
+ response = requests.get(map_url)
+ return re.findall(r"\"entity_id\":\"(\d+)\"", response.text)
-for i, match in enumerate(matches):
- url = f"https://www.natura2000.fr/leaflet-ajax-popup/node/{match}/pop_up_n2000_field/und?_wrapper_format=drupal_ajax"
- response = requests.get(url)
+def get_site_slug(entity_id: str) -> str | None:
+ """Resolve an entity id to its site page URL slug via the popup endpoint.
- regex = r"href=\\u0022\\/site-natura\\/(.*?)\\u0022"
- match = re.search(regex, response.text)
+ Args:
+ entity_id (str): Numeric entity id, as found by get_entity_ids.
+ Returns:
+ str | None: The site URL slug, or None if it could not be found.
+ """
+ response = requests.get(POPUP_URL_TEMPLATE.format(entity_id=entity_id))
+ match = re.search(r"href=\\u0022\\/site-natura\\/(.*?)\\u0022", response.text)
+ return match.group(1) if match else None
- if not match:
- print(f"No match found for entity_id {match}.")
- continue
- site_url = f"https://www.natura2000.fr/site-natura/{match.group(1)}"
+def get_site_details(site_url: str) -> tuple[str, list[list[tuple[float, float]]]] | None:
+ """Fetch a site page and extract its name and outer polygon rings.
+
+ Args:
+ site_url (str): URL of the site page.
+ Returns:
+ tuple[str, list[list[tuple[float, float]]]] | None: (name, rings), where each ring is
+ a list of (longitude, latitude) points, or None if the name, geometry type, or
+ coordinates could not be found on the page.
+ """
response = requests.get(site_url)
- regex = r"<title>(.*?) \| Natura 2000</title>"
- match = re.search(regex, response.text)
- if not match:
+ name_match = re.search(r"<title>(.*?) \| Natura 2000</title>", response.text)
+ if not name_match:
print(f"No name found for {site_url}.")
- continue
- name = match.group(1)
+ return None
+ name = name_match.group(1)
- regex = r"\"type\":\"(multipolygon|polygon)\""
- match = re.search(regex, response.text)
- if not match:
+ type_match = re.search(r"\"type\":\"(multipolygon|polygon)\"", response.text)
+ if not type_match:
print(f"No geometry type found for {site_url}.")
- continue
- geometry_type = match.group(1)
-
+ return None
+ geometry_type = type_match.group(1)
+
if geometry_type == "polygon":
- regex = r"\"points\":(\[\[.*?\]\])"
- elif geometry_type == "multipolygon":
- regex = r"\"points\":(\[\[\[.*?\]\]\])"
+ points_regex = r"\"points\":(\[\[.*?\]\])"
else:
- print(f"Unsupported geometry type {geometry_type} for {site_url}.")
- continue
+ points_regex = r"\"points\":(\[\[\[.*?\]\]\])"
- match = re.search(regex, response.text)
- if not match:
+ points_match = re.search(points_regex, response.text)
+ if not points_match:
print(f"No coordinates found for {site_url}.")
- continue
- polygons = eval(match.group(1))
-
+ return None
+
+ polygons = json.loads(points_match.group(1))
if geometry_type == "polygon":
- # Wrap single polygon in a list to treat it as multipolygon
- polygons = [polygons]
+ polygons = [polygons] # normalize to a list of polygons, like multipolygon
+
+ rings = [[(point["lon"], point["lat"]) for point in polygon[0]] for polygon in polygons]
+ return name, rings
+
+
+def scrape_natura2000(map_url: str = MAP_URL) -> ET.Element:
+ """Scrape natura2000.fr and build a KML tree with one Placemark per site.
+
+ Args:
+ map_url (str, optional): URL of the Natura 2000 map page. Defaults to MAP_URL.
+ Returns:
+ ET.Element: The root <kml> element.
+ """
+ root, document = new_document("Natura 2000")
+
+ entity_ids = get_entity_ids(map_url)
+ for i, entity_id in enumerate(entity_ids):
+ slug = get_site_slug(entity_id)
+ if slug is None:
+ print(f"No site URL found for entity_id {entity_id}.")
+ continue
+
+ site_url = SITE_URL_TEMPLATE.format(slug=slug)
+ details = get_site_details(site_url)
+ if details is None:
+ continue
+ name, rings = details
+
+ placemark = add_placemark(document, name=name, description=f"{name}<br/>{site_url}")
+ add_multi_polygon(placemark, rings)
+
+ print(f"Processed: {name} ({i + 1:5d}/{len(entity_ids)})")
+
+ return root
+
+
+def main():
+ parser = argparse.ArgumentParser(description="Scrape natura2000.fr and export sites to KML")
+ parser.add_argument("-o", "--output", default="natura_2000.kml", help="Path to the output KML file")
+ args = parser.parse_args()
- kml_file.write(f"""
- <Placemark>
- <name>{name}</name>
- <description>
- {name}<br/>{site_url}
- </description>
- <MultiGeometry>
- """)
+ root = scrape_natura2000()
+ write_kml(root, args.output)
- for polygon in polygons:
- kml_file.write("""
- <Polygon>
- <outerBoundaryIs>
- <LinearRing>
- <coordinates>
- """)
- for point in polygon[0]:
- kml_file.write(f"{point['lon']},{point['lat']},0\n")
- kml_file.write("""
- </coordinates>
- </LinearRing>
- </outerBoundaryIs>
- </Polygon>
- """)
- kml_file.write(f"""
- </MultiGeometry>
- </Placemark>
- """)
-
- print(f"Processed: {name} ({i+1: 5d}/{len(matches)})")
-kml_file.write(""" </Document>
-</kml>""")
+if __name__ == "__main__":
+ main()
diff --git a/src/rn.py b/src/rn.py
index 0c6589c..c78f1c2 100644
--- a/src/rn.py
+++ b/src/rn.py
@@ -1,81 +1,102 @@
-import requests
+import argparse
+import json
import re
+import xml.etree.ElementTree as ET
+
+import requests
+
+from kml_utils import add_placemark, add_polygon, new_document, write_kml
+LIST_URL = "https://reserves-naturelles.org/reserves-naturelles/"
-def get_coordinates_from_url(url):
+
+def get_coordinates_from_url(url: str) -> list[tuple[float, float]] | None:
+ """Fetch a reserve page and extract its outer polygon ring.
+
+ Args:
+ url (str): URL of the reserve page.
+ Returns:
+ list[tuple[float, float]] | None: (longitude, latitude) points, or None if no
+ coordinates could be found on the page.
+ """
response = requests.get(url)
- regex = r"\[\[-?\d+\.\d+,-?\d+\.\d+\](?:,\[-?\d+\.\d+,-?\d+\.\d+\])+\]"
- matche = re.search(regex, response.text)
- if not matche:
+ match = re.search(
+ r"\[\[-?\d+\.\d+,-?\d+\.\d+\](?:,\[-?\d+\.\d+,-?\d+\.\d+\])+\]", response.text
+ )
+ if not match:
return None
- coordinates = eval(matche.group(0))
- return coordinates
+ return [tuple(point) for point in json.loads(match.group(0))]
+
+
+def get_reserves(list_url: str = LIST_URL) -> list[dict]:
+ """Fetch the reserves list page and extract each reserve's name and URL.
+
+ Args:
+ list_url (str, optional): URL of the reserves list page. Defaults to LIST_URL.
+ Returns:
+ list[dict]: One {"full_name", "name", "url"} entry per reserve found on the page.
+ """
+ response = requests.get(list_url)
+ items = re.findall(r"<li class=\"reserve\"[\s\S]*?</li>", response.text)
+
+ reserves = []
+ for item in items:
+ full_name_match = re.search(r"title=\"([^\"]+)\"", item)
+ name_match = re.search(r"class=\"rsv_nom\">([^<]+)<", item)
+ url_match = re.search(r"href=\"([^\"]+)\"", item)
+
+ if not full_name_match or not name_match or not url_match:
+ print(f"Skipping incomplete list entry: {item}")
+ continue
+
+ reserves.append(
+ {
+ "full_name": full_name_match.group(1),
+ "name": name_match.group(1),
+ "url": url_match.group(1),
+ }
+ )
+
+ return reserves
-response = requests.get("https://reserves-naturelles.org/reserves-naturelles/")
-regex = r"<li class=\"reserve\"[\s\S]*?</li>"
-matches = re.findall(regex, response.text)
+def scrape_reserves(list_url: str = LIST_URL) -> ET.Element:
+ """Scrape reserves-naturelles.org and build a KML tree with one Placemark per reserve.
-kml_file = open("reserves_naturelles.kml", "w", encoding="utf-8")
-kml_file.write("""<?xml version="1.0" encoding="UTF-8"?>
-<kml xmlns="http://www.opengis.net/kml/2.2">
- <Document>
- <name>RN</name>
-""")
+ Args:
+ list_url (str, optional): URL of the reserves list page. Defaults to LIST_URL.
+ Returns:
+ ET.Element: The root <kml> element.
+ """
+ root, document = new_document("RN")
+ reserves = get_reserves(list_url)
+ for i, reserve in enumerate(reserves):
+ coordinates = get_coordinates_from_url(reserve["url"])
+ if not coordinates:
+ print(f"No coordinates found for URL: {reserve['url']}")
+ continue
-for i, match in enumerate(matches):
+ placemark = add_placemark(
+ document,
+ name=reserve["name"],
+ description=f"{reserve['full_name']}<br/>{reserve['url']}",
+ )
+ add_polygon(placemark, coordinates)
- regex = r"title=\"([^\"]+)\""
- full_name = re.search(regex, match)
- if not full_name:
- print(f"Full name not found in the match: {match}")
- continue
- full_name = full_name.group(1)
+ print(f"Processed: {reserve['name']} ({i + 1:3d}/{len(reserves)})")
- regex = r"class=\"rsv_nom\">([^<]+)<"
- name = re.search(regex, match)
- if not name:
- print(f"Name not found in the match: {match}")
- continue
- name = name.group(1)
+ return root
- regex = r"href=\"([^\"]+)\""
- url = re.search(regex, match)
- if not url:
- print(f"URL not found in the match: {match}")
- continue
- url = url.group(1)
- coordinates = get_coordinates_from_url(url)
- if not coordinates:
- print(f"Coordinates not found for URL: {url}")
- continue
+def main():
+ parser = argparse.ArgumentParser(description="Scrape reserves-naturelles.org and export reserves to KML")
+ parser.add_argument("-o", "--output", default="reserves_naturelles.kml", help="Path to the output KML file")
+ args = parser.parse_args()
- kml_file.write(f"""
- <Placemark>
- <name>{name}</name>
- <description>
- {full_name}<br/>{url}
- </description>
- <Polygon>
- <outerBoundaryIs>
- <LinearRing>
- <coordinates>
-""")
- for coordinate in coordinates:
- kml_file.write(f" {coordinate[0]},{coordinate[1]},0\n")
- kml_file.write("""
- </coordinates>
- </LinearRing>
- </outerBoundaryIs>
- </Polygon>
- </Placemark>
-""")
-
- print(f"Processed: {name} ({i+1: 3d}/{len(matches)})")
+ root = scrape_reserves()
+ write_kml(root, args.output)
-kml_file.write("""
- </Document>
-</kml>""")
+if __name__ == "__main__":
+ main()