voxcity 0.3.2__py3-none-any.whl → 0.3.4__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.
Potentially problematic release.
This version of voxcity might be problematic. Click here for more details.
- voxcity/download/eubucco.py +9 -17
- voxcity/download/gee.py +4 -3
- voxcity/download/mbfp.py +7 -7
- voxcity/download/oemj.py +22 -22
- voxcity/download/omt.py +10 -10
- voxcity/download/osm.py +23 -21
- voxcity/download/overture.py +7 -15
- voxcity/file/envimet.py +4 -4
- voxcity/file/geojson.py +83 -26
- voxcity/geo/draw.py +128 -22
- voxcity/geo/grid.py +9 -143
- voxcity/geo/utils.py +79 -66
- voxcity/sim/solar.py +187 -53
- voxcity/sim/view.py +183 -31
- voxcity/utils/weather.py +7 -7
- {voxcity-0.3.2.dist-info → voxcity-0.3.4.dist-info}/METADATA +61 -5
- voxcity-0.3.4.dist-info/RECORD +34 -0
- {voxcity-0.3.2.dist-info → voxcity-0.3.4.dist-info}/WHEEL +1 -1
- voxcity-0.3.2.dist-info/RECORD +0 -34
- {voxcity-0.3.2.dist-info → voxcity-0.3.4.dist-info}/AUTHORS.rst +0 -0
- {voxcity-0.3.2.dist-info → voxcity-0.3.4.dist-info}/LICENSE +0 -0
- {voxcity-0.3.2.dist-info → voxcity-0.3.4.dist-info}/top_level.txt +0 -0
voxcity/file/geojson.py
CHANGED
|
@@ -28,7 +28,7 @@ def filter_and_convert_gdf_to_geojson(gdf, rectangle_vertices):
|
|
|
28
28
|
|
|
29
29
|
Args:
|
|
30
30
|
gdf (GeoDataFrame): Input GeoDataFrame containing building data
|
|
31
|
-
rectangle_vertices (list): List of (
|
|
31
|
+
rectangle_vertices (list): List of (lon, lat) tuples defining the bounding rectangle
|
|
32
32
|
|
|
33
33
|
Returns:
|
|
34
34
|
list: List of GeoJSON features within the bounding rectangle
|
|
@@ -43,9 +43,8 @@ def filter_and_convert_gdf_to_geojson(gdf, rectangle_vertices):
|
|
|
43
43
|
# Add 'confidence' column with default value
|
|
44
44
|
gdf['confidence'] = -1.0
|
|
45
45
|
|
|
46
|
-
#
|
|
47
|
-
|
|
48
|
-
rectangle_polygon = Polygon(rectangle_vertices_lonlat)
|
|
46
|
+
# Rectangle vertices already in (lon,lat) format for shapely
|
|
47
|
+
rectangle_polygon = Polygon(rectangle_vertices)
|
|
49
48
|
|
|
50
49
|
# Use spatial index to efficiently filter geometries that intersect with rectangle
|
|
51
50
|
gdf.sindex # Ensure spatial index is built
|
|
@@ -57,13 +56,6 @@ def filter_and_convert_gdf_to_geojson(gdf, rectangle_vertices):
|
|
|
57
56
|
# Delete intermediate data to save memory
|
|
58
57
|
del gdf, possible_matches, precise_matches
|
|
59
58
|
|
|
60
|
-
# Helper function to swap coordinate ordering from (lon,lat) to (lat,lon)
|
|
61
|
-
def swap_coordinates(coords):
|
|
62
|
-
if isinstance(coords[0][0], (float, int)):
|
|
63
|
-
return [[lat, lon] for lon, lat in coords]
|
|
64
|
-
else:
|
|
65
|
-
return [swap_coordinates(ring) for ring in coords]
|
|
66
|
-
|
|
67
59
|
# Create GeoJSON features from filtered geometries
|
|
68
60
|
features = []
|
|
69
61
|
for idx, row in filtered_gdf.iterrows():
|
|
@@ -78,7 +70,7 @@ def filter_and_convert_gdf_to_geojson(gdf, rectangle_vertices):
|
|
|
78
70
|
for polygon_coords in geom['coordinates']:
|
|
79
71
|
single_geom = {
|
|
80
72
|
'type': 'Polygon',
|
|
81
|
-
'coordinates':
|
|
73
|
+
'coordinates': polygon_coords
|
|
82
74
|
}
|
|
83
75
|
feature = {
|
|
84
76
|
'type': 'Feature',
|
|
@@ -87,7 +79,6 @@ def filter_and_convert_gdf_to_geojson(gdf, rectangle_vertices):
|
|
|
87
79
|
}
|
|
88
80
|
features.append(feature)
|
|
89
81
|
elif geom['type'] == 'Polygon':
|
|
90
|
-
geom['coordinates'] = swap_coordinates(geom['coordinates'])
|
|
91
82
|
feature = {
|
|
92
83
|
'type': 'Feature',
|
|
93
84
|
'properties': properties,
|
|
@@ -114,7 +105,7 @@ def get_geojson_from_gpkg(gpkg_path, rectangle_vertices):
|
|
|
114
105
|
|
|
115
106
|
Args:
|
|
116
107
|
gpkg_path (str): Path to the GeoPackage file
|
|
117
|
-
rectangle_vertices (list): List of (
|
|
108
|
+
rectangle_vertices (list): List of (lon, lat) tuples defining the bounding rectangle
|
|
118
109
|
|
|
119
110
|
Returns:
|
|
120
111
|
list: List of GeoJSON features within the bounding rectangle
|
|
@@ -404,9 +395,9 @@ def extract_building_heights_from_geotiff(geotiff_path, geojson_data):
|
|
|
404
395
|
for feature in geojson:
|
|
405
396
|
if (feature['geometry']['type'] == 'Polygon') & (feature['properties']['height']<=0):
|
|
406
397
|
count_0 += 1
|
|
407
|
-
# Transform coordinates from (
|
|
398
|
+
# Transform coordinates from (lon, lat) to raster CRS
|
|
408
399
|
coords = feature['geometry']['coordinates'][0]
|
|
409
|
-
transformed_coords = [transformer.transform(lon, lat) for
|
|
400
|
+
transformed_coords = [transformer.transform(lon, lat) for lon, lat in coords]
|
|
410
401
|
|
|
411
402
|
# Create polygon in raster CRS
|
|
412
403
|
polygon = shape({"type": "Polygon", "coordinates": [transformed_coords]})
|
|
@@ -447,7 +438,7 @@ def get_geojson_from_gpkg(gpkg_path, rectangle_vertices):
|
|
|
447
438
|
|
|
448
439
|
Args:
|
|
449
440
|
gpkg_path (str): Path to the GeoPackage file
|
|
450
|
-
rectangle_vertices (list): List of (
|
|
441
|
+
rectangle_vertices (list): List of (lon, lat) tuples defining the bounding rectangle
|
|
451
442
|
|
|
452
443
|
Returns:
|
|
453
444
|
list: List of GeoJSON features within the bounding rectangle
|
|
@@ -460,7 +451,7 @@ def get_geojson_from_gpkg(gpkg_path, rectangle_vertices):
|
|
|
460
451
|
|
|
461
452
|
def swap_coordinates(features):
|
|
462
453
|
"""
|
|
463
|
-
Swap coordinate ordering in GeoJSON features from (
|
|
454
|
+
Swap coordinate ordering in GeoJSON features from (lat, lon) to (lon, lat).
|
|
464
455
|
|
|
465
456
|
Args:
|
|
466
457
|
features (list): List of GeoJSON features to process
|
|
@@ -469,11 +460,11 @@ def swap_coordinates(features):
|
|
|
469
460
|
for feature in features:
|
|
470
461
|
if feature['geometry']['type'] == 'Polygon':
|
|
471
462
|
# Swap coordinates for simple polygons
|
|
472
|
-
new_coords = [[[
|
|
463
|
+
new_coords = [[[lon, lat] for lat, lon in polygon] for polygon in feature['geometry']['coordinates']]
|
|
473
464
|
feature['geometry']['coordinates'] = new_coords
|
|
474
465
|
elif feature['geometry']['type'] == 'MultiPolygon':
|
|
475
466
|
# Swap coordinates for multi-polygons (polygons with holes)
|
|
476
|
-
new_coords = [[[[
|
|
467
|
+
new_coords = [[[[lon, lat] for lat, lon in polygon] for polygon in multipolygon] for multipolygon in feature['geometry']['coordinates']]
|
|
477
468
|
feature['geometry']['coordinates'] = new_coords
|
|
478
469
|
|
|
479
470
|
def save_geojson(features, save_path):
|
|
@@ -506,23 +497,89 @@ def find_building_containing_point(features, target_point):
|
|
|
506
497
|
|
|
507
498
|
Args:
|
|
508
499
|
features (list): List of GeoJSON feature dictionaries
|
|
509
|
-
target_point (tuple): Tuple of (
|
|
500
|
+
target_point (tuple): Tuple of (lon, lat)
|
|
510
501
|
|
|
511
502
|
Returns:
|
|
512
503
|
list: List of building IDs containing the target point
|
|
513
504
|
"""
|
|
514
|
-
# Create Shapely point
|
|
515
|
-
point = Point(target_point[
|
|
505
|
+
# Create Shapely point
|
|
506
|
+
point = Point(target_point[0], target_point[1])
|
|
516
507
|
|
|
517
508
|
id_list = []
|
|
518
509
|
for feature in features:
|
|
519
510
|
# Get polygon coordinates and convert to Shapely polygon
|
|
520
511
|
coords = feature['geometry']['coordinates'][0]
|
|
521
|
-
|
|
522
|
-
polygon = Polygon(polygon_coords)
|
|
512
|
+
polygon = Polygon(coords)
|
|
523
513
|
|
|
524
514
|
# Check if point is within polygon
|
|
525
515
|
if polygon.contains(point):
|
|
526
516
|
id_list.append(feature['properties']['id'])
|
|
527
517
|
|
|
528
|
-
return id_list
|
|
518
|
+
return id_list
|
|
519
|
+
|
|
520
|
+
def get_buildings_in_drawn_polygon(building_geojson, drawn_polygon_vertices,
|
|
521
|
+
operation='within'):
|
|
522
|
+
"""
|
|
523
|
+
Given a list of building footprints and a set of drawn polygon
|
|
524
|
+
vertices (in lon, lat), return the building IDs that fall within or
|
|
525
|
+
intersect the drawn polygon.
|
|
526
|
+
|
|
527
|
+
Args:
|
|
528
|
+
building_geojson (list):
|
|
529
|
+
A list of GeoJSON features, each feature is a dict with:
|
|
530
|
+
{
|
|
531
|
+
"type": "Feature",
|
|
532
|
+
"geometry": {
|
|
533
|
+
"type": "Polygon",
|
|
534
|
+
"coordinates": [
|
|
535
|
+
[
|
|
536
|
+
[lon1, lat1], [lon2, lat2], ...
|
|
537
|
+
]
|
|
538
|
+
]
|
|
539
|
+
},
|
|
540
|
+
"properties": {
|
|
541
|
+
"id": ...
|
|
542
|
+
...
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
drawn_polygon_vertices (list):
|
|
547
|
+
A list of (lon, lat) tuples representing the polygon drawn by the user.
|
|
548
|
+
|
|
549
|
+
operation (str):
|
|
550
|
+
Determines how to include buildings.
|
|
551
|
+
Use "intersect" to include buildings that intersect the drawn polygon.
|
|
552
|
+
Use "within" to include buildings that lie entirely within the drawn polygon.
|
|
553
|
+
|
|
554
|
+
Returns:
|
|
555
|
+
list:
|
|
556
|
+
A list of building IDs (strings or ints) that satisfy the condition.
|
|
557
|
+
"""
|
|
558
|
+
# Create Shapely Polygon from drawn vertices
|
|
559
|
+
drawn_polygon_shapely = Polygon(drawn_polygon_vertices)
|
|
560
|
+
|
|
561
|
+
included_building_ids = []
|
|
562
|
+
|
|
563
|
+
# Check each building in the GeoJSON
|
|
564
|
+
for feature in building_geojson:
|
|
565
|
+
# Skip any feature that is not Polygon
|
|
566
|
+
if feature['geometry']['type'] != 'Polygon':
|
|
567
|
+
continue
|
|
568
|
+
|
|
569
|
+
# Extract coordinates
|
|
570
|
+
coords = feature['geometry']['coordinates'][0]
|
|
571
|
+
|
|
572
|
+
# Create a Shapely polygon for the building
|
|
573
|
+
building_polygon = Polygon(coords)
|
|
574
|
+
|
|
575
|
+
# Depending on the operation, check the relationship
|
|
576
|
+
if operation == 'intersect':
|
|
577
|
+
if building_polygon.intersects(drawn_polygon_shapely):
|
|
578
|
+
included_building_ids.append(feature['properties'].get('id', None))
|
|
579
|
+
elif operation == 'within':
|
|
580
|
+
if building_polygon.within(drawn_polygon_shapely):
|
|
581
|
+
included_building_ids.append(feature['properties'].get('id', None))
|
|
582
|
+
else:
|
|
583
|
+
raise ValueError("operation must be 'intersect' or 'within'")
|
|
584
|
+
|
|
585
|
+
return included_building_ids
|
voxcity/geo/draw.py
CHANGED
|
@@ -4,10 +4,11 @@ This module provides functions for drawing and manipulating rectangles on maps.
|
|
|
4
4
|
|
|
5
5
|
import math
|
|
6
6
|
from pyproj import Proj, transform
|
|
7
|
-
from ipyleaflet import Map, DrawControl, Rectangle
|
|
7
|
+
from ipyleaflet import Map, DrawControl, Rectangle, Polygon as LeafletPolygon
|
|
8
8
|
import ipyleaflet
|
|
9
9
|
from geopy import distance
|
|
10
10
|
from .utils import get_coordinates_from_cityname
|
|
11
|
+
import shapely.geometry as geom
|
|
11
12
|
|
|
12
13
|
def rotate_rectangle(m, rectangle_vertices, angle):
|
|
13
14
|
"""
|
|
@@ -15,11 +16,11 @@ def rotate_rectangle(m, rectangle_vertices, angle):
|
|
|
15
16
|
|
|
16
17
|
Args:
|
|
17
18
|
m (ipyleaflet.Map): Map object to draw the rotated rectangle on
|
|
18
|
-
rectangle_vertices (list): List of (
|
|
19
|
+
rectangle_vertices (list): List of (lon, lat) tuples defining the rectangle vertices
|
|
19
20
|
angle (float): Rotation angle in degrees
|
|
20
21
|
|
|
21
22
|
Returns:
|
|
22
|
-
list: List of rotated (
|
|
23
|
+
list: List of rotated (lon, lat) tuples defining the new rectangle vertices
|
|
23
24
|
"""
|
|
24
25
|
if not rectangle_vertices:
|
|
25
26
|
print("Draw a rectangle first!")
|
|
@@ -30,7 +31,7 @@ def rotate_rectangle(m, rectangle_vertices, angle):
|
|
|
30
31
|
mercator = Proj(init='epsg:3857') # Web Mercator (projection used by most web maps)
|
|
31
32
|
|
|
32
33
|
# Project vertices from WGS84 to Web Mercator for proper distance calculations
|
|
33
|
-
projected_vertices = [transform(wgs84, mercator, lon, lat) for
|
|
34
|
+
projected_vertices = [transform(wgs84, mercator, lon, lat) for lon, lat in rectangle_vertices]
|
|
34
35
|
|
|
35
36
|
# Calculate the centroid to use as rotation center
|
|
36
37
|
centroid_x = sum(x for x, y in projected_vertices) / len(projected_vertices)
|
|
@@ -56,15 +57,12 @@ def rotate_rectangle(m, rectangle_vertices, angle):
|
|
|
56
57
|
|
|
57
58
|
rotated_vertices.append((new_x, new_y))
|
|
58
59
|
|
|
59
|
-
# Convert coordinates back to WGS84 (lat
|
|
60
|
+
# Convert coordinates back to WGS84 (lon/lat)
|
|
60
61
|
new_vertices = [transform(mercator, wgs84, x, y) for x, y in rotated_vertices]
|
|
61
62
|
|
|
62
|
-
# Reorder coordinates from (lon,lat) to (lat,lon) format
|
|
63
|
-
new_vertices = [(lat, lon) for lon, lat in new_vertices]
|
|
64
|
-
|
|
65
63
|
# Create and add new polygon layer to map
|
|
66
64
|
polygon = ipyleaflet.Polygon(
|
|
67
|
-
locations=new_vertices,
|
|
65
|
+
locations=[(lat, lon) for lon, lat in new_vertices], # Convert to (lat,lon) for ipyleaflet
|
|
68
66
|
color="red",
|
|
69
67
|
fill_color="red"
|
|
70
68
|
)
|
|
@@ -103,8 +101,8 @@ def draw_rectangle_map(center=(40, -100), zoom=4):
|
|
|
103
101
|
print("Vertices of the drawn rectangle:")
|
|
104
102
|
# Store all vertices except last (GeoJSON repeats first vertex at end)
|
|
105
103
|
for coord in coordinates[:-1]:
|
|
106
|
-
#
|
|
107
|
-
rectangle_vertices.append((coord[
|
|
104
|
+
# Keep GeoJSON (lon,lat) format
|
|
105
|
+
rectangle_vertices.append((coord[0], coord[1]))
|
|
108
106
|
print(f"Longitude: {coord[0]}, Latitude: {coord[1]}")
|
|
109
107
|
|
|
110
108
|
# Configure drawing controls - only enable rectangle drawing
|
|
@@ -178,9 +176,9 @@ def center_location_map_cityname(cityname, east_west_length, north_south_length,
|
|
|
178
176
|
|
|
179
177
|
# Process only if a point was drawn on the map
|
|
180
178
|
if action == 'created' and geo_json['geometry']['type'] == 'Point':
|
|
181
|
-
# Extract point coordinates
|
|
182
|
-
|
|
183
|
-
print(f"Point drawn at
|
|
179
|
+
# Extract point coordinates from GeoJSON (lon,lat)
|
|
180
|
+
lon, lat = geo_json['geometry']['coordinates'][0], geo_json['geometry']['coordinates'][1]
|
|
181
|
+
print(f"Point drawn at Longitude: {lon}, Latitude: {lat}")
|
|
184
182
|
|
|
185
183
|
# Calculate corner points using geopy's distance calculator
|
|
186
184
|
# Each point is calculated as a destination from center point using bearing
|
|
@@ -189,15 +187,15 @@ def center_location_map_cityname(cityname, east_west_length, north_south_length,
|
|
|
189
187
|
east = distance.distance(meters=east_west_length/2).destination((lat, lon), bearing=90)
|
|
190
188
|
west = distance.distance(meters=east_west_length/2).destination((lat, lon), bearing=270)
|
|
191
189
|
|
|
192
|
-
# Create rectangle vertices in counter-clockwise order
|
|
190
|
+
# Create rectangle vertices in counter-clockwise order (lon,lat)
|
|
193
191
|
rectangle_vertices.extend([
|
|
194
|
-
(
|
|
195
|
-
(
|
|
196
|
-
(
|
|
197
|
-
(
|
|
192
|
+
(west.longitude, south.latitude),
|
|
193
|
+
(west.longitude, north.latitude),
|
|
194
|
+
(east.longitude, north.latitude),
|
|
195
|
+
(east.longitude, south.latitude)
|
|
198
196
|
])
|
|
199
197
|
|
|
200
|
-
# Create and add new rectangle to map
|
|
198
|
+
# Create and add new rectangle to map (ipyleaflet expects lat,lon)
|
|
201
199
|
rectangle = Rectangle(
|
|
202
200
|
bounds=[(north.latitude, west.longitude), (south.latitude, east.longitude)],
|
|
203
201
|
color="red",
|
|
@@ -208,7 +206,7 @@ def center_location_map_cityname(cityname, east_west_length, north_south_length,
|
|
|
208
206
|
|
|
209
207
|
print("Rectangle vertices:")
|
|
210
208
|
for vertex in rectangle_vertices:
|
|
211
|
-
print(f"
|
|
209
|
+
print(f"Longitude: {vertex[0]}, Latitude: {vertex[1]}")
|
|
212
210
|
|
|
213
211
|
# Configure drawing controls - only enable point drawing
|
|
214
212
|
draw_control = DrawControl()
|
|
@@ -222,4 +220,112 @@ def center_location_map_cityname(cityname, east_west_length, north_south_length,
|
|
|
222
220
|
# Register event handler for drawing actions
|
|
223
221
|
draw_control.on_draw(handle_draw)
|
|
224
222
|
|
|
225
|
-
return m, rectangle_vertices
|
|
223
|
+
return m, rectangle_vertices
|
|
224
|
+
|
|
225
|
+
def display_buildings_and_draw_polygon(building_geojson, zoom=17):
|
|
226
|
+
"""
|
|
227
|
+
Displays building footprints (in Lon-Lat order) on an ipyleaflet map,
|
|
228
|
+
and allows the user to draw a polygon whose vertices are returned
|
|
229
|
+
in a Python list (also in Lon-Lat).
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
building_geojson (list): A list of GeoJSON features (Polygons),
|
|
233
|
+
with coordinates in [lon, lat] order.
|
|
234
|
+
zoom (int): Initial zoom level for the map. Default=17.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
(map_object, drawn_polygon_vertices)
|
|
238
|
+
- map_object: ipyleaflet Map instance
|
|
239
|
+
- drawn_polygon_vertices: a Python list that gets updated whenever
|
|
240
|
+
a new polygon is created. The list is in (lon, lat) order.
|
|
241
|
+
"""
|
|
242
|
+
# ---------------------------------------------------------
|
|
243
|
+
# 1. Determine a suitable map center via bounding box logic
|
|
244
|
+
# ---------------------------------------------------------
|
|
245
|
+
all_lons = []
|
|
246
|
+
all_lats = []
|
|
247
|
+
for feature in building_geojson:
|
|
248
|
+
# Handle only Polygons here; skip MultiPolygon if present
|
|
249
|
+
if feature['geometry']['type'] == 'Polygon':
|
|
250
|
+
# Coordinates in this data are [ [lon, lat], [lon, lat], ... ]
|
|
251
|
+
coords = feature['geometry']['coordinates'][0] # outer ring
|
|
252
|
+
all_lons.extend(pt[0] for pt in coords)
|
|
253
|
+
all_lats.extend(pt[1] for pt in coords)
|
|
254
|
+
|
|
255
|
+
if not all_lats or not all_lons:
|
|
256
|
+
# Fallback: If no footprints or invalid data, pick a default
|
|
257
|
+
center_lon, center_lat = -100.0, 40.0
|
|
258
|
+
else:
|
|
259
|
+
min_lon, max_lon = min(all_lons), max(all_lons)
|
|
260
|
+
min_lat, max_lat = min(all_lats), max(all_lats)
|
|
261
|
+
center_lon = (min_lon + max_lon) / 2
|
|
262
|
+
center_lat = (min_lat + max_lat) / 2
|
|
263
|
+
|
|
264
|
+
# Create the ipyleaflet map (needs lat,lon)
|
|
265
|
+
m = Map(center=(center_lat, center_lon), zoom=zoom, scroll_wheel_zoom=True)
|
|
266
|
+
|
|
267
|
+
# -----------------------------------------
|
|
268
|
+
# 2. Add each building footprint to the map
|
|
269
|
+
# -----------------------------------------
|
|
270
|
+
for feature in building_geojson:
|
|
271
|
+
# Only handle simple Polygons
|
|
272
|
+
if feature['geometry']['type'] == 'Polygon':
|
|
273
|
+
coords = feature['geometry']['coordinates'][0]
|
|
274
|
+
# Convert to (lat,lon) for ipyleaflet
|
|
275
|
+
lat_lon_coords = [(c[1], c[0]) for c in coords]
|
|
276
|
+
|
|
277
|
+
# Create the polygon layer
|
|
278
|
+
bldg_layer = LeafletPolygon(
|
|
279
|
+
locations=lat_lon_coords,
|
|
280
|
+
color="blue",
|
|
281
|
+
fill_color="blue",
|
|
282
|
+
fill_opacity=0.2,
|
|
283
|
+
weight=2
|
|
284
|
+
)
|
|
285
|
+
m.add_layer(bldg_layer)
|
|
286
|
+
|
|
287
|
+
# -----------------------------------------------------------------
|
|
288
|
+
# 3. Enable drawing of polygons, capturing the vertices in Lon-Lat
|
|
289
|
+
# -----------------------------------------------------------------
|
|
290
|
+
drawn_polygon_vertices = [] # We'll store the newly drawn polygon's vertices here (lon, lat).
|
|
291
|
+
|
|
292
|
+
draw_control = DrawControl(
|
|
293
|
+
polygon={
|
|
294
|
+
"shapeOptions": {
|
|
295
|
+
"color": "red",
|
|
296
|
+
"fillColor": "red",
|
|
297
|
+
"fillOpacity": 0.2
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
rectangle={}, # Disable rectangles (or enable if needed)
|
|
301
|
+
circle={}, # Disable circles
|
|
302
|
+
circlemarker={}, # Disable circlemarkers
|
|
303
|
+
polyline={}, # Disable polylines
|
|
304
|
+
marker={} # Disable markers
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
def handle_draw(self, action, geo_json):
|
|
308
|
+
"""
|
|
309
|
+
Callback for whenever a shape is created or edited.
|
|
310
|
+
ipyleaflet's DrawControl returns standard GeoJSON (lon, lat).
|
|
311
|
+
We'll keep them as (lon, lat).
|
|
312
|
+
"""
|
|
313
|
+
# Clear any previously stored vertices
|
|
314
|
+
drawn_polygon_vertices.clear()
|
|
315
|
+
|
|
316
|
+
if action == 'created' and geo_json['geometry']['type'] == 'Polygon':
|
|
317
|
+
# The polygon's first ring
|
|
318
|
+
coordinates = geo_json['geometry']['coordinates'][0]
|
|
319
|
+
print("Vertices of the drawn polygon (Lon-Lat):")
|
|
320
|
+
|
|
321
|
+
# Keep GeoJSON (lon,lat) format, skip last repeated coordinate
|
|
322
|
+
for coord in coordinates[:-1]:
|
|
323
|
+
lon = coord[0]
|
|
324
|
+
lat = coord[1]
|
|
325
|
+
drawn_polygon_vertices.append((lon, lat))
|
|
326
|
+
print(f" - (lon, lat) = ({lon}, {lat})")
|
|
327
|
+
|
|
328
|
+
draw_control.on_draw(handle_draw)
|
|
329
|
+
m.add_control(draw_control)
|
|
330
|
+
|
|
331
|
+
return m, drawn_polygon_vertices
|
voxcity/geo/grid.py
CHANGED
|
@@ -235,77 +235,6 @@ def tree_height_grid_from_land_cover(land_cover_grid_ori):
|
|
|
235
235
|
|
|
236
236
|
return tree_height_grid
|
|
237
237
|
|
|
238
|
-
def create_land_cover_grid_from_geotiff(tiff_path, mesh_size, land_cover_classes):
|
|
239
|
-
"""
|
|
240
|
-
Create a land cover grid from a GeoTIFF file.
|
|
241
|
-
|
|
242
|
-
Args:
|
|
243
|
-
tiff_path (str): Path to GeoTIFF file
|
|
244
|
-
mesh_size (float): Size of mesh cells
|
|
245
|
-
land_cover_classes (dict): Dictionary mapping land cover classes
|
|
246
|
-
|
|
247
|
-
Returns:
|
|
248
|
-
numpy.ndarray: Grid of land cover classes
|
|
249
|
-
"""
|
|
250
|
-
with rasterio.open(tiff_path) as src:
|
|
251
|
-
# Read RGB bands
|
|
252
|
-
img = src.read((1,2,3))
|
|
253
|
-
left, bottom, right, top = src.bounds
|
|
254
|
-
src_crs = src.crs
|
|
255
|
-
|
|
256
|
-
# Handle different coordinate reference systems
|
|
257
|
-
if src_crs.to_epsg() == 3857: # Web Mercator
|
|
258
|
-
# Convert bounds from Web Mercator to WGS84 for accurate distance calculations
|
|
259
|
-
wgs84 = CRS.from_epsg(4326)
|
|
260
|
-
transformer = Transformer.from_crs(src_crs, wgs84, always_xy=True)
|
|
261
|
-
left_wgs84, bottom_wgs84 = transformer.transform(left, bottom)
|
|
262
|
-
right_wgs84, top_wgs84 = transformer.transform(right, top)
|
|
263
|
-
|
|
264
|
-
# Calculate actual distances using geodesic calculations
|
|
265
|
-
geod = Geod(ellps="WGS84")
|
|
266
|
-
_, _, width = geod.inv(left_wgs84, bottom_wgs84, right_wgs84, bottom_wgs84)
|
|
267
|
-
_, _, height = geod.inv(left_wgs84, bottom_wgs84, left_wgs84, top_wgs84)
|
|
268
|
-
else:
|
|
269
|
-
# For projections already in meters, use simple subtraction
|
|
270
|
-
width = right - left
|
|
271
|
-
height = top - bottom
|
|
272
|
-
|
|
273
|
-
# Calculate grid dimensions based on mesh size
|
|
274
|
-
num_cells_x = int(width / mesh_size + 0.5)
|
|
275
|
-
num_cells_y = int(height / mesh_size + 0.5)
|
|
276
|
-
|
|
277
|
-
# Adjust mesh size to fit image exactly
|
|
278
|
-
adjusted_mesh_size_x = (right - left) / num_cells_x
|
|
279
|
-
adjusted_mesh_size_y = (top - bottom) / num_cells_y
|
|
280
|
-
|
|
281
|
-
# Create affine transform for new grid
|
|
282
|
-
new_affine = Affine(adjusted_mesh_size_x, 0, left, 0, -adjusted_mesh_size_y, top)
|
|
283
|
-
|
|
284
|
-
# Create coordinate grids
|
|
285
|
-
cols, rows = np.meshgrid(np.arange(num_cells_x), np.arange(num_cells_y))
|
|
286
|
-
xs, ys = new_affine * (cols, rows)
|
|
287
|
-
xs_flat, ys_flat = xs.flatten(), ys.flatten()
|
|
288
|
-
|
|
289
|
-
# Convert coordinates to image indices
|
|
290
|
-
row, col = src.index(xs_flat, ys_flat)
|
|
291
|
-
row, col = np.array(row), np.array(col)
|
|
292
|
-
|
|
293
|
-
# Filter out invalid indices
|
|
294
|
-
valid = (row >= 0) & (row < src.height) & (col >= 0) & (col < src.width)
|
|
295
|
-
row, col = row[valid], col[valid]
|
|
296
|
-
|
|
297
|
-
# Create output grid and fill with land cover classes
|
|
298
|
-
grid = np.full((num_cells_y, num_cells_x), 'No Data', dtype=object)
|
|
299
|
-
|
|
300
|
-
for i, (r, c) in enumerate(zip(row, col)):
|
|
301
|
-
cell_data = img[:, r, c]
|
|
302
|
-
dominant_class = get_dominant_class(cell_data, land_cover_classes)
|
|
303
|
-
grid_row, grid_col = np.unravel_index(i, (num_cells_y, num_cells_x))
|
|
304
|
-
grid[grid_row, grid_col] = dominant_class
|
|
305
|
-
|
|
306
|
-
# Flip grid vertically before returning
|
|
307
|
-
return np.flipud(grid)
|
|
308
|
-
|
|
309
238
|
def create_land_cover_grid_from_geotiff_polygon(tiff_path, mesh_size, land_cover_classes, polygon):
|
|
310
239
|
"""
|
|
311
240
|
Create a land cover grid from a GeoTIFF file within a polygon boundary.
|
|
@@ -329,7 +258,7 @@ def create_land_cover_grid_from_geotiff_polygon(tiff_path, mesh_size, land_cover
|
|
|
329
258
|
poly = Polygon(polygon)
|
|
330
259
|
|
|
331
260
|
# Get bounds of the polygon in WGS84 coordinates
|
|
332
|
-
bottom_wgs84,
|
|
261
|
+
left_wgs84, bottom_wgs84, right_wgs84, top_wgs84 = poly.bounds
|
|
333
262
|
# print(left, bottom, right, top)
|
|
334
263
|
|
|
335
264
|
# Calculate width and height using geodesic calculations for accuracy
|
|
@@ -381,7 +310,7 @@ def create_land_cover_grid_from_geojson_polygon(geojson_data, meshsize, source,
|
|
|
381
310
|
geojson_data (dict): GeoJSON data containing land cover polygons
|
|
382
311
|
meshsize (float): Size of each grid cell in meters
|
|
383
312
|
source (str): Source of the land cover data to determine class priorities
|
|
384
|
-
rectangle_vertices (list): List of 4 (lat
|
|
313
|
+
rectangle_vertices (list): List of 4 (lon,lat) coordinate pairs defining the rectangle bounds
|
|
385
314
|
|
|
386
315
|
Returns:
|
|
387
316
|
numpy.ndarray: 2D grid of land cover classes as strings
|
|
@@ -411,8 +340,8 @@ def create_land_cover_grid_from_geojson_polygon(geojson_data, meshsize, source,
|
|
|
411
340
|
vertex_0, vertex_1, vertex_3 = rectangle_vertices[0], rectangle_vertices[1], rectangle_vertices[3]
|
|
412
341
|
|
|
413
342
|
# Calculate actual distances between vertices using geodesic calculations
|
|
414
|
-
dist_side_1 = calculate_distance(geod, vertex_0[
|
|
415
|
-
dist_side_2 = calculate_distance(geod, vertex_0[
|
|
343
|
+
dist_side_1 = calculate_distance(geod, vertex_0[0], vertex_0[1], vertex_1[0], vertex_1[1])
|
|
344
|
+
dist_side_2 = calculate_distance(geod, vertex_0[0], vertex_0[1], vertex_3[0], vertex_3[1])
|
|
416
345
|
|
|
417
346
|
# Create vectors representing the sides of the rectangle
|
|
418
347
|
side_1 = np.array(vertex_1) - np.array(vertex_0)
|
|
@@ -476,70 +405,6 @@ def create_land_cover_grid_from_geojson_polygon(geojson_data, meshsize, source,
|
|
|
476
405
|
continue
|
|
477
406
|
return grid
|
|
478
407
|
|
|
479
|
-
def create_canopy_height_grid_from_geotiff(tiff_path, mesh_size):
|
|
480
|
-
"""
|
|
481
|
-
Create a canopy height grid from a GeoTIFF file.
|
|
482
|
-
|
|
483
|
-
Args:
|
|
484
|
-
tiff_path (str): Path to GeoTIFF file
|
|
485
|
-
mesh_size (float): Size of mesh cells
|
|
486
|
-
|
|
487
|
-
Returns:
|
|
488
|
-
numpy.ndarray: Grid of canopy heights
|
|
489
|
-
"""
|
|
490
|
-
with rasterio.open(tiff_path) as src:
|
|
491
|
-
# Read single band height data
|
|
492
|
-
img = src.read(1)
|
|
493
|
-
left, bottom, right, top = src.bounds
|
|
494
|
-
src_crs = src.crs
|
|
495
|
-
|
|
496
|
-
# Handle coordinate system conversion and distance calculations
|
|
497
|
-
if src_crs.to_epsg() == 3857: # Web Mercator projection
|
|
498
|
-
# Convert bounds to WGS84 for accurate distance calculation
|
|
499
|
-
wgs84 = CRS.from_epsg(4326)
|
|
500
|
-
transformer = Transformer.from_crs(src_crs, wgs84, always_xy=True)
|
|
501
|
-
left_wgs84, bottom_wgs84 = transformer.transform(left, bottom)
|
|
502
|
-
right_wgs84, top_wgs84 = transformer.transform(right, top)
|
|
503
|
-
|
|
504
|
-
# Calculate actual distances using geodesic methods
|
|
505
|
-
geod = Geod(ellps="WGS84")
|
|
506
|
-
_, _, width = geod.inv(left_wgs84, bottom_wgs84, right_wgs84, bottom_wgs84)
|
|
507
|
-
_, _, height = geod.inv(left_wgs84, bottom_wgs84, left_wgs84, top_wgs84)
|
|
508
|
-
else:
|
|
509
|
-
# For projections already in meters, use simple subtraction
|
|
510
|
-
width = right - left
|
|
511
|
-
height = top - bottom
|
|
512
|
-
|
|
513
|
-
# Calculate grid dimensions and adjust mesh size
|
|
514
|
-
num_cells_x = int(width / mesh_size + 0.5)
|
|
515
|
-
num_cells_y = int(height / mesh_size + 0.5)
|
|
516
|
-
|
|
517
|
-
adjusted_mesh_size_x = (right - left) / num_cells_x
|
|
518
|
-
adjusted_mesh_size_y = (top - bottom) / num_cells_y
|
|
519
|
-
|
|
520
|
-
# Create affine transform for coordinate mapping
|
|
521
|
-
new_affine = Affine(adjusted_mesh_size_x, 0, left, 0, -adjusted_mesh_size_y, top)
|
|
522
|
-
|
|
523
|
-
# Generate coordinate grids
|
|
524
|
-
cols, rows = np.meshgrid(np.arange(num_cells_x), np.arange(num_cells_y))
|
|
525
|
-
xs, ys = new_affine * (cols, rows)
|
|
526
|
-
xs_flat, ys_flat = xs.flatten(), ys.flatten()
|
|
527
|
-
|
|
528
|
-
# Convert to image coordinates
|
|
529
|
-
row, col = src.index(xs_flat, ys_flat)
|
|
530
|
-
row, col = np.array(row), np.array(col)
|
|
531
|
-
|
|
532
|
-
# Filter valid indices
|
|
533
|
-
valid = (row >= 0) & (row < src.height) & (col >= 0) & (col < src.width)
|
|
534
|
-
row, col = row[valid], col[valid]
|
|
535
|
-
|
|
536
|
-
# Create output grid and fill with height values
|
|
537
|
-
grid = np.full((num_cells_y, num_cells_x), np.nan)
|
|
538
|
-
flat_indices = np.ravel_multi_index((row, col), img.shape)
|
|
539
|
-
np.put(grid, np.ravel_multi_index((rows.flatten()[valid], cols.flatten()[valid]), grid.shape), img.flat[flat_indices])
|
|
540
|
-
|
|
541
|
-
return np.flipud(grid)
|
|
542
|
-
|
|
543
408
|
def create_height_grid_from_geotiff_polygon(tiff_path, mesh_size, polygon):
|
|
544
409
|
"""
|
|
545
410
|
Create a height grid from a GeoTIFF file within a polygon boundary.
|
|
@@ -562,8 +427,9 @@ def create_height_grid_from_geotiff_polygon(tiff_path, mesh_size, polygon):
|
|
|
562
427
|
poly = Polygon(polygon)
|
|
563
428
|
|
|
564
429
|
# Get polygon bounds in WGS84
|
|
565
|
-
bottom_wgs84,
|
|
566
|
-
print(left, bottom, right, top)
|
|
430
|
+
left_wgs84, bottom_wgs84, right_wgs84, top_wgs84 = poly.bounds
|
|
431
|
+
# print(left, bottom, right, top)
|
|
432
|
+
# print(left_wgs84, bottom_wgs84, right_wgs84, top_wgs84)
|
|
567
433
|
|
|
568
434
|
# Calculate actual distances using geodesic methods
|
|
569
435
|
geod = Geod(ellps="WGS84")
|
|
@@ -624,8 +490,8 @@ def create_building_height_grid_from_geojson_polygon(geojson_data, meshsize, rec
|
|
|
624
490
|
vertex_0, vertex_1, vertex_3 = rectangle_vertices[0], rectangle_vertices[1], rectangle_vertices[3]
|
|
625
491
|
|
|
626
492
|
# Calculate distances between vertices
|
|
627
|
-
dist_side_1 = calculate_distance(geod, vertex_0[
|
|
628
|
-
dist_side_2 = calculate_distance(geod, vertex_0[
|
|
493
|
+
dist_side_1 = calculate_distance(geod, vertex_0[0], vertex_0[1], vertex_1[0], vertex_1[1])
|
|
494
|
+
dist_side_2 = calculate_distance(geod, vertex_0[0], vertex_0[1], vertex_3[0], vertex_3[1])
|
|
629
495
|
|
|
630
496
|
# Calculate normalized vectors for grid orientation
|
|
631
497
|
side_1 = np.array(vertex_1) - np.array(vertex_0)
|