ObjectNat 0.1.4__tar.gz → 0.1.5__tar.gz

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.

Potentially problematic release.


This version of ObjectNat might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ObjectNat
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: ObjectNat is an open-source library created for geospatial analysis created by IDU team
5
5
  License: BSD-3-Clause
6
6
  Author: Danila
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "ObjectNat"
3
- version = "0.1.4"
3
+ version = "0.1.5"
4
4
  description = "ObjectNat is an open-source library created for geospatial analysis created by IDU team"
5
5
  license = "BSD-3-Clause"
6
6
  authors = ["Danila <63115678+DDonnyy@users.noreply.github.com>"]
@@ -1,4 +1,4 @@
1
- __version__ = "0.1.4"
1
+ __version__ = "0.1.5"
2
2
 
3
3
  from dongraphio.enums import GraphType
4
4
 
@@ -8,6 +8,7 @@ from .methods.cluster_points_in_polygons import get_clusters_polygon
8
8
  from .methods.coverage_zones import get_isochrone_zone_coverage, get_radius_zone_coverage
9
9
  from .methods.demands import get_demands
10
10
  from .methods.isochrones import get_accessibility_isochrones
11
+ from .methods.living_buildings_osm import download_buildings
11
12
  from .methods.osm_graph import get_intermodal_graph_from_osm
12
13
  from .methods.provision import NoOsmIdException, NoWeightAdjacencyException, get_provision
13
14
  from .methods.visibility_analysis import (
@@ -0,0 +1,242 @@
1
+ import geopandas as gpd
2
+ import osm2geojson
3
+ import osmnx as ox
4
+ import pandas as pd
5
+ import requests
6
+ from loguru import logger
7
+ from shapely import MultiPolygon, Polygon
8
+
9
+ from ..utils import get_utm_crs_for_4326_gdf
10
+
11
+ DEFAULT_OVERPASS_URL = "http://overpass-api.de/api/interpreter"
12
+
13
+
14
+ def get_terr_polygon_osm_name(territory_name: str) -> Polygon | MultiPolygon:
15
+ """
16
+ Retrieve the polygon geometry of a specified territory using its name from OpenStreetMap.
17
+
18
+ Parameters
19
+ ----------
20
+ territory_name : str
21
+ The name of the territory to retrieve the polygon for.
22
+
23
+ Returns
24
+ -------
25
+ gpd.GeoDataFrame
26
+ A GeoDataFrame containing the polygon geometry of the specified territory.
27
+
28
+ Examples
29
+ --------
30
+ >>> territory_name = "Saint-Petersburg, Russia"
31
+ >>> polygon = get_terr_polygon_osm_name(territory_name)
32
+ """
33
+ logger.info(f"Retrieving polygon geometry for '{territory_name}'")
34
+ place = ox.geocode_to_gdf(territory_name)
35
+ polygon = place.geometry.values[0]
36
+ return polygon.unary_union
37
+
38
+
39
+ def get_terr_polygon_osm_id(osm_id: int) -> Polygon | MultiPolygon:
40
+ """
41
+ Retrieve the polygon geometry of a specified territory using its OSM ID from OpenStreetMap.
42
+
43
+ Parameters
44
+ ----------
45
+ osm_id : int
46
+ The OpenStreetMap ID of the territory to retrieve the polygon for.
47
+
48
+ Returns
49
+ -------
50
+ Polygon | MultiPolygon
51
+ A Polygon or MultiPolygon geometry of the specified territory.
52
+
53
+ Examples
54
+ --------
55
+ >>> osm_id = 421007
56
+ >>> polygon = get_terr_polygon_osm_id(osm_id)
57
+ """
58
+ overpass_query = f"""
59
+ [out:json];
60
+ (
61
+ relation({osm_id});
62
+ );
63
+ out geom;
64
+ """
65
+ logger.info(f"Retrieving polygon geometry for osm id '{osm_id}'")
66
+ result = requests.get(DEFAULT_OVERPASS_URL, params={"data": overpass_query}, timeout=500)
67
+ json_result = result.json()
68
+ json_result = osm2geojson.json2geojson(json_result)
69
+ json_result = gpd.GeoDataFrame.from_features(json_result["features"]).set_crs(4326)
70
+ return json_result.geometry.unary_union
71
+
72
+
73
+ def eval_is_living(row: gpd.GeoSeries):
74
+ """
75
+ Determine if a building is used for residential purposes based on its attributes.
76
+
77
+ Parameters
78
+ ----------
79
+ row : gpd.GeoSeries
80
+ A GeoSeries representing a row in a GeoDataFrame, containing building attributes.
81
+
82
+ Returns
83
+ -------
84
+ bool
85
+ A boolean indicating whether the building is used for residential purposes.
86
+
87
+ Examples
88
+ --------
89
+ >>> buildings = download_buildings(osm_territory_id=421007)
90
+ >>> buildings['is_living'] = buildings.apply(eval_is_living, axis=1)
91
+ """
92
+ if row["building"] in (
93
+ "apartments",
94
+ "house",
95
+ "residential",
96
+ "detached",
97
+ "dormitory",
98
+ "semidetached_house",
99
+ "bungalow",
100
+ "cabin",
101
+ "farm",
102
+ ):
103
+ return True
104
+ else:
105
+ return False
106
+
107
+
108
+ def eval_population(source: gpd.GeoDataFrame, population_column: str, area_per_person: float = 33):
109
+ """
110
+ Estimate the population of buildings in a GeoDataFrame based on their attributes.
111
+
112
+ Parameters
113
+ ----------
114
+ source : gpd.GeoDataFrame
115
+ A GeoDataFrame containing building geometries and attributes.
116
+ population_column : str
117
+ The name of the column where the estimated population will be stored.
118
+ area_per_person : float
119
+ The standart living space per person im m², (default is 33)
120
+ Returns
121
+ -------
122
+ gpd.GeoDataFrame
123
+ A GeoDataFrame with an added column for estimated population.
124
+
125
+ Raises
126
+ ------
127
+ RuntimeError
128
+ If the 'building:levels' column is not present in the provided GeoDataFrame.
129
+
130
+ Examples
131
+ --------
132
+ >>> source = gpd.read_file('buildings.shp')
133
+ >>> source['is_living'] = source.apply(eval_is_living, axis=1)
134
+ >>> population_df = eval_population(source, 'approximate_pop')
135
+ """
136
+ if "building:levels" not in source.columns:
137
+ raise RuntimeError("No 'building:levels' column in provided GeoDataFrame")
138
+ df = source.copy()
139
+ local_utm_crs = get_utm_crs_for_4326_gdf(source.to_crs(4326))
140
+ df["area"] = df.to_crs(local_utm_crs.to_epsg()).geometry.area.astype(float)
141
+ df["building:levels_is_real"] = df["building:levels"].apply(lambda x: False if pd.isna(x) else True)
142
+ df["building:levels"] = df["building:levels"].fillna(1)
143
+ df["building:levels"] = pd.to_numeric(df["building:levels"], errors="coerce")
144
+ df = df.dropna(subset=["building:levels"])
145
+ df["building:levels"] = df["building:levels"].astype(int)
146
+ df[population_column] = 0.0
147
+ df.loc[df["is_living"] == 1, population_column] = df[df["is_living"] == 1].apply(
148
+ lambda row: (
149
+ 3
150
+ if ((row["area"] <= 400) & (row["building:levels"] <= 2))
151
+ else (row["building:levels"] * row["area"] * 0.8 / area_per_person)
152
+ ),
153
+ axis=1,
154
+ )
155
+ df[population_column] = df[population_column].fillna(0).round(0).astype(int)
156
+ return df
157
+
158
+
159
+ def download_buildings(
160
+ osm_territory_id: int | None = None,
161
+ osm_territory_name: str | None = None,
162
+ terr_polygon: Polygon | MultiPolygon | None = None,
163
+ is_living_column: str = "is_living",
164
+ population_column: str = "approximate_pop",
165
+ area_per_person: float = 33,
166
+ ) -> gpd.GeoDataFrame | None:
167
+ """
168
+ Download building geometries and evaluate 'is_living' and 'population' attributes for a specified territory from OpenStreetMap.
169
+
170
+ Parameters
171
+ ----------
172
+ osm_territory_id : int, optional
173
+ The OpenStreetMap ID of the territory to download buildings for.
174
+ osm_territory_name : str, optional
175
+ The name of the territory to download buildings for.
176
+ terr_polygon : Polygon or MultiPolygon, optional
177
+ A Polygon or MultiPolygon geometry defining the territory to download buildings for.
178
+ is_living_column : str, optional
179
+ The name of the column indicating whether a building is residential (default is "is_living").
180
+ population_column : str, optional
181
+ The name of the column for storing estimated population (default is "approximate_pop").
182
+ area_per_person : float
183
+ The standart living space per person im m², (default is 33)
184
+ Returns
185
+ -------
186
+ gpd.GeoDataFrame or None
187
+ A GeoDataFrame containing building geometries and attributes, or None if no buildings are found or an error occurs.
188
+
189
+ Examples
190
+ --------
191
+ >>> buildings_df = download_buildings(osm_territory_name="Saint-Petersburg, Russia")
192
+ >>> buildings_df.head()
193
+ """
194
+ if osm_territory_id is not None:
195
+ polygon = get_terr_polygon_osm_id(osm_territory_id)
196
+ return download_buildings(
197
+ terr_polygon=polygon,
198
+ area_per_person=area_per_person,
199
+ is_living_column=is_living_column,
200
+ population_column=population_column,
201
+ )
202
+
203
+ if osm_territory_name is not None:
204
+ polygon = get_terr_polygon_osm_name(osm_territory_name)
205
+ return download_buildings(
206
+ terr_polygon=polygon,
207
+ area_per_person=area_per_person,
208
+ is_living_column=is_living_column,
209
+ population_column=population_column,
210
+ )
211
+
212
+ logger.info("Downloading buildings from OpenStreetMap and counting population...")
213
+ buildings = ox.features_from_polygon(terr_polygon, tags={"building": True})
214
+ if not buildings.empty:
215
+ buildings = buildings.loc[
216
+ (buildings["geometry"].geom_type == "Polygon") | (buildings["geometry"].geom_type == "MultiPolygon")
217
+ ]
218
+ if buildings.empty:
219
+ logger.warning(f"There are no buildings in the specified territory. Output GeoDataFrame is empty.")
220
+ return buildings
221
+ else:
222
+ buildings[is_living_column] = buildings.apply(eval_is_living, axis=1)
223
+ buildings = eval_population(buildings, population_column, area_per_person)
224
+ buildings.reset_index(drop=True, inplace=True)
225
+ logger.info("Done!")
226
+ return buildings[
227
+ [
228
+ "building",
229
+ "addr:street",
230
+ "addr:housenumber",
231
+ "amenity",
232
+ "area",
233
+ "name",
234
+ "building:levels",
235
+ "leisure",
236
+ "design:year",
237
+ "is_living",
238
+ "building:levels_is_real",
239
+ "approximate_pop",
240
+ "geometry",
241
+ ]
242
+ ]
@@ -0,0 +1 @@
1
+ from .utils import get_utm_crs_for_4326_gdf
@@ -0,0 +1,19 @@
1
+ import geopandas as gpd
2
+ from pyproj import CRS
3
+ from pyproj.aoi import AreaOfInterest
4
+ from pyproj.database import query_utm_crs_info
5
+
6
+
7
+ def get_utm_crs_for_4326_gdf(gdf: gpd.GeoDataFrame) -> CRS:
8
+ assert gdf.crs == CRS.from_epsg(4326), "provided GeoDataFrame is not in EPSG 4326"
9
+ minx, miny, maxx, maxy = gdf.total_bounds
10
+ utm_crs_list = query_utm_crs_info(
11
+ datum_name="WGS 84",
12
+ area_of_interest=AreaOfInterest(
13
+ west_lon_degree=minx,
14
+ south_lat_degree=miny,
15
+ east_lon_degree=maxx,
16
+ north_lat_degree=maxy,
17
+ ),
18
+ )
19
+ return CRS.from_epsg(utm_crs_list[0].code)
File without changes
File without changes