vector2dggs 0.10.0__tar.gz → 0.11.0__tar.gz
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.
- {vector2dggs-0.10.0 → vector2dggs-0.11.0}/PKG-INFO +28 -12
- {vector2dggs-0.10.0 → vector2dggs-0.11.0}/README.md +24 -8
- {vector2dggs-0.10.0 → vector2dggs-0.11.0}/pyproject.toml +2 -1
- vector2dggs-0.11.0/vector2dggs/__init__.py +1 -0
- {vector2dggs-0.10.0 → vector2dggs-0.11.0}/vector2dggs/common.py +85 -33
- {vector2dggs-0.10.0 → vector2dggs-0.11.0}/vector2dggs/constants.py +80 -14
- {vector2dggs-0.10.0 → vector2dggs-0.11.0}/vector2dggs/geohash.py +3 -5
- {vector2dggs-0.10.0 → vector2dggs-0.11.0}/vector2dggs/h3.py +3 -5
- {vector2dggs-0.10.0 → vector2dggs-0.11.0}/vector2dggs/katana.py +7 -4
- {vector2dggs-0.10.0 → vector2dggs-0.11.0}/vector2dggs/rHP.py +5 -59
- {vector2dggs-0.10.0 → vector2dggs-0.11.0}/vector2dggs/s2.py +5 -5
- vector2dggs-0.10.0/COPYING +0 -674
- vector2dggs-0.10.0/COPYING.LESSER +0 -165
- vector2dggs-0.10.0/vector2dggs/__init__.py +0 -1
- {vector2dggs-0.10.0 → vector2dggs-0.11.0}/vector2dggs/cli.py +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: vector2dggs
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: CLI DGGS indexer for vector geospatial data
|
|
5
|
+
Home-page: https://github.com/manaakiwhenua/vector2dggs
|
|
5
6
|
License: LGPL-3.0-or-later
|
|
6
7
|
Keywords: dggs,vector,h3,rHEALPix,cli
|
|
7
8
|
Author: James Ardo
|
|
@@ -12,8 +13,6 @@ Requires-Python: >=3.11,<4.0
|
|
|
12
13
|
Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)
|
|
13
14
|
Classifier: Programming Language :: Python :: 3
|
|
14
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
17
16
|
Classifier: Topic :: Scientific/Engineering
|
|
18
17
|
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
19
18
|
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
@@ -30,6 +29,7 @@ Requires-Dist: psycopg2 (>=2.9.9,<3.0.0)
|
|
|
30
29
|
Requires-Dist: pyarrow (>=20.0,<21.0)
|
|
31
30
|
Requires-Dist: pyproj (>=3.7,<4.0)
|
|
32
31
|
Requires-Dist: python-geohash (>=0.8.5,<0.9.0)
|
|
32
|
+
Requires-Dist: rhealpixdggs (>=0.5.12,<0.6.0)
|
|
33
33
|
Requires-Dist: rhppandas (>=0.2.0,<0.3.0)
|
|
34
34
|
Requires-Dist: rusty-polygon-geohasher (>=0.2.3,<0.3.0)
|
|
35
35
|
Requires-Dist: s2geometry (>=0.9.0,<0.10.0)
|
|
@@ -121,12 +121,17 @@ Options:
|
|
|
121
121
|
used for cutting large geometries (see
|
|
122
122
|
`--cut_threshold`). Defaults to the same CRS
|
|
123
123
|
as the input. Should be a valid EPSG code.
|
|
124
|
-
-c, --cut_threshold
|
|
125
|
-
geometries based on a target
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
124
|
+
-c, --cut_threshold FLOAT Cutting up large geometries into smaller
|
|
125
|
+
geometries based on a target area. Units are
|
|
126
|
+
assumed to match the input CRS units unless
|
|
127
|
+
the `--cut_crs` is also given, in which case
|
|
128
|
+
units match the units of the supplied CRS.
|
|
129
|
+
If left unspecified, the threshold will be
|
|
130
|
+
the maximum area of a cell at the parent
|
|
131
|
+
resolution, in square metres or feet
|
|
132
|
+
according to the CRS. A threshold of 0 will
|
|
133
|
+
skip bissection entirely (effectively
|
|
134
|
+
ignoring --cut_crs).
|
|
130
135
|
-t, --threads INTEGER Amount of threads used for operation
|
|
131
136
|
[default: NUM_CPUS - 1]
|
|
132
137
|
-cp, --compression TEXT Compression method to use for the output
|
|
@@ -239,6 +244,17 @@ Alternatively, it is also possible to install using pip with `pip install -e .`,
|
|
|
239
244
|
|
|
240
245
|
Please run `black .` before committing.
|
|
241
246
|
|
|
247
|
+
#### Tests
|
|
248
|
+
|
|
249
|
+
Tests are included. To run them, set up a poetry environment, then follow these instructons:
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
cd tests
|
|
253
|
+
python ./test_vector2dggs.py
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Test data are included at `tests/data/`.
|
|
257
|
+
|
|
242
258
|
## Example commands
|
|
243
259
|
|
|
244
260
|
With a local GPKG:
|
|
@@ -261,14 +277,14 @@ vector2dggs h3 -v DEBUG -id ogc_fid -r 9 -p 5 -t 4 --overwrite -lyr topo50_lake
|
|
|
261
277
|
title={{vector2dggs}},
|
|
262
278
|
author={Ardo, James and Law, Richard},
|
|
263
279
|
url={https://github.com/manaakiwhenua/vector2dggs},
|
|
264
|
-
version={0.
|
|
280
|
+
version={0.11.0},
|
|
265
281
|
date={2023-04-20}
|
|
266
282
|
}
|
|
267
283
|
```
|
|
268
284
|
|
|
269
285
|
APA/Harvard
|
|
270
286
|
|
|
271
|
-
> Ardo, J., & Law, R. (2023). vector2dggs (0.
|
|
287
|
+
> Ardo, J., & Law, R. (2023). vector2dggs (0.11.0) [Computer software]. https://github.com/manaakiwhenua/vector2dggs
|
|
272
288
|
|
|
273
289
|
[](https://github.com/manaakiwhenua/manaakiwhenua-standards)
|
|
274
290
|
|
|
@@ -80,12 +80,17 @@ Options:
|
|
|
80
80
|
used for cutting large geometries (see
|
|
81
81
|
`--cut_threshold`). Defaults to the same CRS
|
|
82
82
|
as the input. Should be a valid EPSG code.
|
|
83
|
-
-c, --cut_threshold
|
|
84
|
-
geometries based on a target
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
-c, --cut_threshold FLOAT Cutting up large geometries into smaller
|
|
84
|
+
geometries based on a target area. Units are
|
|
85
|
+
assumed to match the input CRS units unless
|
|
86
|
+
the `--cut_crs` is also given, in which case
|
|
87
|
+
units match the units of the supplied CRS.
|
|
88
|
+
If left unspecified, the threshold will be
|
|
89
|
+
the maximum area of a cell at the parent
|
|
90
|
+
resolution, in square metres or feet
|
|
91
|
+
according to the CRS. A threshold of 0 will
|
|
92
|
+
skip bissection entirely (effectively
|
|
93
|
+
ignoring --cut_crs).
|
|
89
94
|
-t, --threads INTEGER Amount of threads used for operation
|
|
90
95
|
[default: NUM_CPUS - 1]
|
|
91
96
|
-cp, --compression TEXT Compression method to use for the output
|
|
@@ -198,6 +203,17 @@ Alternatively, it is also possible to install using pip with `pip install -e .`,
|
|
|
198
203
|
|
|
199
204
|
Please run `black .` before committing.
|
|
200
205
|
|
|
206
|
+
#### Tests
|
|
207
|
+
|
|
208
|
+
Tests are included. To run them, set up a poetry environment, then follow these instructons:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
cd tests
|
|
212
|
+
python ./test_vector2dggs.py
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Test data are included at `tests/data/`.
|
|
216
|
+
|
|
201
217
|
## Example commands
|
|
202
218
|
|
|
203
219
|
With a local GPKG:
|
|
@@ -220,13 +236,13 @@ vector2dggs h3 -v DEBUG -id ogc_fid -r 9 -p 5 -t 4 --overwrite -lyr topo50_lake
|
|
|
220
236
|
title={{vector2dggs}},
|
|
221
237
|
author={Ardo, James and Law, Richard},
|
|
222
238
|
url={https://github.com/manaakiwhenua/vector2dggs},
|
|
223
|
-
version={0.
|
|
239
|
+
version={0.11.0},
|
|
224
240
|
date={2023-04-20}
|
|
225
241
|
}
|
|
226
242
|
```
|
|
227
243
|
|
|
228
244
|
APA/Harvard
|
|
229
245
|
|
|
230
|
-
> Ardo, J., & Law, R. (2023). vector2dggs (0.
|
|
246
|
+
> Ardo, J., & Law, R. (2023). vector2dggs (0.11.0) [Computer software]. https://github.com/manaakiwhenua/vector2dggs
|
|
231
247
|
|
|
232
248
|
[](https://github.com/manaakiwhenua/manaakiwhenua-standards)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "vector2dggs"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.11.0"
|
|
4
4
|
description = "CLI DGGS indexer for vector geospatial data"
|
|
5
5
|
authors = ["James Ardo <ardoj@landcareresearch.co.nz>"]
|
|
6
6
|
maintainers = ["Richard Law <lawr@landcareresearch.co.nz>"]
|
|
@@ -35,6 +35,7 @@ pillow = "^11.2.1"
|
|
|
35
35
|
s2geometry = "^0.9.0"
|
|
36
36
|
rusty-polygon-geohasher = "^0.2.3"
|
|
37
37
|
python-geohash = "^0.8.5"
|
|
38
|
+
rhealpixdggs = "^0.5.12"
|
|
38
39
|
|
|
39
40
|
[tool.poetry.group.dev.dependencies]
|
|
40
41
|
pytest = "^7.2.2"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__: str = "0.11.0"
|
|
@@ -18,7 +18,7 @@ from pathlib import Path, PurePath
|
|
|
18
18
|
from urllib.parse import urlparse
|
|
19
19
|
from tqdm import tqdm
|
|
20
20
|
from tqdm.dask import TqdmCallback
|
|
21
|
-
from
|
|
21
|
+
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor, as_completed
|
|
22
22
|
from shapely.geometry import GeometryCollection
|
|
23
23
|
|
|
24
24
|
import vector2dggs.constants as const
|
|
@@ -313,6 +313,47 @@ def polyfill(
|
|
|
313
313
|
def polyfill_star(args) -> None:
|
|
314
314
|
return polyfill(*args)
|
|
315
315
|
|
|
316
|
+
def bisection_preparation(df: pd.DataFrame, dggs: str, parent_res: int, cut_crs: pyproj.CRS = None, cut_threshold: Union[None, float] = None) -> tuple[pd.DataFrame, pyproj.CRS, Union[None, float]]:
|
|
317
|
+
cut_threshold = float(cut_threshold) if cut_threshold != None else None
|
|
318
|
+
|
|
319
|
+
if cut_threshold and cut_crs:
|
|
320
|
+
df = df.to_crs(cut_crs)
|
|
321
|
+
else:
|
|
322
|
+
cut_crs = df.crs
|
|
323
|
+
|
|
324
|
+
if cut_crs is None:
|
|
325
|
+
LOGGER.warning("Input has no defined CRS, and cut_crs is not specified")
|
|
326
|
+
elif cut_threshold != 0:
|
|
327
|
+
LOGGER.debug("Cutting with CRS: %s", df.crs)
|
|
328
|
+
|
|
329
|
+
if not cut_crs.is_projected and cut_threshold != 0:
|
|
330
|
+
LOGGER.warning(
|
|
331
|
+
f"CRS {cut_crs} is not a projected coordinate system. (units: {cut_crs.axis_info[0].unit_name}) Bisection will result in sections of varying area"
|
|
332
|
+
)
|
|
333
|
+
elif cut_threshold != 0:
|
|
334
|
+
LOGGER.debug(
|
|
335
|
+
f"Using CRS units for input polygon bisection: {cut_crs.axis_info[0].unit_name}"
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
if cut_threshold == None:
|
|
339
|
+
unit_name = cut_crs.axis_info[0].unit_name
|
|
340
|
+
cut_threshold_m2 = const.DEFAULT_AREA_THRESHOLD_M2(dggs, (int(parent_res)))
|
|
341
|
+
if unit_name == "metre":
|
|
342
|
+
cut_threshold = cut_threshold_m2
|
|
343
|
+
elif unit_name == "feet":
|
|
344
|
+
cut_threshold = cut_threshold_m2 * 3.28084
|
|
345
|
+
else:
|
|
346
|
+
cut_threshold = 100000000 if cut_crs.is_projected else 0.5
|
|
347
|
+
LOGGER.warning(
|
|
348
|
+
f'Unspecified cut_threshold for {"projected" if cut_crs.is_projected else "geographic"} CRS: {cut_crs}, with squared units: {unit_name}'
|
|
349
|
+
)
|
|
350
|
+
LOGGER.debug(f"Using default cut_threshold of {cut_threshold} ({unit_name}^2)")
|
|
351
|
+
|
|
352
|
+
return df, cut_crs, cut_threshold
|
|
353
|
+
|
|
354
|
+
def bisect_geometry(geometry, cut_threshold):
|
|
355
|
+
return GeometryCollection(katana.katana(geometry, cut_threshold))
|
|
356
|
+
|
|
316
357
|
|
|
317
358
|
def index(
|
|
318
359
|
dggs: str,
|
|
@@ -326,7 +367,7 @@ def index(
|
|
|
326
367
|
keep_attributes: bool,
|
|
327
368
|
chunksize: int,
|
|
328
369
|
spatial_sorting: str,
|
|
329
|
-
cut_threshold:
|
|
370
|
+
cut_threshold: Union[None, float],
|
|
330
371
|
processes: int,
|
|
331
372
|
compression: str = "snappy",
|
|
332
373
|
id_field: str = None,
|
|
@@ -356,9 +397,7 @@ def index(
|
|
|
356
397
|
# Read file
|
|
357
398
|
df = gpd.read_file(input_file, layer=layer)
|
|
358
399
|
|
|
359
|
-
|
|
360
|
-
df = df.to_crs(cut_crs)
|
|
361
|
-
LOGGER.debug("Cutting with CRS: %s", df.crs)
|
|
400
|
+
df, cut_crs, cut_threshold = bisection_preparation(df, dggs, parent_res, cut_crs, cut_threshold)
|
|
362
401
|
|
|
363
402
|
if id_field:
|
|
364
403
|
df = df.set_index(id_field)
|
|
@@ -370,13 +409,21 @@ def index(
|
|
|
370
409
|
# Remove all attributes except the geometry
|
|
371
410
|
df = df.loc[:, ["geometry"]]
|
|
372
411
|
|
|
373
|
-
LOGGER.debug("
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
)
|
|
379
|
-
|
|
412
|
+
LOGGER.debug("Bisecting large geometries")
|
|
413
|
+
|
|
414
|
+
if cut_threshold is not None and cut_threshold > 0:
|
|
415
|
+
with ThreadPoolExecutor(max_workers=max(1, processes)) as executor:
|
|
416
|
+
futures = []
|
|
417
|
+
for index, row in df.iterrows():
|
|
418
|
+
future = executor.submit(bisect_geometry, row.geometry, cut_threshold)
|
|
419
|
+
futures.append((index, future))
|
|
420
|
+
|
|
421
|
+
with tqdm(total=len(futures), desc="Bisection") as pbar:
|
|
422
|
+
for index, future in futures:
|
|
423
|
+
df.at[index, "geometry"] = future.result()
|
|
424
|
+
pbar.update(1)
|
|
425
|
+
else:
|
|
426
|
+
LOGGER.debug("No bisection applied to input.")
|
|
380
427
|
|
|
381
428
|
LOGGER.debug("Exploding geometry collections and multipolygons")
|
|
382
429
|
df = (
|
|
@@ -427,28 +474,33 @@ def index(
|
|
|
427
474
|
resolution,
|
|
428
475
|
)
|
|
429
476
|
with tempfile.TemporaryDirectory(suffix=".parquet") as tmpdir2:
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
)
|
|
443
|
-
for filepath in filepaths
|
|
444
|
-
]
|
|
445
|
-
list(
|
|
446
|
-
tqdm(
|
|
447
|
-
pool.imap(polyfill_star, args),
|
|
448
|
-
total=len(args),
|
|
449
|
-
desc="DGGS indexing",
|
|
450
|
-
)
|
|
477
|
+
|
|
478
|
+
args = [
|
|
479
|
+
(
|
|
480
|
+
dggs,
|
|
481
|
+
dggsfunc,
|
|
482
|
+
secondary_index_func,
|
|
483
|
+
filepath,
|
|
484
|
+
spatial_sort_col,
|
|
485
|
+
resolution,
|
|
486
|
+
parent_res,
|
|
487
|
+
tmpdir2,
|
|
488
|
+
compression,
|
|
451
489
|
)
|
|
490
|
+
for filepath in filepaths
|
|
491
|
+
]
|
|
492
|
+
|
|
493
|
+
with ProcessPoolExecutor(max_workers=processes) as executor:
|
|
494
|
+
futures = {executor.submit(polyfill_star, arg): arg for arg in args}
|
|
495
|
+
|
|
496
|
+
for future in tqdm(
|
|
497
|
+
as_completed(futures), total=len(futures), desc="DGGS indexing"
|
|
498
|
+
):
|
|
499
|
+
try:
|
|
500
|
+
future.result()
|
|
501
|
+
except Exception as e:
|
|
502
|
+
LOGGER.error(f"Task failed with {e}")
|
|
503
|
+
raise (e)
|
|
452
504
|
|
|
453
505
|
parent_partitioning(
|
|
454
506
|
dggs,
|
|
@@ -8,20 +8,6 @@ MIN_RHP, MAX_RHP = 0, 15
|
|
|
8
8
|
MIN_S2, MAX_S2 = 0, 30
|
|
9
9
|
MIN_GEOHASH, MAX_GEOHASH = 1, 12
|
|
10
10
|
|
|
11
|
-
DEFAULTS = {
|
|
12
|
-
"id": None,
|
|
13
|
-
"k": False,
|
|
14
|
-
"ch": 50,
|
|
15
|
-
"s": "none",
|
|
16
|
-
"crs": None,
|
|
17
|
-
"c": 5000,
|
|
18
|
-
"t": (multiprocessing.cpu_count() - 1),
|
|
19
|
-
"cp": "snappy",
|
|
20
|
-
"lyr": None,
|
|
21
|
-
"g": "geom",
|
|
22
|
-
"tempdir": tempfile.tempdir,
|
|
23
|
-
}
|
|
24
|
-
|
|
25
11
|
SPATIAL_SORTING_METHODS = ["hilbert", "morton", "geohash", "none"]
|
|
26
12
|
|
|
27
13
|
DEFAULT_DGGS_PARENT_RES = {
|
|
@@ -70,6 +56,86 @@ S2_CELLS_MAX_AREA_M2_BY_LEVEL = {
|
|
|
70
56
|
30: 0.93 * 1e-4,
|
|
71
57
|
}
|
|
72
58
|
|
|
59
|
+
# https://h3geo.org/docs/core-library/restable/
|
|
60
|
+
H3_CELLS_MAX_AREA_KM2_BY_LEVEL = {
|
|
61
|
+
0: 4977807.027442012,
|
|
62
|
+
1: 729486.875275344,
|
|
63
|
+
2: 104599.807218925,
|
|
64
|
+
3: 14950.773301379,
|
|
65
|
+
4: 2135.986983965,
|
|
66
|
+
5: 305.144308779,
|
|
67
|
+
6: 43.592111685,
|
|
68
|
+
7: 6.227445905,
|
|
69
|
+
8: 0.889635157,
|
|
70
|
+
9: 0.127090737,
|
|
71
|
+
10: 0.018155820,
|
|
72
|
+
11: 0.002593689,
|
|
73
|
+
12: 0.000370527,
|
|
74
|
+
13: 0.000052932,
|
|
75
|
+
14: 0.000007562,
|
|
76
|
+
15: 0.000001080,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
RHP_CELLS_AREA_KM2_BY_LEVEL = {
|
|
80
|
+
0: 100151150.62856922,
|
|
81
|
+
1: 11127905.62539658,
|
|
82
|
+
2: 1236433.9583773974,
|
|
83
|
+
3: 137381.55093082195,
|
|
84
|
+
4: 15264.616770091328,
|
|
85
|
+
5: 1696.0685300101482,
|
|
86
|
+
6: 188.4520588900164,
|
|
87
|
+
7: 20.939117654446267,
|
|
88
|
+
8: 2.326568628271808,
|
|
89
|
+
9: 0.25850762536353417,
|
|
90
|
+
10: 0.02872306948483713,
|
|
91
|
+
11: 0.003191452164981904,
|
|
92
|
+
12: 0.0003546057961091004,
|
|
93
|
+
13: 3.940064401212227 * 1e-5,
|
|
94
|
+
14: 4.377849334680252 * 1e-6,
|
|
95
|
+
15: 4.864277038533613 * 1e-7,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# https://www.movable-type.co.uk/scripts/geohash.html
|
|
99
|
+
GEOHASH_MAX_CELL_AREA_KM2_BY_LEVEL = {
|
|
100
|
+
1: 5000 * 5000,
|
|
101
|
+
2: 1250 * 625,
|
|
102
|
+
3: 156 * 156,
|
|
103
|
+
4: 39.1 * 19.5,
|
|
104
|
+
5: 4.89 * 4.89,
|
|
105
|
+
6: 1.22 * 0.61,
|
|
106
|
+
7: 153 * 153 / 1e6,
|
|
107
|
+
8: 38.2 * 19.1 / 1e6,
|
|
108
|
+
9: 4.77 * 4.77 / 1e6,
|
|
109
|
+
10: 1.19 * 0.596 / 1e6,
|
|
110
|
+
11: 149 * 149 / 1e9,
|
|
111
|
+
12: 37.2 * 18.6 / 1e9,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
DGGS_CELL_AREA_M2_BY_RES = {
|
|
115
|
+
"s2": lambda res: S2_CELLS_MAX_AREA_M2_BY_LEVEL[res],
|
|
116
|
+
"h3": lambda res: H3_CELLS_MAX_AREA_KM2_BY_LEVEL[res] * 1e6,
|
|
117
|
+
"rhp": lambda res: RHP_CELLS_AREA_KM2_BY_LEVEL[res] * 1e6,
|
|
118
|
+
"geohash": lambda res: GEOHASH_MAX_CELL_AREA_KM2_BY_LEVEL[res] * 1e6,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
DEFAULT_AREA_THRESHOLD_M2 = lambda dggs, parent_res: DGGS_CELL_AREA_M2_BY_RES[dggs](
|
|
122
|
+
parent_res
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
DEFAULTS = {
|
|
126
|
+
"id": None,
|
|
127
|
+
"k": False,
|
|
128
|
+
"ch": 50,
|
|
129
|
+
"s": "none",
|
|
130
|
+
"crs": None,
|
|
131
|
+
"c": None,
|
|
132
|
+
"t": (multiprocessing.cpu_count() - 1),
|
|
133
|
+
"cp": "snappy",
|
|
134
|
+
"lyr": None,
|
|
135
|
+
"g": "geom",
|
|
136
|
+
"tempdir": tempfile.tempdir,
|
|
137
|
+
}
|
|
138
|
+
|
|
73
139
|
|
|
74
140
|
warnings.filterwarnings(
|
|
75
141
|
"ignore"
|
|
@@ -217,10 +217,10 @@ def gh_compaction(
|
|
|
217
217
|
@click.option(
|
|
218
218
|
"-c",
|
|
219
219
|
"--cut_threshold",
|
|
220
|
-
required=
|
|
220
|
+
required=False,
|
|
221
221
|
default=const.DEFAULTS["c"],
|
|
222
|
-
type=
|
|
223
|
-
help="Cutting up large geometries into smaller geometries based on a target
|
|
222
|
+
type=float,
|
|
223
|
+
help="Cutting up large geometries into smaller geometries based on a target area. Units are assumed to match the input CRS units unless the `--cut_crs` is also given, in which case units match the units of the supplied CRS. If left unspecified, the threshold will be the maximum area of a cell at the parent resolution, in square metres or feet according to the CRS. A threshold of 0 will skip bissection entirely (effectively ignoring --cut_crs).",
|
|
224
224
|
nargs=1,
|
|
225
225
|
)
|
|
226
226
|
@click.option(
|
|
@@ -333,5 +333,3 @@ def geohash(
|
|
|
333
333
|
)
|
|
334
334
|
except:
|
|
335
335
|
raise
|
|
336
|
-
else:
|
|
337
|
-
sys.exit(0)
|
|
@@ -136,10 +136,10 @@ def h3compaction(
|
|
|
136
136
|
@click.option(
|
|
137
137
|
"-c",
|
|
138
138
|
"--cut_threshold",
|
|
139
|
-
required=
|
|
139
|
+
required=False,
|
|
140
140
|
default=const.DEFAULTS["c"],
|
|
141
|
-
type=
|
|
142
|
-
help="Cutting up large geometries into smaller geometries based on a target
|
|
141
|
+
type=float,
|
|
142
|
+
help="Cutting up large geometries into smaller geometries based on a target area. Units are assumed to match the input CRS units unless the `--cut_crs` is also given, in which case units match the units of the supplied CRS. If left unspecified, the threshold will be the maximum area of a cell at the parent resolution, in square metres or feet according to the CRS. A threshold of 0 will skip bissection entirely (effectively ignoring --cut_crs).",
|
|
143
143
|
nargs=1,
|
|
144
144
|
)
|
|
145
145
|
@click.option(
|
|
@@ -253,5 +253,3 @@ def h3(
|
|
|
253
253
|
)
|
|
254
254
|
except:
|
|
255
255
|
raise
|
|
256
|
-
else:
|
|
257
|
-
sys.exit(0)
|
|
@@ -30,14 +30,17 @@ def katana(
|
|
|
30
30
|
geometry: Union[BaseGeometry, None],
|
|
31
31
|
threshold: float,
|
|
32
32
|
count: int = 0,
|
|
33
|
+
max_recursion_depth: int = 250,
|
|
33
34
|
check_2D: bool = True,
|
|
34
35
|
) -> List[BaseGeometry]:
|
|
35
36
|
"""
|
|
36
37
|
Recursively split a geometry into two parts across its shortest dimension.
|
|
37
38
|
Invalid input `geometry` will silently be made valid (if possible).
|
|
38
39
|
Any LinearRings will be converted to Polygons.
|
|
40
|
+
`threshold`: maximum acceptable area of the bounding box for any output geometry.
|
|
41
|
+
`count`: used to track recursion depth
|
|
39
42
|
"""
|
|
40
|
-
if geometry is None:
|
|
43
|
+
if (geometry is None) or (geometry.is_empty):
|
|
41
44
|
return []
|
|
42
45
|
if isinstance(geometry, LinearRing):
|
|
43
46
|
geometry = Polygon(geometry)
|
|
@@ -52,9 +55,7 @@ def katana(
|
|
|
52
55
|
bounds = geometry.bounds
|
|
53
56
|
width = bounds[2] - bounds[0]
|
|
54
57
|
height = bounds[3] - bounds[1]
|
|
55
|
-
if
|
|
56
|
-
# either the polygon is smaller than the threshold, or the maximum
|
|
57
|
-
# number of recursions has been reached
|
|
58
|
+
if ((width * height) <= threshold) or (count >= max_recursion_depth):
|
|
58
59
|
return [geometry]
|
|
59
60
|
if height >= width:
|
|
60
61
|
# split left to right
|
|
@@ -64,6 +65,8 @@ def katana(
|
|
|
64
65
|
# split top to bottom
|
|
65
66
|
a = box(bounds[0], bounds[1], bounds[0] + width / 2, bounds[3])
|
|
66
67
|
b = box(bounds[0] + width / 2, bounds[1], bounds[2], bounds[3])
|
|
68
|
+
# Add additional vertices to help prevent indexing errors from use of EPSG:4386 later under the presence of long edges
|
|
69
|
+
a, b = map(lambda g: g.segmentize(min(width, height) / 4), [a, b])
|
|
67
70
|
result = []
|
|
68
71
|
for d in (
|
|
69
72
|
a,
|
|
@@ -12,13 +12,9 @@ import geopandas as gpd
|
|
|
12
12
|
from typing import Union
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
from rhealpixdggs.conversion import compress_order_cells
|
|
15
|
+
from rhealpixdggs.rhp_wrappers import rhp_to_center_child
|
|
15
16
|
from rhppandas.util.const import COLUMNS
|
|
16
17
|
|
|
17
|
-
# from rhealpixdggs.rhp_wrappers import rhp_to_center_child, rhp_is_valid
|
|
18
|
-
from rhealpixdggs.rhp_wrappers import rhp_is_valid
|
|
19
|
-
from rhealpixdggs.dggs import RHEALPixDGGS
|
|
20
|
-
from rhealpixdggs.dggs import WGS84_003
|
|
21
|
-
|
|
22
18
|
import vector2dggs.constants as const
|
|
23
19
|
import vector2dggs.common as common
|
|
24
20
|
|
|
@@ -57,54 +53,6 @@ def rhppolyfill(df: gpd.GeoDataFrame, resolution: int) -> pd.DataFrame:
|
|
|
57
53
|
)
|
|
58
54
|
|
|
59
55
|
|
|
60
|
-
# TODO replace when merged https://github.com/manaakiwhenua/rhealpixdggs-py/pull/37
|
|
61
|
-
def rhp_to_center_child(
|
|
62
|
-
rhpindex: str, res: int = None, dggs: RHEALPixDGGS = WGS84_003
|
|
63
|
-
) -> str:
|
|
64
|
-
"""
|
|
65
|
-
Returns central child of rhpindex at resolution res (immediate central
|
|
66
|
-
child if res == None).
|
|
67
|
-
|
|
68
|
-
Returns None if the cell index is invalid.
|
|
69
|
-
|
|
70
|
-
Returns None if the DGGS has an even number of cells on a side.
|
|
71
|
-
|
|
72
|
-
EXAMPLES::
|
|
73
|
-
|
|
74
|
-
>>> rhp_to_center_child('S001450634')
|
|
75
|
-
'S0014506344'
|
|
76
|
-
>>> rhp_to_center_child('S001450634', res=13)
|
|
77
|
-
'S001450634444'
|
|
78
|
-
>>> rhp_to_center_child('INVALID')
|
|
79
|
-
"""
|
|
80
|
-
# Stop early if the cell index is invalid
|
|
81
|
-
if not rhp_is_valid(rhpindex, dggs):
|
|
82
|
-
return None
|
|
83
|
-
|
|
84
|
-
# DGGSs with even numbers of cells on a side never have a cell at the centre
|
|
85
|
-
if (dggs.N_side % 2) == 0:
|
|
86
|
-
return None
|
|
87
|
-
|
|
88
|
-
# Handle mismatch between cell resolution and requested child resolution
|
|
89
|
-
parent_res = len(rhpindex) - 1
|
|
90
|
-
if res is not None and res < parent_res:
|
|
91
|
-
return rhpindex
|
|
92
|
-
|
|
93
|
-
# Standard case (including parent_res == res)
|
|
94
|
-
else:
|
|
95
|
-
# res == None returns the central child from one level down (by convention)
|
|
96
|
-
added_levels = 1 if res is None else res - parent_res
|
|
97
|
-
|
|
98
|
-
# Derive index of centre child and append that to rhpindex
|
|
99
|
-
# NOTE: only works for odd values of N_side
|
|
100
|
-
c_index = int((dggs.N_side**2 - 1) / 2)
|
|
101
|
-
|
|
102
|
-
# Append the required number of child digits to cell index
|
|
103
|
-
child_index = rhpindex + "".join(str(c_index) for _ in range(0, added_levels))
|
|
104
|
-
|
|
105
|
-
return child_index
|
|
106
|
-
|
|
107
|
-
|
|
108
56
|
def compact_cells(cells: set[str]) -> set[str]:
|
|
109
57
|
"""
|
|
110
58
|
Compact a set of rHEALPix DGGS cells.
|
|
@@ -205,17 +153,17 @@ def rhpcompaction(
|
|
|
205
153
|
@click.option(
|
|
206
154
|
"-c",
|
|
207
155
|
"--cut_threshold",
|
|
208
|
-
required=
|
|
156
|
+
required=False,
|
|
209
157
|
default=const.DEFAULTS["c"],
|
|
210
|
-
type=
|
|
211
|
-
help="Cutting up large geometries into smaller geometries based on a target
|
|
158
|
+
type=float,
|
|
159
|
+
help="Cutting up large geometries into smaller geometries based on a target area. Units are assumed to match the input CRS units unless the `--cut_crs` is also given, in which case units match the units of the supplied CRS. If left unspecified, the threshold will be the maximum area of a cell at the parent resolution, in square metres or feet according to the CRS. A threshold of 0 will skip bissection entirely (effectively ignoring --cut_crs).",
|
|
212
160
|
nargs=1,
|
|
213
161
|
)
|
|
214
162
|
@click.option(
|
|
215
163
|
"-t",
|
|
216
164
|
"--threads",
|
|
217
165
|
required=False,
|
|
218
|
-
default=
|
|
166
|
+
default=1,
|
|
219
167
|
type=int,
|
|
220
168
|
help="Amount of threads used for operation",
|
|
221
169
|
nargs=1,
|
|
@@ -322,5 +270,3 @@ def rhp(
|
|
|
322
270
|
)
|
|
323
271
|
except:
|
|
324
272
|
raise
|
|
325
|
-
else:
|
|
326
|
-
sys.exit(0)
|
|
@@ -9,6 +9,7 @@ from s2geometry import pywraps2 as S2
|
|
|
9
9
|
|
|
10
10
|
import pandas as pd
|
|
11
11
|
import geopandas as gpd
|
|
12
|
+
from shapely import force_2d
|
|
12
13
|
from shapely.geometry import box, Polygon, LineString, Point
|
|
13
14
|
from shapely.ops import transform
|
|
14
15
|
from pyproj import CRS, Transformer
|
|
@@ -71,6 +72,7 @@ def s2_polyfill_polygons(df: gpd.GeoDataFrame, level: int) -> gpd.GeoDataFrame:
|
|
|
71
72
|
def generate_s2_covering(
|
|
72
73
|
geom: Polygon, level: int, centroid_inside: bool = True
|
|
73
74
|
) -> set[S2.S2CellId]:
|
|
75
|
+
geom = force_2d(geom)
|
|
74
76
|
# Prepare loops: first the exterior loop, then the interior loops
|
|
75
77
|
loops = []
|
|
76
78
|
# Exterior ring
|
|
@@ -296,10 +298,10 @@ def s2_compaction(
|
|
|
296
298
|
@click.option(
|
|
297
299
|
"-c",
|
|
298
300
|
"--cut_threshold",
|
|
299
|
-
required=
|
|
301
|
+
required=False,
|
|
300
302
|
default=const.DEFAULTS["c"],
|
|
301
|
-
type=
|
|
302
|
-
help="Cutting up large geometries into smaller geometries based on a target
|
|
303
|
+
type=float,
|
|
304
|
+
help="Cutting up large geometries into smaller geometries based on a target area. Units are assumed to match the input CRS units unless the `--cut_crs` is also given, in which case units match the units of the supplied CRS. If left unspecified, the threshold will be the maximum area of a cell at the parent resolution, in square metres or feet according to the CRS. A threshold of 0 will skip bissection entirely (effectively ignoring --cut_crs).",
|
|
303
305
|
nargs=1,
|
|
304
306
|
)
|
|
305
307
|
@click.option(
|
|
@@ -412,5 +414,3 @@ def s2(
|
|
|
412
414
|
)
|
|
413
415
|
except:
|
|
414
416
|
raise
|
|
415
|
-
else:
|
|
416
|
-
sys.exit(0)
|