voxcity 0.3.3__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 +25 -39
- voxcity/geo/draw.py +41 -45
- voxcity/geo/grid.py +9 -143
- voxcity/geo/utils.py +79 -66
- voxcity/sim/solar.py +5 -5
- voxcity/sim/view.py +5 -5
- voxcity/utils/weather.py +7 -7
- {voxcity-0.3.3.dist-info → voxcity-0.3.4.dist-info}/METADATA +5 -5
- voxcity-0.3.4.dist-info/RECORD +34 -0
- {voxcity-0.3.3.dist-info → voxcity-0.3.4.dist-info}/WHEEL +1 -1
- voxcity-0.3.3.dist-info/RECORD +0 -34
- {voxcity-0.3.3.dist-info → voxcity-0.3.4.dist-info}/AUTHORS.rst +0 -0
- {voxcity-0.3.3.dist-info → voxcity-0.3.4.dist-info}/LICENSE +0 -0
- {voxcity-0.3.3.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,20 +497,19 @@ 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):
|
|
@@ -530,8 +520,8 @@ def find_building_containing_point(features, target_point):
|
|
|
530
520
|
def get_buildings_in_drawn_polygon(building_geojson, drawn_polygon_vertices,
|
|
531
521
|
operation='within'):
|
|
532
522
|
"""
|
|
533
|
-
Given a list of building footprints
|
|
534
|
-
vertices (
|
|
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
|
|
535
525
|
intersect the drawn polygon.
|
|
536
526
|
|
|
537
527
|
Args:
|
|
@@ -543,7 +533,7 @@ def get_buildings_in_drawn_polygon(building_geojson, drawn_polygon_vertices,
|
|
|
543
533
|
"type": "Polygon",
|
|
544
534
|
"coordinates": [
|
|
545
535
|
[
|
|
546
|
-
[
|
|
536
|
+
[lon1, lat1], [lon2, lat2], ...
|
|
547
537
|
]
|
|
548
538
|
]
|
|
549
539
|
},
|
|
@@ -552,10 +542,9 @@ def get_buildings_in_drawn_polygon(building_geojson, drawn_polygon_vertices,
|
|
|
552
542
|
...
|
|
553
543
|
}
|
|
554
544
|
}
|
|
555
|
-
Note: These coordinates are in (lat, lon) order, not standard (lon, lat).
|
|
556
545
|
|
|
557
546
|
drawn_polygon_vertices (list):
|
|
558
|
-
A list of (
|
|
547
|
+
A list of (lon, lat) tuples representing the polygon drawn by the user.
|
|
559
548
|
|
|
560
549
|
operation (str):
|
|
561
550
|
Determines how to include buildings.
|
|
@@ -566,27 +555,24 @@ def get_buildings_in_drawn_polygon(building_geojson, drawn_polygon_vertices,
|
|
|
566
555
|
list:
|
|
567
556
|
A list of building IDs (strings or ints) that satisfy the condition.
|
|
568
557
|
"""
|
|
569
|
-
#
|
|
570
|
-
|
|
571
|
-
# So we'll do (lon, lat) for each vertex.
|
|
572
|
-
drawn_polygon_shapely = Polygon([(lon, lat) for (lat, lon) in drawn_polygon_vertices])
|
|
558
|
+
# Create Shapely Polygon from drawn vertices
|
|
559
|
+
drawn_polygon_shapely = Polygon(drawn_polygon_vertices)
|
|
573
560
|
|
|
574
561
|
included_building_ids = []
|
|
575
562
|
|
|
576
|
-
#
|
|
563
|
+
# Check each building in the GeoJSON
|
|
577
564
|
for feature in building_geojson:
|
|
578
565
|
# Skip any feature that is not Polygon
|
|
579
566
|
if feature['geometry']['type'] != 'Polygon':
|
|
580
567
|
continue
|
|
581
568
|
|
|
582
|
-
# Extract coordinates
|
|
569
|
+
# Extract coordinates
|
|
583
570
|
coords = feature['geometry']['coordinates'][0]
|
|
584
571
|
|
|
585
572
|
# Create a Shapely polygon for the building
|
|
586
|
-
|
|
587
|
-
building_polygon = Polygon([(lon, lat) for (lat, lon) in coords])
|
|
573
|
+
building_polygon = Polygon(coords)
|
|
588
574
|
|
|
589
|
-
#
|
|
575
|
+
# Depending on the operation, check the relationship
|
|
590
576
|
if operation == 'intersect':
|
|
591
577
|
if building_polygon.intersects(drawn_polygon_shapely):
|
|
592
578
|
included_building_ids.append(feature['properties'].get('id', None))
|
voxcity/geo/draw.py
CHANGED
|
@@ -16,11 +16,11 @@ def rotate_rectangle(m, rectangle_vertices, angle):
|
|
|
16
16
|
|
|
17
17
|
Args:
|
|
18
18
|
m (ipyleaflet.Map): Map object to draw the rotated rectangle on
|
|
19
|
-
rectangle_vertices (list): List of (
|
|
19
|
+
rectangle_vertices (list): List of (lon, lat) tuples defining the rectangle vertices
|
|
20
20
|
angle (float): Rotation angle in degrees
|
|
21
21
|
|
|
22
22
|
Returns:
|
|
23
|
-
list: List of rotated (
|
|
23
|
+
list: List of rotated (lon, lat) tuples defining the new rectangle vertices
|
|
24
24
|
"""
|
|
25
25
|
if not rectangle_vertices:
|
|
26
26
|
print("Draw a rectangle first!")
|
|
@@ -31,7 +31,7 @@ def rotate_rectangle(m, rectangle_vertices, angle):
|
|
|
31
31
|
mercator = Proj(init='epsg:3857') # Web Mercator (projection used by most web maps)
|
|
32
32
|
|
|
33
33
|
# Project vertices from WGS84 to Web Mercator for proper distance calculations
|
|
34
|
-
projected_vertices = [transform(wgs84, mercator, lon, lat) for
|
|
34
|
+
projected_vertices = [transform(wgs84, mercator, lon, lat) for lon, lat in rectangle_vertices]
|
|
35
35
|
|
|
36
36
|
# Calculate the centroid to use as rotation center
|
|
37
37
|
centroid_x = sum(x for x, y in projected_vertices) / len(projected_vertices)
|
|
@@ -57,15 +57,12 @@ def rotate_rectangle(m, rectangle_vertices, angle):
|
|
|
57
57
|
|
|
58
58
|
rotated_vertices.append((new_x, new_y))
|
|
59
59
|
|
|
60
|
-
# Convert coordinates back to WGS84 (lat
|
|
60
|
+
# Convert coordinates back to WGS84 (lon/lat)
|
|
61
61
|
new_vertices = [transform(mercator, wgs84, x, y) for x, y in rotated_vertices]
|
|
62
62
|
|
|
63
|
-
# Reorder coordinates from (lon,lat) to (lat,lon) format
|
|
64
|
-
new_vertices = [(lat, lon) for lon, lat in new_vertices]
|
|
65
|
-
|
|
66
63
|
# Create and add new polygon layer to map
|
|
67
64
|
polygon = ipyleaflet.Polygon(
|
|
68
|
-
locations=new_vertices,
|
|
65
|
+
locations=[(lat, lon) for lon, lat in new_vertices], # Convert to (lat,lon) for ipyleaflet
|
|
69
66
|
color="red",
|
|
70
67
|
fill_color="red"
|
|
71
68
|
)
|
|
@@ -104,8 +101,8 @@ def draw_rectangle_map(center=(40, -100), zoom=4):
|
|
|
104
101
|
print("Vertices of the drawn rectangle:")
|
|
105
102
|
# Store all vertices except last (GeoJSON repeats first vertex at end)
|
|
106
103
|
for coord in coordinates[:-1]:
|
|
107
|
-
#
|
|
108
|
-
rectangle_vertices.append((coord[
|
|
104
|
+
# Keep GeoJSON (lon,lat) format
|
|
105
|
+
rectangle_vertices.append((coord[0], coord[1]))
|
|
109
106
|
print(f"Longitude: {coord[0]}, Latitude: {coord[1]}")
|
|
110
107
|
|
|
111
108
|
# Configure drawing controls - only enable rectangle drawing
|
|
@@ -179,9 +176,9 @@ def center_location_map_cityname(cityname, east_west_length, north_south_length,
|
|
|
179
176
|
|
|
180
177
|
# Process only if a point was drawn on the map
|
|
181
178
|
if action == 'created' and geo_json['geometry']['type'] == 'Point':
|
|
182
|
-
# Extract point coordinates
|
|
183
|
-
|
|
184
|
-
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}")
|
|
185
182
|
|
|
186
183
|
# Calculate corner points using geopy's distance calculator
|
|
187
184
|
# Each point is calculated as a destination from center point using bearing
|
|
@@ -190,15 +187,15 @@ def center_location_map_cityname(cityname, east_west_length, north_south_length,
|
|
|
190
187
|
east = distance.distance(meters=east_west_length/2).destination((lat, lon), bearing=90)
|
|
191
188
|
west = distance.distance(meters=east_west_length/2).destination((lat, lon), bearing=270)
|
|
192
189
|
|
|
193
|
-
# Create rectangle vertices in counter-clockwise order
|
|
190
|
+
# Create rectangle vertices in counter-clockwise order (lon,lat)
|
|
194
191
|
rectangle_vertices.extend([
|
|
195
|
-
(
|
|
196
|
-
(
|
|
197
|
-
(
|
|
198
|
-
(
|
|
192
|
+
(west.longitude, south.latitude),
|
|
193
|
+
(west.longitude, north.latitude),
|
|
194
|
+
(east.longitude, north.latitude),
|
|
195
|
+
(east.longitude, south.latitude)
|
|
199
196
|
])
|
|
200
197
|
|
|
201
|
-
# Create and add new rectangle to map
|
|
198
|
+
# Create and add new rectangle to map (ipyleaflet expects lat,lon)
|
|
202
199
|
rectangle = Rectangle(
|
|
203
200
|
bounds=[(north.latitude, west.longitude), (south.latitude, east.longitude)],
|
|
204
201
|
color="red",
|
|
@@ -209,7 +206,7 @@ def center_location_map_cityname(cityname, east_west_length, north_south_length,
|
|
|
209
206
|
|
|
210
207
|
print("Rectangle vertices:")
|
|
211
208
|
for vertex in rectangle_vertices:
|
|
212
|
-
print(f"
|
|
209
|
+
print(f"Longitude: {vertex[0]}, Latitude: {vertex[1]}")
|
|
213
210
|
|
|
214
211
|
# Configure drawing controls - only enable point drawing
|
|
215
212
|
draw_control = DrawControl()
|
|
@@ -227,44 +224,44 @@ def center_location_map_cityname(cityname, east_west_length, north_south_length,
|
|
|
227
224
|
|
|
228
225
|
def display_buildings_and_draw_polygon(building_geojson, zoom=17):
|
|
229
226
|
"""
|
|
230
|
-
Displays building footprints (in Lat
|
|
227
|
+
Displays building footprints (in Lon-Lat order) on an ipyleaflet map,
|
|
231
228
|
and allows the user to draw a polygon whose vertices are returned
|
|
232
|
-
in a Python list (also in Lat
|
|
229
|
+
in a Python list (also in Lon-Lat).
|
|
233
230
|
|
|
234
231
|
Args:
|
|
235
232
|
building_geojson (list): A list of GeoJSON features (Polygons),
|
|
236
|
-
|
|
233
|
+
with coordinates in [lon, lat] order.
|
|
237
234
|
zoom (int): Initial zoom level for the map. Default=17.
|
|
238
235
|
|
|
239
236
|
Returns:
|
|
240
237
|
(map_object, drawn_polygon_vertices)
|
|
241
238
|
- map_object: ipyleaflet Map instance
|
|
242
239
|
- drawn_polygon_vertices: a Python list that gets updated whenever
|
|
243
|
-
a new polygon is created. The list is in (
|
|
240
|
+
a new polygon is created. The list is in (lon, lat) order.
|
|
244
241
|
"""
|
|
245
242
|
# ---------------------------------------------------------
|
|
246
243
|
# 1. Determine a suitable map center via bounding box logic
|
|
247
244
|
# ---------------------------------------------------------
|
|
248
|
-
all_lats = []
|
|
249
245
|
all_lons = []
|
|
246
|
+
all_lats = []
|
|
250
247
|
for feature in building_geojson:
|
|
251
248
|
# Handle only Polygons here; skip MultiPolygon if present
|
|
252
249
|
if feature['geometry']['type'] == 'Polygon':
|
|
253
|
-
# Coordinates in this data are [ [
|
|
250
|
+
# Coordinates in this data are [ [lon, lat], [lon, lat], ... ]
|
|
254
251
|
coords = feature['geometry']['coordinates'][0] # outer ring
|
|
255
|
-
|
|
256
|
-
|
|
252
|
+
all_lons.extend(pt[0] for pt in coords)
|
|
253
|
+
all_lats.extend(pt[1] for pt in coords)
|
|
257
254
|
|
|
258
255
|
if not all_lats or not all_lons:
|
|
259
256
|
# Fallback: If no footprints or invalid data, pick a default
|
|
260
|
-
|
|
257
|
+
center_lon, center_lat = -100.0, 40.0
|
|
261
258
|
else:
|
|
262
|
-
min_lat, max_lat = min(all_lats), max(all_lats)
|
|
263
259
|
min_lon, max_lon = min(all_lons), max(all_lons)
|
|
264
|
-
|
|
260
|
+
min_lat, max_lat = min(all_lats), max(all_lats)
|
|
265
261
|
center_lon = (min_lon + max_lon) / 2
|
|
262
|
+
center_lat = (min_lat + max_lat) / 2
|
|
266
263
|
|
|
267
|
-
# Create the ipyleaflet map
|
|
264
|
+
# Create the ipyleaflet map (needs lat,lon)
|
|
268
265
|
m = Map(center=(center_lat, center_lon), zoom=zoom, scroll_wheel_zoom=True)
|
|
269
266
|
|
|
270
267
|
# -----------------------------------------
|
|
@@ -274,8 +271,8 @@ def display_buildings_and_draw_polygon(building_geojson, zoom=17):
|
|
|
274
271
|
# Only handle simple Polygons
|
|
275
272
|
if feature['geometry']['type'] == 'Polygon':
|
|
276
273
|
coords = feature['geometry']['coordinates'][0]
|
|
277
|
-
#
|
|
278
|
-
lat_lon_coords = [(c[
|
|
274
|
+
# Convert to (lat,lon) for ipyleaflet
|
|
275
|
+
lat_lon_coords = [(c[1], c[0]) for c in coords]
|
|
279
276
|
|
|
280
277
|
# Create the polygon layer
|
|
281
278
|
bldg_layer = LeafletPolygon(
|
|
@@ -288,15 +285,15 @@ def display_buildings_and_draw_polygon(building_geojson, zoom=17):
|
|
|
288
285
|
m.add_layer(bldg_layer)
|
|
289
286
|
|
|
290
287
|
# -----------------------------------------------------------------
|
|
291
|
-
# 3. Enable drawing of polygons, capturing the vertices in Lat
|
|
288
|
+
# 3. Enable drawing of polygons, capturing the vertices in Lon-Lat
|
|
292
289
|
# -----------------------------------------------------------------
|
|
293
|
-
drawn_polygon_vertices = [] # We'll store the newly drawn polygon's vertices here (
|
|
290
|
+
drawn_polygon_vertices = [] # We'll store the newly drawn polygon's vertices here (lon, lat).
|
|
294
291
|
|
|
295
292
|
draw_control = DrawControl(
|
|
296
293
|
polygon={
|
|
297
294
|
"shapeOptions": {
|
|
298
|
-
"color": "
|
|
299
|
-
"fillColor": "
|
|
295
|
+
"color": "red",
|
|
296
|
+
"fillColor": "red",
|
|
300
297
|
"fillOpacity": 0.2
|
|
301
298
|
}
|
|
302
299
|
},
|
|
@@ -311,23 +308,22 @@ def display_buildings_and_draw_polygon(building_geojson, zoom=17):
|
|
|
311
308
|
"""
|
|
312
309
|
Callback for whenever a shape is created or edited.
|
|
313
310
|
ipyleaflet's DrawControl returns standard GeoJSON (lon, lat).
|
|
314
|
-
We'll
|
|
311
|
+
We'll keep them as (lon, lat).
|
|
315
312
|
"""
|
|
316
313
|
# Clear any previously stored vertices
|
|
317
314
|
drawn_polygon_vertices.clear()
|
|
318
315
|
|
|
319
316
|
if action == 'created' and geo_json['geometry']['type'] == 'Polygon':
|
|
320
|
-
# The polygon
|
|
317
|
+
# The polygon's first ring
|
|
321
318
|
coordinates = geo_json['geometry']['coordinates'][0]
|
|
322
|
-
print("Vertices of the drawn polygon (Lat
|
|
319
|
+
print("Vertices of the drawn polygon (Lon-Lat):")
|
|
323
320
|
|
|
324
|
-
#
|
|
325
|
-
# The last coordinate repeats the first -> skip it with [:-1]
|
|
321
|
+
# Keep GeoJSON (lon,lat) format, skip last repeated coordinate
|
|
326
322
|
for coord in coordinates[:-1]:
|
|
327
323
|
lon = coord[0]
|
|
328
324
|
lat = coord[1]
|
|
329
|
-
drawn_polygon_vertices.append((
|
|
330
|
-
print(f" - (
|
|
325
|
+
drawn_polygon_vertices.append((lon, lat))
|
|
326
|
+
print(f" - (lon, lat) = ({lon}, {lat})")
|
|
331
327
|
|
|
332
328
|
draw_control.on_draw(handle_draw)
|
|
333
329
|
m.add_control(draw_control)
|
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)
|