voxcity 0.5.26__py3-none-any.whl → 0.5.28__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.

@@ -25,10 +25,19 @@ Dependencies:
25
25
 
26
26
  import math
27
27
  from pyproj import Proj, transform
28
- from ipyleaflet import Map, DrawControl, Rectangle, Polygon as LeafletPolygon
29
- import ipyleaflet
28
+ from ipyleaflet import (
29
+ Map,
30
+ DrawControl,
31
+ Rectangle,
32
+ Polygon as LeafletPolygon,
33
+ WidgetControl
34
+ )
30
35
  from geopy import distance
31
36
  import shapely.geometry as geom
37
+ import geopandas as gpd
38
+ from ipywidgets import VBox, HBox, Button, FloatText, Label, Output, HTML
39
+ import pandas as pd
40
+ from IPython.display import display, clear_output
32
41
 
33
42
  from .utils import get_coordinates_from_cityname
34
43
 
@@ -484,4 +493,268 @@ def display_buildings_and_draw_polygon(building_gdf=None, rectangle_vertices=Non
484
493
  draw_control.on_draw(handle_draw)
485
494
  m.add_control(draw_control)
486
495
 
487
- return m, drawn_polygon_vertices
496
+ return m, drawn_polygon_vertices
497
+
498
+ def draw_additional_buildings(building_gdf=None, initial_center=None, zoom=17):
499
+ """
500
+ Creates an interactive map for drawing building footprints with height input.
501
+
502
+ This function provides an interface for users to:
503
+ 1. Draw building footprints on an interactive map
504
+ 2. Set building height values through a UI widget
505
+ 3. Add new buildings to the existing building_gdf
506
+
507
+ The workflow is:
508
+ - User draws a polygon on the map
509
+ - Height input widget appears
510
+ - User enters height and clicks "Add Building"
511
+ - Building is added to GeoDataFrame and displayed on map
512
+
513
+ Args:
514
+ building_gdf (GeoDataFrame, optional): Existing building footprints to display.
515
+ If None, creates a new empty GeoDataFrame.
516
+ Must have 'geometry' column and optionally 'height' column.
517
+ initial_center (tuple, optional): Initial map center as (lon, lat).
518
+ If None, centers on existing buildings or defaults to (-100, 40).
519
+ zoom (int): Initial zoom level (default=17).
520
+
521
+ Returns:
522
+ tuple: (map_object, updated_building_gdf)
523
+ - map_object: ipyleaflet Map instance with drawing controls
524
+ - updated_building_gdf: GeoDataFrame that automatically updates when buildings are added
525
+
526
+ Example:
527
+ >>> # Start with empty buildings
528
+ >>> m, buildings = draw_additional_buildings()
529
+ >>> # Draw buildings on the map...
530
+ >>> print(buildings) # Will contain all drawn buildings
531
+ """
532
+
533
+ # Initialize or copy the building GeoDataFrame
534
+ if building_gdf is None:
535
+ # Create empty GeoDataFrame with required columns
536
+ updated_gdf = gpd.GeoDataFrame(
537
+ columns=['geometry', 'height', 'building_id'],
538
+ crs='EPSG:4326'
539
+ )
540
+ else:
541
+ # Make a copy to avoid modifying the original
542
+ updated_gdf = building_gdf.copy()
543
+ if 'height' not in updated_gdf.columns:
544
+ updated_gdf['height'] = 10.0 # Default height
545
+ if 'building_id' not in updated_gdf.columns:
546
+ updated_gdf['building_id'] = range(len(updated_gdf))
547
+
548
+ # Determine map center
549
+ if initial_center is not None:
550
+ center_lon, center_lat = initial_center
551
+ elif updated_gdf is not None and len(updated_gdf) > 0:
552
+ bounds = updated_gdf.total_bounds
553
+ min_lon, min_lat, max_lon, max_lat = bounds
554
+ center_lon = (min_lon + max_lon) / 2
555
+ center_lat = (min_lat + max_lat) / 2
556
+ else:
557
+ center_lon, center_lat = -100.0, 40.0
558
+
559
+ # Create the map
560
+ m = Map(center=(center_lat, center_lon), zoom=zoom, scroll_wheel_zoom=True)
561
+
562
+ # Display existing buildings
563
+ building_layers = {}
564
+ for idx, row in updated_gdf.iterrows():
565
+ if isinstance(row.geometry, geom.Polygon):
566
+ coords = list(row.geometry.exterior.coords)
567
+ lat_lon_coords = [(c[1], c[0]) for c in coords[:-1]]
568
+
569
+ height = row.get('height', 10.0)
570
+ bldg_layer = LeafletPolygon(
571
+ locations=lat_lon_coords,
572
+ color="blue",
573
+ fill_color="blue",
574
+ fill_opacity=0.3,
575
+ weight=2,
576
+ popup=HTML(f"<b>Building ID:</b> {row.get('building_id', idx)}<br>"
577
+ f"<b>Height:</b> {height}m")
578
+ )
579
+ m.add_layer(bldg_layer)
580
+ building_layers[idx] = bldg_layer
581
+
582
+ # Create UI widgets
583
+ height_input = FloatText(
584
+ value=10.0,
585
+ description='Height (m):',
586
+ disabled=False,
587
+ style={'description_width': 'initial'}
588
+ )
589
+
590
+ add_button = Button(
591
+ description='Add Building',
592
+ button_style='success',
593
+ disabled=True
594
+ )
595
+
596
+ clear_button = Button(
597
+ description='Clear Drawing',
598
+ button_style='warning',
599
+ disabled=True
600
+ )
601
+
602
+ status_output = Output()
603
+
604
+ # Create control panel
605
+ control_panel = VBox([
606
+ HTML("<h3>Draw Building Tool</h3>"),
607
+ HTML("<p>1. Draw a polygon on the map<br>2. Set height<br>3. Click 'Add Building'</p>"),
608
+ height_input,
609
+ HBox([add_button, clear_button]),
610
+ status_output
611
+ ])
612
+
613
+ # Add control panel to map
614
+ widget_control = WidgetControl(widget=control_panel, position='topright')
615
+ m.add_control(widget_control)
616
+
617
+ # Store the current drawn polygon
618
+ current_polygon = {'vertices': [], 'layer': None}
619
+
620
+ # Drawing control
621
+ draw_control = DrawControl(
622
+ polygon={
623
+ "shapeOptions": {
624
+ "color": "red",
625
+ "fillColor": "red",
626
+ "fillOpacity": 0.3,
627
+ "weight": 3
628
+ }
629
+ },
630
+ rectangle={},
631
+ circle={},
632
+ circlemarker={},
633
+ polyline={},
634
+ marker={}
635
+ )
636
+
637
+ def handle_draw(self, action, geo_json):
638
+ """Handle polygon drawing events"""
639
+ with status_output:
640
+ clear_output()
641
+
642
+ if action == 'created' and geo_json['geometry']['type'] == 'Polygon':
643
+ # Store vertices
644
+ coordinates = geo_json['geometry']['coordinates'][0]
645
+ current_polygon['vertices'] = [(coord[0], coord[1]) for coord in coordinates[:-1]]
646
+
647
+ # Enable buttons
648
+ add_button.disabled = False
649
+ clear_button.disabled = False
650
+
651
+ with status_output:
652
+ print(f"Polygon drawn with {len(current_polygon['vertices'])} vertices")
653
+ print("Set height and click 'Add Building'")
654
+
655
+ def add_building_click(b):
656
+ """Handle add building button click"""
657
+ # Use nonlocal to modify the outer scope variable
658
+ nonlocal updated_gdf
659
+
660
+ with status_output:
661
+ clear_output()
662
+
663
+ if current_polygon['vertices']:
664
+ # Create polygon geometry
665
+ polygon = geom.Polygon(current_polygon['vertices'])
666
+
667
+ # Get next building ID
668
+ if len(updated_gdf) > 0:
669
+ next_id = int(updated_gdf['building_id'].max() + 1)
670
+ else:
671
+ next_id = 1
672
+
673
+ # Create new row data
674
+ new_row_data = {
675
+ 'geometry': polygon,
676
+ 'height': float(height_input.value),
677
+ 'building_id': next_id
678
+ }
679
+
680
+ # Add any additional columns
681
+ for col in updated_gdf.columns:
682
+ if col not in new_row_data:
683
+ new_row_data[col] = None
684
+
685
+ # Append the new building in-place
686
+ new_index = len(updated_gdf)
687
+ updated_gdf.loc[new_index] = new_row_data
688
+
689
+ # Add to map
690
+ coords = list(polygon.exterior.coords)
691
+ lat_lon_coords = [(c[1], c[0]) for c in coords[:-1]]
692
+
693
+ new_layer = LeafletPolygon(
694
+ locations=lat_lon_coords,
695
+ color="blue",
696
+ fill_color="blue",
697
+ fill_opacity=0.3,
698
+ weight=2,
699
+ popup=HTML(f"<b>Building ID:</b> {next_id}<br>"
700
+ f"<b>Height:</b> {height_input.value}m")
701
+ )
702
+ m.add_layer(new_layer)
703
+
704
+ # Clear drawing
705
+ draw_control.clear()
706
+ current_polygon['vertices'] = []
707
+ add_button.disabled = True
708
+ clear_button.disabled = True
709
+
710
+ print(f"Building {next_id} added successfully!")
711
+ print(f"Height: {height_input.value}m")
712
+ print(f"Total buildings: {len(updated_gdf)}")
713
+
714
+ def clear_drawing_click(b):
715
+ """Handle clear drawing button click"""
716
+ with status_output:
717
+ clear_output()
718
+ draw_control.clear()
719
+ current_polygon['vertices'] = []
720
+ add_button.disabled = True
721
+ clear_button.disabled = True
722
+ print("Drawing cleared")
723
+
724
+ # Connect event handlers
725
+ draw_control.on_draw(handle_draw)
726
+ add_button.on_click(add_building_click)
727
+ clear_button.on_click(clear_drawing_click)
728
+
729
+ # Add draw control to map
730
+ m.add_control(draw_control)
731
+
732
+ # Display initial status
733
+ with status_output:
734
+ print(f"Total buildings loaded: {len(updated_gdf)}")
735
+ print("Draw a polygon to add a new building")
736
+
737
+ return m, updated_gdf
738
+
739
+
740
+ # Simple convenience function
741
+ def create_building_editor(building_gdf=None, initial_center=None, zoom=17):
742
+ """
743
+ Creates and displays an interactive building editor.
744
+
745
+ Args:
746
+ building_gdf: Existing buildings GeoDataFrame (optional)
747
+ initial_center: Map center as (lon, lat) tuple (optional)
748
+ zoom: Initial zoom level (default=17)
749
+
750
+ Returns:
751
+ GeoDataFrame: The building GeoDataFrame that automatically updates
752
+
753
+ Example:
754
+ >>> buildings = create_building_editor()
755
+ >>> # Draw buildings on the displayed map
756
+ >>> print(buildings) # Automatically contains all drawn buildings
757
+ """
758
+ m, gdf = draw_additional_buildings(building_gdf, initial_center, zoom)
759
+ display(m)
760
+ return gdf
@@ -412,7 +412,7 @@ def create_land_cover_grid_from_geotiff_polygon(tiff_path, mesh_size, land_cover
412
412
  # Flip grid vertically to match geographic orientation
413
413
  return np.flipud(grid)
414
414
 
415
- def create_land_cover_grid_from_gdf_polygon(gdf, meshsize, source, rectangle_vertices):
415
+ def create_land_cover_grid_from_gdf_polygon(gdf, meshsize, source, rectangle_vertices, default_class='Developed space'):
416
416
  """Create a grid of land cover classes from GeoDataFrame polygon data.
417
417
 
418
418
  Args:
@@ -420,6 +420,8 @@ def create_land_cover_grid_from_gdf_polygon(gdf, meshsize, source, rectangle_ver
420
420
  meshsize (float): Size of each grid cell in meters
421
421
  source (str): Source of the land cover data to determine class priorities
422
422
  rectangle_vertices (list): List of 4 (lon,lat) coordinate pairs defining the rectangle bounds
423
+ default_class (str, optional): Default land cover class for cells with no intersecting polygons.
424
+ Defaults to 'Developed space'.
423
425
 
424
426
  Returns:
425
427
  numpy.ndarray: 2D grid of land cover classes as strings
@@ -466,7 +468,7 @@ def create_land_cover_grid_from_gdf_polygon(gdf, meshsize, source, rectangle_ver
466
468
  print(f"Adjusted mesh size: {adjusted_meshsize}")
467
469
 
468
470
  # Initialize grid with default land cover class
469
- grid = np.full(grid_size, 'Developed space', dtype=object)
471
+ grid = np.full(grid_size, default_class, dtype=object)
470
472
 
471
473
  # Calculate bounding box for spatial indexing
472
474
  extent = [min(coord[1] for coord in rectangle_vertices), max(coord[1] for coord in rectangle_vertices),
@@ -485,7 +487,7 @@ def create_land_cover_grid_from_gdf_polygon(gdf, meshsize, source, rectangle_ver
485
487
  # Iterate through each grid cell
486
488
  for i in range(grid_size[0]):
487
489
  for j in range(grid_size[1]):
488
- land_cover_class = 'Developed space'
490
+ land_cover_class = default_class
489
491
  cell = create_cell_polygon(origin, i, j, adjusted_meshsize, u_vec, v_vec)
490
492
 
491
493
  # Check intersections with polygons that could overlap this cell
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voxcity
3
- Version: 0.5.26
3
+ Version: 0.5.28
4
4
  Summary: voxcity is an easy and one-stop tool to output 3d city models for microclimate simulation by integrating multiple geospatial open-data
5
5
  Author-email: Kunihiko Fujiwara <kunihiko@nus.edu.sg>
6
6
  Maintainer-email: Kunihiko Fujiwara <kunihiko@nus.edu.sg>
@@ -1,21 +1,22 @@
1
1
  voxcity/__init__.py,sha256=el9v3gfybHOF_GUYPeSOqN0-vCrTW0eU1mcvi0sEfeU,252
2
- voxcity/generator.py,sha256=h1C2AaLQ1fqdU47UIUZTK1ox8d_c2I1Dql5mdscr7X8,52552
2
+ voxcity/generator.py,sha256=mEggM4FxE7LChTPCspAQmJlAoUz1PVcbaUfY11cMfzQ,54176
3
3
  voxcity/downloader/__init__.py,sha256=o_T_EU7hZLGyXxX9wVWn1x-OAa3ThGYdnpgB1_2v3AE,151
4
4
  voxcity/downloader/citygml.py,sha256=jVeHCLlJTf7k55OQGX0lZGQAngz_DD2V5TldSqRFlvc,36024
5
5
  voxcity/downloader/eubucco.py,sha256=ln1YNaaOgJfxNfCtVbYaMm775-bUvpAA_LDv60_i22w,17875
6
6
  voxcity/downloader/gee.py,sha256=O6HhQnUUumg_tTm4pP_cuyu5YjupDA1uKFxZWxD-i2E,23205
7
7
  voxcity/downloader/mbfp.py,sha256=UXDVjsO0fnb0fSal9yqrSFEIBThnRmnutnp08kZTmCA,6595
8
8
  voxcity/downloader/oemj.py,sha256=iDacTpiqn7RAXuqyEtHP29m0Cycwta5sMy9-GdvX3Fg,12293
9
- voxcity/downloader/osm.py,sha256=kXiUedT7dwPOQ_4nxXptfeqNb5RKTIsQLG5km7Q8kKk,41645
9
+ voxcity/downloader/osm.py,sha256=9nOVcVE50N76F5uquJbNIFr8Xajff4ac2Uj2oSGcFrc,42591
10
10
  voxcity/downloader/overture.py,sha256=4YG2DMwUSSyZKUw_o8cGhMmAkPJon82aPqOFBvrre-Y,11987
11
11
  voxcity/downloader/utils.py,sha256=tz6wt4B9BhEOyvoF5OYXlr8rUd5cBEDedWL3j__oT70,3099
12
- voxcity/exporter/__init__.py,sha256=2htEXMrq6knO8JMROtfz-0HFGddsAJkXqNUB2_MhqvM,70
12
+ voxcity/exporter/__init__.py,sha256=dvyWJ184Eik9tFc0VviGbzTQzZi7O0JNyrqi_n39pVI,94
13
+ voxcity/exporter/cityles.py,sha256=zp119kSfsmwReCv6hJEonzPL3vxKZBk6LxD0NUk_j1E,12978
13
14
  voxcity/exporter/envimet.py,sha256=Sh7s1JdQ6SgT_L2Xd_c4gtEGWK2hTS87bccaoIqik-s,31105
14
15
  voxcity/exporter/magicavoxel.py,sha256=SfGEgTZRlossKx3Xrv9d3iKSX-HmfQJEL9lZHgWMDX4,12782
15
16
  voxcity/exporter/obj.py,sha256=h1_aInpemcsu96fSTwjKMqX2VZAFYbZbElWd4M1ogyI,27973
16
17
  voxcity/geoprocessor/__init__.py,sha256=JzPVhhttxBWvaZ0IGX2w7OWL5bCo_TIvpHefWeNXruA,133
17
- voxcity/geoprocessor/draw.py,sha256=lcBtf4VAWXjk46cInPdSBeiLCTESdxzLOayr_0YOnus,22184
18
- voxcity/geoprocessor/grid.py,sha256=Uy8Oz4iL7vnPMqp_qtp4dQrs00yd2L9CSIPmF5KnbtI,63961
18
+ voxcity/geoprocessor/draw.py,sha256=qTYzXEF8GKWh3hquirspzlzwXTIzl39y9VuEJ0WjOis,32031
19
+ voxcity/geoprocessor/grid.py,sha256=lhELyznlk4Jt7vnd0uOpMCLPCjrYQjX7qtQq-xHkYE4,64161
19
20
  voxcity/geoprocessor/mesh.py,sha256=ElqAE2MA8KZs7yD7B1P88XYmryC6F9nkkP6cXv7FzIk,30777
20
21
  voxcity/geoprocessor/network.py,sha256=YynqR0nq_NUra_cQ3Z_56KxfRia1b6-hIzGCj3QT-wE,25137
21
22
  voxcity/geoprocessor/polygon.py,sha256=-LonxtW5du3UP61oygqtDJl6GGsCYnUuN9KYwl1UFdc,53707
@@ -29,9 +30,9 @@ voxcity/utils/lc.py,sha256=h2yOWLUIrrummkyMyhRK5VbyrsPtslS0MJov_y0WGIQ,18925
29
30
  voxcity/utils/material.py,sha256=H8K8Lq4wBL6dQtgj7esUW2U6wLCOTeOtelkTDJoRgMo,10007
30
31
  voxcity/utils/visualization.py,sha256=T-jKrCA4UMm93p-1O678RWM7e99iE0_Lj4wD07efcwI,112918
31
32
  voxcity/utils/weather.py,sha256=2Jtg-rIVJcsTtiKE-KuDnhIqS1-MSS16_zFRzj6zmu4,36435
32
- voxcity-0.5.26.dist-info/licenses/AUTHORS.rst,sha256=m82vkI5QokEGdcHof2OxK39lf81w1P58kG9ZNNAKS9U,175
33
- voxcity-0.5.26.dist-info/licenses/LICENSE,sha256=s_jE1Df1nTPL4A_5GCGic5Zwex0CVaPKcAmSilxJPPE,1089
34
- voxcity-0.5.26.dist-info/METADATA,sha256=EdbTSCTEB_oW67EK3cHHmBB5V7LrlDECoEsB5ATGgVI,26724
35
- voxcity-0.5.26.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
36
- voxcity-0.5.26.dist-info/top_level.txt,sha256=00b2U-LKfDllt6RL1R33MXie5MvxzUFye0NGD96t_8I,8
37
- voxcity-0.5.26.dist-info/RECORD,,
33
+ voxcity-0.5.28.dist-info/licenses/AUTHORS.rst,sha256=m82vkI5QokEGdcHof2OxK39lf81w1P58kG9ZNNAKS9U,175
34
+ voxcity-0.5.28.dist-info/licenses/LICENSE,sha256=s_jE1Df1nTPL4A_5GCGic5Zwex0CVaPKcAmSilxJPPE,1089
35
+ voxcity-0.5.28.dist-info/METADATA,sha256=1-Vf32nGdVvGxmOMXMcfTIi4IM92x4uGq5T7qzlDbxY,26724
36
+ voxcity-0.5.28.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
37
+ voxcity-0.5.28.dist-info/top_level.txt,sha256=00b2U-LKfDllt6RL1R33MXie5MvxzUFye0NGD96t_8I,8
38
+ voxcity-0.5.28.dist-info/RECORD,,