getopendatafvg 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,119 @@
1
+ """getopendatafvg: extract and visualize open geographic, socio-economic and
2
+ environmental data for Friuli Venezia Giulia.
3
+
4
+ This is an early, incremental extraction from the data pipeline behind
5
+ Mappa Civica (https://github.com/BeneComune/mappa-civica), a civic mapping
6
+ platform for the comune of Montereale Valcellina. Modules are being ported
7
+ and generalized one at a time rather than all at once, so the surface area
8
+ here will keep growing.
9
+ """
10
+
11
+ from ._errors import NoCleanSceneFoundError
12
+ from .avalanche import AvalancheSite, fetch_avalanche_sites
13
+ from .catalog import (
14
+ CATALOG,
15
+ KnownDataset,
16
+ fetch_known_dataset,
17
+ list_known_datasets,
18
+ search_known_datasets,
19
+ )
20
+ from .catasto import fetch_cadastral_parcels, region_parquet_name
21
+ from .coverage import build_coverage_union, line_coverage_pct
22
+ from .dem import DemSampler, compute_line_grade, compute_line_slope, slope_class_for_value
23
+ from .elevation import fetch_elevation
24
+ from .indices import IndexRaster, compute_lst, compute_nbr, compute_ndvi
25
+ from .istat import (
26
+ FVG_NUTS_AREAS,
27
+ fetch_bank_branches,
28
+ fetch_consumer_price_index,
29
+ fetch_demographic_balance,
30
+ fetch_demographic_indicators,
31
+ fetch_employment_rate,
32
+ fetch_income_series,
33
+ fetch_istat_dataflow,
34
+ fetch_population_series,
35
+ fetch_tourism_capacity,
36
+ fetch_tourism_flows,
37
+ )
38
+ from .landsat import LandsatScene, UsgsCredentials, fetch_landsat_scene
39
+ from .open_data_fvg import (
40
+ PortalAsset,
41
+ fetch_open_data_fvg,
42
+ lon_lat_box_clause,
43
+ search_open_data_fvg,
44
+ within_box_clause,
45
+ )
46
+ from .overpass import boundary_poly_filter, element_point, fetch_overpass_elements, query_overpass
47
+ from .pmtiles import TippecanoeNotFoundError, build_pmtiles
48
+ from .scene_date import landsat_scene_date, sentinel2_scene_date
49
+ from .sentinel2 import DEFAULT_BANDS, Band, CdseCredentials, Sentinel2Scene, fetch_sentinel2_scene
50
+ from .vectorize import ClassBreak, classify_and_vectorize
51
+ from .viz import plot_index
52
+ from .weather import DailyWeather, fetch_historical_weather
53
+ from .wfs import clip_to_boundary, fetch_and_clip_wfs_features, fetch_wfs_features
54
+ from .wikipedia import fetch_wikipedia_summary
55
+
56
+ __all__ = [
57
+ 'CATALOG',
58
+ 'DEFAULT_BANDS',
59
+ 'FVG_NUTS_AREAS',
60
+ 'AvalancheSite',
61
+ 'Band',
62
+ 'CdseCredentials',
63
+ 'ClassBreak',
64
+ 'DailyWeather',
65
+ 'DemSampler',
66
+ 'IndexRaster',
67
+ 'KnownDataset',
68
+ 'LandsatScene',
69
+ 'NoCleanSceneFoundError',
70
+ 'PortalAsset',
71
+ 'Sentinel2Scene',
72
+ 'TippecanoeNotFoundError',
73
+ 'UsgsCredentials',
74
+ 'boundary_poly_filter',
75
+ 'build_coverage_union',
76
+ 'build_pmtiles',
77
+ 'classify_and_vectorize',
78
+ 'clip_to_boundary',
79
+ 'compute_line_grade',
80
+ 'compute_line_slope',
81
+ 'compute_lst',
82
+ 'compute_nbr',
83
+ 'compute_ndvi',
84
+ 'element_point',
85
+ 'fetch_and_clip_wfs_features',
86
+ 'fetch_avalanche_sites',
87
+ 'fetch_bank_branches',
88
+ 'fetch_cadastral_parcels',
89
+ 'fetch_consumer_price_index',
90
+ 'fetch_demographic_balance',
91
+ 'fetch_demographic_indicators',
92
+ 'fetch_elevation',
93
+ 'fetch_employment_rate',
94
+ 'fetch_historical_weather',
95
+ 'fetch_income_series',
96
+ 'fetch_istat_dataflow',
97
+ 'fetch_known_dataset',
98
+ 'fetch_landsat_scene',
99
+ 'fetch_open_data_fvg',
100
+ 'fetch_overpass_elements',
101
+ 'fetch_population_series',
102
+ 'fetch_sentinel2_scene',
103
+ 'fetch_tourism_capacity',
104
+ 'fetch_tourism_flows',
105
+ 'fetch_wfs_features',
106
+ 'fetch_wikipedia_summary',
107
+ 'landsat_scene_date',
108
+ 'line_coverage_pct',
109
+ 'list_known_datasets',
110
+ 'lon_lat_box_clause',
111
+ 'plot_index',
112
+ 'query_overpass',
113
+ 'region_parquet_name',
114
+ 'search_known_datasets',
115
+ 'search_open_data_fvg',
116
+ 'sentinel2_scene_date',
117
+ 'slope_class_for_value',
118
+ 'within_box_clause',
119
+ ]
@@ -0,0 +1,6 @@
1
+ class NoCleanSceneFoundError(RuntimeError):
2
+ """No scene under the cloud-cover threshold was found within the
3
+ lookback window. Shared by every scene-fetching module (Sentinel-2,
4
+ Landsat, ...) since it means the same thing regardless of provider:
5
+ leave whatever data you already have alone, don't wipe it.
6
+ """
@@ -0,0 +1,135 @@
1
+ """Avalanche sites in Friuli Venezia Giulia, merged from the region's two
2
+ avalanche layers into one list.
3
+
4
+ The region publishes the Catasto Valanghe as two WFS layers, and the
5
+ split is by survey method rather than by area: `CV_VALANGHE_RILEVATE`
6
+ holds sites surveyed on the ground (3875 features) and
7
+ `CV_VALANGHE_FOTOINT` sites mapped from aerial photography (3255). They
8
+ do not overlap - checked live, the two share not one `ID_SITO` - so a
9
+ caller wanting "avalanche sites here" needs both and has no way to tell
10
+ from either layer alone that the other exists. That is what this module
11
+ is for.
12
+
13
+ Deliberately not a hazard model. The region publishes perimeters and
14
+ site attributes, not a hazard class, so this exposes what the data says
15
+ (release and runout elevations, aspect, site type, which survey it came
16
+ from) and stops there. Anything resembling a danger rating would be
17
+ invented here rather than sourced, and the catalog entries are still the
18
+ way to get at the raw layers.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from dataclasses import dataclass
24
+ from typing import Any
25
+
26
+ from shapely.geometry import shape
27
+ from shapely.geometry.base import BaseGeometry
28
+
29
+ from .catalog import WFS_BASE_URL
30
+ from .wfs import fetch_and_clip_wfs_features, fetch_wfs_features
31
+
32
+ SURVEYED_LAYER = 'ZONE_RISC:CV_VALANGHE_RILEVATE'
33
+ PHOTO_INTERPRETED_LAYER = 'ZONE_RISC:CV_VALANGHE_FOTOINT'
34
+
35
+ # Both layers are several thousand features and fetch_wfs_features caps
36
+ # at 1000 by default, which would silently truncate to a quarter of the
37
+ # catasto. This is above the current totals with room to grow.
38
+ DEFAULT_COUNT = 10000
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class AvalancheSite:
43
+ """One avalanche site. `geometries` is a tuple because a site is
44
+ occasionally stored as more than one polygon row sharing an
45
+ `ID_SITO` - two such sites exist in the surveyed layer - and dropping
46
+ the extra rows would quietly shrink the site's footprint.
47
+ """
48
+
49
+ site_id: int | None
50
+ comune: str
51
+ toponym: str
52
+ site_type: str
53
+ release_elevation_max_m: int | None
54
+ runout_elevation_min_m: int | None
55
+ aspect: str
56
+ photo_interpreted: bool
57
+ geometries: tuple[BaseGeometry, ...]
58
+
59
+
60
+ def fetch_avalanche_sites(
61
+ boundary: BaseGeometry | None = None,
62
+ photo_interpreted: bool | None = None,
63
+ count: int = DEFAULT_COUNT,
64
+ ) -> list[AvalancheSite]:
65
+ """Avalanche sites from both layers as one list, sorted by comune then
66
+ toponym.
67
+
68
+ With a `boundary` the geometries come back clipped to it, exactly,
69
+ and sites falling outside are dropped. Without one, every site in the
70
+ region is returned - about 7100 of them.
71
+
72
+ `photo_interpreted` filters by survey method: `False` for
73
+ ground-surveyed sites only, `True` for photo-interpreted only, `None`
74
+ (the default) for both, which is the point of the module.
75
+ """
76
+ sites: list[AvalancheSite] = []
77
+ layers = {SURVEYED_LAYER: False, PHOTO_INTERPRETED_LAYER: True}
78
+ for type_name, from_photo in layers.items():
79
+ if photo_interpreted is not None and photo_interpreted != from_photo:
80
+ continue
81
+ sites.extend(_sites_from_layer(type_name, from_photo, boundary, count))
82
+ return sorted(sites, key=lambda s: (s.comune, s.toponym))
83
+
84
+
85
+ def _sites_from_layer(
86
+ type_name: str,
87
+ from_photo: bool,
88
+ boundary: BaseGeometry | None,
89
+ count: int,
90
+ ) -> list[AvalancheSite]:
91
+ if boundary is not None:
92
+ pairs = fetch_and_clip_wfs_features(
93
+ WFS_BASE_URL, type_name, boundary, count=count
94
+ )
95
+ else:
96
+ pairs = [
97
+ (feature.get('properties', {}), shape(feature['geometry']))
98
+ for feature in fetch_wfs_features(WFS_BASE_URL, type_name, count=count)
99
+ if feature.get('geometry')
100
+ ]
101
+
102
+ # Group the multi-row sites back together before building anything.
103
+ grouped: dict[Any, list[tuple[dict[str, Any], BaseGeometry]]] = {}
104
+ for props, geometry in pairs:
105
+ grouped.setdefault(props.get('ID_SITO'), []).append((props, geometry))
106
+
107
+ sites = []
108
+ for site_id, rows in grouped.items():
109
+ props = rows[0][0]
110
+ sites.append(
111
+ AvalancheSite(
112
+ site_id=site_id,
113
+ comune=str(props.get('COMUNE') or ''),
114
+ toponym=str(props.get('TOPONIMO') or ''),
115
+ site_type=str(props.get('TIPO_SITO') or ''),
116
+ release_elevation_max_m=_as_int(props.get('QUOTA_DISTACCO_MAX')),
117
+ runout_elevation_min_m=_as_int(props.get('QUOTA_ARRESTO_MIN')),
118
+ aspect=str(props.get('ESPOSIZIONE_SITO') or ''),
119
+ photo_interpreted=from_photo,
120
+ geometries=tuple(geometry for _, geometry in rows),
121
+ )
122
+ )
123
+ return sites
124
+
125
+
126
+ def _as_int(value: Any) -> int | None:
127
+ """Elevations arrive as numbers, but a missing one can be an empty
128
+ string rather than null, which int() would raise on.
129
+ """
130
+ if value in (None, ''):
131
+ return None
132
+ try:
133
+ return int(float(value))
134
+ except (TypeError, ValueError):
135
+ return None