voxcity 0.6.10__py3-none-any.whl → 0.6.12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of voxcity might be problematic. Click here for more details.
- voxcity/downloader/citygml.py +124 -8
- voxcity/downloader/osm.py +75 -10
- voxcity/generator.py +1136 -1136
- voxcity/geoprocessor/draw.py +1085 -832
- voxcity/utils/visualization.py +14 -6
- {voxcity-0.6.10.dist-info → voxcity-0.6.12.dist-info}/METADATA +1 -1
- {voxcity-0.6.10.dist-info → voxcity-0.6.12.dist-info}/RECORD +10 -10
- {voxcity-0.6.10.dist-info → voxcity-0.6.12.dist-info}/AUTHORS.rst +0 -0
- {voxcity-0.6.10.dist-info → voxcity-0.6.12.dist-info}/LICENSE +0 -0
- {voxcity-0.6.10.dist-info → voxcity-0.6.12.dist-info}/WHEEL +0 -0
voxcity/geoprocessor/draw.py
CHANGED
|
@@ -1,833 +1,1086 @@
|
|
|
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 Proj, transform
|
|
28
|
-
from ipyleaflet import (
|
|
29
|
-
Map,
|
|
30
|
-
DrawControl,
|
|
31
|
-
Rectangle,
|
|
32
|
-
Polygon as LeafletPolygon,
|
|
33
|
-
WidgetControl
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
from
|
|
39
|
-
import
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
draw_control
|
|
192
|
-
draw_control.
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
centered on
|
|
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
|
-
This is the dimension along the
|
|
274
|
-
The actual ground distance is maintained regardless of projection distortion.
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
- Rectangle
|
|
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
|
-
draw_control
|
|
348
|
-
draw_control.
|
|
349
|
-
draw_control.
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
print(f"
|
|
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
|
-
center_lon
|
|
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
|
-
|
|
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
|
-
|
|
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 Proj, transform
|
|
28
|
+
from ipyleaflet import (
|
|
29
|
+
Map,
|
|
30
|
+
DrawControl,
|
|
31
|
+
Rectangle,
|
|
32
|
+
Polygon as LeafletPolygon,
|
|
33
|
+
WidgetControl,
|
|
34
|
+
Circle,
|
|
35
|
+
basemaps,
|
|
36
|
+
basemap_to_tiles
|
|
37
|
+
)
|
|
38
|
+
from geopy import distance
|
|
39
|
+
import shapely.geometry as geom
|
|
40
|
+
import geopandas as gpd
|
|
41
|
+
from ipywidgets import VBox, HBox, Button, FloatText, Label, Output, HTML
|
|
42
|
+
import pandas as pd
|
|
43
|
+
from IPython.display import display, clear_output
|
|
44
|
+
|
|
45
|
+
from .utils import get_coordinates_from_cityname
|
|
46
|
+
|
|
47
|
+
def rotate_rectangle(m, rectangle_vertices, angle):
|
|
48
|
+
"""
|
|
49
|
+
Project rectangle to Mercator, rotate, and re-project to lat-lon coordinates.
|
|
50
|
+
|
|
51
|
+
This function performs a rotation of a rectangle in geographic space by:
|
|
52
|
+
1. Converting coordinates from WGS84 (lat/lon) to Web Mercator projection
|
|
53
|
+
2. Performing the rotation in the projected space for accurate distance preservation
|
|
54
|
+
3. Converting back to WGS84 coordinates
|
|
55
|
+
4. Visualizing the result on the provided map
|
|
56
|
+
|
|
57
|
+
The rotation is performed around the rectangle's centroid using a standard 2D rotation matrix.
|
|
58
|
+
The function handles coordinate system transformations to ensure geometrically accurate rotations
|
|
59
|
+
despite the distortions inherent in geographic projections.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
m (ipyleaflet.Map): Map object to draw the rotated rectangle on.
|
|
63
|
+
The map must be initialized and have a valid center and zoom level.
|
|
64
|
+
rectangle_vertices (list): List of (lon, lat) tuples defining the rectangle vertices.
|
|
65
|
+
The vertices should be ordered in a counter-clockwise direction.
|
|
66
|
+
Example: [(lon1,lat1), (lon2,lat2), (lon3,lat3), (lon4,lat4)]
|
|
67
|
+
angle (float): Rotation angle in degrees.
|
|
68
|
+
Positive angles rotate counter-clockwise.
|
|
69
|
+
Negative angles rotate clockwise.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
list: List of rotated (lon, lat) tuples defining the new rectangle vertices.
|
|
73
|
+
The vertices maintain their original ordering.
|
|
74
|
+
Returns None if no rectangle vertices are provided.
|
|
75
|
+
|
|
76
|
+
Note:
|
|
77
|
+
The function uses EPSG:4326 (WGS84) for geographic coordinates and
|
|
78
|
+
EPSG:3857 (Web Mercator) for the rotation calculations.
|
|
79
|
+
"""
|
|
80
|
+
if not rectangle_vertices:
|
|
81
|
+
print("Draw a rectangle first!")
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
# Define projections - need to convert between coordinate systems for accurate rotation
|
|
85
|
+
wgs84 = Proj(init='epsg:4326') # WGS84 lat-lon (standard GPS coordinates)
|
|
86
|
+
mercator = Proj(init='epsg:3857') # Web Mercator (projection used by most web maps)
|
|
87
|
+
|
|
88
|
+
# Project vertices from WGS84 to Web Mercator for proper distance calculations
|
|
89
|
+
projected_vertices = [transform(wgs84, mercator, lon, lat) for lon, lat in rectangle_vertices]
|
|
90
|
+
|
|
91
|
+
# Calculate the centroid to use as rotation center
|
|
92
|
+
centroid_x = sum(x for x, y in projected_vertices) / len(projected_vertices)
|
|
93
|
+
centroid_y = sum(y for x, y in projected_vertices) / len(projected_vertices)
|
|
94
|
+
|
|
95
|
+
# Convert angle to radians (negative for clockwise rotation)
|
|
96
|
+
angle_rad = -math.radians(angle)
|
|
97
|
+
|
|
98
|
+
# Rotate each vertex around the centroid using standard 2D rotation matrix
|
|
99
|
+
rotated_vertices = []
|
|
100
|
+
for x, y in projected_vertices:
|
|
101
|
+
# Translate point to origin for rotation
|
|
102
|
+
temp_x = x - centroid_x
|
|
103
|
+
temp_y = y - centroid_y
|
|
104
|
+
|
|
105
|
+
# Apply rotation matrix
|
|
106
|
+
rotated_x = temp_x * math.cos(angle_rad) - temp_y * math.sin(angle_rad)
|
|
107
|
+
rotated_y = temp_x * math.sin(angle_rad) + temp_y * math.cos(angle_rad)
|
|
108
|
+
|
|
109
|
+
# Translate point back to original position
|
|
110
|
+
new_x = rotated_x + centroid_x
|
|
111
|
+
new_y = rotated_y + centroid_y
|
|
112
|
+
|
|
113
|
+
rotated_vertices.append((new_x, new_y))
|
|
114
|
+
|
|
115
|
+
# Convert coordinates back to WGS84 (lon/lat)
|
|
116
|
+
new_vertices = [transform(mercator, wgs84, x, y) for x, y in rotated_vertices]
|
|
117
|
+
|
|
118
|
+
# Create and add new polygon layer to map
|
|
119
|
+
polygon = LeafletPolygon(
|
|
120
|
+
locations=[(lat, lon) for lon, lat in new_vertices], # Convert to (lat,lon) for ipyleaflet
|
|
121
|
+
color="red",
|
|
122
|
+
fill_color="red"
|
|
123
|
+
)
|
|
124
|
+
m.add_layer(polygon)
|
|
125
|
+
|
|
126
|
+
return new_vertices
|
|
127
|
+
|
|
128
|
+
def draw_rectangle_map(center=(40, -100), zoom=4):
|
|
129
|
+
"""
|
|
130
|
+
Create an interactive map for drawing rectangles with ipyleaflet.
|
|
131
|
+
|
|
132
|
+
This function initializes an interactive map that allows users to draw rectangles
|
|
133
|
+
by clicking and dragging on the map surface. The drawn rectangles are captured
|
|
134
|
+
and their vertices are stored in geographic coordinates.
|
|
135
|
+
|
|
136
|
+
The map interface provides:
|
|
137
|
+
- A rectangle drawing tool activated by default
|
|
138
|
+
- Real-time coordinate capture of drawn shapes
|
|
139
|
+
- Automatic vertex ordering in counter-clockwise direction
|
|
140
|
+
- Console output of vertex coordinates for verification
|
|
141
|
+
|
|
142
|
+
Drawing Controls:
|
|
143
|
+
- Click and drag to draw a rectangle
|
|
144
|
+
- Release to complete the rectangle
|
|
145
|
+
- Only one rectangle can be active at a time
|
|
146
|
+
- Drawing a new rectangle clears the previous one
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
center (tuple): Center coordinates (lat, lon) for the map view.
|
|
150
|
+
Defaults to (40, -100) which centers on the continental United States.
|
|
151
|
+
Format: (latitude, longitude) in decimal degrees.
|
|
152
|
+
zoom (int): Initial zoom level for the map. Defaults to 4.
|
|
153
|
+
Range: 0 (most zoomed out) to 18 (most zoomed in).
|
|
154
|
+
Recommended: 3-6 for countries, 10-15 for cities.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
tuple: (Map object, list of rectangle vertices)
|
|
158
|
+
- Map object: ipyleaflet.Map instance for displaying and interacting with the map
|
|
159
|
+
- rectangle_vertices: Empty list that will be populated with (lon,lat) tuples
|
|
160
|
+
when a rectangle is drawn. Coordinates are stored in GeoJSON order (lon,lat).
|
|
161
|
+
|
|
162
|
+
Note:
|
|
163
|
+
The function disables all drawing tools except rectangles to ensure
|
|
164
|
+
consistent shape creation. The rectangle vertices are automatically
|
|
165
|
+
converted to (lon,lat) format when stored, regardless of the input
|
|
166
|
+
center coordinate order.
|
|
167
|
+
"""
|
|
168
|
+
# Initialize the map centered at specified coordinates
|
|
169
|
+
m = Map(center=center, zoom=zoom)
|
|
170
|
+
|
|
171
|
+
# List to store the vertices of drawn rectangle
|
|
172
|
+
rectangle_vertices = []
|
|
173
|
+
|
|
174
|
+
def handle_draw(target, action, geo_json):
|
|
175
|
+
"""Handle draw events on the map."""
|
|
176
|
+
# Clear any previously stored vertices
|
|
177
|
+
rectangle_vertices.clear()
|
|
178
|
+
|
|
179
|
+
# Process only if a rectangle polygon was drawn
|
|
180
|
+
if action == 'created' and geo_json['geometry']['type'] == 'Polygon':
|
|
181
|
+
# Extract coordinates from GeoJSON format
|
|
182
|
+
coordinates = geo_json['geometry']['coordinates'][0]
|
|
183
|
+
print("Vertices of the drawn rectangle:")
|
|
184
|
+
# Store all vertices except last (GeoJSON repeats first vertex at end)
|
|
185
|
+
for coord in coordinates[:-1]:
|
|
186
|
+
# Keep GeoJSON (lon,lat) format
|
|
187
|
+
rectangle_vertices.append((coord[0], coord[1]))
|
|
188
|
+
print(f"Longitude: {coord[0]}, Latitude: {coord[1]}")
|
|
189
|
+
|
|
190
|
+
# Configure drawing controls - only enable rectangle drawing
|
|
191
|
+
draw_control = DrawControl()
|
|
192
|
+
draw_control.polyline = {}
|
|
193
|
+
draw_control.polygon = {}
|
|
194
|
+
draw_control.circle = {}
|
|
195
|
+
draw_control.rectangle = {
|
|
196
|
+
"shapeOptions": {
|
|
197
|
+
"color": "#6bc2e5",
|
|
198
|
+
"weight": 4,
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
m.add_control(draw_control)
|
|
202
|
+
|
|
203
|
+
# Register event handler for drawing actions
|
|
204
|
+
draw_control.on_draw(handle_draw)
|
|
205
|
+
|
|
206
|
+
return m, rectangle_vertices
|
|
207
|
+
|
|
208
|
+
def draw_rectangle_map_cityname(cityname, zoom=15):
|
|
209
|
+
"""
|
|
210
|
+
Create an interactive map centered on a specified city for drawing rectangles.
|
|
211
|
+
|
|
212
|
+
This function extends draw_rectangle_map() by automatically centering the map
|
|
213
|
+
on a specified city using geocoding. It provides a convenient way to focus
|
|
214
|
+
the drawing interface on a particular urban area without needing to know
|
|
215
|
+
its exact coordinates.
|
|
216
|
+
|
|
217
|
+
The function uses the utils.get_coordinates_from_cityname() function to
|
|
218
|
+
geocode the city name and obtain its coordinates. The resulting map is
|
|
219
|
+
zoomed to an appropriate level for urban-scale analysis.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
cityname (str): Name of the city to center the map on.
|
|
223
|
+
Can include country or state for better accuracy.
|
|
224
|
+
Examples: "Tokyo, Japan", "New York, NY", "Paris, France"
|
|
225
|
+
zoom (int): Initial zoom level for the map. Defaults to 15.
|
|
226
|
+
Range: 0 (most zoomed out) to 18 (most zoomed in).
|
|
227
|
+
Default of 15 is optimized for city-level visualization.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
tuple: (Map object, list of rectangle vertices)
|
|
231
|
+
- Map object: ipyleaflet.Map instance centered on the specified city
|
|
232
|
+
- rectangle_vertices: Empty list that will be populated with (lon,lat)
|
|
233
|
+
tuples when a rectangle is drawn
|
|
234
|
+
|
|
235
|
+
Note:
|
|
236
|
+
If the city name cannot be geocoded, the function will raise an error.
|
|
237
|
+
For better results, provide specific city names with country/state context.
|
|
238
|
+
The function inherits all drawing controls and behavior from draw_rectangle_map().
|
|
239
|
+
"""
|
|
240
|
+
# Get coordinates for the specified city
|
|
241
|
+
center = get_coordinates_from_cityname(cityname)
|
|
242
|
+
m, rectangle_vertices = draw_rectangle_map(center=center, zoom=zoom)
|
|
243
|
+
return m, rectangle_vertices
|
|
244
|
+
|
|
245
|
+
def center_location_map_cityname(cityname, east_west_length, north_south_length, zoom=15):
|
|
246
|
+
"""
|
|
247
|
+
Create an interactive map centered on a city where clicking creates a rectangle of specified dimensions.
|
|
248
|
+
|
|
249
|
+
This function provides a specialized interface for creating fixed-size rectangles
|
|
250
|
+
centered on user-selected points. Instead of drawing rectangles by dragging,
|
|
251
|
+
users click a point on the map and a rectangle of the specified dimensions
|
|
252
|
+
is automatically created centered on that point.
|
|
253
|
+
|
|
254
|
+
The function handles:
|
|
255
|
+
- Automatic city geocoding and map centering
|
|
256
|
+
- Distance calculations in meters using geopy
|
|
257
|
+
- Conversion between geographic and metric distances
|
|
258
|
+
- Rectangle creation with specified dimensions
|
|
259
|
+
- Visualization of created rectangles
|
|
260
|
+
|
|
261
|
+
Workflow:
|
|
262
|
+
1. Map is centered on the specified city
|
|
263
|
+
2. User clicks a point on the map
|
|
264
|
+
3. A rectangle is created centered on that point
|
|
265
|
+
4. Rectangle dimensions are maintained in meters regardless of latitude
|
|
266
|
+
5. Previous rectangles are automatically cleared
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
cityname (str): Name of the city to center the map on.
|
|
270
|
+
Can include country or state for better accuracy.
|
|
271
|
+
Examples: "Tokyo, Japan", "New York, NY"
|
|
272
|
+
east_west_length (float): Width of the rectangle in meters.
|
|
273
|
+
This is the dimension along the east-west direction.
|
|
274
|
+
The actual ground distance is maintained regardless of projection distortion.
|
|
275
|
+
north_south_length (float): Height of the rectangle in meters.
|
|
276
|
+
This is the dimension along the north-south direction.
|
|
277
|
+
The actual ground distance is maintained regardless of projection distortion.
|
|
278
|
+
zoom (int): Initial zoom level for the map. Defaults to 15.
|
|
279
|
+
Range: 0 (most zoomed out) to 18 (most zoomed in).
|
|
280
|
+
Default of 15 is optimized for city-level visualization.
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
tuple: (Map object, list of rectangle vertices)
|
|
284
|
+
- Map object: ipyleaflet.Map instance centered on the specified city
|
|
285
|
+
- rectangle_vertices: Empty list that will be populated with (lon,lat)
|
|
286
|
+
tuples when a point is clicked and the rectangle is created
|
|
287
|
+
|
|
288
|
+
Note:
|
|
289
|
+
- Rectangle dimensions are specified in meters but stored as geographic coordinates
|
|
290
|
+
- The function uses geopy's distance calculations for accurate metric distances
|
|
291
|
+
- Only one rectangle can exist at a time; clicking a new point removes the previous rectangle
|
|
292
|
+
- Rectangle vertices are returned in GeoJSON (lon,lat) order
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
# Get coordinates for the specified city
|
|
296
|
+
center = get_coordinates_from_cityname(cityname)
|
|
297
|
+
|
|
298
|
+
# Initialize map centered on the city
|
|
299
|
+
m = Map(center=center, zoom=zoom)
|
|
300
|
+
|
|
301
|
+
# List to store rectangle vertices
|
|
302
|
+
rectangle_vertices = []
|
|
303
|
+
|
|
304
|
+
def handle_draw(target, action, geo_json):
|
|
305
|
+
"""Handle draw events on the map."""
|
|
306
|
+
# Clear previous vertices and remove any existing rectangles
|
|
307
|
+
rectangle_vertices.clear()
|
|
308
|
+
for layer in m.layers:
|
|
309
|
+
if isinstance(layer, Rectangle):
|
|
310
|
+
m.remove_layer(layer)
|
|
311
|
+
|
|
312
|
+
# Process only if a point was drawn on the map
|
|
313
|
+
if action == 'created' and geo_json['geometry']['type'] == 'Point':
|
|
314
|
+
# Extract point coordinates from GeoJSON (lon,lat)
|
|
315
|
+
lon, lat = geo_json['geometry']['coordinates'][0], geo_json['geometry']['coordinates'][1]
|
|
316
|
+
print(f"Point drawn at Longitude: {lon}, Latitude: {lat}")
|
|
317
|
+
|
|
318
|
+
# Calculate corner points using geopy's distance calculator
|
|
319
|
+
# Each point is calculated as a destination from center point using bearing
|
|
320
|
+
north = distance.distance(meters=north_south_length/2).destination((lat, lon), bearing=0)
|
|
321
|
+
south = distance.distance(meters=north_south_length/2).destination((lat, lon), bearing=180)
|
|
322
|
+
east = distance.distance(meters=east_west_length/2).destination((lat, lon), bearing=90)
|
|
323
|
+
west = distance.distance(meters=east_west_length/2).destination((lat, lon), bearing=270)
|
|
324
|
+
|
|
325
|
+
# Create rectangle vertices in counter-clockwise order (lon,lat)
|
|
326
|
+
rectangle_vertices.extend([
|
|
327
|
+
(west.longitude, south.latitude),
|
|
328
|
+
(west.longitude, north.latitude),
|
|
329
|
+
(east.longitude, north.latitude),
|
|
330
|
+
(east.longitude, south.latitude)
|
|
331
|
+
])
|
|
332
|
+
|
|
333
|
+
# Create and add new rectangle to map (ipyleaflet expects lat,lon)
|
|
334
|
+
rectangle = Rectangle(
|
|
335
|
+
bounds=[(north.latitude, west.longitude), (south.latitude, east.longitude)],
|
|
336
|
+
color="red",
|
|
337
|
+
fill_color="red",
|
|
338
|
+
fill_opacity=0.2
|
|
339
|
+
)
|
|
340
|
+
m.add_layer(rectangle)
|
|
341
|
+
|
|
342
|
+
print("Rectangle vertices:")
|
|
343
|
+
for vertex in rectangle_vertices:
|
|
344
|
+
print(f"Longitude: {vertex[0]}, Latitude: {vertex[1]}")
|
|
345
|
+
|
|
346
|
+
# Configure drawing controls - only enable point drawing
|
|
347
|
+
draw_control = DrawControl()
|
|
348
|
+
draw_control.polyline = {}
|
|
349
|
+
draw_control.polygon = {}
|
|
350
|
+
draw_control.circle = {}
|
|
351
|
+
draw_control.rectangle = {}
|
|
352
|
+
draw_control.marker = {}
|
|
353
|
+
m.add_control(draw_control)
|
|
354
|
+
|
|
355
|
+
# Register event handler for drawing actions
|
|
356
|
+
draw_control.on_draw(handle_draw)
|
|
357
|
+
|
|
358
|
+
return m, rectangle_vertices
|
|
359
|
+
|
|
360
|
+
def display_buildings_and_draw_polygon(building_gdf=None, rectangle_vertices=None, zoom=17):
|
|
361
|
+
"""
|
|
362
|
+
Displays building footprints and enables polygon drawing on an interactive map.
|
|
363
|
+
|
|
364
|
+
This function creates an interactive map that visualizes building footprints and
|
|
365
|
+
allows users to draw arbitrary polygons. It's particularly useful for selecting
|
|
366
|
+
specific buildings or areas within an urban context.
|
|
367
|
+
|
|
368
|
+
The function provides three key features:
|
|
369
|
+
1. Building Footprint Visualization:
|
|
370
|
+
- Displays building polygons from a GeoDataFrame
|
|
371
|
+
- Uses consistent styling for all buildings
|
|
372
|
+
- Handles simple polygon geometries only
|
|
373
|
+
|
|
374
|
+
2. Interactive Polygon Drawing:
|
|
375
|
+
- Enables free-form polygon drawing
|
|
376
|
+
- Captures vertices in consistent (lon,lat) format
|
|
377
|
+
- Maintains GeoJSON compatibility
|
|
378
|
+
- Supports multiple polygons with unique IDs and colors
|
|
379
|
+
|
|
380
|
+
3. Map Initialization:
|
|
381
|
+
- Automatic centering based on input data
|
|
382
|
+
- Fallback to default location if no data provided
|
|
383
|
+
- Support for both building data and rectangle bounds
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
building_gdf (GeoDataFrame, optional): A GeoDataFrame containing building footprints.
|
|
387
|
+
Must have geometry column with Polygon type features.
|
|
388
|
+
Geometries should be in [lon, lat] coordinate order.
|
|
389
|
+
If None, only the base map is displayed.
|
|
390
|
+
rectangle_vertices (list, optional): List of [lon, lat] coordinates defining rectangle corners.
|
|
391
|
+
Used to set the initial map view extent.
|
|
392
|
+
Takes precedence over building_gdf for determining map center.
|
|
393
|
+
zoom (int): Initial zoom level for the map. Default=17.
|
|
394
|
+
Range: 0 (most zoomed out) to 18 (most zoomed in).
|
|
395
|
+
Default of 17 is optimized for building-level detail.
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
tuple: (map_object, drawn_polygons)
|
|
399
|
+
- map_object: ipyleaflet Map instance with building footprints and drawing controls
|
|
400
|
+
- drawn_polygons: List of dictionaries with 'id', 'vertices', and 'color' keys for all drawn polygons.
|
|
401
|
+
Each polygon has a unique ID and color for easy identification.
|
|
402
|
+
|
|
403
|
+
Note:
|
|
404
|
+
- Building footprints are displayed in blue with 20% opacity
|
|
405
|
+
- Only simple Polygon geometries are supported (no MultiPolygons)
|
|
406
|
+
- Drawing tools are restricted to polygon creation only
|
|
407
|
+
- All coordinates are handled in (lon,lat) order internally
|
|
408
|
+
- The function automatically determines appropriate map bounds
|
|
409
|
+
- Each polygon gets a unique ID and different colors for easy identification
|
|
410
|
+
- Use get_polygon_vertices() helper function to extract specific polygon data
|
|
411
|
+
"""
|
|
412
|
+
# ---------------------------------------------------------
|
|
413
|
+
# 1. Determine a suitable map center via bounding box logic
|
|
414
|
+
# ---------------------------------------------------------
|
|
415
|
+
if rectangle_vertices is not None:
|
|
416
|
+
# Get bounds from rectangle vertices
|
|
417
|
+
lons = [v[0] for v in rectangle_vertices]
|
|
418
|
+
lats = [v[1] for v in rectangle_vertices]
|
|
419
|
+
min_lon, max_lon = min(lons), max(lons)
|
|
420
|
+
min_lat, max_lat = min(lats), max(lats)
|
|
421
|
+
center_lon = (min_lon + max_lon) / 2
|
|
422
|
+
center_lat = (min_lat + max_lat) / 2
|
|
423
|
+
elif building_gdf is not None and len(building_gdf) > 0:
|
|
424
|
+
# Get bounds from GeoDataFrame
|
|
425
|
+
bounds = building_gdf.total_bounds # Returns [minx, miny, maxx, maxy]
|
|
426
|
+
min_lon, min_lat, max_lon, max_lat = bounds
|
|
427
|
+
center_lon = (min_lon + max_lon) / 2
|
|
428
|
+
center_lat = (min_lat + max_lat) / 2
|
|
429
|
+
else:
|
|
430
|
+
# Fallback: If no inputs or invalid data, pick a default
|
|
431
|
+
center_lon, center_lat = -100.0, 40.0
|
|
432
|
+
|
|
433
|
+
# Create the ipyleaflet map (needs lat,lon)
|
|
434
|
+
m = Map(center=(center_lat, center_lon), zoom=zoom, scroll_wheel_zoom=True)
|
|
435
|
+
|
|
436
|
+
# -----------------------------------------
|
|
437
|
+
# 2. Add building footprints to the map if provided
|
|
438
|
+
# -----------------------------------------
|
|
439
|
+
if building_gdf is not None:
|
|
440
|
+
for idx, row in building_gdf.iterrows():
|
|
441
|
+
# Only handle simple Polygons
|
|
442
|
+
if isinstance(row.geometry, geom.Polygon):
|
|
443
|
+
# Get coordinates from geometry
|
|
444
|
+
coords = list(row.geometry.exterior.coords)
|
|
445
|
+
# Convert to (lat,lon) for ipyleaflet, skip last repeated coordinate
|
|
446
|
+
lat_lon_coords = [(c[1], c[0]) for c in coords[:-1]]
|
|
447
|
+
|
|
448
|
+
# Create the polygon layer
|
|
449
|
+
bldg_layer = LeafletPolygon(
|
|
450
|
+
locations=lat_lon_coords,
|
|
451
|
+
color="blue",
|
|
452
|
+
fill_color="blue",
|
|
453
|
+
fill_opacity=0.2,
|
|
454
|
+
weight=2
|
|
455
|
+
)
|
|
456
|
+
m.add_layer(bldg_layer)
|
|
457
|
+
|
|
458
|
+
# -----------------------------------------------------------------
|
|
459
|
+
# 3. Enable drawing of polygons, capturing the vertices in Lon-Lat
|
|
460
|
+
# -----------------------------------------------------------------
|
|
461
|
+
# Store multiple polygons with IDs and colors
|
|
462
|
+
drawn_polygons = [] # List of dicts with 'id', 'vertices', 'color' keys
|
|
463
|
+
polygon_counter = 0
|
|
464
|
+
polygon_colors = ['red', 'blue', 'green', 'orange', 'purple', 'brown', 'pink', 'gray', 'olive', 'cyan']
|
|
465
|
+
|
|
466
|
+
draw_control = DrawControl(
|
|
467
|
+
polygon={
|
|
468
|
+
"shapeOptions": {
|
|
469
|
+
"color": "red",
|
|
470
|
+
"fillColor": "red",
|
|
471
|
+
"fillOpacity": 0.2
|
|
472
|
+
}
|
|
473
|
+
},
|
|
474
|
+
rectangle={}, # Disable rectangles (or enable if needed)
|
|
475
|
+
circle={}, # Disable circles
|
|
476
|
+
circlemarker={}, # Disable circlemarkers
|
|
477
|
+
polyline={}, # Disable polylines
|
|
478
|
+
marker={} # Disable markers
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
def handle_draw(self, action, geo_json):
|
|
482
|
+
"""
|
|
483
|
+
Callback for whenever a shape is created or edited.
|
|
484
|
+
ipyleaflet's DrawControl returns standard GeoJSON (lon, lat).
|
|
485
|
+
We'll keep them as (lon, lat).
|
|
486
|
+
"""
|
|
487
|
+
if action == 'created' and geo_json['geometry']['type'] == 'Polygon':
|
|
488
|
+
nonlocal polygon_counter
|
|
489
|
+
polygon_counter += 1
|
|
490
|
+
|
|
491
|
+
# The polygon's first ring
|
|
492
|
+
coordinates = geo_json['geometry']['coordinates'][0]
|
|
493
|
+
vertices = [(coord[0], coord[1]) for coord in coordinates[:-1]]
|
|
494
|
+
|
|
495
|
+
# Assign color (cycle through colors)
|
|
496
|
+
color = polygon_colors[polygon_counter % len(polygon_colors)]
|
|
497
|
+
|
|
498
|
+
# Store polygon data
|
|
499
|
+
polygon_data = {
|
|
500
|
+
'id': polygon_counter,
|
|
501
|
+
'vertices': vertices,
|
|
502
|
+
'color': color
|
|
503
|
+
}
|
|
504
|
+
drawn_polygons.append(polygon_data)
|
|
505
|
+
|
|
506
|
+
print(f"Polygon {polygon_counter} drawn with {len(vertices)} vertices (color: {color}):")
|
|
507
|
+
for i, (lon, lat) in enumerate(vertices):
|
|
508
|
+
print(f" Vertex {i+1}: (lon, lat) = ({lon}, {lat})")
|
|
509
|
+
print(f"Total polygons: {len(drawn_polygons)}")
|
|
510
|
+
|
|
511
|
+
draw_control.on_draw(handle_draw)
|
|
512
|
+
m.add_control(draw_control)
|
|
513
|
+
|
|
514
|
+
return m, drawn_polygons
|
|
515
|
+
|
|
516
|
+
def draw_additional_buildings(building_gdf=None, initial_center=None, zoom=17, rectangle_vertices=None):
|
|
517
|
+
"""
|
|
518
|
+
Creates an interactive map for drawing building footprints with height input.
|
|
519
|
+
|
|
520
|
+
This function provides an interface for users to:
|
|
521
|
+
1. Draw building footprints on an interactive map
|
|
522
|
+
2. Set building height values through a UI widget
|
|
523
|
+
3. Add new buildings to the existing building_gdf
|
|
524
|
+
|
|
525
|
+
The workflow is:
|
|
526
|
+
- User draws a polygon on the map
|
|
527
|
+
- Height input widget appears
|
|
528
|
+
- User enters height and clicks "Add Building"
|
|
529
|
+
- Building is added to GeoDataFrame and displayed on map
|
|
530
|
+
|
|
531
|
+
Args:
|
|
532
|
+
building_gdf (GeoDataFrame, optional): Existing building footprints to display.
|
|
533
|
+
If None, creates a new empty GeoDataFrame.
|
|
534
|
+
Expected columns: ['id', 'height', 'min_height', 'geometry', 'building_id']
|
|
535
|
+
- 'id': Integer ID from data sources (e.g., OSM building id)
|
|
536
|
+
- 'height': Building height in meters (set by user input)
|
|
537
|
+
- 'min_height': Minimum height in meters (defaults to 0.0)
|
|
538
|
+
- 'geometry': Building footprint polygon
|
|
539
|
+
- 'building_id': Unique building identifier
|
|
540
|
+
initial_center (tuple, optional): Initial map center as (lon, lat).
|
|
541
|
+
If None, centers on existing buildings or defaults to (-100, 40).
|
|
542
|
+
zoom (int): Initial zoom level (default=17).
|
|
543
|
+
|
|
544
|
+
Returns:
|
|
545
|
+
tuple: (map_object, updated_building_gdf)
|
|
546
|
+
- map_object: ipyleaflet Map instance with drawing controls
|
|
547
|
+
- updated_building_gdf: GeoDataFrame that automatically updates when buildings are added
|
|
548
|
+
|
|
549
|
+
Example:
|
|
550
|
+
>>> # Start with empty buildings
|
|
551
|
+
>>> m, buildings = draw_additional_buildings()
|
|
552
|
+
>>> # Draw buildings on the map...
|
|
553
|
+
>>> print(buildings) # Will contain all drawn buildings
|
|
554
|
+
"""
|
|
555
|
+
|
|
556
|
+
# Initialize or copy the building GeoDataFrame
|
|
557
|
+
if building_gdf is None:
|
|
558
|
+
# Create empty GeoDataFrame with required columns
|
|
559
|
+
updated_gdf = gpd.GeoDataFrame(
|
|
560
|
+
columns=['id', 'height', 'min_height', 'geometry', 'building_id'],
|
|
561
|
+
crs='EPSG:4326'
|
|
562
|
+
)
|
|
563
|
+
else:
|
|
564
|
+
# Make a copy to avoid modifying the original
|
|
565
|
+
updated_gdf = building_gdf.copy()
|
|
566
|
+
# Ensure all required columns exist
|
|
567
|
+
if 'height' not in updated_gdf.columns:
|
|
568
|
+
updated_gdf['height'] = 10.0 # Default height
|
|
569
|
+
if 'min_height' not in updated_gdf.columns:
|
|
570
|
+
updated_gdf['min_height'] = 0.0 # Default min_height
|
|
571
|
+
if 'building_id' not in updated_gdf.columns:
|
|
572
|
+
updated_gdf['building_id'] = range(len(updated_gdf))
|
|
573
|
+
if 'id' not in updated_gdf.columns:
|
|
574
|
+
updated_gdf['id'] = range(len(updated_gdf))
|
|
575
|
+
|
|
576
|
+
# Determine map center
|
|
577
|
+
if initial_center is not None:
|
|
578
|
+
center_lon, center_lat = initial_center
|
|
579
|
+
elif updated_gdf is not None and len(updated_gdf) > 0:
|
|
580
|
+
bounds = updated_gdf.total_bounds
|
|
581
|
+
min_lon, min_lat, max_lon, max_lat = bounds
|
|
582
|
+
center_lon = (min_lon + max_lon) / 2
|
|
583
|
+
center_lat = (min_lat + max_lat) / 2
|
|
584
|
+
elif rectangle_vertices is not None:
|
|
585
|
+
center_lon, center_lat = (rectangle_vertices[0][0] + rectangle_vertices[2][0]) / 2, (rectangle_vertices[0][1] + rectangle_vertices[2][1]) / 2
|
|
586
|
+
else:
|
|
587
|
+
center_lon, center_lat = -100.0, 40.0
|
|
588
|
+
|
|
589
|
+
# Create the map
|
|
590
|
+
m = Map(center=(center_lat, center_lon), zoom=zoom, scroll_wheel_zoom=True)
|
|
591
|
+
|
|
592
|
+
# Display existing buildings
|
|
593
|
+
building_layers = {}
|
|
594
|
+
for idx, row in updated_gdf.iterrows():
|
|
595
|
+
if isinstance(row.geometry, geom.Polygon):
|
|
596
|
+
coords = list(row.geometry.exterior.coords)
|
|
597
|
+
lat_lon_coords = [(c[1], c[0]) for c in coords[:-1]]
|
|
598
|
+
|
|
599
|
+
height = row.get('height', 10.0)
|
|
600
|
+
min_height = row.get('min_height', 0.0)
|
|
601
|
+
building_id = row.get('building_id', idx)
|
|
602
|
+
bldg_id = row.get('id', idx)
|
|
603
|
+
bldg_layer = LeafletPolygon(
|
|
604
|
+
locations=lat_lon_coords,
|
|
605
|
+
color="blue",
|
|
606
|
+
fill_color="blue",
|
|
607
|
+
fill_opacity=0.3,
|
|
608
|
+
weight=2,
|
|
609
|
+
popup=HTML(f"<b>Building ID:</b> {building_id}<br>"
|
|
610
|
+
f"<b>ID:</b> {bldg_id}<br>"
|
|
611
|
+
f"<b>Height:</b> {height}m<br>"
|
|
612
|
+
f"<b>Min Height:</b> {min_height}m")
|
|
613
|
+
)
|
|
614
|
+
m.add_layer(bldg_layer)
|
|
615
|
+
building_layers[idx] = bldg_layer
|
|
616
|
+
|
|
617
|
+
# Create UI widgets
|
|
618
|
+
height_input = FloatText(
|
|
619
|
+
value=10.0,
|
|
620
|
+
description='Height (m):',
|
|
621
|
+
disabled=False,
|
|
622
|
+
style={'description_width': 'initial'}
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
add_button = Button(
|
|
626
|
+
description='Add Building',
|
|
627
|
+
button_style='success',
|
|
628
|
+
disabled=True
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
clear_button = Button(
|
|
632
|
+
description='Clear Drawing',
|
|
633
|
+
button_style='warning',
|
|
634
|
+
disabled=True
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
status_output = Output()
|
|
638
|
+
hover_info = HTML("")
|
|
639
|
+
|
|
640
|
+
# Create control panel
|
|
641
|
+
control_panel = VBox([
|
|
642
|
+
HTML("<h3>Draw Building Tool</h3>"),
|
|
643
|
+
HTML("<p>1. Draw a polygon on the map<br>2. Set height<br>3. Click 'Add Building'</p>"),
|
|
644
|
+
height_input,
|
|
645
|
+
HBox([add_button, clear_button]),
|
|
646
|
+
status_output
|
|
647
|
+
])
|
|
648
|
+
|
|
649
|
+
# Add control panel to map
|
|
650
|
+
widget_control = WidgetControl(widget=control_panel, position='topright')
|
|
651
|
+
m.add_control(widget_control)
|
|
652
|
+
|
|
653
|
+
# Store the current drawn polygon
|
|
654
|
+
current_polygon = {'vertices': [], 'layer': None}
|
|
655
|
+
|
|
656
|
+
# Drawing control
|
|
657
|
+
draw_control = DrawControl(
|
|
658
|
+
polygon={
|
|
659
|
+
"shapeOptions": {
|
|
660
|
+
"color": "red",
|
|
661
|
+
"fillColor": "red",
|
|
662
|
+
"fillOpacity": 0.3,
|
|
663
|
+
"weight": 3
|
|
664
|
+
}
|
|
665
|
+
},
|
|
666
|
+
rectangle={},
|
|
667
|
+
circle={},
|
|
668
|
+
circlemarker={},
|
|
669
|
+
polyline={},
|
|
670
|
+
marker={}
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
def handle_draw(self, action, geo_json):
|
|
674
|
+
"""Handle polygon drawing events"""
|
|
675
|
+
with status_output:
|
|
676
|
+
clear_output()
|
|
677
|
+
|
|
678
|
+
if action == 'created' and geo_json['geometry']['type'] == 'Polygon':
|
|
679
|
+
# Store vertices
|
|
680
|
+
coordinates = geo_json['geometry']['coordinates'][0]
|
|
681
|
+
current_polygon['vertices'] = [(coord[0], coord[1]) for coord in coordinates[:-1]]
|
|
682
|
+
|
|
683
|
+
# Enable buttons
|
|
684
|
+
add_button.disabled = False
|
|
685
|
+
clear_button.disabled = False
|
|
686
|
+
|
|
687
|
+
with status_output:
|
|
688
|
+
print(f"Polygon drawn with {len(current_polygon['vertices'])} vertices")
|
|
689
|
+
print("Set height and click 'Add Building'")
|
|
690
|
+
|
|
691
|
+
def add_building_click(b):
|
|
692
|
+
"""Handle add building button click"""
|
|
693
|
+
# Use nonlocal to modify the outer scope variable
|
|
694
|
+
nonlocal updated_gdf
|
|
695
|
+
|
|
696
|
+
with status_output:
|
|
697
|
+
clear_output()
|
|
698
|
+
|
|
699
|
+
if current_polygon['vertices']:
|
|
700
|
+
# Create polygon geometry
|
|
701
|
+
polygon = geom.Polygon(current_polygon['vertices'])
|
|
702
|
+
|
|
703
|
+
# Get next building ID and ID values (ensure uniqueness)
|
|
704
|
+
if len(updated_gdf) > 0:
|
|
705
|
+
next_building_id = int(updated_gdf['building_id'].max() + 1)
|
|
706
|
+
next_id = int(updated_gdf['id'].max() + 1)
|
|
707
|
+
else:
|
|
708
|
+
next_building_id = 1
|
|
709
|
+
next_id = 1
|
|
710
|
+
|
|
711
|
+
# Create new row data
|
|
712
|
+
new_row_data = {
|
|
713
|
+
'geometry': polygon,
|
|
714
|
+
'height': float(height_input.value),
|
|
715
|
+
'min_height': 0.0, # Default value as requested
|
|
716
|
+
'building_id': next_building_id,
|
|
717
|
+
'id': next_id
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
# Add any additional columns
|
|
721
|
+
for col in updated_gdf.columns:
|
|
722
|
+
if col not in new_row_data:
|
|
723
|
+
new_row_data[col] = None
|
|
724
|
+
|
|
725
|
+
# Append the new building in-place
|
|
726
|
+
new_index = len(updated_gdf)
|
|
727
|
+
updated_gdf.loc[new_index] = new_row_data
|
|
728
|
+
|
|
729
|
+
# Add to map
|
|
730
|
+
coords = list(polygon.exterior.coords)
|
|
731
|
+
lat_lon_coords = [(c[1], c[0]) for c in coords[:-1]]
|
|
732
|
+
|
|
733
|
+
new_layer = LeafletPolygon(
|
|
734
|
+
locations=lat_lon_coords,
|
|
735
|
+
color="blue",
|
|
736
|
+
fill_color="blue",
|
|
737
|
+
fill_opacity=0.3,
|
|
738
|
+
weight=2,
|
|
739
|
+
popup=HTML(f"<b>Building ID:</b> {next_building_id}<br>"
|
|
740
|
+
f"<b>ID:</b> {next_id}<br>"
|
|
741
|
+
f"<b>Height:</b> {height_input.value}m<br>"
|
|
742
|
+
f"<b>Min Height:</b> 0.0m")
|
|
743
|
+
)
|
|
744
|
+
m.add_layer(new_layer)
|
|
745
|
+
|
|
746
|
+
# Clear drawing
|
|
747
|
+
draw_control.clear()
|
|
748
|
+
current_polygon['vertices'] = []
|
|
749
|
+
add_button.disabled = True
|
|
750
|
+
clear_button.disabled = True
|
|
751
|
+
|
|
752
|
+
print(f"Building {next_building_id} added successfully!")
|
|
753
|
+
print(f"ID: {next_id}, Height: {height_input.value}m, Min Height: 0.0m")
|
|
754
|
+
print(f"Total buildings: {len(updated_gdf)}")
|
|
755
|
+
|
|
756
|
+
def clear_drawing_click(b):
|
|
757
|
+
"""Handle clear drawing button click"""
|
|
758
|
+
with status_output:
|
|
759
|
+
clear_output()
|
|
760
|
+
draw_control.clear()
|
|
761
|
+
current_polygon['vertices'] = []
|
|
762
|
+
add_button.disabled = True
|
|
763
|
+
clear_button.disabled = True
|
|
764
|
+
print("Drawing cleared")
|
|
765
|
+
|
|
766
|
+
# Connect event handlers
|
|
767
|
+
draw_control.on_draw(handle_draw)
|
|
768
|
+
add_button.on_click(add_building_click)
|
|
769
|
+
clear_button.on_click(clear_drawing_click)
|
|
770
|
+
|
|
771
|
+
# Add draw control to map
|
|
772
|
+
m.add_control(draw_control)
|
|
773
|
+
|
|
774
|
+
# Display initial status
|
|
775
|
+
with status_output:
|
|
776
|
+
print(f"Total buildings loaded: {len(updated_gdf)}")
|
|
777
|
+
print("Draw a polygon to add a new building")
|
|
778
|
+
|
|
779
|
+
return m, updated_gdf
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
def get_polygon_vertices(drawn_polygons, polygon_id=None):
|
|
783
|
+
"""
|
|
784
|
+
Extract vertices from drawn polygons data structure.
|
|
785
|
+
|
|
786
|
+
This helper function provides a convenient way to extract polygon vertices
|
|
787
|
+
from the drawn_polygons list returned by display_buildings_and_draw_polygon().
|
|
788
|
+
|
|
789
|
+
Args:
|
|
790
|
+
drawn_polygons: The drawn_polygons list returned from display_buildings_and_draw_polygon()
|
|
791
|
+
polygon_id (int, optional): Specific polygon ID to extract. If None, returns all polygons.
|
|
792
|
+
|
|
793
|
+
Returns:
|
|
794
|
+
If polygon_id is specified: List of (lon, lat) tuples for that polygon
|
|
795
|
+
If polygon_id is None: List of lists, where each inner list contains (lon, lat) tuples
|
|
796
|
+
|
|
797
|
+
Example:
|
|
798
|
+
>>> m, polygons = display_buildings_and_draw_polygon()
|
|
799
|
+
>>> # Draw some polygons...
|
|
800
|
+
>>> vertices = get_polygon_vertices(polygons, polygon_id=1) # Get polygon 1
|
|
801
|
+
>>> all_vertices = get_polygon_vertices(polygons) # Get all polygons
|
|
802
|
+
"""
|
|
803
|
+
if not drawn_polygons:
|
|
804
|
+
return []
|
|
805
|
+
|
|
806
|
+
if polygon_id is not None:
|
|
807
|
+
# Return specific polygon
|
|
808
|
+
for polygon in drawn_polygons:
|
|
809
|
+
if polygon['id'] == polygon_id:
|
|
810
|
+
return polygon['vertices']
|
|
811
|
+
return [] # Polygon not found
|
|
812
|
+
else:
|
|
813
|
+
# Return all polygons
|
|
814
|
+
return [polygon['vertices'] for polygon in drawn_polygons]
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
# Simple convenience function
|
|
818
|
+
def create_building_editor(building_gdf=None, initial_center=None, zoom=17, rectangle_vertices=None):
|
|
819
|
+
"""
|
|
820
|
+
Creates and displays an interactive building editor.
|
|
821
|
+
|
|
822
|
+
Args:
|
|
823
|
+
building_gdf: Existing buildings GeoDataFrame (optional)
|
|
824
|
+
initial_center: Map center as (lon, lat) tuple (optional)
|
|
825
|
+
zoom: Initial zoom level (default=17)
|
|
826
|
+
|
|
827
|
+
Returns:
|
|
828
|
+
GeoDataFrame: The building GeoDataFrame that automatically updates
|
|
829
|
+
|
|
830
|
+
Example:
|
|
831
|
+
>>> buildings = create_building_editor()
|
|
832
|
+
>>> # Draw buildings on the displayed map
|
|
833
|
+
>>> print(buildings) # Automatically contains all drawn buildings
|
|
834
|
+
"""
|
|
835
|
+
m, gdf = draw_additional_buildings(building_gdf, initial_center, zoom, rectangle_vertices)
|
|
836
|
+
display(m)
|
|
837
|
+
return gdf
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
def draw_additional_trees(tree_gdf=None, initial_center=None, zoom=17, rectangle_vertices=None):
|
|
841
|
+
"""
|
|
842
|
+
Creates an interactive map to add trees by clicking and setting parameters.
|
|
843
|
+
|
|
844
|
+
Users can:
|
|
845
|
+
- Set tree parameters: top height, bottom height, crown diameter
|
|
846
|
+
- Click multiple times to add multiple trees with the same parameters
|
|
847
|
+
- Update parameters at any time to change subsequent trees
|
|
848
|
+
|
|
849
|
+
Args:
|
|
850
|
+
tree_gdf (GeoDataFrame, optional): Existing trees to display.
|
|
851
|
+
Expected columns: ['tree_id', 'top_height', 'bottom_height', 'crown_diameter', 'geometry']
|
|
852
|
+
initial_center (tuple, optional): (lon, lat) for initial map center.
|
|
853
|
+
zoom (int): Initial zoom level. Default=17.
|
|
854
|
+
rectangle_vertices (list, optional): If provided, used to set center like buildings.
|
|
855
|
+
|
|
856
|
+
Returns:
|
|
857
|
+
tuple: (map_object, updated_tree_gdf)
|
|
858
|
+
"""
|
|
859
|
+
# Initialize or copy the tree GeoDataFrame
|
|
860
|
+
if tree_gdf is None:
|
|
861
|
+
updated_trees = gpd.GeoDataFrame(
|
|
862
|
+
columns=['tree_id', 'top_height', 'bottom_height', 'crown_diameter', 'geometry'],
|
|
863
|
+
crs='EPSG:4326'
|
|
864
|
+
)
|
|
865
|
+
else:
|
|
866
|
+
updated_trees = tree_gdf.copy()
|
|
867
|
+
# Ensure required columns exist
|
|
868
|
+
if 'tree_id' not in updated_trees.columns:
|
|
869
|
+
updated_trees['tree_id'] = range(1, len(updated_trees) + 1)
|
|
870
|
+
for col, default in [('top_height', 10.0), ('bottom_height', 4.0), ('crown_diameter', 6.0)]:
|
|
871
|
+
if col not in updated_trees.columns:
|
|
872
|
+
updated_trees[col] = default
|
|
873
|
+
|
|
874
|
+
# Determine map center
|
|
875
|
+
if initial_center is not None:
|
|
876
|
+
center_lon, center_lat = initial_center
|
|
877
|
+
elif updated_trees is not None and len(updated_trees) > 0:
|
|
878
|
+
min_lon, min_lat, max_lon, max_lat = updated_trees.total_bounds
|
|
879
|
+
center_lon = (min_lon + max_lon) / 2
|
|
880
|
+
center_lat = (min_lat + max_lat) / 2
|
|
881
|
+
elif rectangle_vertices is not None:
|
|
882
|
+
center_lon, center_lat = (rectangle_vertices[0][0] + rectangle_vertices[2][0]) / 2, (rectangle_vertices[0][1] + rectangle_vertices[2][1]) / 2
|
|
883
|
+
else:
|
|
884
|
+
center_lon, center_lat = -100.0, 40.0
|
|
885
|
+
|
|
886
|
+
# Create map
|
|
887
|
+
m = Map(center=(center_lat, center_lon), zoom=zoom, scroll_wheel_zoom=True)
|
|
888
|
+
# Add aerial/satellite basemap
|
|
889
|
+
try:
|
|
890
|
+
m.add_layer(basemap_to_tiles(basemaps.Esri.WorldImagery))
|
|
891
|
+
except Exception:
|
|
892
|
+
# Fallback silently if basemap cannot be added
|
|
893
|
+
pass
|
|
894
|
+
|
|
895
|
+
# If rectangle_vertices provided, draw its edges on the map
|
|
896
|
+
if rectangle_vertices is not None and len(rectangle_vertices) >= 4:
|
|
897
|
+
try:
|
|
898
|
+
lat_lon_coords = [(lat, lon) for lon, lat in rectangle_vertices]
|
|
899
|
+
rect_outline = LeafletPolygon(
|
|
900
|
+
locations=lat_lon_coords,
|
|
901
|
+
color="#fed766",
|
|
902
|
+
weight=2,
|
|
903
|
+
fill_color="#fed766",
|
|
904
|
+
fill_opacity=0.0
|
|
905
|
+
)
|
|
906
|
+
m.add_layer(rect_outline)
|
|
907
|
+
except Exception:
|
|
908
|
+
pass
|
|
909
|
+
|
|
910
|
+
# Display existing trees as circles
|
|
911
|
+
tree_layers = {}
|
|
912
|
+
for idx, row in updated_trees.iterrows():
|
|
913
|
+
if row.geometry is not None and hasattr(row.geometry, 'x'):
|
|
914
|
+
lat = row.geometry.y
|
|
915
|
+
lon = row.geometry.x
|
|
916
|
+
# Ensure integer radius in meters as required by ipyleaflet Circle
|
|
917
|
+
radius_m = max(int(round(float(row.get('crown_diameter', 6.0)) / 2.0)), 1)
|
|
918
|
+
tree_id_val = int(row.get('tree_id', idx+1))
|
|
919
|
+
circle = Circle(location=(lat, lon), radius=radius_m, color='#2ab7ca', weight=1, opacity=1.0, fill_color='#2ab7ca', fill_opacity=0.3)
|
|
920
|
+
m.add_layer(circle)
|
|
921
|
+
tree_layers[tree_id_val] = circle
|
|
922
|
+
|
|
923
|
+
# UI widgets for parameters
|
|
924
|
+
top_height_input = FloatText(value=10.0, description='Top height (m):', disabled=False, style={'description_width': 'initial'})
|
|
925
|
+
bottom_height_input = FloatText(value=4.0, description='Bottom height (m):', disabled=False, style={'description_width': 'initial'})
|
|
926
|
+
crown_diameter_input = FloatText(value=6.0, description='Crown diameter (m):', disabled=False, style={'description_width': 'initial'})
|
|
927
|
+
|
|
928
|
+
add_mode_button = Button(description='Add', button_style='success')
|
|
929
|
+
remove_mode_button = Button(description='Remove', button_style='')
|
|
930
|
+
status_output = Output()
|
|
931
|
+
hover_info = HTML("")
|
|
932
|
+
|
|
933
|
+
control_panel = VBox([
|
|
934
|
+
HTML("<h3 style=\"margin:0 0 4px 0;\">Tree Placement Tool</h3>"),
|
|
935
|
+
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>"),
|
|
936
|
+
HBox([add_mode_button, remove_mode_button]),
|
|
937
|
+
top_height_input,
|
|
938
|
+
bottom_height_input,
|
|
939
|
+
crown_diameter_input,
|
|
940
|
+
hover_info,
|
|
941
|
+
status_output
|
|
942
|
+
])
|
|
943
|
+
|
|
944
|
+
widget_control = WidgetControl(widget=control_panel, position='topright')
|
|
945
|
+
m.add_control(widget_control)
|
|
946
|
+
|
|
947
|
+
# State for mode
|
|
948
|
+
mode = 'add'
|
|
949
|
+
|
|
950
|
+
def set_mode(new_mode):
|
|
951
|
+
nonlocal mode
|
|
952
|
+
mode = new_mode
|
|
953
|
+
# Visual feedback
|
|
954
|
+
add_mode_button.button_style = 'success' if mode == 'add' else ''
|
|
955
|
+
remove_mode_button.button_style = 'danger' if mode == 'remove' else ''
|
|
956
|
+
# No on-screen mode label
|
|
957
|
+
|
|
958
|
+
def on_click_add(b):
|
|
959
|
+
set_mode('add')
|
|
960
|
+
|
|
961
|
+
def on_click_remove(b):
|
|
962
|
+
set_mode('remove')
|
|
963
|
+
|
|
964
|
+
add_mode_button.on_click(on_click_add)
|
|
965
|
+
remove_mode_button.on_click(on_click_remove)
|
|
966
|
+
|
|
967
|
+
# Consecutive placements by map click
|
|
968
|
+
def handle_map_click(**kwargs):
|
|
969
|
+
nonlocal updated_trees
|
|
970
|
+
with status_output:
|
|
971
|
+
clear_output()
|
|
972
|
+
|
|
973
|
+
if kwargs.get('type') == 'click':
|
|
974
|
+
lat, lon = kwargs.get('coordinates', (None, None))
|
|
975
|
+
if lat is None or lon is None:
|
|
976
|
+
return
|
|
977
|
+
if mode == 'add':
|
|
978
|
+
# Determine next tree_id
|
|
979
|
+
next_tree_id = int(updated_trees['tree_id'].max() + 1) if len(updated_trees) > 0 else 1
|
|
980
|
+
|
|
981
|
+
# Clamp/validate parameters
|
|
982
|
+
th = float(top_height_input.value)
|
|
983
|
+
bh = float(bottom_height_input.value)
|
|
984
|
+
cd = float(crown_diameter_input.value)
|
|
985
|
+
if bh > th:
|
|
986
|
+
bh, th = th, bh
|
|
987
|
+
if cd < 0:
|
|
988
|
+
cd = 0.0
|
|
989
|
+
|
|
990
|
+
# Create new tree row
|
|
991
|
+
new_row = {
|
|
992
|
+
'tree_id': next_tree_id,
|
|
993
|
+
'top_height': th,
|
|
994
|
+
'bottom_height': bh,
|
|
995
|
+
'crown_diameter': cd,
|
|
996
|
+
'geometry': geom.Point(lon, lat)
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
# Append
|
|
1000
|
+
new_index = len(updated_trees)
|
|
1001
|
+
updated_trees.loc[new_index] = new_row
|
|
1002
|
+
|
|
1003
|
+
# Add circle layer representing crown diameter (radius in meters)
|
|
1004
|
+
radius_m = max(int(round(new_row['crown_diameter'] / 2.0)), 1)
|
|
1005
|
+
circle = Circle(location=(lat, lon), radius=radius_m, color='#2ab7ca', weight=1, opacity=1.0, fill_color='#2ab7ca', fill_opacity=0.3)
|
|
1006
|
+
m.add_layer(circle)
|
|
1007
|
+
|
|
1008
|
+
tree_layers[next_tree_id] = circle
|
|
1009
|
+
|
|
1010
|
+
with status_output:
|
|
1011
|
+
print(f"Tree {next_tree_id} added at (lon, lat)=({lon:.6f}, {lat:.6f})")
|
|
1012
|
+
print(f"Top: {new_row['top_height']} m, Bottom: {new_row['bottom_height']} m, Crown: {new_row['crown_diameter']} m")
|
|
1013
|
+
print(f"Total trees: {len(updated_trees)}")
|
|
1014
|
+
else:
|
|
1015
|
+
# Remove mode: find the nearest tree within its crown radius + 5m
|
|
1016
|
+
candidate_id = None
|
|
1017
|
+
candidate_idx = None
|
|
1018
|
+
candidate_dist = None
|
|
1019
|
+
for idx2, row2 in updated_trees.iterrows():
|
|
1020
|
+
if row2.geometry is None or not hasattr(row2.geometry, 'x'):
|
|
1021
|
+
continue
|
|
1022
|
+
lat2 = row2.geometry.y
|
|
1023
|
+
lon2 = row2.geometry.x
|
|
1024
|
+
dist_m = distance.distance((lat, lon), (lat2, lon2)).meters
|
|
1025
|
+
rad_m = max(int(round(float(row2.get('crown_diameter', 6.0)) / 2.0)), 1)
|
|
1026
|
+
thr_m = rad_m + 5
|
|
1027
|
+
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):
|
|
1028
|
+
candidate_dist = dist_m
|
|
1029
|
+
candidate_id = int(row2.get('tree_id', idx2+1))
|
|
1030
|
+
candidate_idx = idx2
|
|
1031
|
+
|
|
1032
|
+
if candidate_id is not None:
|
|
1033
|
+
# Remove layer
|
|
1034
|
+
layer = tree_layers.get(candidate_id)
|
|
1035
|
+
if layer is not None:
|
|
1036
|
+
m.remove_layer(layer)
|
|
1037
|
+
del tree_layers[candidate_id]
|
|
1038
|
+
# Remove from gdf
|
|
1039
|
+
updated_trees.drop(index=candidate_idx, inplace=True)
|
|
1040
|
+
updated_trees.reset_index(drop=True, inplace=True)
|
|
1041
|
+
with status_output:
|
|
1042
|
+
print(f"Removed tree {candidate_id} (distance {candidate_dist:.2f} m)")
|
|
1043
|
+
print(f"Total trees: {len(updated_trees)}")
|
|
1044
|
+
else:
|
|
1045
|
+
with status_output:
|
|
1046
|
+
print("No tree near the clicked location to remove")
|
|
1047
|
+
elif kwargs.get('type') == 'mousemove':
|
|
1048
|
+
lat, lon = kwargs.get('coordinates', (None, None))
|
|
1049
|
+
if lat is None or lon is None:
|
|
1050
|
+
return
|
|
1051
|
+
# Find a tree the cursor is over (within crown radius)
|
|
1052
|
+
shown = False
|
|
1053
|
+
for _, row2 in updated_trees.iterrows():
|
|
1054
|
+
if row2.geometry is None or not hasattr(row2.geometry, 'x'):
|
|
1055
|
+
continue
|
|
1056
|
+
lat2 = row2.geometry.y
|
|
1057
|
+
lon2 = row2.geometry.x
|
|
1058
|
+
dist_m = distance.distance((lat, lon), (lat2, lon2)).meters
|
|
1059
|
+
rad_m = max(int(round(float(row2.get('crown_diameter', 6.0)) / 2.0)), 1)
|
|
1060
|
+
if dist_m <= rad_m:
|
|
1061
|
+
hover_info.value = (
|
|
1062
|
+
f"<div style=\"color:#d61f1f; font-weight:600; margin:2px 0;\">"
|
|
1063
|
+
f"Tree {int(row2.get('tree_id', 0))} | Top {float(row2.get('top_height', 10.0))} m | "
|
|
1064
|
+
f"Bottom {float(row2.get('bottom_height', 0.0))} m | Crown {float(row2.get('crown_diameter', 6.0))} m"
|
|
1065
|
+
f"</div>"
|
|
1066
|
+
)
|
|
1067
|
+
shown = True
|
|
1068
|
+
break
|
|
1069
|
+
if not shown:
|
|
1070
|
+
hover_info.value = ""
|
|
1071
|
+
m.on_interaction(handle_map_click)
|
|
1072
|
+
|
|
1073
|
+
with status_output:
|
|
1074
|
+
print(f"Total trees loaded: {len(updated_trees)}")
|
|
1075
|
+
print("Set parameters, then click on the map to add trees")
|
|
1076
|
+
|
|
1077
|
+
return m, updated_trees
|
|
1078
|
+
|
|
1079
|
+
|
|
1080
|
+
def create_tree_editor(tree_gdf=None, initial_center=None, zoom=17, rectangle_vertices=None):
|
|
1081
|
+
"""
|
|
1082
|
+
Convenience wrapper to display the tree editor map and return the GeoDataFrame.
|
|
1083
|
+
"""
|
|
1084
|
+
m, gdf = draw_additional_trees(tree_gdf, initial_center, zoom, rectangle_vertices)
|
|
1085
|
+
display(m)
|
|
833
1086
|
return gdf
|