voxcity 0.7.0__py3-none-any.whl → 1.0.13__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.
- voxcity/__init__.py +14 -14
- voxcity/downloader/ocean.py +559 -0
- voxcity/exporter/__init__.py +12 -12
- voxcity/exporter/cityles.py +633 -633
- voxcity/exporter/envimet.py +733 -728
- voxcity/exporter/magicavoxel.py +333 -333
- voxcity/exporter/netcdf.py +238 -238
- voxcity/exporter/obj.py +1480 -1480
- voxcity/generator/__init__.py +47 -44
- voxcity/generator/api.py +727 -675
- voxcity/generator/grids.py +394 -379
- voxcity/generator/io.py +94 -94
- voxcity/generator/pipeline.py +582 -282
- voxcity/generator/update.py +429 -0
- voxcity/generator/voxelizer.py +18 -6
- voxcity/geoprocessor/__init__.py +75 -75
- voxcity/geoprocessor/draw.py +1494 -1219
- voxcity/geoprocessor/merge_utils.py +91 -91
- voxcity/geoprocessor/mesh.py +806 -806
- voxcity/geoprocessor/network.py +708 -708
- voxcity/geoprocessor/raster/__init__.py +2 -0
- voxcity/geoprocessor/raster/buildings.py +435 -428
- voxcity/geoprocessor/raster/core.py +31 -0
- voxcity/geoprocessor/raster/export.py +93 -93
- voxcity/geoprocessor/raster/landcover.py +178 -51
- voxcity/geoprocessor/raster/raster.py +1 -1
- voxcity/geoprocessor/utils.py +824 -824
- voxcity/models.py +115 -113
- voxcity/simulator/solar/__init__.py +66 -43
- voxcity/simulator/solar/integration.py +336 -336
- voxcity/simulator/solar/sky.py +668 -0
- voxcity/simulator/solar/temporal.py +792 -434
- voxcity/simulator_gpu/__init__.py +115 -0
- voxcity/simulator_gpu/common/__init__.py +9 -0
- voxcity/simulator_gpu/common/geometry.py +11 -0
- voxcity/simulator_gpu/core.py +322 -0
- voxcity/simulator_gpu/domain.py +262 -0
- voxcity/simulator_gpu/environment.yml +11 -0
- voxcity/simulator_gpu/init_taichi.py +154 -0
- voxcity/simulator_gpu/integration.py +15 -0
- voxcity/simulator_gpu/kernels.py +56 -0
- voxcity/simulator_gpu/radiation.py +28 -0
- voxcity/simulator_gpu/raytracing.py +623 -0
- voxcity/simulator_gpu/sky.py +9 -0
- voxcity/simulator_gpu/solar/__init__.py +178 -0
- voxcity/simulator_gpu/solar/core.py +66 -0
- voxcity/simulator_gpu/solar/csf.py +1249 -0
- voxcity/simulator_gpu/solar/domain.py +561 -0
- voxcity/simulator_gpu/solar/epw.py +421 -0
- voxcity/simulator_gpu/solar/integration.py +2953 -0
- voxcity/simulator_gpu/solar/radiation.py +3019 -0
- voxcity/simulator_gpu/solar/raytracing.py +686 -0
- voxcity/simulator_gpu/solar/reflection.py +533 -0
- voxcity/simulator_gpu/solar/sky.py +907 -0
- voxcity/simulator_gpu/solar/solar.py +337 -0
- voxcity/simulator_gpu/solar/svf.py +446 -0
- voxcity/simulator_gpu/solar/volumetric.py +1151 -0
- voxcity/simulator_gpu/solar/voxcity.py +2953 -0
- voxcity/simulator_gpu/temporal.py +13 -0
- voxcity/simulator_gpu/utils.py +25 -0
- voxcity/simulator_gpu/view.py +32 -0
- voxcity/simulator_gpu/visibility/__init__.py +109 -0
- voxcity/simulator_gpu/visibility/geometry.py +278 -0
- voxcity/simulator_gpu/visibility/integration.py +808 -0
- voxcity/simulator_gpu/visibility/landmark.py +753 -0
- voxcity/simulator_gpu/visibility/view.py +944 -0
- voxcity/utils/__init__.py +11 -0
- voxcity/utils/classes.py +194 -0
- voxcity/utils/lc.py +80 -39
- voxcity/utils/shape.py +230 -0
- voxcity/visualizer/__init__.py +24 -24
- voxcity/visualizer/builder.py +43 -43
- voxcity/visualizer/grids.py +141 -141
- voxcity/visualizer/maps.py +187 -187
- voxcity/visualizer/renderer.py +1146 -928
- {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/METADATA +56 -52
- voxcity-1.0.13.dist-info/RECORD +116 -0
- voxcity-0.7.0.dist-info/RECORD +0 -77
- {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/WHEEL +0 -0
- {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/licenses/AUTHORS.rst +0 -0
- {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/licenses/LICENSE +0 -0
voxcity/geoprocessor/draw.py
CHANGED
|
@@ -1,1220 +1,1495 @@
|
|
|
1
|
-
"""
|
|
2
|
-
This module provides functions for drawing and manipulating rectangles and polygons on interactive maps.
|
|
3
|
-
It serves as a core component for defining geographical regions of interest in the VoxCity library.
|
|
4
|
-
|
|
5
|
-
Key Features:
|
|
6
|
-
- Interactive rectangle drawing on maps using ipyleaflet
|
|
7
|
-
- Rectangle rotation with coordinate system transformations
|
|
8
|
-
- City-centered map initialization
|
|
9
|
-
- Fixed-dimension rectangle creation from center points
|
|
10
|
-
- Building footprint visualization and polygon drawing
|
|
11
|
-
- Support for both WGS84 and Web Mercator projections
|
|
12
|
-
- Coordinate format handling between (lon,lat) and (lat,lon)
|
|
13
|
-
|
|
14
|
-
The module maintains consistent coordinate order conventions:
|
|
15
|
-
- Internal storage: (lon,lat) format to match GeoJSON standard
|
|
16
|
-
- ipyleaflet interface: (lat,lon) format as required by the library
|
|
17
|
-
- All return values: (lon,lat) format for consistency
|
|
18
|
-
|
|
19
|
-
Dependencies:
|
|
20
|
-
- ipyleaflet: For interactive map display and drawing controls
|
|
21
|
-
- pyproj: For coordinate system transformations
|
|
22
|
-
- geopy: For distance calculations
|
|
23
|
-
- shapely: For geometric operations
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
import math
|
|
27
|
-
from pyproj import Transformer
|
|
28
|
-
from ipyleaflet import (
|
|
29
|
-
Map,
|
|
30
|
-
DrawControl,
|
|
31
|
-
Rectangle,
|
|
32
|
-
Polygon as LeafletPolygon,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
import
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
for
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
Create
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
draw_control.
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
)
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
for
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
#
|
|
608
|
-
if
|
|
609
|
-
#
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
#
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
""
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
if
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1
|
+
"""
|
|
2
|
+
This module provides functions for drawing and manipulating rectangles and polygons on interactive maps.
|
|
3
|
+
It serves as a core component for defining geographical regions of interest in the VoxCity library.
|
|
4
|
+
|
|
5
|
+
Key Features:
|
|
6
|
+
- Interactive rectangle drawing on maps using ipyleaflet
|
|
7
|
+
- Rectangle rotation with coordinate system transformations
|
|
8
|
+
- City-centered map initialization
|
|
9
|
+
- Fixed-dimension rectangle creation from center points
|
|
10
|
+
- Building footprint visualization and polygon drawing
|
|
11
|
+
- Support for both WGS84 and Web Mercator projections
|
|
12
|
+
- Coordinate format handling between (lon,lat) and (lat,lon)
|
|
13
|
+
|
|
14
|
+
The module maintains consistent coordinate order conventions:
|
|
15
|
+
- Internal storage: (lon,lat) format to match GeoJSON standard
|
|
16
|
+
- ipyleaflet interface: (lat,lon) format as required by the library
|
|
17
|
+
- All return values: (lon,lat) format for consistency
|
|
18
|
+
|
|
19
|
+
Dependencies:
|
|
20
|
+
- ipyleaflet: For interactive map display and drawing controls
|
|
21
|
+
- pyproj: For coordinate system transformations
|
|
22
|
+
- geopy: For distance calculations
|
|
23
|
+
- shapely: For geometric operations
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import math
|
|
27
|
+
from pyproj import Transformer
|
|
28
|
+
from ipyleaflet import (
|
|
29
|
+
Map,
|
|
30
|
+
DrawControl,
|
|
31
|
+
Rectangle,
|
|
32
|
+
Polygon as LeafletPolygon,
|
|
33
|
+
Polyline,
|
|
34
|
+
WidgetControl,
|
|
35
|
+
Circle,
|
|
36
|
+
basemaps,
|
|
37
|
+
basemap_to_tiles,
|
|
38
|
+
TileLayer,
|
|
39
|
+
)
|
|
40
|
+
from geopy import distance
|
|
41
|
+
import shapely.geometry as geom
|
|
42
|
+
import geopandas as gpd
|
|
43
|
+
from ipywidgets import (
|
|
44
|
+
VBox,
|
|
45
|
+
HBox,
|
|
46
|
+
Button,
|
|
47
|
+
FloatText,
|
|
48
|
+
Label,
|
|
49
|
+
Output,
|
|
50
|
+
HTML,
|
|
51
|
+
Checkbox,
|
|
52
|
+
ToggleButton,
|
|
53
|
+
Layout,
|
|
54
|
+
)
|
|
55
|
+
import pandas as pd
|
|
56
|
+
from IPython.display import display, clear_output
|
|
57
|
+
|
|
58
|
+
from .utils import get_coordinates_from_cityname
|
|
59
|
+
|
|
60
|
+
# Import VoxCity for type checking (avoid circular import with TYPE_CHECKING)
|
|
61
|
+
try:
|
|
62
|
+
from typing import TYPE_CHECKING
|
|
63
|
+
if TYPE_CHECKING:
|
|
64
|
+
from ..models import VoxCity
|
|
65
|
+
except ImportError:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
def rotate_rectangle(m, rectangle_vertices, angle):
|
|
69
|
+
"""
|
|
70
|
+
Project rectangle to Mercator, rotate, and re-project to lat-lon coordinates.
|
|
71
|
+
|
|
72
|
+
This function performs a rotation of a rectangle in geographic space by:
|
|
73
|
+
1. Converting coordinates from WGS84 (lat/lon) to Web Mercator projection
|
|
74
|
+
2. Performing the rotation in the projected space for accurate distance preservation
|
|
75
|
+
3. Converting back to WGS84 coordinates
|
|
76
|
+
4. Visualizing the result on the provided map
|
|
77
|
+
|
|
78
|
+
The rotation is performed around the rectangle's centroid using a standard 2D rotation matrix.
|
|
79
|
+
The function handles coordinate system transformations to ensure geometrically accurate rotations
|
|
80
|
+
despite the distortions inherent in geographic projections.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
m (ipyleaflet.Map): Map object to draw the rotated rectangle on.
|
|
84
|
+
The map must be initialized and have a valid center and zoom level.
|
|
85
|
+
rectangle_vertices (list): List of (lon, lat) tuples defining the rectangle vertices.
|
|
86
|
+
The vertices should be ordered in a counter-clockwise direction.
|
|
87
|
+
Example: [(lon1,lat1), (lon2,lat2), (lon3,lat3), (lon4,lat4)]
|
|
88
|
+
angle (float): Rotation angle in degrees.
|
|
89
|
+
Positive angles rotate counter-clockwise.
|
|
90
|
+
Negative angles rotate clockwise.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
list: List of rotated (lon, lat) tuples defining the new rectangle vertices.
|
|
94
|
+
The vertices maintain their original ordering.
|
|
95
|
+
Returns None if no rectangle vertices are provided.
|
|
96
|
+
|
|
97
|
+
Note:
|
|
98
|
+
The function uses EPSG:4326 (WGS84) for geographic coordinates and
|
|
99
|
+
EPSG:3857 (Web Mercator) for the rotation calculations.
|
|
100
|
+
"""
|
|
101
|
+
if not rectangle_vertices:
|
|
102
|
+
print("Draw a rectangle first!")
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
# Define transformers (modern pyproj API)
|
|
106
|
+
to_merc = Transformer.from_crs("EPSG:4326", "EPSG:3857", always_xy=True)
|
|
107
|
+
to_wgs84 = Transformer.from_crs("EPSG:3857", "EPSG:4326", always_xy=True)
|
|
108
|
+
|
|
109
|
+
# Project vertices from WGS84 to Web Mercator for proper distance calculations
|
|
110
|
+
projected_vertices = [to_merc.transform(lon, lat) for lon, lat in rectangle_vertices]
|
|
111
|
+
|
|
112
|
+
# Calculate the centroid to use as rotation center
|
|
113
|
+
centroid_x = sum(x for x, y in projected_vertices) / len(projected_vertices)
|
|
114
|
+
centroid_y = sum(y for x, y in projected_vertices) / len(projected_vertices)
|
|
115
|
+
|
|
116
|
+
# Convert angle to radians (negative for clockwise rotation)
|
|
117
|
+
angle_rad = -math.radians(angle)
|
|
118
|
+
|
|
119
|
+
# Rotate each vertex around the centroid using standard 2D rotation matrix
|
|
120
|
+
rotated_vertices = []
|
|
121
|
+
for x, y in projected_vertices:
|
|
122
|
+
# Translate point to origin for rotation
|
|
123
|
+
temp_x = x - centroid_x
|
|
124
|
+
temp_y = y - centroid_y
|
|
125
|
+
|
|
126
|
+
# Apply rotation matrix
|
|
127
|
+
rotated_x = temp_x * math.cos(angle_rad) - temp_y * math.sin(angle_rad)
|
|
128
|
+
rotated_y = temp_x * math.sin(angle_rad) + temp_y * math.cos(angle_rad)
|
|
129
|
+
|
|
130
|
+
# Translate point back to original position
|
|
131
|
+
new_x = rotated_x + centroid_x
|
|
132
|
+
new_y = rotated_y + centroid_y
|
|
133
|
+
|
|
134
|
+
rotated_vertices.append((new_x, new_y))
|
|
135
|
+
|
|
136
|
+
# Convert coordinates back to WGS84 (lon/lat)
|
|
137
|
+
new_vertices = [to_wgs84.transform(x, y) for x, y in rotated_vertices]
|
|
138
|
+
|
|
139
|
+
# Create and add new polygon layer to map
|
|
140
|
+
polygon = LeafletPolygon(
|
|
141
|
+
locations=[(lat, lon) for lon, lat in new_vertices], # Convert to (lat,lon) for ipyleaflet
|
|
142
|
+
color="red",
|
|
143
|
+
fill_color="red"
|
|
144
|
+
)
|
|
145
|
+
m.add_layer(polygon)
|
|
146
|
+
|
|
147
|
+
return new_vertices
|
|
148
|
+
|
|
149
|
+
def draw_rectangle_map(center=(40, -100), zoom=4):
|
|
150
|
+
"""
|
|
151
|
+
Create an interactive map for drawing rectangles with ipyleaflet.
|
|
152
|
+
|
|
153
|
+
This function initializes an interactive map that allows users to draw rectangles
|
|
154
|
+
by clicking and dragging on the map surface. The drawn rectangles are captured
|
|
155
|
+
and their vertices are stored in geographic coordinates.
|
|
156
|
+
|
|
157
|
+
The map interface provides:
|
|
158
|
+
- A rectangle drawing tool activated by default
|
|
159
|
+
- Real-time coordinate capture of drawn shapes
|
|
160
|
+
- Automatic vertex ordering in counter-clockwise direction
|
|
161
|
+
- Console output of vertex coordinates for verification
|
|
162
|
+
|
|
163
|
+
Drawing Controls:
|
|
164
|
+
- Click and drag to draw a rectangle
|
|
165
|
+
- Release to complete the rectangle
|
|
166
|
+
- Only one rectangle can be active at a time
|
|
167
|
+
- Drawing a new rectangle clears the previous one
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
center (tuple): Center coordinates (lat, lon) for the map view.
|
|
171
|
+
Defaults to (40, -100) which centers on the continental United States.
|
|
172
|
+
Format: (latitude, longitude) in decimal degrees.
|
|
173
|
+
zoom (int): Initial zoom level for the map. Defaults to 4.
|
|
174
|
+
Range: 0 (most zoomed out) to 18 (most zoomed in).
|
|
175
|
+
Recommended: 3-6 for countries, 10-15 for cities.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
tuple: (Map object, list of rectangle vertices)
|
|
179
|
+
- Map object: ipyleaflet.Map instance for displaying and interacting with the map
|
|
180
|
+
- rectangle_vertices: Empty list that will be populated with (lon,lat) tuples
|
|
181
|
+
when a rectangle is drawn. Coordinates are stored in GeoJSON order (lon,lat).
|
|
182
|
+
|
|
183
|
+
Note:
|
|
184
|
+
The function disables all drawing tools except rectangles to ensure
|
|
185
|
+
consistent shape creation. The rectangle vertices are automatically
|
|
186
|
+
converted to (lon,lat) format when stored, regardless of the input
|
|
187
|
+
center coordinate order.
|
|
188
|
+
"""
|
|
189
|
+
# Initialize the map centered at specified coordinates
|
|
190
|
+
m = Map(center=center, zoom=zoom)
|
|
191
|
+
|
|
192
|
+
# List to store the vertices of drawn rectangle
|
|
193
|
+
rectangle_vertices = []
|
|
194
|
+
|
|
195
|
+
def handle_draw(target, action, geo_json):
|
|
196
|
+
"""Handle draw events on the map."""
|
|
197
|
+
# Clear any previously stored vertices
|
|
198
|
+
rectangle_vertices.clear()
|
|
199
|
+
|
|
200
|
+
# Process only if a rectangle polygon was drawn
|
|
201
|
+
if action == 'created' and geo_json['geometry']['type'] == 'Polygon':
|
|
202
|
+
# Extract coordinates from GeoJSON format
|
|
203
|
+
coordinates = geo_json['geometry']['coordinates'][0]
|
|
204
|
+
print("Vertices of the drawn rectangle:")
|
|
205
|
+
# Store all vertices except last (GeoJSON repeats first vertex at end)
|
|
206
|
+
for coord in coordinates[:-1]:
|
|
207
|
+
# Keep GeoJSON (lon,lat) format
|
|
208
|
+
rectangle_vertices.append((coord[0], coord[1]))
|
|
209
|
+
print(f"Longitude: {coord[0]}, Latitude: {coord[1]}")
|
|
210
|
+
|
|
211
|
+
# Configure drawing controls - only enable rectangle drawing
|
|
212
|
+
draw_control = DrawControl()
|
|
213
|
+
draw_control.polyline = {}
|
|
214
|
+
draw_control.polygon = {}
|
|
215
|
+
draw_control.circle = {}
|
|
216
|
+
draw_control.rectangle = {
|
|
217
|
+
"shapeOptions": {
|
|
218
|
+
"color": "#6bc2e5",
|
|
219
|
+
"weight": 4,
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
m.add_control(draw_control)
|
|
223
|
+
|
|
224
|
+
# Register event handler for drawing actions
|
|
225
|
+
draw_control.on_draw(handle_draw)
|
|
226
|
+
|
|
227
|
+
return m, rectangle_vertices
|
|
228
|
+
|
|
229
|
+
def draw_rectangle_map_cityname(cityname, zoom=15):
|
|
230
|
+
"""
|
|
231
|
+
Create an interactive map centered on a specified city for drawing rectangles.
|
|
232
|
+
|
|
233
|
+
This function extends draw_rectangle_map() by automatically centering the map
|
|
234
|
+
on a specified city using geocoding. It provides a convenient way to focus
|
|
235
|
+
the drawing interface on a particular urban area without needing to know
|
|
236
|
+
its exact coordinates.
|
|
237
|
+
|
|
238
|
+
The function uses the utils.get_coordinates_from_cityname() function to
|
|
239
|
+
geocode the city name and obtain its coordinates. The resulting map is
|
|
240
|
+
zoomed to an appropriate level for urban-scale analysis.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
cityname (str): Name of the city to center the map on.
|
|
244
|
+
Can include country or state for better accuracy.
|
|
245
|
+
Examples: "Tokyo, Japan", "New York, NY", "Paris, France"
|
|
246
|
+
zoom (int): Initial zoom level for the map. Defaults to 15.
|
|
247
|
+
Range: 0 (most zoomed out) to 18 (most zoomed in).
|
|
248
|
+
Default of 15 is optimized for city-level visualization.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
tuple: (Map object, list of rectangle vertices)
|
|
252
|
+
- Map object: ipyleaflet.Map instance centered on the specified city
|
|
253
|
+
- rectangle_vertices: Empty list that will be populated with (lon,lat)
|
|
254
|
+
tuples when a rectangle is drawn
|
|
255
|
+
|
|
256
|
+
Note:
|
|
257
|
+
If the city name cannot be geocoded, the function will raise an error.
|
|
258
|
+
For better results, provide specific city names with country/state context.
|
|
259
|
+
The function inherits all drawing controls and behavior from draw_rectangle_map().
|
|
260
|
+
"""
|
|
261
|
+
# Get coordinates for the specified city
|
|
262
|
+
center = get_coordinates_from_cityname(cityname)
|
|
263
|
+
m, rectangle_vertices = draw_rectangle_map(center=center, zoom=zoom)
|
|
264
|
+
return m, rectangle_vertices
|
|
265
|
+
|
|
266
|
+
def center_location_map_cityname(cityname, east_west_length, north_south_length, zoom=15):
|
|
267
|
+
"""
|
|
268
|
+
Create an interactive map centered on a city where clicking creates a rectangle of specified dimensions.
|
|
269
|
+
|
|
270
|
+
This function provides a specialized interface for creating fixed-size rectangles
|
|
271
|
+
centered on user-selected points. Instead of drawing rectangles by dragging,
|
|
272
|
+
users click a point on the map and a rectangle of the specified dimensions
|
|
273
|
+
is automatically created centered on that point.
|
|
274
|
+
|
|
275
|
+
The function handles:
|
|
276
|
+
- Automatic city geocoding and map centering
|
|
277
|
+
- Distance calculations in meters using geopy
|
|
278
|
+
- Conversion between geographic and metric distances
|
|
279
|
+
- Rectangle creation with specified dimensions
|
|
280
|
+
- Visualization of created rectangles
|
|
281
|
+
|
|
282
|
+
Workflow:
|
|
283
|
+
1. Map is centered on the specified city
|
|
284
|
+
2. User clicks a point on the map
|
|
285
|
+
3. A rectangle is created centered on that point
|
|
286
|
+
4. Rectangle dimensions are maintained in meters regardless of latitude
|
|
287
|
+
5. Previous rectangles are automatically cleared
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
cityname (str): Name of the city to center the map on.
|
|
291
|
+
Can include country or state for better accuracy.
|
|
292
|
+
Examples: "Tokyo, Japan", "New York, NY"
|
|
293
|
+
east_west_length (float): Width of the rectangle in meters.
|
|
294
|
+
This is the dimension along the east-west direction.
|
|
295
|
+
The actual ground distance is maintained regardless of projection distortion.
|
|
296
|
+
north_south_length (float): Height of the rectangle in meters.
|
|
297
|
+
This is the dimension along the north-south direction.
|
|
298
|
+
The actual ground distance is maintained regardless of projection distortion.
|
|
299
|
+
zoom (int): Initial zoom level for the map. Defaults to 15.
|
|
300
|
+
Range: 0 (most zoomed out) to 18 (most zoomed in).
|
|
301
|
+
Default of 15 is optimized for city-level visualization.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
tuple: (Map object, list of rectangle vertices)
|
|
305
|
+
- Map object: ipyleaflet.Map instance centered on the specified city
|
|
306
|
+
- rectangle_vertices: Empty list that will be populated with (lon,lat)
|
|
307
|
+
tuples when a point is clicked and the rectangle is created
|
|
308
|
+
|
|
309
|
+
Note:
|
|
310
|
+
- Rectangle dimensions are specified in meters but stored as geographic coordinates
|
|
311
|
+
- The function uses geopy's distance calculations for accurate metric distances
|
|
312
|
+
- Only one rectangle can exist at a time; clicking a new point removes the previous rectangle
|
|
313
|
+
- Rectangle vertices are returned in GeoJSON (lon,lat) order
|
|
314
|
+
"""
|
|
315
|
+
|
|
316
|
+
# Get coordinates for the specified city
|
|
317
|
+
center = get_coordinates_from_cityname(cityname)
|
|
318
|
+
|
|
319
|
+
# Initialize map centered on the city
|
|
320
|
+
m = Map(center=center, zoom=zoom)
|
|
321
|
+
|
|
322
|
+
# List to store rectangle vertices
|
|
323
|
+
rectangle_vertices = []
|
|
324
|
+
|
|
325
|
+
def handle_draw(target, action, geo_json):
|
|
326
|
+
"""Handle draw events on the map."""
|
|
327
|
+
# Clear previous vertices and remove any existing rectangles
|
|
328
|
+
rectangle_vertices.clear()
|
|
329
|
+
for layer in m.layers:
|
|
330
|
+
if isinstance(layer, Rectangle):
|
|
331
|
+
m.remove_layer(layer)
|
|
332
|
+
|
|
333
|
+
# Process only if a point was drawn on the map
|
|
334
|
+
if action == 'created' and geo_json['geometry']['type'] == 'Point':
|
|
335
|
+
# Extract point coordinates from GeoJSON (lon,lat)
|
|
336
|
+
lon, lat = geo_json['geometry']['coordinates'][0], geo_json['geometry']['coordinates'][1]
|
|
337
|
+
print(f"Point drawn at Longitude: {lon}, Latitude: {lat}")
|
|
338
|
+
|
|
339
|
+
# Calculate corner points using geopy's distance calculator
|
|
340
|
+
# First calculate north/south latitudes from center
|
|
341
|
+
north = distance.distance(meters=north_south_length/2).destination((lat, lon), bearing=0)
|
|
342
|
+
south = distance.distance(meters=north_south_length/2).destination((lat, lon), bearing=180)
|
|
343
|
+
|
|
344
|
+
# Calculate east/west at the SOUTH latitude to ensure correct grid dimensions
|
|
345
|
+
# The grid size calculation uses the SW corner (vertex_0) as reference,
|
|
346
|
+
# measuring E-W distance along the south edge. By calculating east/west
|
|
347
|
+
# at the south latitude, we ensure the E-W distance matches the requested length.
|
|
348
|
+
east_at_south = distance.distance(meters=east_west_length/2).destination((south.latitude, lon), bearing=90)
|
|
349
|
+
west_at_south = distance.distance(meters=east_west_length/2).destination((south.latitude, lon), bearing=270)
|
|
350
|
+
|
|
351
|
+
# Create rectangle vertices in counter-clockwise order (lon,lat)
|
|
352
|
+
# Using the east/west longitudes calculated at south latitude for all corners
|
|
353
|
+
rectangle_vertices.extend([
|
|
354
|
+
(west_at_south.longitude, south.latitude),
|
|
355
|
+
(west_at_south.longitude, north.latitude),
|
|
356
|
+
(east_at_south.longitude, north.latitude),
|
|
357
|
+
(east_at_south.longitude, south.latitude)
|
|
358
|
+
])
|
|
359
|
+
|
|
360
|
+
# Create and add new rectangle to map (ipyleaflet expects lat,lon)
|
|
361
|
+
rectangle = Rectangle(
|
|
362
|
+
bounds=[(north.latitude, west_at_south.longitude), (south.latitude, east_at_south.longitude)],
|
|
363
|
+
color="red",
|
|
364
|
+
fill_color="red",
|
|
365
|
+
fill_opacity=0.2
|
|
366
|
+
)
|
|
367
|
+
m.add_layer(rectangle)
|
|
368
|
+
|
|
369
|
+
print("Rectangle vertices:")
|
|
370
|
+
for vertex in rectangle_vertices:
|
|
371
|
+
print(f"Longitude: {vertex[0]}, Latitude: {vertex[1]}")
|
|
372
|
+
|
|
373
|
+
# Configure drawing controls - only enable point drawing
|
|
374
|
+
draw_control = DrawControl()
|
|
375
|
+
draw_control.polyline = {}
|
|
376
|
+
draw_control.polygon = {}
|
|
377
|
+
draw_control.circle = {}
|
|
378
|
+
draw_control.rectangle = {}
|
|
379
|
+
draw_control.marker = {}
|
|
380
|
+
m.add_control(draw_control)
|
|
381
|
+
|
|
382
|
+
# Register event handler for drawing actions
|
|
383
|
+
draw_control.on_draw(handle_draw)
|
|
384
|
+
|
|
385
|
+
return m, rectangle_vertices
|
|
386
|
+
|
|
387
|
+
def display_buildings_and_draw_polygon(voxcity=None, building_gdf=None, rectangle_vertices=None, zoom=17):
|
|
388
|
+
"""
|
|
389
|
+
Displays building footprints and enables polygon drawing on an interactive map.
|
|
390
|
+
|
|
391
|
+
This function creates an interactive map that visualizes building footprints and
|
|
392
|
+
allows users to draw arbitrary polygons. It's particularly useful for selecting
|
|
393
|
+
specific buildings or areas within an urban context.
|
|
394
|
+
|
|
395
|
+
The function provides three key features:
|
|
396
|
+
1. Building Footprint Visualization:
|
|
397
|
+
- Displays building polygons from a GeoDataFrame
|
|
398
|
+
- Uses consistent styling for all buildings
|
|
399
|
+
- Handles simple polygon geometries only
|
|
400
|
+
|
|
401
|
+
2. Interactive Polygon Drawing:
|
|
402
|
+
- Enables free-form polygon drawing
|
|
403
|
+
- Captures vertices in consistent (lon,lat) format
|
|
404
|
+
- Maintains GeoJSON compatibility
|
|
405
|
+
- Supports multiple polygons with unique IDs and colors
|
|
406
|
+
|
|
407
|
+
3. Map Initialization:
|
|
408
|
+
- Automatic centering based on input data
|
|
409
|
+
- Fallback to default location if no data provided
|
|
410
|
+
- Support for both building data and rectangle bounds
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
voxcity (VoxCity, optional): A VoxCity object from which to extract building_gdf
|
|
414
|
+
and rectangle_vertices. If provided, these values will be used unless
|
|
415
|
+
explicitly overridden by the building_gdf or rectangle_vertices parameters.
|
|
416
|
+
building_gdf (GeoDataFrame, optional): A GeoDataFrame containing building footprints.
|
|
417
|
+
Must have geometry column with Polygon type features.
|
|
418
|
+
Geometries should be in [lon, lat] coordinate order.
|
|
419
|
+
If None and voxcity is provided, uses voxcity.extras['building_gdf'].
|
|
420
|
+
If None and no voxcity provided, only the base map is displayed.
|
|
421
|
+
rectangle_vertices (list, optional): List of [lon, lat] coordinates defining rectangle corners.
|
|
422
|
+
Used to set the initial map view extent.
|
|
423
|
+
Takes precedence over building_gdf for determining map center.
|
|
424
|
+
If None and voxcity is provided, uses voxcity.extras['rectangle_vertices'].
|
|
425
|
+
zoom (int): Initial zoom level for the map. Default=17.
|
|
426
|
+
Range: 0 (most zoomed out) to 18 (most zoomed in).
|
|
427
|
+
Default of 17 is optimized for building-level detail.
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
tuple: (map_object, drawn_polygons)
|
|
431
|
+
- map_object: ipyleaflet Map instance with building footprints and drawing controls
|
|
432
|
+
- drawn_polygons: List of dictionaries with 'id', 'vertices', and 'color' keys for all drawn polygons.
|
|
433
|
+
Each polygon has a unique ID and color for easy identification.
|
|
434
|
+
|
|
435
|
+
Examples:
|
|
436
|
+
Using a VoxCity object:
|
|
437
|
+
>>> m, polygons = display_buildings_and_draw_polygon(voxcity=my_voxcity)
|
|
438
|
+
|
|
439
|
+
Using explicit parameters:
|
|
440
|
+
>>> m, polygons = display_buildings_and_draw_polygon(building_gdf=buildings, rectangle_vertices=rect)
|
|
441
|
+
|
|
442
|
+
Override specific parameters from VoxCity:
|
|
443
|
+
>>> m, polygons = display_buildings_and_draw_polygon(voxcity=my_voxcity, zoom=15)
|
|
444
|
+
|
|
445
|
+
Note:
|
|
446
|
+
- Building footprints are displayed in blue with 20% opacity
|
|
447
|
+
- Only simple Polygon geometries are supported (no MultiPolygons)
|
|
448
|
+
- Drawing tools are restricted to polygon creation only
|
|
449
|
+
- All coordinates are handled in (lon,lat) order internally
|
|
450
|
+
- The function automatically determines appropriate map bounds
|
|
451
|
+
- Each polygon gets a unique ID and different colors for easy identification
|
|
452
|
+
- Use get_polygon_vertices() helper function to extract specific polygon data
|
|
453
|
+
"""
|
|
454
|
+
# ---------------------------------------------------------
|
|
455
|
+
# 0. Extract data from VoxCity object if provided
|
|
456
|
+
# ---------------------------------------------------------
|
|
457
|
+
if voxcity is not None:
|
|
458
|
+
# Extract building_gdf if not explicitly provided
|
|
459
|
+
if building_gdf is None:
|
|
460
|
+
building_gdf = voxcity.extras.get('building_gdf', None)
|
|
461
|
+
|
|
462
|
+
# Extract rectangle_vertices if not explicitly provided
|
|
463
|
+
if rectangle_vertices is None:
|
|
464
|
+
rectangle_vertices = voxcity.extras.get('rectangle_vertices', None)
|
|
465
|
+
|
|
466
|
+
# ---------------------------------------------------------
|
|
467
|
+
# 1. Determine a suitable map center via bounding box logic
|
|
468
|
+
# ---------------------------------------------------------
|
|
469
|
+
if rectangle_vertices is not None:
|
|
470
|
+
# Get bounds from rectangle vertices
|
|
471
|
+
lons = [v[0] for v in rectangle_vertices]
|
|
472
|
+
lats = [v[1] for v in rectangle_vertices]
|
|
473
|
+
min_lon, max_lon = min(lons), max(lons)
|
|
474
|
+
min_lat, max_lat = min(lats), max(lats)
|
|
475
|
+
center_lon = (min_lon + max_lon) / 2
|
|
476
|
+
center_lat = (min_lat + max_lat) / 2
|
|
477
|
+
elif building_gdf is not None and len(building_gdf) > 0:
|
|
478
|
+
# Get bounds from GeoDataFrame
|
|
479
|
+
bounds = building_gdf.total_bounds # Returns [minx, miny, maxx, maxy]
|
|
480
|
+
min_lon, min_lat, max_lon, max_lat = bounds
|
|
481
|
+
center_lon = (min_lon + max_lon) / 2
|
|
482
|
+
center_lat = (min_lat + max_lat) / 2
|
|
483
|
+
else:
|
|
484
|
+
# Fallback: If no inputs or invalid data, pick a default
|
|
485
|
+
center_lon, center_lat = -100.0, 40.0
|
|
486
|
+
|
|
487
|
+
# Create the ipyleaflet map (needs lat,lon)
|
|
488
|
+
m = Map(center=(center_lat, center_lon), zoom=zoom, scroll_wheel_zoom=True)
|
|
489
|
+
|
|
490
|
+
# -----------------------------------------
|
|
491
|
+
# 2. Add building footprints to the map if provided
|
|
492
|
+
# -----------------------------------------
|
|
493
|
+
if building_gdf is not None:
|
|
494
|
+
for idx, row in building_gdf.iterrows():
|
|
495
|
+
# Only handle simple Polygons
|
|
496
|
+
if isinstance(row.geometry, geom.Polygon):
|
|
497
|
+
# Get coordinates from geometry
|
|
498
|
+
coords = list(row.geometry.exterior.coords)
|
|
499
|
+
# Convert to (lat,lon) for ipyleaflet, skip last repeated coordinate
|
|
500
|
+
lat_lon_coords = [(c[1], c[0]) for c in coords[:-1]]
|
|
501
|
+
|
|
502
|
+
# Create the polygon layer
|
|
503
|
+
bldg_layer = LeafletPolygon(
|
|
504
|
+
locations=lat_lon_coords,
|
|
505
|
+
color="blue",
|
|
506
|
+
fill_color="blue",
|
|
507
|
+
fill_opacity=0.2,
|
|
508
|
+
weight=2
|
|
509
|
+
)
|
|
510
|
+
m.add_layer(bldg_layer)
|
|
511
|
+
|
|
512
|
+
# -----------------------------------------------------------------
|
|
513
|
+
# 3. Enable drawing of polygons, capturing the vertices in Lon-Lat
|
|
514
|
+
# -----------------------------------------------------------------
|
|
515
|
+
# Store multiple polygons with IDs and colors
|
|
516
|
+
drawn_polygons = [] # List of dicts with 'id', 'vertices', 'color' keys
|
|
517
|
+
polygon_counter = 0
|
|
518
|
+
polygon_colors = ['red', 'blue', 'green', 'orange', 'purple', 'brown', 'pink', 'gray', 'olive', 'cyan']
|
|
519
|
+
|
|
520
|
+
draw_control = DrawControl(
|
|
521
|
+
polygon={
|
|
522
|
+
"shapeOptions": {
|
|
523
|
+
"color": "red",
|
|
524
|
+
"fillColor": "red",
|
|
525
|
+
"fillOpacity": 0.2
|
|
526
|
+
}
|
|
527
|
+
},
|
|
528
|
+
rectangle={}, # Disable rectangles (or enable if needed)
|
|
529
|
+
circle={}, # Disable circles
|
|
530
|
+
circlemarker={}, # Disable circlemarkers
|
|
531
|
+
polyline={}, # Disable polylines
|
|
532
|
+
marker={} # Disable markers
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
def handle_draw(self, action, geo_json):
|
|
536
|
+
"""
|
|
537
|
+
Callback for whenever a shape is created or edited.
|
|
538
|
+
ipyleaflet's DrawControl returns standard GeoJSON (lon, lat).
|
|
539
|
+
We'll keep them as (lon, lat).
|
|
540
|
+
"""
|
|
541
|
+
if action == 'created' and geo_json['geometry']['type'] == 'Polygon':
|
|
542
|
+
nonlocal polygon_counter
|
|
543
|
+
polygon_counter += 1
|
|
544
|
+
|
|
545
|
+
# The polygon's first ring
|
|
546
|
+
coordinates = geo_json['geometry']['coordinates'][0]
|
|
547
|
+
vertices = [(coord[0], coord[1]) for coord in coordinates[:-1]]
|
|
548
|
+
|
|
549
|
+
# Assign color (cycle through colors)
|
|
550
|
+
color = polygon_colors[polygon_counter % len(polygon_colors)]
|
|
551
|
+
|
|
552
|
+
# Store polygon data
|
|
553
|
+
polygon_data = {
|
|
554
|
+
'id': polygon_counter,
|
|
555
|
+
'vertices': vertices,
|
|
556
|
+
'color': color
|
|
557
|
+
}
|
|
558
|
+
drawn_polygons.append(polygon_data)
|
|
559
|
+
|
|
560
|
+
print(f"Polygon {polygon_counter} drawn with {len(vertices)} vertices (color: {color}):")
|
|
561
|
+
for i, (lon, lat) in enumerate(vertices):
|
|
562
|
+
print(f" Vertex {i+1}: (lon, lat) = ({lon}, {lat})")
|
|
563
|
+
print(f"Total polygons: {len(drawn_polygons)}")
|
|
564
|
+
|
|
565
|
+
draw_control.on_draw(handle_draw)
|
|
566
|
+
m.add_control(draw_control)
|
|
567
|
+
|
|
568
|
+
return m, drawn_polygons
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def draw_additional_buildings(
|
|
573
|
+
voxcity=None,
|
|
574
|
+
building_gdf=None,
|
|
575
|
+
initial_center=None,
|
|
576
|
+
zoom=17,
|
|
577
|
+
rectangle_vertices=None,
|
|
578
|
+
):
|
|
579
|
+
"""
|
|
580
|
+
Interactive map editor: Draw rectangles, freehand polygons, and DELETE existing buildings.
|
|
581
|
+
|
|
582
|
+
Args:
|
|
583
|
+
initial_center (tuple): (Longitude, Latitude) - Standard GeoJSON order.
|
|
584
|
+
"""
|
|
585
|
+
# --- Data Initialization ---
|
|
586
|
+
if voxcity is not None:
|
|
587
|
+
if building_gdf is None:
|
|
588
|
+
building_gdf = voxcity.extras.get("building_gdf", None)
|
|
589
|
+
if rectangle_vertices is None:
|
|
590
|
+
rectangle_vertices = voxcity.extras.get("rectangle_vertices", None)
|
|
591
|
+
|
|
592
|
+
if building_gdf is None:
|
|
593
|
+
updated_gdf = gpd.GeoDataFrame(
|
|
594
|
+
columns=["id", "height", "min_height", "geometry", "building_id"],
|
|
595
|
+
crs="EPSG:4326",
|
|
596
|
+
)
|
|
597
|
+
else:
|
|
598
|
+
updated_gdf = building_gdf.copy()
|
|
599
|
+
updated_gdf = updated_gdf.reset_index(drop=True)
|
|
600
|
+
defaults = {"height": 10.0, "min_height": 0.0, "building_id": 0, "id": 0}
|
|
601
|
+
for col, val in defaults.items():
|
|
602
|
+
if col not in updated_gdf.columns:
|
|
603
|
+
updated_gdf[col] = (
|
|
604
|
+
val if col not in ["building_id", "id"] else range(len(updated_gdf))
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
# --- Map Setup (Corrected for Lon, Lat input) ---
|
|
608
|
+
if initial_center is not None:
|
|
609
|
+
# User provides (Lon, Lat) per GeoJSON standard
|
|
610
|
+
center_lon, center_lat = initial_center
|
|
611
|
+
elif not updated_gdf.empty:
|
|
612
|
+
# GDF bounds are (minx, miny, maxx, maxy) -> (Lon, Lat, Lon, Lat)
|
|
613
|
+
b = updated_gdf.total_bounds
|
|
614
|
+
center_lon, center_lat = (b[0] + b[2]) / 2, (b[1] + b[3]) / 2
|
|
615
|
+
else:
|
|
616
|
+
center_lon, center_lat = -100.0, 40.0
|
|
617
|
+
|
|
618
|
+
# ipyleaflet expects (Lat, Lon), so we flip it here
|
|
619
|
+
m = Map(center=(center_lat, center_lon), zoom=zoom, scroll_wheel_zoom=True)
|
|
620
|
+
|
|
621
|
+
# --- UI Setup ---
|
|
622
|
+
style_html = HTML(
|
|
623
|
+
"""
|
|
624
|
+
<style>
|
|
625
|
+
.vox-panel { font-family: 'Segoe UI', sans-serif; }
|
|
626
|
+
.vox-header { font-size: 14px; font-weight: 600; color: #333; border-bottom: 2px solid #2196F3; padding-bottom: 4px; margin-bottom: 2px; }
|
|
627
|
+
.vox-section { font-size: 10px; font-weight: 600; color: #666; text-transform: uppercase; letter-spacing: 0.5px; margin: 4px 0 2px 0; }
|
|
628
|
+
.vox-section-add { color: #2e7d32; }
|
|
629
|
+
.vox-section-remove { color: #c62828; }
|
|
630
|
+
.vox-status { padding: 6px 8px; border-radius: 3px; font-size: 11px; margin-top: 6px; font-weight: 500; line-height: 1.3; }
|
|
631
|
+
.vox-status-info { background-color: #e3f2fd; color: #0d47a1; border-left: 3px solid #0d47a1; }
|
|
632
|
+
.vox-status-success { background-color: #e8f5e9; color: #1b5e20; border-left: 3px solid #1b5e20; }
|
|
633
|
+
.vox-status-warn { background-color: #fff3e0; color: #e65100; border-left: 3px solid #e65100; }
|
|
634
|
+
.vox-status-danger { background-color: #ffebee; color: #c62828; border-left: 3px solid #c62828; }
|
|
635
|
+
.vox-divider { height: 1px; background: #e0e0e0; margin: 6px 0; }
|
|
636
|
+
</style>
|
|
637
|
+
"""
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
# --- ADD BUILDINGS Section ---
|
|
641
|
+
add_section_label = HTML("<div class='vox-panel vox-section vox-section-add'>➕ ADD</div>")
|
|
642
|
+
|
|
643
|
+
rect_btn = ToggleButton(
|
|
644
|
+
value=False,
|
|
645
|
+
description="📐 Rectangle",
|
|
646
|
+
icon="",
|
|
647
|
+
layout=Layout(width="100px"),
|
|
648
|
+
tooltip="Click 3 corners on map to draw rectangle",
|
|
649
|
+
)
|
|
650
|
+
freehand_btn = HTML("<span style='font-size:10px; color:#666; margin-left:5px;'>or 🖊️ left toolbar</span>")
|
|
651
|
+
|
|
652
|
+
h_in = FloatText(
|
|
653
|
+
value=10.0,
|
|
654
|
+
description="Height:",
|
|
655
|
+
layout=Layout(width="120px"),
|
|
656
|
+
style={"description_width": "45px"},
|
|
657
|
+
)
|
|
658
|
+
mh_in = FloatText(
|
|
659
|
+
value=0.0,
|
|
660
|
+
description="Base:",
|
|
661
|
+
layout=Layout(width="100px"),
|
|
662
|
+
style={"description_width": "35px"},
|
|
663
|
+
)
|
|
664
|
+
add_btn = Button(
|
|
665
|
+
description="Add Building",
|
|
666
|
+
button_style="success",
|
|
667
|
+
icon="plus",
|
|
668
|
+
disabled=True,
|
|
669
|
+
layout=Layout(flex="1"),
|
|
670
|
+
)
|
|
671
|
+
clr_btn = Button(
|
|
672
|
+
description="Clear",
|
|
673
|
+
button_style="warning",
|
|
674
|
+
icon="eraser",
|
|
675
|
+
disabled=True,
|
|
676
|
+
layout=Layout(width="70px"),
|
|
677
|
+
tooltip="Clear drawing",
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
# --- REMOVE BUILDINGS Section ---
|
|
681
|
+
divider = HTML("<div class='vox-divider'></div>")
|
|
682
|
+
remove_section_label = HTML("<div class='vox-panel vox-section vox-section-remove'>🗑️ REMOVE</div>")
|
|
683
|
+
|
|
684
|
+
del_btn = ToggleButton(
|
|
685
|
+
value=False,
|
|
686
|
+
description="👆 Click",
|
|
687
|
+
icon="",
|
|
688
|
+
button_style="danger",
|
|
689
|
+
layout=Layout(width="80px"),
|
|
690
|
+
tooltip="Click on buildings to remove",
|
|
691
|
+
)
|
|
692
|
+
poly_del_btn = ToggleButton(
|
|
693
|
+
value=False,
|
|
694
|
+
description="⬡ Area",
|
|
695
|
+
icon="",
|
|
696
|
+
button_style="danger",
|
|
697
|
+
layout=Layout(width="75px"),
|
|
698
|
+
tooltip="Draw polygon to remove buildings inside",
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
# --- Status Bar ---
|
|
702
|
+
status_bar = HTML(
|
|
703
|
+
value="<div class='vox-panel vox-status vox-status-info'>Ready. Select a tool above.</div>"
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
# Layout rows - compact
|
|
707
|
+
add_tools_row = HBox([rect_btn, freehand_btn], layout=Layout(margin="1px 0"))
|
|
708
|
+
input_row = HBox([h_in, mh_in], layout=Layout(margin="2px 0"))
|
|
709
|
+
action_row = HBox([add_btn, clr_btn], layout=Layout(margin="2px 0"))
|
|
710
|
+
remove_tools_row = HBox([del_btn, poly_del_btn], layout=Layout(margin="1px 0"))
|
|
711
|
+
|
|
712
|
+
panel = VBox(
|
|
713
|
+
[
|
|
714
|
+
style_html,
|
|
715
|
+
HTML("<div class='vox-panel'><div class='vox-header'>🏙️ Building Editor</div></div>"),
|
|
716
|
+
add_section_label,
|
|
717
|
+
add_tools_row,
|
|
718
|
+
input_row,
|
|
719
|
+
action_row,
|
|
720
|
+
divider,
|
|
721
|
+
remove_section_label,
|
|
722
|
+
remove_tools_row,
|
|
723
|
+
status_bar,
|
|
724
|
+
],
|
|
725
|
+
layout=Layout(
|
|
726
|
+
width="280px",
|
|
727
|
+
padding="8px",
|
|
728
|
+
background_color="white",
|
|
729
|
+
border_radius="6px",
|
|
730
|
+
box_shadow="0px 2px 8px rgba(0,0,0,0.12)",
|
|
731
|
+
),
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
m.add_control(WidgetControl(widget=panel, position="topright"))
|
|
735
|
+
|
|
736
|
+
# --- Global State & Transformers ---
|
|
737
|
+
state = {"poly": [], "clicks": [], "temp_layers": [], "preview": None, "removal_poly": None, "removal_preview": None}
|
|
738
|
+
to_merc = Transformer.from_crs("EPSG:4326", "EPSG:3857", always_xy=True)
|
|
739
|
+
to_geo = Transformer.from_crs("EPSG:3857", "EPSG:4326", always_xy=True)
|
|
740
|
+
_style_attr = getattr(m, "default_style", {})
|
|
741
|
+
original_style = _style_attr.copy() if isinstance(_style_attr, dict) else {"cursor": "grab"}
|
|
742
|
+
|
|
743
|
+
# Track building layers for polygon removal mode
|
|
744
|
+
building_layers = {}
|
|
745
|
+
|
|
746
|
+
# --- Helper Functions ---
|
|
747
|
+
def set_status(msg, type="info"):
|
|
748
|
+
status_bar.value = (
|
|
749
|
+
f"<div class='vox-panel vox-status vox-status-{type}'>{msg}</div>"
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
def add_polygon_to_map(poly_geom, gdf_index, height):
|
|
753
|
+
coords = list(poly_geom.exterior.coords)
|
|
754
|
+
leaflet_poly = LeafletPolygon(
|
|
755
|
+
locations=[(c[1], c[0]) for c in coords[:-1]], # Flip (Lon, Lat) -> (Lat, Lon) for Leaflet
|
|
756
|
+
color="#2196F3",
|
|
757
|
+
fill_color="#2196F3",
|
|
758
|
+
fill_opacity=0.4,
|
|
759
|
+
weight=1,
|
|
760
|
+
popup=HTML(f"<b>ID:</b> {gdf_index}<br><b>H:</b> {height}m"),
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
def on_poly_click(**kwargs):
|
|
764
|
+
if del_btn.value:
|
|
765
|
+
m.remove_layer(leaflet_poly)
|
|
766
|
+
if gdf_index in building_layers:
|
|
767
|
+
del building_layers[gdf_index]
|
|
768
|
+
try:
|
|
769
|
+
updated_gdf.drop(index=gdf_index, inplace=True)
|
|
770
|
+
set_status(f"🗑️ Removed #{gdf_index}. Click more or deselect.", "danger")
|
|
771
|
+
except KeyError:
|
|
772
|
+
pass
|
|
773
|
+
|
|
774
|
+
leaflet_poly.on_click(on_poly_click)
|
|
775
|
+
m.add_layer(leaflet_poly)
|
|
776
|
+
building_layers[gdf_index] = leaflet_poly
|
|
777
|
+
|
|
778
|
+
# --- Render Existing ---
|
|
779
|
+
for idx, row in updated_gdf.iterrows():
|
|
780
|
+
if isinstance(row.geometry, geom.Polygon):
|
|
781
|
+
add_polygon_to_map(row.geometry, idx, row.get("height", 0))
|
|
782
|
+
|
|
783
|
+
def clear_removal_preview():
|
|
784
|
+
if state["removal_preview"]:
|
|
785
|
+
try:
|
|
786
|
+
m.remove_layer(state["removal_preview"])
|
|
787
|
+
except Exception:
|
|
788
|
+
pass
|
|
789
|
+
state["removal_preview"] = None
|
|
790
|
+
state["removal_poly"] = None
|
|
791
|
+
|
|
792
|
+
# --- Logic ---
|
|
793
|
+
def on_mode_change(change):
|
|
794
|
+
if change["owner"] is rect_btn and change["new"]:
|
|
795
|
+
del_btn.value = False
|
|
796
|
+
poly_del_btn.value = False
|
|
797
|
+
draw_control.clear()
|
|
798
|
+
clear_removal_preview()
|
|
799
|
+
m.default_style = {"cursor": "crosshair"}
|
|
800
|
+
set_status("📐 <b>Step 1/3:</b> Click first corner", "info")
|
|
801
|
+
elif change["owner"] is del_btn and change["new"]:
|
|
802
|
+
rect_btn.value = False
|
|
803
|
+
poly_del_btn.value = False
|
|
804
|
+
clear_all(None)
|
|
805
|
+
clear_removal_preview()
|
|
806
|
+
m.default_style = {"cursor": "no-drop"}
|
|
807
|
+
set_status("👆 Click on buildings to delete", "danger")
|
|
808
|
+
elif change["owner"] is poly_del_btn and change["new"]:
|
|
809
|
+
rect_btn.value = False
|
|
810
|
+
del_btn.value = False
|
|
811
|
+
clear_all(None)
|
|
812
|
+
clear_removal_preview()
|
|
813
|
+
m.default_style = {"cursor": "crosshair"}
|
|
814
|
+
set_status("⬡ Use polygon tool (◇) on left", "danger")
|
|
815
|
+
elif not rect_btn.value and not del_btn.value and not poly_del_btn.value:
|
|
816
|
+
m.default_style = original_style
|
|
817
|
+
clear_removal_preview()
|
|
818
|
+
set_status("Ready. Select a tool above.", "info")
|
|
819
|
+
|
|
820
|
+
rect_btn.observe(on_mode_change, names="value")
|
|
821
|
+
del_btn.observe(on_mode_change, names="value")
|
|
822
|
+
poly_del_btn.observe(on_mode_change, names="value")
|
|
823
|
+
|
|
824
|
+
def clear_preview():
|
|
825
|
+
if state["preview"]:
|
|
826
|
+
try:
|
|
827
|
+
m.remove_layer(state["preview"])
|
|
828
|
+
except Exception:
|
|
829
|
+
pass
|
|
830
|
+
state["preview"] = None
|
|
831
|
+
|
|
832
|
+
def clear_temps():
|
|
833
|
+
while state["temp_layers"]:
|
|
834
|
+
try:
|
|
835
|
+
m.remove_layer(state["temp_layers"].pop())
|
|
836
|
+
except Exception:
|
|
837
|
+
pass
|
|
838
|
+
|
|
839
|
+
def refresh_markers():
|
|
840
|
+
clear_temps()
|
|
841
|
+
for lon, lat in state["clicks"]:
|
|
842
|
+
pt = Circle(
|
|
843
|
+
location=(lat, lon),
|
|
844
|
+
radius=2,
|
|
845
|
+
color="red",
|
|
846
|
+
fill_color="red",
|
|
847
|
+
fill_opacity=1.0,
|
|
848
|
+
)
|
|
849
|
+
m.add_layer(pt)
|
|
850
|
+
state["temp_layers"].append(pt)
|
|
851
|
+
if len(state["clicks"]) >= 2:
|
|
852
|
+
(l1, la1), (l2, la2) = state["clicks"][0], state["clicks"][1]
|
|
853
|
+
line = Polyline(
|
|
854
|
+
locations=[(la1, l1), (la2, l2)],
|
|
855
|
+
color="red",
|
|
856
|
+
weight=3,
|
|
857
|
+
)
|
|
858
|
+
m.add_layer(line)
|
|
859
|
+
state["temp_layers"].append(line)
|
|
860
|
+
|
|
861
|
+
def build_rect(points):
|
|
862
|
+
(lon1, lat1), (lon2, lat2), (lon3, lat3) = points[:3]
|
|
863
|
+
x1, y1 = to_merc.transform(lon1, lat1)
|
|
864
|
+
x2, y2 = to_merc.transform(lon2, lat2)
|
|
865
|
+
x3, y3 = to_merc.transform(lon3, lat3)
|
|
866
|
+
wx, wy = x2 - x1, y2 - y1
|
|
867
|
+
if math.hypot(wx, wy) < 0.5:
|
|
868
|
+
return None, "Width too small"
|
|
869
|
+
ux, uy = wx / math.hypot(wx, wy), wy / math.hypot(wx, wy)
|
|
870
|
+
px, py = -uy, ux
|
|
871
|
+
vx, vy = x3 - x1, y3 - y1
|
|
872
|
+
h_len = vx * px + vy * py
|
|
873
|
+
if abs(h_len) < 0.5:
|
|
874
|
+
return None, "Height too small"
|
|
875
|
+
hx, hy = px * h_len, py * h_len
|
|
876
|
+
corners_merc = [
|
|
877
|
+
(x1, y1),
|
|
878
|
+
(x2, y2),
|
|
879
|
+
(x2 + hx, y2 + hy),
|
|
880
|
+
(x1 + hx, y1 + hy),
|
|
881
|
+
]
|
|
882
|
+
return [to_geo.transform(*p) for p in corners_merc], None
|
|
883
|
+
|
|
884
|
+
def handle_map_interaction(**kwargs):
|
|
885
|
+
if not rect_btn.value:
|
|
886
|
+
return
|
|
887
|
+
|
|
888
|
+
if kwargs.get("type") == "click":
|
|
889
|
+
coords = kwargs.get("coordinates")
|
|
890
|
+
if not coords:
|
|
891
|
+
return
|
|
892
|
+
lat, lon = coords
|
|
893
|
+
state["clicks"].append((lon, lat))
|
|
894
|
+
refresh_markers()
|
|
895
|
+
|
|
896
|
+
count = len(state["clicks"])
|
|
897
|
+
if count == 1:
|
|
898
|
+
set_status("📐 <b>Step 2/3:</b> Click second corner", "info")
|
|
899
|
+
elif count == 2:
|
|
900
|
+
(l1, la1), (l2, la2) = state["clicks"]
|
|
901
|
+
x1, y1 = to_merc.transform(l1, la1)
|
|
902
|
+
x2, y2 = to_merc.transform(l2, la2)
|
|
903
|
+
if math.hypot(x2 - x1, y2 - y1) < 0.5:
|
|
904
|
+
state["clicks"].pop()
|
|
905
|
+
refresh_markers()
|
|
906
|
+
set_status("⚠️ Too close! Click further away", "warn")
|
|
907
|
+
else:
|
|
908
|
+
set_status("📐 <b>Step 3/3:</b> Click opposite side", "info")
|
|
909
|
+
elif count == 3:
|
|
910
|
+
verts, err = build_rect(state["clicks"])
|
|
911
|
+
if err:
|
|
912
|
+
state["clicks"].pop()
|
|
913
|
+
set_status(f"⚠️ {err} - try again", "warn")
|
|
914
|
+
else:
|
|
915
|
+
clear_preview()
|
|
916
|
+
clear_temps()
|
|
917
|
+
state["poly"] = verts
|
|
918
|
+
poly_locs = [(lat, lon) for lon, lat in verts]
|
|
919
|
+
preview = LeafletPolygon(
|
|
920
|
+
locations=poly_locs,
|
|
921
|
+
color="#4CAF50",
|
|
922
|
+
fill_color="#4CAF50",
|
|
923
|
+
fill_opacity=0.3,
|
|
924
|
+
)
|
|
925
|
+
state["preview"] = preview
|
|
926
|
+
m.add_layer(preview)
|
|
927
|
+
add_btn.disabled = False
|
|
928
|
+
clr_btn.disabled = False
|
|
929
|
+
state["clicks"] = []
|
|
930
|
+
rect_btn.value = False
|
|
931
|
+
set_status("✅ Shape ready! Set height → <b>Add</b>", "success")
|
|
932
|
+
|
|
933
|
+
elif kwargs.get("type") == "mousemove":
|
|
934
|
+
coords = kwargs.get("coordinates")
|
|
935
|
+
if not coords:
|
|
936
|
+
return
|
|
937
|
+
lat_c, lon_c = coords
|
|
938
|
+
if len(state["clicks"]) == 1:
|
|
939
|
+
clear_preview()
|
|
940
|
+
(lon1, lat1) = state["clicks"][0]
|
|
941
|
+
line = Polyline(
|
|
942
|
+
locations=[(lat1, lon1), (lat_c, lon_c)],
|
|
943
|
+
color="#FF5722",
|
|
944
|
+
weight=2,
|
|
945
|
+
dash_array="5, 5",
|
|
946
|
+
)
|
|
947
|
+
state["preview"] = line
|
|
948
|
+
m.add_layer(line)
|
|
949
|
+
elif len(state["clicks"]) == 2:
|
|
950
|
+
tentative_clicks = state["clicks"] + [(lon_c, lat_c)]
|
|
951
|
+
verts, err = build_rect(tentative_clicks)
|
|
952
|
+
clear_preview()
|
|
953
|
+
if not err:
|
|
954
|
+
poly_locs = [(lat, lon) for lon, lat in verts]
|
|
955
|
+
poly = LeafletPolygon(
|
|
956
|
+
locations=poly_locs,
|
|
957
|
+
color="#FF5722",
|
|
958
|
+
weight=2,
|
|
959
|
+
fill_color="#FF5722",
|
|
960
|
+
fill_opacity=0.1,
|
|
961
|
+
dash_array="5, 5",
|
|
962
|
+
)
|
|
963
|
+
state["preview"] = poly
|
|
964
|
+
m.add_layer(poly)
|
|
965
|
+
|
|
966
|
+
m.on_interaction(handle_map_interaction)
|
|
967
|
+
|
|
968
|
+
def handle_freehand(self, action, geo_json):
|
|
969
|
+
if action == "created" and geo_json["geometry"]["type"] == "Polygon":
|
|
970
|
+
coords = geo_json["geometry"]["coordinates"][0]
|
|
971
|
+
polygon_coords = [(c[0], c[1]) for c in coords[:-1]]
|
|
972
|
+
|
|
973
|
+
# Check if we're in polygon removal mode
|
|
974
|
+
if poly_del_btn.value:
|
|
975
|
+
# Create the removal polygon and find buildings to remove
|
|
976
|
+
removal_polygon = geom.Polygon(polygon_coords)
|
|
977
|
+
state["removal_poly"] = removal_polygon
|
|
978
|
+
|
|
979
|
+
# Find all buildings within or intersecting the drawn polygon
|
|
980
|
+
buildings_to_remove = []
|
|
981
|
+
for idx, row in updated_gdf.iterrows():
|
|
982
|
+
if isinstance(row.geometry, geom.Polygon):
|
|
983
|
+
if removal_polygon.contains(row.geometry) or removal_polygon.intersects(row.geometry):
|
|
984
|
+
buildings_to_remove.append(idx)
|
|
985
|
+
|
|
986
|
+
if buildings_to_remove:
|
|
987
|
+
# Highlight buildings that will be removed
|
|
988
|
+
clear_removal_preview()
|
|
989
|
+
|
|
990
|
+
# Create a preview polygon showing the selection area
|
|
991
|
+
preview_locs = [(lat, lon) for lon, lat in polygon_coords]
|
|
992
|
+
preview = LeafletPolygon(
|
|
993
|
+
locations=preview_locs,
|
|
994
|
+
color="#FF0000",
|
|
995
|
+
fill_color="#FF0000",
|
|
996
|
+
fill_opacity=0.2,
|
|
997
|
+
weight=2,
|
|
998
|
+
dash_array="5, 5",
|
|
999
|
+
)
|
|
1000
|
+
state["removal_preview"] = preview
|
|
1001
|
+
m.add_layer(preview)
|
|
1002
|
+
|
|
1003
|
+
# Remove the buildings
|
|
1004
|
+
removed_count = 0
|
|
1005
|
+
for idx in buildings_to_remove:
|
|
1006
|
+
if idx in building_layers:
|
|
1007
|
+
try:
|
|
1008
|
+
m.remove_layer(building_layers[idx])
|
|
1009
|
+
del building_layers[idx]
|
|
1010
|
+
except Exception:
|
|
1011
|
+
pass
|
|
1012
|
+
try:
|
|
1013
|
+
updated_gdf.drop(index=idx, inplace=True)
|
|
1014
|
+
removed_count += 1
|
|
1015
|
+
except KeyError:
|
|
1016
|
+
pass
|
|
1017
|
+
|
|
1018
|
+
draw_control.clear()
|
|
1019
|
+
clear_removal_preview()
|
|
1020
|
+
poly_del_btn.value = False # Exit poly delete mode after removal
|
|
1021
|
+
m.default_style = original_style
|
|
1022
|
+
set_status(f"🗑️ Removed {removed_count} building(s)", "success")
|
|
1023
|
+
else:
|
|
1024
|
+
draw_control.clear()
|
|
1025
|
+
set_status("⚠️ No buildings in area", "warn")
|
|
1026
|
+
else:
|
|
1027
|
+
# Normal mode - adding a freehand polygon as building
|
|
1028
|
+
rect_btn.value = False
|
|
1029
|
+
del_btn.value = False
|
|
1030
|
+
poly_del_btn.value = False
|
|
1031
|
+
state["clicks"] = []
|
|
1032
|
+
clear_preview()
|
|
1033
|
+
clear_temps()
|
|
1034
|
+
state["poly"] = polygon_coords
|
|
1035
|
+
add_btn.disabled = False
|
|
1036
|
+
clr_btn.disabled = False
|
|
1037
|
+
set_status("✅ Shape ready! Set height → <b>Add</b>", "success")
|
|
1038
|
+
|
|
1039
|
+
draw_control = DrawControl(
|
|
1040
|
+
polygon={"shapeOptions": {"color": "#FF5722", "fillColor": "#FF5722", "fillOpacity": 0.2}},
|
|
1041
|
+
rectangle={},
|
|
1042
|
+
circle={},
|
|
1043
|
+
polyline={},
|
|
1044
|
+
marker={},
|
|
1045
|
+
circlemarker={},
|
|
1046
|
+
)
|
|
1047
|
+
draw_control.on_draw(handle_freehand)
|
|
1048
|
+
m.add_control(draw_control)
|
|
1049
|
+
|
|
1050
|
+
def add_geom(b):
|
|
1051
|
+
if not state["poly"]:
|
|
1052
|
+
return
|
|
1053
|
+
try:
|
|
1054
|
+
poly = geom.Polygon(state["poly"])
|
|
1055
|
+
new_idx = (updated_gdf.index.max() + 1) if not updated_gdf.empty else 1
|
|
1056
|
+
updated_gdf.loc[new_idx] = {
|
|
1057
|
+
"geometry": poly,
|
|
1058
|
+
"height": h_in.value,
|
|
1059
|
+
"min_height": mh_in.value,
|
|
1060
|
+
"building_id": new_idx,
|
|
1061
|
+
"id": new_idx,
|
|
1062
|
+
}
|
|
1063
|
+
add_polygon_to_map(poly, new_idx, h_in.value)
|
|
1064
|
+
clear_all(None)
|
|
1065
|
+
set_status(f"🏢 Added! H={h_in.value}m (ID:{new_idx})", "success")
|
|
1066
|
+
except Exception as e:
|
|
1067
|
+
set_status(f"❌ Error: {str(e)[:30]}", "danger")
|
|
1068
|
+
|
|
1069
|
+
def clear_all(b):
|
|
1070
|
+
draw_control.clear()
|
|
1071
|
+
clear_preview()
|
|
1072
|
+
clear_temps()
|
|
1073
|
+
state["clicks"] = []
|
|
1074
|
+
state["poly"] = []
|
|
1075
|
+
add_btn.disabled = True
|
|
1076
|
+
clr_btn.disabled = True
|
|
1077
|
+
if b:
|
|
1078
|
+
set_status("Cleared. Draw new shape.", "warn")
|
|
1079
|
+
|
|
1080
|
+
add_btn.on_click(add_geom)
|
|
1081
|
+
clr_btn.on_click(clear_all)
|
|
1082
|
+
|
|
1083
|
+
return m, updated_gdf
|
|
1084
|
+
|
|
1085
|
+
|
|
1086
|
+
def get_polygon_vertices(drawn_polygons, polygon_id=None):
|
|
1087
|
+
"""
|
|
1088
|
+
Extract vertices from drawn polygons data structure.
|
|
1089
|
+
|
|
1090
|
+
This helper function provides a convenient way to extract polygon vertices
|
|
1091
|
+
from the drawn_polygons list returned by display_buildings_and_draw_polygon().
|
|
1092
|
+
|
|
1093
|
+
Args:
|
|
1094
|
+
drawn_polygons: The drawn_polygons list returned from display_buildings_and_draw_polygon()
|
|
1095
|
+
polygon_id (int, optional): Specific polygon ID to extract. If None, returns all polygons.
|
|
1096
|
+
|
|
1097
|
+
Returns:
|
|
1098
|
+
If polygon_id is specified: List of (lon, lat) tuples for that polygon
|
|
1099
|
+
If polygon_id is None: List of lists, where each inner list contains (lon, lat) tuples
|
|
1100
|
+
|
|
1101
|
+
Example:
|
|
1102
|
+
>>> m, polygons = display_buildings_and_draw_polygon()
|
|
1103
|
+
>>> # Draw some polygons...
|
|
1104
|
+
>>> vertices = get_polygon_vertices(polygons, polygon_id=1) # Get polygon 1
|
|
1105
|
+
>>> all_vertices = get_polygon_vertices(polygons) # Get all polygons
|
|
1106
|
+
"""
|
|
1107
|
+
if not drawn_polygons:
|
|
1108
|
+
return []
|
|
1109
|
+
|
|
1110
|
+
if polygon_id is not None:
|
|
1111
|
+
# Return specific polygon
|
|
1112
|
+
for polygon in drawn_polygons:
|
|
1113
|
+
if polygon['id'] == polygon_id:
|
|
1114
|
+
return polygon['vertices']
|
|
1115
|
+
return [] # Polygon not found
|
|
1116
|
+
else:
|
|
1117
|
+
# Return all polygons
|
|
1118
|
+
return [polygon['vertices'] for polygon in drawn_polygons]
|
|
1119
|
+
|
|
1120
|
+
|
|
1121
|
+
# Simple convenience function
|
|
1122
|
+
def create_building_editor(building_gdf=None, initial_center=None, zoom=17, rectangle_vertices=None):
|
|
1123
|
+
"""
|
|
1124
|
+
Creates and displays an interactive building editor.
|
|
1125
|
+
|
|
1126
|
+
Args:
|
|
1127
|
+
building_gdf: Existing buildings GeoDataFrame (optional)
|
|
1128
|
+
initial_center: Map center as (lon, lat) tuple (optional)
|
|
1129
|
+
zoom: Initial zoom level (default=17)
|
|
1130
|
+
|
|
1131
|
+
Returns:
|
|
1132
|
+
GeoDataFrame: The building GeoDataFrame that automatically updates
|
|
1133
|
+
|
|
1134
|
+
Example:
|
|
1135
|
+
>>> buildings = create_building_editor()
|
|
1136
|
+
>>> # Draw buildings on the displayed map
|
|
1137
|
+
>>> print(buildings) # Automatically contains all drawn buildings
|
|
1138
|
+
"""
|
|
1139
|
+
m, gdf = draw_additional_buildings(
|
|
1140
|
+
building_gdf=building_gdf,
|
|
1141
|
+
initial_center=initial_center,
|
|
1142
|
+
zoom=zoom,
|
|
1143
|
+
rectangle_vertices=rectangle_vertices,
|
|
1144
|
+
)
|
|
1145
|
+
display(m)
|
|
1146
|
+
return gdf
|
|
1147
|
+
|
|
1148
|
+
|
|
1149
|
+
def draw_additional_trees(voxcity=None, initial_center=None, zoom=17):
|
|
1150
|
+
"""
|
|
1151
|
+
Creates an interactive map to add trees by clicking and setting parameters.
|
|
1152
|
+
|
|
1153
|
+
Users can:
|
|
1154
|
+
- Set tree parameters: top height, bottom height, crown diameter
|
|
1155
|
+
- Click multiple times to add multiple trees with the same parameters
|
|
1156
|
+
- Update parameters at any time to change subsequent trees
|
|
1157
|
+
|
|
1158
|
+
Args:
|
|
1159
|
+
voxcity (VoxCity, optional): A VoxCity object from which to extract tree_gdf
|
|
1160
|
+
and rectangle_vertices. If provided, tree_gdf is extracted from
|
|
1161
|
+
voxcity.extras['tree_gdf'] and rectangle_vertices from
|
|
1162
|
+
voxcity.extras['rectangle_vertices'].
|
|
1163
|
+
initial_center (tuple, optional): (lon, lat) for initial map center.
|
|
1164
|
+
zoom (int): Initial zoom level. Default=17.
|
|
1165
|
+
|
|
1166
|
+
Returns:
|
|
1167
|
+
tuple: (map_object, updated_tree_gdf)
|
|
1168
|
+
|
|
1169
|
+
Examples:
|
|
1170
|
+
Using a VoxCity object:
|
|
1171
|
+
>>> m, tree_gdf = draw_additional_trees(voxcity=my_voxcity)
|
|
1172
|
+
|
|
1173
|
+
Using with custom center and zoom:
|
|
1174
|
+
>>> m, tree_gdf = draw_additional_trees(voxcity=my_voxcity, initial_center=(-73.98, 40.75), zoom=18)
|
|
1175
|
+
"""
|
|
1176
|
+
# ---------------------------------------------------------
|
|
1177
|
+
# Extract data from VoxCity object if provided
|
|
1178
|
+
# ---------------------------------------------------------
|
|
1179
|
+
tree_gdf = None
|
|
1180
|
+
rectangle_vertices = None
|
|
1181
|
+
if voxcity is not None:
|
|
1182
|
+
tree_gdf = voxcity.extras.get('tree_gdf', None)
|
|
1183
|
+
rectangle_vertices = voxcity.extras.get('rectangle_vertices', None)
|
|
1184
|
+
|
|
1185
|
+
# Initialize or copy the tree GeoDataFrame
|
|
1186
|
+
if tree_gdf is None:
|
|
1187
|
+
updated_trees = gpd.GeoDataFrame(
|
|
1188
|
+
columns=['tree_id', 'top_height', 'bottom_height', 'crown_diameter', 'geometry'],
|
|
1189
|
+
crs='EPSG:4326'
|
|
1190
|
+
)
|
|
1191
|
+
else:
|
|
1192
|
+
updated_trees = tree_gdf.copy()
|
|
1193
|
+
# Ensure required columns exist
|
|
1194
|
+
if 'tree_id' not in updated_trees.columns:
|
|
1195
|
+
updated_trees['tree_id'] = range(1, len(updated_trees) + 1)
|
|
1196
|
+
for col, default in [('top_height', 10.0), ('bottom_height', 4.0), ('crown_diameter', 6.0)]:
|
|
1197
|
+
if col not in updated_trees.columns:
|
|
1198
|
+
updated_trees[col] = default
|
|
1199
|
+
|
|
1200
|
+
# Determine map center
|
|
1201
|
+
if initial_center is not None:
|
|
1202
|
+
center_lon, center_lat = initial_center
|
|
1203
|
+
elif updated_trees is not None and len(updated_trees) > 0:
|
|
1204
|
+
min_lon, min_lat, max_lon, max_lat = updated_trees.total_bounds
|
|
1205
|
+
center_lon = (min_lon + max_lon) / 2
|
|
1206
|
+
center_lat = (min_lat + max_lat) / 2
|
|
1207
|
+
elif rectangle_vertices is not None:
|
|
1208
|
+
center_lon, center_lat = (rectangle_vertices[0][0] + rectangle_vertices[2][0]) / 2, (rectangle_vertices[0][1] + rectangle_vertices[2][1]) / 2
|
|
1209
|
+
else:
|
|
1210
|
+
center_lon, center_lat = -100.0, 40.0
|
|
1211
|
+
|
|
1212
|
+
# Create map
|
|
1213
|
+
m = Map(center=(center_lat, center_lon), zoom=zoom, scroll_wheel_zoom=True)
|
|
1214
|
+
# Add Google Satellite basemap with Esri fallback
|
|
1215
|
+
try:
|
|
1216
|
+
google_sat = TileLayer(
|
|
1217
|
+
url='https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}',
|
|
1218
|
+
name='Google Satellite',
|
|
1219
|
+
attribution='Google Satellite'
|
|
1220
|
+
)
|
|
1221
|
+
# Replace default base layer with Google Satellite
|
|
1222
|
+
m.layers = tuple([google_sat])
|
|
1223
|
+
except Exception:
|
|
1224
|
+
try:
|
|
1225
|
+
m.layers = tuple([basemap_to_tiles(basemaps.Esri.WorldImagery)])
|
|
1226
|
+
except Exception:
|
|
1227
|
+
# Fallback silently if basemap cannot be added
|
|
1228
|
+
pass
|
|
1229
|
+
|
|
1230
|
+
# If rectangle_vertices provided, draw its edges on the map
|
|
1231
|
+
if rectangle_vertices is not None and len(rectangle_vertices) >= 4:
|
|
1232
|
+
try:
|
|
1233
|
+
lat_lon_coords = [(lat, lon) for lon, lat in rectangle_vertices]
|
|
1234
|
+
rect_outline = LeafletPolygon(
|
|
1235
|
+
locations=lat_lon_coords,
|
|
1236
|
+
color="#fed766",
|
|
1237
|
+
weight=2,
|
|
1238
|
+
fill_color="#fed766",
|
|
1239
|
+
fill_opacity=0.0
|
|
1240
|
+
)
|
|
1241
|
+
m.add_layer(rect_outline)
|
|
1242
|
+
except Exception:
|
|
1243
|
+
pass
|
|
1244
|
+
|
|
1245
|
+
# Display existing trees as circles
|
|
1246
|
+
tree_layers = {}
|
|
1247
|
+
for idx, row in updated_trees.iterrows():
|
|
1248
|
+
if row.geometry is not None and hasattr(row.geometry, 'x'):
|
|
1249
|
+
lat = row.geometry.y
|
|
1250
|
+
lon = row.geometry.x
|
|
1251
|
+
# Ensure integer radius in meters as required by ipyleaflet Circle
|
|
1252
|
+
radius_m = max(int(round(float(row.get('crown_diameter', 6.0)) / 2.0)), 1)
|
|
1253
|
+
tree_id_val = int(row.get('tree_id', idx+1))
|
|
1254
|
+
circle = Circle(location=(lat, lon), radius=radius_m, color='#2ab7ca', weight=1, opacity=1.0, fill_color='#2ab7ca', fill_opacity=0.3)
|
|
1255
|
+
m.add_layer(circle)
|
|
1256
|
+
tree_layers[tree_id_val] = circle
|
|
1257
|
+
|
|
1258
|
+
# UI widgets for parameters
|
|
1259
|
+
top_height_input = FloatText(value=10.0, description='Top height (m):', disabled=False, style={'description_width': 'initial'})
|
|
1260
|
+
bottom_height_input = FloatText(value=4.0, description='Bottom height (m):', disabled=False, style={'description_width': 'initial'})
|
|
1261
|
+
crown_diameter_input = FloatText(value=6.0, description='Crown diameter (m):', disabled=False, style={'description_width': 'initial'})
|
|
1262
|
+
fixed_prop_checkbox = Checkbox(value=True, description='Fixed proportion', indent=False)
|
|
1263
|
+
|
|
1264
|
+
add_mode_button = Button(description='Add', button_style='success')
|
|
1265
|
+
remove_mode_button = Button(description='Remove', button_style='')
|
|
1266
|
+
status_output = Output()
|
|
1267
|
+
hover_info = HTML("")
|
|
1268
|
+
|
|
1269
|
+
control_panel = VBox([
|
|
1270
|
+
HTML("<h3 style=\"margin:0 0 4px 0;\">Tree Placement Tool</h3>"),
|
|
1271
|
+
HTML("<div style=\"margin:0 0 6px 0;\">1. Choose Add/Remove mode<br>2. Set tree parameters (top, bottom, crown)<br>3. Click on the map to add/remove consecutively<br>4. Hover over a tree to view parameters</div>"),
|
|
1272
|
+
HBox([add_mode_button, remove_mode_button]),
|
|
1273
|
+
top_height_input,
|
|
1274
|
+
bottom_height_input,
|
|
1275
|
+
crown_diameter_input,
|
|
1276
|
+
fixed_prop_checkbox,
|
|
1277
|
+
hover_info,
|
|
1278
|
+
status_output
|
|
1279
|
+
])
|
|
1280
|
+
|
|
1281
|
+
widget_control = WidgetControl(widget=control_panel, position='topright')
|
|
1282
|
+
m.add_control(widget_control)
|
|
1283
|
+
|
|
1284
|
+
# State for mode
|
|
1285
|
+
mode = 'add'
|
|
1286
|
+
# Fixed proportion state
|
|
1287
|
+
base_bottom_ratio = bottom_height_input.value / top_height_input.value if top_height_input.value else 0.4
|
|
1288
|
+
base_crown_ratio = crown_diameter_input.value / top_height_input.value if top_height_input.value else 0.6
|
|
1289
|
+
updating_params = False
|
|
1290
|
+
|
|
1291
|
+
def recompute_from_top(new_top: float):
|
|
1292
|
+
nonlocal updating_params
|
|
1293
|
+
if new_top <= 0:
|
|
1294
|
+
return
|
|
1295
|
+
new_bottom = max(0.0, base_bottom_ratio * new_top)
|
|
1296
|
+
new_crown = max(0.0, base_crown_ratio * new_top)
|
|
1297
|
+
updating_params = True
|
|
1298
|
+
bottom_height_input.value = new_bottom
|
|
1299
|
+
crown_diameter_input.value = new_crown
|
|
1300
|
+
updating_params = False
|
|
1301
|
+
|
|
1302
|
+
def recompute_from_bottom(new_bottom: float):
|
|
1303
|
+
nonlocal updating_params
|
|
1304
|
+
if base_bottom_ratio <= 0:
|
|
1305
|
+
return
|
|
1306
|
+
new_top = max(0.0, new_bottom / base_bottom_ratio)
|
|
1307
|
+
new_crown = max(0.0, base_crown_ratio * new_top)
|
|
1308
|
+
updating_params = True
|
|
1309
|
+
top_height_input.value = new_top
|
|
1310
|
+
crown_diameter_input.value = new_crown
|
|
1311
|
+
updating_params = False
|
|
1312
|
+
|
|
1313
|
+
def recompute_from_crown(new_crown: float):
|
|
1314
|
+
nonlocal updating_params
|
|
1315
|
+
if base_crown_ratio <= 0:
|
|
1316
|
+
return
|
|
1317
|
+
new_top = max(0.0, new_crown / base_crown_ratio)
|
|
1318
|
+
new_bottom = max(0.0, base_bottom_ratio * new_top)
|
|
1319
|
+
updating_params = True
|
|
1320
|
+
top_height_input.value = new_top
|
|
1321
|
+
bottom_height_input.value = new_bottom
|
|
1322
|
+
updating_params = False
|
|
1323
|
+
|
|
1324
|
+
def on_toggle_fixed(change):
|
|
1325
|
+
nonlocal base_bottom_ratio, base_crown_ratio
|
|
1326
|
+
if change['name'] == 'value':
|
|
1327
|
+
if change['new']:
|
|
1328
|
+
# Capture current ratios as baseline
|
|
1329
|
+
top = float(top_height_input.value) or 1.0
|
|
1330
|
+
bot = float(bottom_height_input.value)
|
|
1331
|
+
crn = float(crown_diameter_input.value)
|
|
1332
|
+
base_bottom_ratio = max(0.0, bot / top)
|
|
1333
|
+
base_crown_ratio = max(0.0, crn / top)
|
|
1334
|
+
else:
|
|
1335
|
+
# Keep last ratios but do not auto-update
|
|
1336
|
+
pass
|
|
1337
|
+
|
|
1338
|
+
def on_top_change(change):
|
|
1339
|
+
if change['name'] == 'value' and fixed_prop_checkbox.value and not updating_params:
|
|
1340
|
+
try:
|
|
1341
|
+
recompute_from_top(float(change['new']))
|
|
1342
|
+
except Exception:
|
|
1343
|
+
pass
|
|
1344
|
+
|
|
1345
|
+
def on_bottom_change(change):
|
|
1346
|
+
if change['name'] == 'value' and fixed_prop_checkbox.value and not updating_params:
|
|
1347
|
+
try:
|
|
1348
|
+
recompute_from_bottom(float(change['new']))
|
|
1349
|
+
except Exception:
|
|
1350
|
+
pass
|
|
1351
|
+
|
|
1352
|
+
def on_crown_change(change):
|
|
1353
|
+
if change['name'] == 'value' and fixed_prop_checkbox.value and not updating_params:
|
|
1354
|
+
try:
|
|
1355
|
+
recompute_from_crown(float(change['new']))
|
|
1356
|
+
except Exception:
|
|
1357
|
+
pass
|
|
1358
|
+
|
|
1359
|
+
fixed_prop_checkbox.observe(on_toggle_fixed, names='value')
|
|
1360
|
+
top_height_input.observe(on_top_change, names='value')
|
|
1361
|
+
bottom_height_input.observe(on_bottom_change, names='value')
|
|
1362
|
+
crown_diameter_input.observe(on_crown_change, names='value')
|
|
1363
|
+
|
|
1364
|
+
def set_mode(new_mode):
|
|
1365
|
+
nonlocal mode
|
|
1366
|
+
mode = new_mode
|
|
1367
|
+
# Visual feedback
|
|
1368
|
+
add_mode_button.button_style = 'success' if mode == 'add' else ''
|
|
1369
|
+
remove_mode_button.button_style = 'danger' if mode == 'remove' else ''
|
|
1370
|
+
# No on-screen mode label
|
|
1371
|
+
|
|
1372
|
+
def on_click_add(b):
|
|
1373
|
+
set_mode('add')
|
|
1374
|
+
|
|
1375
|
+
def on_click_remove(b):
|
|
1376
|
+
set_mode('remove')
|
|
1377
|
+
|
|
1378
|
+
add_mode_button.on_click(on_click_add)
|
|
1379
|
+
remove_mode_button.on_click(on_click_remove)
|
|
1380
|
+
|
|
1381
|
+
# Consecutive placements by map click
|
|
1382
|
+
def handle_map_click(**kwargs):
|
|
1383
|
+
nonlocal updated_trees
|
|
1384
|
+
with status_output:
|
|
1385
|
+
clear_output()
|
|
1386
|
+
|
|
1387
|
+
if kwargs.get('type') == 'click':
|
|
1388
|
+
lat, lon = kwargs.get('coordinates', (None, None))
|
|
1389
|
+
if lat is None or lon is None:
|
|
1390
|
+
return
|
|
1391
|
+
if mode == 'add':
|
|
1392
|
+
# Determine next tree_id
|
|
1393
|
+
next_tree_id = int(updated_trees['tree_id'].max() + 1) if len(updated_trees) > 0 else 1
|
|
1394
|
+
|
|
1395
|
+
# Clamp/validate parameters
|
|
1396
|
+
th = float(top_height_input.value)
|
|
1397
|
+
bh = float(bottom_height_input.value)
|
|
1398
|
+
cd = float(crown_diameter_input.value)
|
|
1399
|
+
if bh > th:
|
|
1400
|
+
bh, th = th, bh
|
|
1401
|
+
if cd < 0:
|
|
1402
|
+
cd = 0.0
|
|
1403
|
+
|
|
1404
|
+
# Create new tree row
|
|
1405
|
+
new_row = {
|
|
1406
|
+
'tree_id': next_tree_id,
|
|
1407
|
+
'top_height': th,
|
|
1408
|
+
'bottom_height': bh,
|
|
1409
|
+
'crown_diameter': cd,
|
|
1410
|
+
'geometry': geom.Point(lon, lat)
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
# Append
|
|
1414
|
+
new_index = len(updated_trees)
|
|
1415
|
+
updated_trees.loc[new_index] = new_row
|
|
1416
|
+
|
|
1417
|
+
# Add circle layer representing crown diameter (radius in meters)
|
|
1418
|
+
radius_m = max(int(round(new_row['crown_diameter'] / 2.0)), 1)
|
|
1419
|
+
circle = Circle(location=(lat, lon), radius=radius_m, color='#2ab7ca', weight=1, opacity=1.0, fill_color='#2ab7ca', fill_opacity=0.3)
|
|
1420
|
+
m.add_layer(circle)
|
|
1421
|
+
|
|
1422
|
+
tree_layers[next_tree_id] = circle
|
|
1423
|
+
|
|
1424
|
+
# Suppress status prints on add
|
|
1425
|
+
else:
|
|
1426
|
+
# Remove mode: find the nearest tree within its crown radius + 5m
|
|
1427
|
+
candidate_id = None
|
|
1428
|
+
candidate_idx = None
|
|
1429
|
+
candidate_dist = None
|
|
1430
|
+
for idx2, row2 in updated_trees.iterrows():
|
|
1431
|
+
if row2.geometry is None or not hasattr(row2.geometry, 'x'):
|
|
1432
|
+
continue
|
|
1433
|
+
lat2 = row2.geometry.y
|
|
1434
|
+
lon2 = row2.geometry.x
|
|
1435
|
+
dist_m = distance.distance((lat, lon), (lat2, lon2)).meters
|
|
1436
|
+
rad_m = max(int(round(float(row2.get('crown_diameter', 6.0)) / 2.0)), 1)
|
|
1437
|
+
thr_m = rad_m + 5
|
|
1438
|
+
if (candidate_dist is None and dist_m <= thr_m) or (candidate_dist is not None and dist_m < candidate_dist and dist_m <= thr_m):
|
|
1439
|
+
candidate_dist = dist_m
|
|
1440
|
+
candidate_id = int(row2.get('tree_id', idx2+1))
|
|
1441
|
+
candidate_idx = idx2
|
|
1442
|
+
|
|
1443
|
+
if candidate_id is not None:
|
|
1444
|
+
# Remove layer
|
|
1445
|
+
layer = tree_layers.get(candidate_id)
|
|
1446
|
+
if layer is not None:
|
|
1447
|
+
m.remove_layer(layer)
|
|
1448
|
+
del tree_layers[candidate_id]
|
|
1449
|
+
# Remove from gdf
|
|
1450
|
+
updated_trees.drop(index=candidate_idx, inplace=True)
|
|
1451
|
+
updated_trees.reset_index(drop=True, inplace=True)
|
|
1452
|
+
# Suppress status prints on remove
|
|
1453
|
+
else:
|
|
1454
|
+
# Suppress status prints when nothing to remove
|
|
1455
|
+
pass
|
|
1456
|
+
elif kwargs.get('type') == 'mousemove':
|
|
1457
|
+
lat, lon = kwargs.get('coordinates', (None, None))
|
|
1458
|
+
if lat is None or lon is None:
|
|
1459
|
+
return
|
|
1460
|
+
# Find a tree the cursor is over (within crown radius)
|
|
1461
|
+
shown = False
|
|
1462
|
+
for _, row2 in updated_trees.iterrows():
|
|
1463
|
+
if row2.geometry is None or not hasattr(row2.geometry, 'x'):
|
|
1464
|
+
continue
|
|
1465
|
+
lat2 = row2.geometry.y
|
|
1466
|
+
lon2 = row2.geometry.x
|
|
1467
|
+
dist_m = distance.distance((lat, lon), (lat2, lon2)).meters
|
|
1468
|
+
rad_m = max(int(round(float(row2.get('crown_diameter', 6.0)) / 2.0)), 1)
|
|
1469
|
+
if dist_m <= rad_m:
|
|
1470
|
+
hover_info.value = (
|
|
1471
|
+
f"<div style=\"color:#d61f1f; font-weight:600; margin:2px 0;\">"
|
|
1472
|
+
f"Tree {int(row2.get('tree_id', 0))} | Top {float(row2.get('top_height', 10.0))} m | "
|
|
1473
|
+
f"Bottom {float(row2.get('bottom_height', 0.0))} m | Crown {float(row2.get('crown_diameter', 6.0))} m"
|
|
1474
|
+
f"</div>"
|
|
1475
|
+
)
|
|
1476
|
+
shown = True
|
|
1477
|
+
break
|
|
1478
|
+
if not shown:
|
|
1479
|
+
hover_info.value = ""
|
|
1480
|
+
m.on_interaction(handle_map_click)
|
|
1481
|
+
|
|
1482
|
+
with status_output:
|
|
1483
|
+
print(f"Total trees loaded: {len(updated_trees)}")
|
|
1484
|
+
print("Set parameters, then click on the map to add trees")
|
|
1485
|
+
|
|
1486
|
+
return m, updated_trees
|
|
1487
|
+
|
|
1488
|
+
|
|
1489
|
+
def create_tree_editor(tree_gdf=None, initial_center=None, zoom=17, rectangle_vertices=None):
|
|
1490
|
+
"""
|
|
1491
|
+
Convenience wrapper to display the tree editor map and return the GeoDataFrame.
|
|
1492
|
+
"""
|
|
1493
|
+
m, gdf = draw_additional_trees(tree_gdf, initial_center, zoom, rectangle_vertices)
|
|
1494
|
+
display(m)
|
|
1220
1495
|
return gdf
|