ObjectNat 0.1.3__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.
- {objectnat-0.1.3 → objectnat-0.1.5}/PKG-INFO +2 -2
- {objectnat-0.1.3 → objectnat-0.1.5}/pyproject.toml +2 -2
- {objectnat-0.1.3 → objectnat-0.1.5}/src/objectnat/__init__.py +2 -1
- objectnat-0.1.5/src/objectnat/methods/living_buildings_osm.py +242 -0
- objectnat-0.1.5/src/objectnat/utils/__init__.py +1 -0
- objectnat-0.1.5/src/objectnat/utils/utils.py +19 -0
- {objectnat-0.1.3 → objectnat-0.1.5}/LICENSE.txt +0 -0
- {objectnat-0.1.3 → objectnat-0.1.5}/README.md +0 -0
- {objectnat-0.1.3 → objectnat-0.1.5}/src/objectnat/methods/__init__.py +0 -0
- {objectnat-0.1.3 → objectnat-0.1.5}/src/objectnat/methods/adjacency_matrix.py +0 -0
- {objectnat-0.1.3 → objectnat-0.1.5}/src/objectnat/methods/balanced_buildings.py +0 -0
- {objectnat-0.1.3 → objectnat-0.1.5}/src/objectnat/methods/cluster_points_in_polygons.py +0 -0
- {objectnat-0.1.3 → objectnat-0.1.5}/src/objectnat/methods/coverage_zones.py +0 -0
- {objectnat-0.1.3 → objectnat-0.1.5}/src/objectnat/methods/demands.py +0 -0
- {objectnat-0.1.3 → objectnat-0.1.5}/src/objectnat/methods/isochrones.py +0 -0
- {objectnat-0.1.3 → objectnat-0.1.5}/src/objectnat/methods/osm_graph.py +0 -0
- {objectnat-0.1.3 → objectnat-0.1.5}/src/objectnat/methods/provision.py +0 -0
- {objectnat-0.1.3 → objectnat-0.1.5}/src/objectnat/methods/visibility_analysis.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: ObjectNat
|
|
3
|
-
Version: 0.1.
|
|
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
|
|
@@ -11,7 +11,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.10
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
-
Requires-Dist: dongraphio (>=0.3.
|
|
14
|
+
Requires-Dist: dongraphio (>=0.3.13,<0.4.0)
|
|
15
15
|
Requires-Dist: geopandas (>=0.14.3,<0.15.0)
|
|
16
16
|
Requires-Dist: joblib (>=1.4.2,<2.0.0)
|
|
17
17
|
Requires-Dist: networkit (>=11.0,<12.0)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "ObjectNat"
|
|
3
|
-
version = "0.1.
|
|
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>"]
|
|
@@ -17,7 +17,7 @@ numpy = "^1.23.5"
|
|
|
17
17
|
pandas = "^2.2.0"
|
|
18
18
|
networkx = "^3.2.1"
|
|
19
19
|
population-restorator = "^0.2.3"
|
|
20
|
-
dongraphio = "^0.3.
|
|
20
|
+
dongraphio = "^0.3.13"
|
|
21
21
|
provisio = "^0.1.7"
|
|
22
22
|
joblib = "^1.4.2"
|
|
23
23
|
pandarallel = "^1.6.5"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
__version__ = "0.1.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|