aboutsummaryrefslogtreecommitdiff
path: root/src/n2000.py
blob: a0741a032a7a860d51e4a36571e38f96a9d53f47 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
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}"


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.

    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)


def get_site_slug(entity_id: str) -> str | None:
    """Resolve an entity id to its site page URL slug via the popup endpoint.

    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


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)

    name_match = re.search(r"<title>(.*?) \| Natura 2000</title>", response.text)
    if not name_match:
        print(f"No name found for {site_url}.")
        return None
    name = name_match.group(1)

    type_match = re.search(r"\"type\":\"(multipolygon|polygon)\"", response.text)
    if not type_match:
        print(f"No geometry type found for {site_url}.")
        return None
    geometry_type = type_match.group(1)

    if geometry_type == "polygon":
        points_regex = r"\"points\":(\[\[.*?\]\])"
    else:
        points_regex = r"\"points\":(\[\[\[.*?\]\]\])"

    points_match = re.search(points_regex, response.text)
    if not points_match:
        print(f"No coordinates found for {site_url}.")
        return None

    polygons = json.loads(points_match.group(1))
    if geometry_type == "polygon":
        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()

    root = scrape_natura2000()
    write_kml(root, args.output)


if __name__ == "__main__":
    main()