openforis-whisp 3.0.0a2__py3-none-any.whl → 3.0.0a4__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.
- openforis_whisp/__init__.py +8 -8
- openforis_whisp/advanced_stats.py +476 -312
- openforis_whisp/data_checks.py +80 -28
- openforis_whisp/datasets.py +14 -0
- openforis_whisp/logger.py +15 -3
- openforis_whisp/parameters/lookup_gee_datasets.csv +3 -2
- openforis_whisp/pd_schemas.py +7 -2
- openforis_whisp/reformat.py +8 -30
- openforis_whisp/stats.py +16 -62
- openforis_whisp/utils.py +468 -80
- {openforis_whisp-3.0.0a2.dist-info → openforis_whisp-3.0.0a4.dist-info}/METADATA +1 -1
- openforis_whisp-3.0.0a4.dist-info/RECORD +20 -0
- openforis_whisp-3.0.0a2.dist-info/RECORD +0 -20
- {openforis_whisp-3.0.0a2.dist-info → openforis_whisp-3.0.0a4.dist-info}/LICENSE +0 -0
- {openforis_whisp-3.0.0a2.dist-info → openforis_whisp-3.0.0a4.dist-info}/WHEEL +0 -0
openforis_whisp/utils.py
CHANGED
|
@@ -5,6 +5,8 @@ import os
|
|
|
5
5
|
import pandas as pd
|
|
6
6
|
import random
|
|
7
7
|
import numpy as np
|
|
8
|
+
import logging
|
|
9
|
+
import sys
|
|
8
10
|
|
|
9
11
|
import urllib.request
|
|
10
12
|
import os
|
|
@@ -19,6 +21,23 @@ from shapely.validation import make_valid
|
|
|
19
21
|
|
|
20
22
|
from .logger import StdoutLogger
|
|
21
23
|
|
|
24
|
+
# Configure the "whisp" logger with auto-flush handler for Colab visibility
|
|
25
|
+
_whisp_logger = logging.getLogger("whisp")
|
|
26
|
+
if not _whisp_logger.handlers:
|
|
27
|
+
_handler = logging.StreamHandler(sys.stdout)
|
|
28
|
+
_handler.setLevel(logging.DEBUG)
|
|
29
|
+
_handler.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
|
|
30
|
+
# Override emit to force flush after each message for Colab
|
|
31
|
+
_original_emit = _handler.emit
|
|
32
|
+
|
|
33
|
+
def _emit_with_flush(record):
|
|
34
|
+
_original_emit(record)
|
|
35
|
+
sys.stdout.flush()
|
|
36
|
+
|
|
37
|
+
_handler.emit = _emit_with_flush
|
|
38
|
+
_whisp_logger.addHandler(_handler)
|
|
39
|
+
_whisp_logger.setLevel(logging.INFO)
|
|
40
|
+
_whisp_logger.propagate = False # Don't propagate to root to avoid duplicates
|
|
22
41
|
|
|
23
42
|
logger = StdoutLogger(__name__)
|
|
24
43
|
|
|
@@ -238,11 +257,11 @@ def generate_random_polygon(
|
|
|
238
257
|
center_point = Point(center_lon, center_lat)
|
|
239
258
|
|
|
240
259
|
# Use buffer with resolution to control vertices for smaller vertex counts
|
|
241
|
-
if vertex_count <=
|
|
260
|
+
if vertex_count <= 20:
|
|
242
261
|
poly = center_point.buffer(radius_degrees, resolution=vertex_count // 4)
|
|
243
262
|
|
|
244
|
-
# Manual vertex creation for higher vertex counts
|
|
245
|
-
if vertex_count >
|
|
263
|
+
# Manual vertex creation for higher vertex counts (sine wave distortions for realistic shapes)
|
|
264
|
+
if vertex_count > 20:
|
|
246
265
|
angles = np.linspace(0, 2 * math.pi, vertex_count, endpoint=False)
|
|
247
266
|
|
|
248
267
|
base_radius = radius_degrees
|
|
@@ -319,6 +338,10 @@ def generate_test_polygons(
|
|
|
319
338
|
"""
|
|
320
339
|
Generate synthetic test polygons with exact vertex count control.
|
|
321
340
|
|
|
341
|
+
**Deprecated**: This is a legacy alias for generate_random_polygons().
|
|
342
|
+
Use generate_random_polygons() for new code, which provides additional
|
|
343
|
+
features like save_path and seed parameters.
|
|
344
|
+
|
|
322
345
|
This utility is useful for testing WHISP processing with controlled test data,
|
|
323
346
|
especially when you need polygons with specific characteristics (area, complexity).
|
|
324
347
|
|
|
@@ -326,10 +349,6 @@ def generate_test_polygons(
|
|
|
326
349
|
----------
|
|
327
350
|
bounds : list or ee.Geometry
|
|
328
351
|
Either a list of [min_lon, min_lat, max_lon, max_lat] or an Earth Engine Geometry.
|
|
329
|
-
Examples:
|
|
330
|
-
- Simple bounds: [-81.0, -19.3, -31.5, 9.6]
|
|
331
|
-
- EE Geometry: ee.FeatureCollection('USDOS/LSIB_SIMPLE/2017').filter(
|
|
332
|
-
ee.Filter.eq('country_na', 'Brazil')).first().geometry()
|
|
333
352
|
num_polygons : int, optional
|
|
334
353
|
Number of polygons to generate (default: 25)
|
|
335
354
|
min_area_ha : float, optional
|
|
@@ -344,36 +363,153 @@ def generate_test_polygons(
|
|
|
344
363
|
Returns
|
|
345
364
|
-------
|
|
346
365
|
dict
|
|
347
|
-
GeoJSON FeatureCollection with generated polygons.
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
366
|
+
GeoJSON FeatureCollection with generated polygons.
|
|
367
|
+
|
|
368
|
+
See Also
|
|
369
|
+
--------
|
|
370
|
+
generate_random_polygons : Recommended replacement with additional options
|
|
371
|
+
"""
|
|
372
|
+
return generate_random_polygons(
|
|
373
|
+
bounds=bounds,
|
|
374
|
+
num_polygons=num_polygons,
|
|
375
|
+
min_area_ha=min_area_ha,
|
|
376
|
+
max_area_ha=max_area_ha,
|
|
377
|
+
min_number_vert=min_number_vert,
|
|
378
|
+
max_number_vert=max_number_vert,
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def generate_random_features(
|
|
383
|
+
bounds,
|
|
384
|
+
feature_type="point",
|
|
385
|
+
num_features=10,
|
|
386
|
+
seed=None,
|
|
387
|
+
min_area_ha=1,
|
|
388
|
+
max_area_ha=10,
|
|
389
|
+
min_number_vert=10,
|
|
390
|
+
max_number_vert=20,
|
|
391
|
+
multipolygon_pct=20,
|
|
392
|
+
min_parts=2,
|
|
393
|
+
max_parts=4,
|
|
394
|
+
save_path=None,
|
|
395
|
+
return_path=False,
|
|
396
|
+
):
|
|
397
|
+
"""
|
|
398
|
+
Generate random test features (points, polygons, or mixed) with exact geographic bounds control.
|
|
399
|
+
|
|
400
|
+
This utility is useful for testing WHISP processing with random feature data,
|
|
401
|
+
especially when you need features with specific geographic characteristics.
|
|
402
|
+
For polygon features, reuses the production polygon generation logic from
|
|
403
|
+
generate_random_polygon() to ensure consistency.
|
|
404
|
+
|
|
405
|
+
Parameters
|
|
406
|
+
----------
|
|
407
|
+
bounds : list or ee.Geometry
|
|
408
|
+
Either a list of [min_lon, min_lat, max_lon, max_lat] or an Earth Engine Geometry.
|
|
409
|
+
Examples:
|
|
410
|
+
- Simple bounds: [-81.0, -19.3, -31.5, 9.6]
|
|
411
|
+
- EE Geometry: ee.FeatureCollection('USDOS/LSIB_SIMPLE/2017').filter(
|
|
412
|
+
ee.Filter.eq('country_na', 'Brazil')).first().geometry()
|
|
413
|
+
feature_type : str, optional
|
|
414
|
+
Type of features to generate:
|
|
415
|
+
- 'point': Random points
|
|
416
|
+
- 'polygon': Single-part polygons
|
|
417
|
+
- 'mixed': Blend of single polygons and multipolygons (default: 'point')
|
|
418
|
+
num_features : int, optional
|
|
419
|
+
Number of features to generate (default: 10)
|
|
420
|
+
seed : int or None, optional
|
|
421
|
+
Random seed for reproducibility. None = different each run to avoid GEE caching (default: None)
|
|
422
|
+
min_area_ha : float, optional
|
|
423
|
+
Minimum area in hectares for polygons (default: 1)
|
|
424
|
+
max_area_ha : float, optional
|
|
425
|
+
Maximum area in hectares for polygons (default: 10)
|
|
426
|
+
min_number_vert : int, optional
|
|
427
|
+
Minimum vertices per polygon (default: 10)
|
|
428
|
+
max_number_vert : int, optional
|
|
429
|
+
Maximum vertices per polygon (default: 20)
|
|
430
|
+
multipolygon_pct : float, optional
|
|
431
|
+
Percentage of features that are multipolygons for 'mixed' type (0-100, default: 20)
|
|
432
|
+
min_parts : int, optional
|
|
433
|
+
Minimum polygon parts per multipolygon (default: 2)
|
|
434
|
+
max_parts : int, optional
|
|
435
|
+
Maximum polygon parts per multipolygon (default: 4)
|
|
436
|
+
save_path : str or Path, optional
|
|
437
|
+
Directory path where to save the GeoJSON file. If None, file is not saved (default: None)
|
|
438
|
+
return_path : bool, optional
|
|
439
|
+
If True, return the file path instead of the GeoJSON dict. Only used if save_path is provided (default: False)
|
|
440
|
+
|
|
441
|
+
Returns
|
|
442
|
+
-------
|
|
443
|
+
dict or Path
|
|
444
|
+
If save_path is None: GeoJSON FeatureCollection dict
|
|
445
|
+
If save_path is provided and return_path=False: GeoJSON FeatureCollection dict
|
|
446
|
+
If save_path is provided and return_path=True: Path to saved GeoJSON file
|
|
447
|
+
|
|
448
|
+
All features include:
|
|
449
|
+
- internal_id: Sequential feature ID
|
|
450
|
+
- geometry_type: 'Point', 'Polygon', or 'MultiPolygon' (for distinguishing feature types)
|
|
451
|
+
|
|
452
|
+
For polygons, features include:
|
|
453
|
+
- requested_vertices: Requested vertex count
|
|
454
|
+
- actual_vertices: Actual vertices created
|
|
351
455
|
- requested_area_ha: Target area in hectares
|
|
352
456
|
- actual_area_ha: Actual area in hectares
|
|
353
457
|
|
|
354
458
|
Examples
|
|
355
459
|
--------
|
|
356
460
|
>>> import openforis_whisp as whisp
|
|
357
|
-
>>> import ee
|
|
358
461
|
>>>
|
|
359
|
-
>>> #
|
|
360
|
-
>>>
|
|
361
|
-
>>> geojson = whisp.
|
|
462
|
+
>>> # Generate random points (in-memory)
|
|
463
|
+
>>> bounds = [-81.0, -19.3, -31.5, 9.6]
|
|
464
|
+
>>> geojson = whisp.generate_random_features(bounds, feature_type='point', num_features=50)
|
|
362
465
|
>>>
|
|
363
|
-
>>> #
|
|
364
|
-
>>>
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
...
|
|
466
|
+
>>> # Generate and save to file
|
|
467
|
+
>>> from pathlib import Path
|
|
468
|
+
>>> save_dir = Path.home() / 'downloads'
|
|
469
|
+
>>> geojson = whisp.generate_random_features(
|
|
470
|
+
... bounds,
|
|
471
|
+
... feature_type='polygon',
|
|
472
|
+
... num_features=100,
|
|
473
|
+
... save_path=save_dir
|
|
474
|
+
... )
|
|
369
475
|
>>>
|
|
370
|
-
>>> #
|
|
371
|
-
>>>
|
|
372
|
-
|
|
373
|
-
...
|
|
476
|
+
>>> # Generate mixed geometries and return file path
|
|
477
|
+
>>> file_path = whisp.generate_random_features(
|
|
478
|
+
... bounds,
|
|
479
|
+
... feature_type='mixed',
|
|
480
|
+
... num_features=100,
|
|
481
|
+
... multipolygon_pct=30,
|
|
482
|
+
... min_parts=2,
|
|
483
|
+
... max_parts=5,
|
|
484
|
+
... save_path=save_dir,
|
|
485
|
+
... return_path=True
|
|
486
|
+
... )
|
|
374
487
|
"""
|
|
488
|
+
# Validate feature_type
|
|
489
|
+
if feature_type not in ("point", "polygon", "mixed"):
|
|
490
|
+
raise ValueError(
|
|
491
|
+
f"feature_type must be 'point', 'polygon', or 'mixed' (got {feature_type!r})"
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
# Validate num_features
|
|
495
|
+
if num_features < 1:
|
|
496
|
+
raise ValueError(f"num_features must be at least 1 (got {num_features})")
|
|
497
|
+
|
|
498
|
+
# Validate multipolygon_pct
|
|
499
|
+
if not (0 <= multipolygon_pct <= 100):
|
|
500
|
+
raise ValueError(
|
|
501
|
+
f"multipolygon_pct must be between 0-100 (got {multipolygon_pct})"
|
|
502
|
+
)
|
|
375
503
|
|
|
376
|
-
#
|
|
504
|
+
# Validate min/max parts
|
|
505
|
+
if min_parts < 2:
|
|
506
|
+
raise ValueError(f"min_parts must be at least 2 (got {min_parts})")
|
|
507
|
+
if min_parts > max_parts:
|
|
508
|
+
raise ValueError(
|
|
509
|
+
f"min_parts ({min_parts}) cannot be greater than max_parts ({max_parts})"
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
# Extract and validate bounds
|
|
377
513
|
if hasattr(bounds, "bounds"): # It's an ee.Geometry
|
|
378
514
|
logger.logger.info("Extracting bounds from Earth Engine Geometry...")
|
|
379
515
|
try:
|
|
@@ -404,7 +540,13 @@ def generate_test_polygons(
|
|
|
404
540
|
" - An Earth Engine Geometry (ee.Geometry, ee.Feature.geometry(), etc.)"
|
|
405
541
|
)
|
|
406
542
|
|
|
407
|
-
# Validate
|
|
543
|
+
# Validate bounds
|
|
544
|
+
if min_lon >= max_lon:
|
|
545
|
+
raise ValueError(f"min_lon ({min_lon}) must be less than max_lon ({max_lon})")
|
|
546
|
+
if min_lat >= max_lat:
|
|
547
|
+
raise ValueError(f"min_lat ({min_lat}) must be less than max_lat ({max_lat})")
|
|
548
|
+
|
|
549
|
+
# Validate vertex and area parameters
|
|
408
550
|
if min_number_vert > max_number_vert:
|
|
409
551
|
raise ValueError(
|
|
410
552
|
f"min_number_vert ({min_number_vert}) cannot be greater than max_number_vert ({max_number_vert})"
|
|
@@ -413,75 +555,321 @@ def generate_test_polygons(
|
|
|
413
555
|
raise ValueError(
|
|
414
556
|
f"min_area_ha ({min_area_ha}) cannot be greater than max_area_ha ({max_area_ha})"
|
|
415
557
|
)
|
|
416
|
-
if num_polygons < 1:
|
|
417
|
-
raise ValueError(f"num_polygons must be at least 1 (got {num_polygons})")
|
|
418
558
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
559
|
+
# Set random seed if provided
|
|
560
|
+
if seed is not None:
|
|
561
|
+
random.seed(seed)
|
|
562
|
+
np.random.seed(seed)
|
|
563
|
+
|
|
564
|
+
logger.logger.info(f"Generating {num_features} test {feature_type} features...")
|
|
422
565
|
|
|
423
566
|
features = []
|
|
424
567
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
568
|
+
if feature_type == "point":
|
|
569
|
+
# Generate point features
|
|
570
|
+
for i in range(num_features):
|
|
571
|
+
lon = random.uniform(min_lon, max_lon)
|
|
572
|
+
lat = random.uniform(min_lat, max_lat)
|
|
573
|
+
feature = {
|
|
574
|
+
"type": "Feature",
|
|
575
|
+
"properties": {
|
|
576
|
+
"internal_id": i + 1,
|
|
577
|
+
"name": f"Point_{i+1}",
|
|
578
|
+
"geometry_type": "Point",
|
|
579
|
+
},
|
|
580
|
+
"geometry": {"type": "Point", "coordinates": [lon, lat]},
|
|
581
|
+
}
|
|
582
|
+
features.append(feature)
|
|
583
|
+
|
|
584
|
+
elif feature_type == "polygon":
|
|
585
|
+
# Generate single polygon features - reuse production polygon generation
|
|
586
|
+
vertex_counts = np.random.randint(
|
|
587
|
+
min_number_vert, max_number_vert + 1, num_features
|
|
588
|
+
)
|
|
430
589
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
590
|
+
for i in range(num_features):
|
|
591
|
+
if i > 0 and i % 250 == 0:
|
|
592
|
+
logger.logger.info(
|
|
593
|
+
f"Generated {i}/{num_features} polygons ({i/num_features*100:.0f}%)..."
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
polygon, actual_area = generate_random_polygon(
|
|
597
|
+
min_lon,
|
|
598
|
+
min_lat,
|
|
599
|
+
max_lon,
|
|
600
|
+
max_lat,
|
|
601
|
+
min_area_ha=min_area_ha,
|
|
602
|
+
max_area_ha=max_area_ha,
|
|
603
|
+
vertex_count=vertex_counts[i],
|
|
435
604
|
)
|
|
436
605
|
|
|
437
|
-
|
|
606
|
+
actual_vertex_count = len(list(polygon.exterior.coords)) - 1
|
|
607
|
+
|
|
608
|
+
properties = {
|
|
609
|
+
"internal_id": i + 1,
|
|
610
|
+
"geometry_type": "Polygon",
|
|
611
|
+
"requested_vertices": int(vertex_counts[i]),
|
|
612
|
+
"actual_vertices": int(actual_vertex_count),
|
|
613
|
+
"requested_area_ha": round(random.uniform(min_area_ha, max_area_ha), 2),
|
|
614
|
+
"actual_area_ha": round(actual_area, 2),
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
feature = {
|
|
618
|
+
"type": "Feature",
|
|
619
|
+
"properties": properties,
|
|
620
|
+
"geometry": mapping(polygon),
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
features.append(feature)
|
|
624
|
+
|
|
625
|
+
else: # mixed
|
|
626
|
+
# Generate blend of single polygons and multipolygons
|
|
627
|
+
num_multipolygons = int(num_features * multipolygon_pct / 100)
|
|
628
|
+
num_polygons = num_features - num_multipolygons
|
|
629
|
+
|
|
630
|
+
# Generate single polygons
|
|
631
|
+
if num_polygons > 0:
|
|
632
|
+
vertex_counts = np.random.randint(
|
|
633
|
+
min_number_vert, max_number_vert + 1, num_polygons
|
|
634
|
+
)
|
|
438
635
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
636
|
+
for i in range(num_polygons):
|
|
637
|
+
if i > 0 and i % 250 == 0:
|
|
638
|
+
logger.logger.info(
|
|
639
|
+
f"Generated {i}/{num_features} features ({i/num_features*100:.0f}%)..."
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
polygon, actual_area = generate_random_polygon(
|
|
643
|
+
min_lon,
|
|
644
|
+
min_lat,
|
|
645
|
+
max_lon,
|
|
646
|
+
max_lat,
|
|
647
|
+
min_area_ha=min_area_ha,
|
|
648
|
+
max_area_ha=max_area_ha,
|
|
649
|
+
vertex_count=vertex_counts[i],
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
actual_vertex_count = len(list(polygon.exterior.coords)) - 1
|
|
653
|
+
|
|
654
|
+
properties = {
|
|
655
|
+
"internal_id": i + 1,
|
|
656
|
+
"geometry_type": "Polygon",
|
|
657
|
+
"requested_vertices": int(vertex_counts[i]),
|
|
658
|
+
"actual_vertices": int(actual_vertex_count),
|
|
659
|
+
"requested_area_ha": round(
|
|
660
|
+
random.uniform(min_area_ha, max_area_ha), 2
|
|
661
|
+
),
|
|
662
|
+
"actual_area_ha": round(actual_area, 2),
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
feature = {
|
|
666
|
+
"type": "Feature",
|
|
667
|
+
"properties": properties,
|
|
668
|
+
"geometry": mapping(polygon),
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
features.append(feature)
|
|
672
|
+
|
|
673
|
+
# Generate multipolygons
|
|
674
|
+
if num_multipolygons > 0:
|
|
675
|
+
for i in range(num_multipolygons):
|
|
676
|
+
if i > 0 and i % 50 == 0:
|
|
677
|
+
logger.logger.info(
|
|
678
|
+
f"Generated {num_polygons + i}/{num_features} features ({(num_polygons + i)/num_features*100:.0f}%)..."
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
num_parts = random.randint(min_parts, max_parts)
|
|
682
|
+
polygon_parts = []
|
|
683
|
+
total_area_ha = 0
|
|
684
|
+
|
|
685
|
+
# Generate a target total area for this multipolygon
|
|
686
|
+
target_total_area = random.uniform(min_area_ha, max_area_ha)
|
|
687
|
+
# Divide target area among parts (with some randomness)
|
|
688
|
+
part_areas = []
|
|
689
|
+
remaining_area = target_total_area
|
|
690
|
+
for part_idx in range(num_parts):
|
|
691
|
+
if part_idx == num_parts - 1:
|
|
692
|
+
# Last part gets all remaining area
|
|
693
|
+
part_area = remaining_area
|
|
694
|
+
else:
|
|
695
|
+
# Distribute remaining area with randomness
|
|
696
|
+
max_for_part = remaining_area * 0.8
|
|
697
|
+
part_area = random.uniform(remaining_area * 0.3, max_for_part)
|
|
698
|
+
part_areas.append(part_area)
|
|
699
|
+
remaining_area -= part_area
|
|
700
|
+
|
|
701
|
+
for part, area_budget in enumerate(part_areas):
|
|
702
|
+
# Generate each part in the full bounds (randomization spreads them naturally)
|
|
703
|
+
polygon, actual_area = generate_random_polygon(
|
|
704
|
+
min_lon,
|
|
705
|
+
min_lat,
|
|
706
|
+
max_lon,
|
|
707
|
+
max_lat,
|
|
708
|
+
min_area_ha=area_budget * 0.8,
|
|
709
|
+
max_area_ha=area_budget * 1.2,
|
|
710
|
+
vertex_count=random.randint(min_number_vert, max_number_vert),
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
polygon_parts.append([list(polygon.exterior.coords)])
|
|
714
|
+
total_area_ha += actual_area
|
|
715
|
+
|
|
716
|
+
properties = {
|
|
717
|
+
"internal_id": num_polygons + i + 1,
|
|
718
|
+
"geometry_type": "MultiPolygon",
|
|
719
|
+
"num_parts": num_parts,
|
|
720
|
+
"total_area_ha": round(total_area_ha, 2),
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
feature = {
|
|
724
|
+
"type": "Feature",
|
|
725
|
+
"properties": properties,
|
|
726
|
+
"geometry": {
|
|
727
|
+
"type": "MultiPolygon",
|
|
728
|
+
"coordinates": polygon_parts,
|
|
729
|
+
},
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
features.append(feature)
|
|
448
733
|
|
|
449
|
-
|
|
734
|
+
geojson = {"type": "FeatureCollection", "features": features}
|
|
450
735
|
|
|
451
|
-
|
|
452
|
-
"internal_id": i + 1,
|
|
453
|
-
"requested_vertices": int(requested_vertices),
|
|
454
|
-
"actual_vertices": int(actual_vertex_count),
|
|
455
|
-
"requested_area_ha": round(target_areas[i], 2),
|
|
456
|
-
"actual_area_ha": round(actual_area, 2),
|
|
457
|
-
}
|
|
736
|
+
logger.logger.info(f"Generated {num_features} test {feature_type} features!")
|
|
458
737
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
"geometry": mapping(polygon),
|
|
463
|
-
}
|
|
738
|
+
# Save to file if save_path is provided
|
|
739
|
+
if save_path is not None:
|
|
740
|
+
import json
|
|
464
741
|
|
|
465
|
-
|
|
742
|
+
save_path = Path(save_path)
|
|
743
|
+
save_path.mkdir(parents=True, exist_ok=True)
|
|
466
744
|
|
|
467
|
-
|
|
745
|
+
output_file = save_path / f"{feature_type}s_{num_features}_features.geojson"
|
|
468
746
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
requested_vertex_counts = [f["properties"]["requested_vertices"] for f in features]
|
|
747
|
+
with open(output_file, "w") as f:
|
|
748
|
+
json.dump(geojson, f, indent=2)
|
|
472
749
|
|
|
473
|
-
|
|
474
|
-
f"Vertex count - Requested: {min(requested_vertex_counts)}-{max(requested_vertex_counts)}, "
|
|
475
|
-
f"Actual: {min(actual_vertex_counts)}-{max(actual_vertex_counts)}"
|
|
476
|
-
)
|
|
750
|
+
logger.logger.info(f"GeoJSON saved to: {output_file}")
|
|
477
751
|
|
|
478
|
-
|
|
479
|
-
|
|
752
|
+
if return_path:
|
|
753
|
+
return output_file
|
|
480
754
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
755
|
+
return geojson
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
def generate_random_points(bounds, num_features=10, seed=None, save_path=None):
|
|
759
|
+
"""
|
|
760
|
+
Generate random test points with optional save.
|
|
761
|
+
|
|
762
|
+
Simplified wrapper around generate_random_features for point-only generation.
|
|
763
|
+
|
|
764
|
+
Parameters
|
|
765
|
+
----------
|
|
766
|
+
bounds : list or ee.Geometry
|
|
767
|
+
Either a list of [min_lon, min_lat, max_lon, max_lat] or an Earth Engine Geometry.
|
|
768
|
+
num_features : int, optional
|
|
769
|
+
Number of points to generate (default: 10)
|
|
770
|
+
seed : int or None, optional
|
|
771
|
+
Random seed for reproducibility (default: None)
|
|
772
|
+
save_path : str or Path, optional
|
|
773
|
+
Directory path where to save the GeoJSON file. If None, file is not saved (default: None)
|
|
774
|
+
|
|
775
|
+
Returns
|
|
776
|
+
-------
|
|
777
|
+
dict or Path
|
|
778
|
+
If save_path is None: GeoJSON FeatureCollection dict
|
|
779
|
+
If save_path is provided: Path to saved GeoJSON file
|
|
780
|
+
|
|
781
|
+
Examples
|
|
782
|
+
--------
|
|
783
|
+
>>> import openforis_whisp as whisp
|
|
784
|
+
>>>
|
|
785
|
+
>>> bounds = [-81.0, -19.3, -31.5, 9.6]
|
|
786
|
+
>>> geojson = whisp.generate_random_points(bounds, num_features=100)
|
|
787
|
+
>>>
|
|
788
|
+
>>> # Generate and save
|
|
789
|
+
>>> from pathlib import Path
|
|
790
|
+
>>> file_path = whisp.generate_random_points(
|
|
791
|
+
... bounds,
|
|
792
|
+
... num_features=500,
|
|
793
|
+
... save_path=Path.home() / 'downloads'
|
|
794
|
+
... )
|
|
795
|
+
"""
|
|
796
|
+
return generate_random_features(
|
|
797
|
+
bounds=bounds,
|
|
798
|
+
feature_type="point",
|
|
799
|
+
num_features=num_features,
|
|
800
|
+
seed=seed,
|
|
801
|
+
save_path=save_path,
|
|
802
|
+
return_path=True if save_path else False,
|
|
484
803
|
)
|
|
485
804
|
|
|
486
|
-
|
|
487
|
-
|
|
805
|
+
|
|
806
|
+
def generate_random_polygons(
|
|
807
|
+
bounds,
|
|
808
|
+
num_polygons=25,
|
|
809
|
+
min_area_ha=1,
|
|
810
|
+
max_area_ha=10,
|
|
811
|
+
min_number_vert=10,
|
|
812
|
+
max_number_vert=20,
|
|
813
|
+
seed=None,
|
|
814
|
+
save_path=None,
|
|
815
|
+
):
|
|
816
|
+
"""
|
|
817
|
+
Generate random test polygons with optional save.
|
|
818
|
+
|
|
819
|
+
Wrapper around generate_random_features for polygon-only generation.
|
|
820
|
+
Uses the same production-quality polygon generation as generate_test_polygons.
|
|
821
|
+
|
|
822
|
+
Parameters
|
|
823
|
+
----------
|
|
824
|
+
bounds : list or ee.Geometry
|
|
825
|
+
Either a list of [min_lon, min_lat, max_lon, max_lon] or an Earth Engine Geometry.
|
|
826
|
+
num_polygons : int, optional
|
|
827
|
+
Number of polygons to generate (default: 25)
|
|
828
|
+
min_area_ha : float, optional
|
|
829
|
+
Minimum area in hectares (default: 1)
|
|
830
|
+
max_area_ha : float, optional
|
|
831
|
+
Maximum area in hectares (default: 10)
|
|
832
|
+
min_number_vert : int, optional
|
|
833
|
+
Minimum number of vertices per polygon (default: 10)
|
|
834
|
+
max_number_vert : int, optional
|
|
835
|
+
Maximum number of vertices per polygon (default: 20)
|
|
836
|
+
seed : int or None, optional
|
|
837
|
+
Random seed for reproducibility (default: None)
|
|
838
|
+
save_path : str or Path, optional
|
|
839
|
+
Directory path where to save the GeoJSON file. If None, file is not saved (default: None)
|
|
840
|
+
|
|
841
|
+
Returns
|
|
842
|
+
-------
|
|
843
|
+
dict or Path
|
|
844
|
+
If save_path is None: GeoJSON FeatureCollection dict
|
|
845
|
+
If save_path is provided: Path to saved GeoJSON file
|
|
846
|
+
|
|
847
|
+
Examples
|
|
848
|
+
--------
|
|
849
|
+
>>> import openforis_whisp as whisp
|
|
850
|
+
>>>
|
|
851
|
+
>>> bounds = [-81.0, -19.3, -31.5, 9.6]
|
|
852
|
+
>>> geojson = whisp.generate_random_polygons(bounds, num_polygons=50)
|
|
853
|
+
>>>
|
|
854
|
+
>>> # Generate and save
|
|
855
|
+
>>> from pathlib import Path
|
|
856
|
+
>>> file_path = whisp.generate_random_polygons(
|
|
857
|
+
... bounds,
|
|
858
|
+
... num_polygons=100,
|
|
859
|
+
... min_area_ha=5,
|
|
860
|
+
... max_area_ha=50,
|
|
861
|
+
... save_path=Path.home() / 'downloads'
|
|
862
|
+
... )
|
|
863
|
+
"""
|
|
864
|
+
return generate_random_features(
|
|
865
|
+
bounds=bounds,
|
|
866
|
+
feature_type="polygon",
|
|
867
|
+
num_features=num_polygons,
|
|
868
|
+
seed=seed,
|
|
869
|
+
min_area_ha=min_area_ha,
|
|
870
|
+
max_area_ha=max_area_ha,
|
|
871
|
+
min_number_vert=min_number_vert,
|
|
872
|
+
max_number_vert=max_number_vert,
|
|
873
|
+
save_path=save_path,
|
|
874
|
+
return_path=True if save_path else False,
|
|
875
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: openforis-whisp
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.0a4
|
|
4
4
|
Summary: Whisp (What is in that plot) is an open-source solution which helps to produce relevant forest monitoring information and support compliance with deforestation-related regulations.
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: whisp,geospatial,data-processing
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
openforis_whisp/__init__.py,sha256=5zJK84LYnlslxSajdCz6ZIYxRS4xgN3sGxSD6_GXEHs,3547
|
|
2
|
+
openforis_whisp/advanced_stats.py,sha256=FC1YasSZ93jplF1qBgDopzBIsO2ueXnidomQU3rpP_Q,100006
|
|
3
|
+
openforis_whisp/data_checks.py,sha256=ErIKGbCa3R8eYP0sVoAl-ZUl607W1QrG0Jr2SIVgm2I,34056
|
|
4
|
+
openforis_whisp/data_conversion.py,sha256=L2IsiUyQUt3aHgSYGbIhgPGwM7eyS3nLVEoNO9YqQeM,21888
|
|
5
|
+
openforis_whisp/datasets.py,sha256=F1WxXc93mxxmN-WHa0bf-XX-FloSQyEBJKmnrQEHYn8,53855
|
|
6
|
+
openforis_whisp/logger.py,sha256=gFkRTwJDJKIBWcHDOK74Uln3JM7fAybURo7pQpGL790,3395
|
|
7
|
+
openforis_whisp/parameters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
openforis_whisp/parameters/config_runtime.py,sha256=NOo39MAi60XCwEx5pwkS0EHKJBh0XY1q06y4j0HAABg,1421
|
|
9
|
+
openforis_whisp/parameters/lookup_context_and_metadata.csv,sha256=KgK0ik_Gd4t_Nq5cUkGPT4ZFZVO93HWSG82jRrOukt4,1298
|
|
10
|
+
openforis_whisp/parameters/lookup_gaul1_admin.py,sha256=cQr5liRdXi85QieTxrz4VAkn0COvRCp82ZV0dYFWOio,474980
|
|
11
|
+
openforis_whisp/parameters/lookup_gee_datasets.csv,sha256=7KdnFocEgbZO5m8JmWQchzZTurg9rJ96y17z8UyLtI0,17537
|
|
12
|
+
openforis_whisp/pd_schemas.py,sha256=0z-oPmYIDUIn7mNY41W_uUpmTwjoR7e254mOCoHVsOg,2878
|
|
13
|
+
openforis_whisp/reformat.py,sha256=gvhIa-_kTT5BSO8LuVmJ1TQcf_NwheskXboFM9e0KJY,32758
|
|
14
|
+
openforis_whisp/risk.py,sha256=d_Di5XB8BnHdVXG56xdHTcpB4-CIF5vo2ZRMQRG7Pek,34420
|
|
15
|
+
openforis_whisp/stats.py,sha256=pTSYs77ISRBOIglRpq4SUx3lKRkrUZOKROLRX5IP9IY,63941
|
|
16
|
+
openforis_whisp/utils.py,sha256=AISWF-MpfFdYkhd6bei4BViw2Iag20mmq61ykrF9YTk,31287
|
|
17
|
+
openforis_whisp-3.0.0a4.dist-info/LICENSE,sha256=nqyqICO95iw_iwzP1t_IIAf7ZX3DPbL_M9WyQfh2q1k,1085
|
|
18
|
+
openforis_whisp-3.0.0a4.dist-info/METADATA,sha256=ak2Dw632lgOtXEXkl5-haYK7vF3hPaJ6IkaRRJRdH0Y,16684
|
|
19
|
+
openforis_whisp-3.0.0a4.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
|
|
20
|
+
openforis_whisp-3.0.0a4.dist-info/RECORD,,
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
openforis_whisp/__init__.py,sha256=-r_9LFxbV6d-o4s0_huhaXxve6GIzCwl3pXKuJo6ixE,3663
|
|
2
|
-
openforis_whisp/advanced_stats.py,sha256=xrwKHG-c44_UkFha7TFgf71mo9UMw5ZZL3XQTPF5luM,92681
|
|
3
|
-
openforis_whisp/data_checks.py,sha256=KwgD72FA_n7joiJadGRpzntd2sLo0aqGNbOjRkB8iQI,32293
|
|
4
|
-
openforis_whisp/data_conversion.py,sha256=L2IsiUyQUt3aHgSYGbIhgPGwM7eyS3nLVEoNO9YqQeM,21888
|
|
5
|
-
openforis_whisp/datasets.py,sha256=aGJy0OYN4d0nsH3_IOYlHl-WCB7KFwZwMJ-dBi5Hc5Y,53470
|
|
6
|
-
openforis_whisp/logger.py,sha256=9M6_3mdpoiWfC-pDwM9vKmB2l5Gul6Rb5rNTNh-_nzs,3054
|
|
7
|
-
openforis_whisp/parameters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
openforis_whisp/parameters/config_runtime.py,sha256=NOo39MAi60XCwEx5pwkS0EHKJBh0XY1q06y4j0HAABg,1421
|
|
9
|
-
openforis_whisp/parameters/lookup_context_and_metadata.csv,sha256=KgK0ik_Gd4t_Nq5cUkGPT4ZFZVO93HWSG82jRrOukt4,1298
|
|
10
|
-
openforis_whisp/parameters/lookup_gaul1_admin.py,sha256=cQr5liRdXi85QieTxrz4VAkn0COvRCp82ZV0dYFWOio,474980
|
|
11
|
-
openforis_whisp/parameters/lookup_gee_datasets.csv,sha256=UDvZrQsL5rXJn6CW6P3wofUrPLRmUFZWt6ETbXaxBMs,17454
|
|
12
|
-
openforis_whisp/pd_schemas.py,sha256=W_ocS773LHfc05dJqvWRa-bRdX0wKFoNp0lMxgFx94Y,2681
|
|
13
|
-
openforis_whisp/reformat.py,sha256=mIooJ3zfSTDY3_Mx3OAW4jpfQ72q3zasG9tl58PdfN4,33729
|
|
14
|
-
openforis_whisp/risk.py,sha256=d_Di5XB8BnHdVXG56xdHTcpB4-CIF5vo2ZRMQRG7Pek,34420
|
|
15
|
-
openforis_whisp/stats.py,sha256=dCQXx6KKEV99owqyPURk6CL97kQQARjetFrIz1ZbIvs,65725
|
|
16
|
-
openforis_whisp/utils.py,sha256=5HHtbK62Swn4-jnlSe1Jc-hVnJhLKMuDW0_ayHY7mIg,17130
|
|
17
|
-
openforis_whisp-3.0.0a2.dist-info/LICENSE,sha256=nqyqICO95iw_iwzP1t_IIAf7ZX3DPbL_M9WyQfh2q1k,1085
|
|
18
|
-
openforis_whisp-3.0.0a2.dist-info/METADATA,sha256=wG4vc7B-f0JXmNkTUh4wJ-H0KPpbgyU9OfMwGewZq_A,16684
|
|
19
|
-
openforis_whisp-3.0.0a2.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
|
|
20
|
-
openforis_whisp-3.0.0a2.dist-info/RECORD,,
|