Add downloader configs and update geodata pipeline
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
- `geodata_to_unity.py` is the main CLI; library code lives in `geodata_pipeline/` (`heightmaps.py`, `orthophotos.py`, `config.py`, `setup_helpers.py`). Legacy wrapper scripts have been removed; use `geodata_to_unity.py` directly.
|
||||
- Working inputs (ignored): `raw/dgm1/`, `raw/dop20/jp2/`, `raw/citygml/lod1/`, `raw/citygml/lod2/`.
|
||||
- Archives (ignored): `archive/dgm1/`, `archive/dop20/`, `archive/citygml/lod1/`, `archive/citygml/lod2/` (zip storage + dop20 filelist).
|
||||
- Config: `geodata_config.json` (generated) or `geodata_config.example.json` for defaults.
|
||||
- Config: `geodata_config.toml` (generated) or `geodata_config.example.toml` for defaults.
|
||||
- `export_unity/` is generated output (heightmaps, orthophotos, manifest). `work/` holds intermediates and is disposable.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
@@ -52,4 +52,4 @@
|
||||
- Orthophotos: `export_orthophotos` reuses the manifest for target windows and will abort if it is missing; JPEGs are resampled to `ortho.out_res` with worldfiles and default JPEG quality 90.
|
||||
- Temporary files are written to `work/*_tmp.tif` and cleaned with broad `*.aux.xml` patterns in `work/` and the raw DGM1 directory—avoid placing non-GDAL aux files there.
|
||||
- `materialize_archives` unzips every `*.zip` under `archive/*` into the matching raw folders and copies `archive/dop20/filelist.txt` next to `raw/dop20/` for the downloader.
|
||||
- `geodata_config.example.json` includes `archives.dop20_filelist` for human reference; the dataclass ignores it, so keep the example in sync with actual CLI options rather than adding new unused keys.
|
||||
- `geodata_config.example.toml` includes `archives.dop20_filelist` for human reference; the dataclass ignores it, so keep the example in sync with actual CLI options rather than adding new unused keys.
|
||||
|
||||
@@ -21,7 +21,7 @@ This repository converts DGM1 elevation tiles into Unity-ready 16-bit PNG height
|
||||
- `export_unity/ortho_jpg/` — cropped orthophoto tiles aligned to the terrain grid (JPEG + worldfiles).
|
||||
- `geodata_to_unity.py` — main CLI (uses `geodata_pipeline/` library modules).
|
||||
- `scripts/` — helpers to create the directory tree and fetch DOP20 inputs.
|
||||
- `geodata_config.json` — generated config (see `geodata_config.example.json` for defaults).
|
||||
- `geodata_config.toml` — generated config (see `geodata_config.example.toml` for defaults).
|
||||
- `AGENTS.md` — contributor guide.
|
||||
|
||||
### Quick Start
|
||||
@@ -52,7 +52,7 @@ This repository converts DGM1 elevation tiles into Unity-ready 16-bit PNG height
|
||||
- Rebuild VRTs after moving data: add `--force-vrt`.
|
||||
|
||||
### Workflow Notes
|
||||
- The pipeline computes a global min/max from the VRT to scale all tiles consistently; adjust `heightmap.out_res` or `heightmap.resample` in `geodata_config.json` if your AOI or target resolution changes.
|
||||
- The pipeline computes a global min/max from the VRT to scale all tiles consistently; adjust `heightmap.out_res` or `heightmap.resample` in `geodata_config.toml` if your AOI or target resolution changes.
|
||||
- `_tmp.tif` files in `work/` are transient; you can delete `work/` to force a clean rebuild.
|
||||
- Keep file names stable to avoid churn in Unity scenes; re-exports overwrite in place.
|
||||
- Large raw datasets are intentionally excluded from version control—document download sources or scripts instead of committing data.
|
||||
@@ -70,6 +70,10 @@ This repository converts DGM1 elevation tiles into Unity-ready 16-bit PNG height
|
||||
- The download script relies on a Linux/OpenSSL toolchain with system CA bundle at `/etc/ssl/certs/ca-certificates.crt`; it builds a trust chain by fetching the geobasis intermediate. macOS/Windows users should either provide a combined CA via `CURL_CA_BUNDLE` or download with a browser/wget and place files manually.
|
||||
- Place companion `.j2w` and `.xml` files under `raw/dop20/j2w/` and `raw/dop20/meta/` if available; they are not required for the VRT but help provenance.
|
||||
|
||||
### Downloads (raw data)
|
||||
- Run: `uv run python geodata_to_unity.py --download` (uses `geodata_download.toml`).
|
||||
- Shows a progress bar while downloading and exits cleanly on Ctrl+C (exit code 130).
|
||||
|
||||
### Buildings (automated exporter)
|
||||
- Run: `uv run python geodata_to_unity.py --export buildings`
|
||||
- What it does per tile:
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
# List DOP asset URLs (JP2, J2W, XML), one per line. Lines starting with # are ignored.
|
||||
https://geobasis-rlp.de/data/dop20rgb/current/jp2/dop20rgb_32_328_5510_2_rp_2023.jp2
|
||||
https://geobasis-rlp.de/data/dop20rgb/current/jp2/dop20rgb_32_328_5510_2_rp_2023.j2w
|
||||
https://geobasis-rlp.de/data/dop20rgb/current/metadata/dop20rgb_32_328_5510_2_rp_2023_meta.xml
|
||||
https://geobasis-rlp.de/data/dop20rgb/current/jp2/dop20rgb_32_328_5512_2_rp_2023.jp2
|
||||
https://geobasis-rlp.de/data/dop20rgb/current/jp2/dop20rgb_32_328_5512_2_rp_2023.j2w
|
||||
https://geobasis-rlp.de/data/dop20rgb/current/metadata/dop20rgb_32_328_5512_2_rp_2023_meta.xml
|
||||
https://geobasis-rlp.de/data/dop20rgb/current/jp2/dop20rgb_32_328_5514_2_rp_2023.jp2
|
||||
https://geobasis-rlp.de/data/dop20rgb/current/jp2/dop20rgb_32_328_5514_2_rp_2023.j2w
|
||||
https://geobasis-rlp.de/data/dop20rgb/current/metadata/dop20rgb_32_328_5514_2_rp_2023_meta.xml
|
||||
3889
certs/geobasis-ca.pem
Normal file
3889
certs/geobasis-ca.pem
Normal file
File diff suppressed because it is too large
Load Diff
BIN
certs/geobasis-intermediate.der
Normal file
BIN
certs/geobasis-intermediate.der
Normal file
Binary file not shown.
36
certs/geobasis-intermediate.pem
Normal file
36
certs/geobasis-intermediate.pem
Normal file
@@ -0,0 +1,36 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIGTDCCBDSgAwIBAgIQOXpmzCdWNi4NqofKbqvjsTANBgkqhkiG9w0BAQwFADBf
|
||||
MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQD
|
||||
Ey1TZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYw
|
||||
HhcNMjEwMzIyMDAwMDAwWhcNMzYwMzIxMjM1OTU5WjBgMQswCQYDVQQGEwJHQjEY
|
||||
MBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTcwNQYDVQQDEy5TZWN0aWdvIFB1Ymxp
|
||||
YyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gQ0EgRFYgUjM2MIIBojANBgkqhkiG9w0B
|
||||
AQEFAAOCAY8AMIIBigKCAYEAljZf2HIz7+SPUPQCQObZYcrxLTHYdf1ZtMRe7Yeq
|
||||
RPSwygz16qJ9cAWtWNTcuICc++p8Dct7zNGxCpqmEtqifO7NvuB5dEVexXn9RFFH
|
||||
12Hm+NtPRQgXIFjx6MSJcNWuVO3XGE57L1mHlcQYj+g4hny90aFh2SCZCDEVkAja
|
||||
EMMfYPKuCjHuuF+bzHFb/9gV8P9+ekcHENF2nR1efGWSKwnfG5RawlkaQDpRtZTm
|
||||
M64TIsv/r7cyFO4nSjs1jLdXYdz5q3a4L0NoabZfbdxVb+CUEHfB0bpulZQtH1Rv
|
||||
38e/lIdP7OTTIlZh6OYL6NhxP8So0/sht/4J9mqIGxRFc0/pC8suja+wcIUna0HB
|
||||
pXKfXTKpzgis+zmXDL06ASJf5E4A2/m+Hp6b84sfPAwQ766rI65mh50S0Di9E3Pn
|
||||
2WcaJc+PILsBmYpgtmgWTR9eV9otfKRUBfzHUHcVgarub/XluEpRlTtZudU5xbFN
|
||||
xx/DgMrXLUAPaI60fZ6wA+PTAgMBAAGjggGBMIIBfTAfBgNVHSMEGDAWgBRWc1hk
|
||||
lfmSGrASKgRieaFAFYghSTAdBgNVHQ4EFgQUaMASFhgOr872h6YyV6NGUV3LBycw
|
||||
DgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYI
|
||||
KwYBBQUHAwEGCCsGAQUFBwMCMBsGA1UdIAQUMBIwBgYEVR0gADAIBgZngQwBAgEw
|
||||
VAYDVR0fBE0wSzBJoEegRYZDaHR0cDovL2NybC5zZWN0aWdvLmNvbS9TZWN0aWdv
|
||||
UHVibGljU2VydmVyQXV0aGVudGljYXRpb25Sb290UjQ2LmNybDCBhAYIKwYBBQUH
|
||||
AQEEeDB2ME8GCCsGAQUFBzAChkNodHRwOi8vY3J0LnNlY3RpZ28uY29tL1NlY3Rp
|
||||
Z29QdWJsaWNTZXJ2ZXJBdXRoZW50aWNhdGlvblJvb3RSNDYucDdjMCMGCCsGAQUF
|
||||
BzABhhdodHRwOi8vb2NzcC5zZWN0aWdvLmNvbTANBgkqhkiG9w0BAQwFAAOCAgEA
|
||||
YtOC9Fy+TqECFw40IospI92kLGgoSZGPOSQXMBqmsGWZUQ7rux7cj1du6d9rD6C8
|
||||
ze1B2eQjkrGkIL/OF1s7vSmgYVafsRoZd/IHUrkoQvX8FZwUsmPu7amgBfaY3g+d
|
||||
q1x0jNGKb6I6Bzdl6LgMD9qxp+3i7GQOnd9J8LFSietY6Z4jUBzVoOoz8iAU84OF
|
||||
h2HhAuiPw1ai0VnY38RTI+8kepGWVfGxfBWzwH9uIjeooIeaosVFvE8cmYUB4TSH
|
||||
5dUyD0jHct2+8ceKEtIoFU/FfHq/mDaVnvcDCZXtIgitdMFQdMZaVehmObyhRdDD
|
||||
4NQCs0gaI9AAgFj4L9QtkARzhQLNyRf87Kln+YU0lgCGr9HLg3rGO8q+Y4ppLsOd
|
||||
unQZ6ZxPNGIfOApbPVf5hCe58EZwiWdHIMn9lPP6+F404y8NNugbQixBber+x536
|
||||
WrZhFZLjEkhp7fFXf9r32rNPfb74X/U90Bdy4lzp3+X1ukh1BuMxA/EEhDoTOS3l
|
||||
7ABvc7BYSQubQ2490OcdkIzUh3ZwDrakMVrbaTxUM2p24N6dB+ns2zptWCva6jzW
|
||||
r8IWKIMxzxLPv5Kt3ePKcUdvkBU/smqujSczTzzSjIoR5QqQA6lN1ZRSnuHIWCvh
|
||||
JEltkYnTAH41QJ6SAWO66GrrUESwN/cgZzL4JLEqz1Y=
|
||||
-----END CERTIFICATE-----
|
||||
37
certs/geobasis-leaf.pem
Normal file
37
certs/geobasis-leaf.pem
Normal file
@@ -0,0 +1,37 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIGgzCCBOugAwIBAgIRAP5ioHwYfhl9YJpVkKnEMe0wDQYJKoZIhvcNAQELBQAw
|
||||
YDELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1NlY3RpZ28gTGltaXRlZDE3MDUGA1UE
|
||||
AxMuU2VjdGlnbyBQdWJsaWMgU2VydmVyIEF1dGhlbnRpY2F0aW9uIENBIERWIFIz
|
||||
NjAeFw0yNTEwMTcwMDAwMDBaFw0yNjExMDcyMzU5NTlaMBwxGjAYBgNVBAMMESou
|
||||
Z2VvYmFzaXMtcmxwLmRlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
|
||||
yfc3FMrzYYK4ewmUKrJSTQbfoSBfoj8aipuCM7mbg4Ty4diPgJvPLLsRkEValNc3
|
||||
MXXZWA0QATLvV4bYuk9mwyaM/+tOKHuiinHyG+w8cc0kSfw0fv4jvVso+a7zXkRV
|
||||
mPJEbGkfOUuSp0zpaugUr2M8SaSL0Vv0KYGj72OnBHQF3+RGzvSE4Ndqchi0KQ8U
|
||||
qvaWlS3bErrcxLxdL2Tl2xiSvuqOtYXVAq4B+1hLW2QW9SoxuzW3Q5Q3wvAmdENm
|
||||
8F1SPMHtcUuSLa++v33qEBGvR4qwa7XTOA1V7faTNvk9yZ6JOv+WqmpXEn6fBMyB
|
||||
0QZOPSTrk9rKtzWMw3pmGwIDAQABo4IC+jCCAvYwHwYDVR0jBBgwFoAUaMASFhgO
|
||||
r872h6YyV6NGUV3LBycwHQYDVR0OBBYEFBcm9pABHMtlZ7ZEdMIcvp0JQGbrMA4G
|
||||
A1UdDwEB/wQEAwIFoDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoGCCsGAQUFBwMB
|
||||
MEkGA1UdIARCMEAwNAYLKwYBBAGyMQECAgcwJTAjBggrBgEFBQcCARYXaHR0cHM6
|
||||
Ly9zZWN0aWdvLmNvbS9DUFMwCAYGZ4EMAQIBMIGEBggrBgEFBQcBAQR4MHYwTwYI
|
||||
KwYBBQUHMAKGQ2h0dHA6Ly9jcnQuc2VjdGlnby5jb20vU2VjdGlnb1B1YmxpY1Nl
|
||||
cnZlckF1dGhlbnRpY2F0aW9uQ0FEVlIzNi5jcnQwIwYIKwYBBQUHMAGGF2h0dHA6
|
||||
Ly9vY3NwLnNlY3RpZ28uY29tMIIBfgYKKwYBBAHWeQIEAgSCAW4EggFqAWgAdgDY
|
||||
CVU7lE96/8gWGW+UT4WrsPj8XodVJg8V0S5yu0VLFAAAAZnw4/G9AAAEAwBHMEUC
|
||||
IQDuAlkI8jIfnctBqbFzDTd3Lq/7Ea1TtswE/qkxiekbWgIgRDAS9Jn7tzNZCK5F
|
||||
yXAkxVvI0Lfr8LaRh+ebEBr8UQkAdwCvZ4g7V7BO3Y+m2X72LqjrgQrHcWDwJF5V
|
||||
1gwv54WHOgAAAZnw4/IZAAAEAwBIMEYCIQCxuUg18GWq8eKjgTNUxpfexMduI+ac
|
||||
hyH4xenFMyfrQwIhAM0D2f82D3PVKFoz6pL0RzWXykbCLc41dQiiDO+MJ8PzAHUA
|
||||
rKswcGzr7IQx9BPS9JFfER5CJEOx8qaMTzwrO6ceAsMAAAGZ8OPxYwAABAMARjBE
|
||||
AiARYM5X56Jss1n2M+RQvwDIHMU/Znef+1cqKqqcJr6VoAIgf5OVbeGGhCFnipEA
|
||||
al51d6rV3lkKfpQ6ZQ1hwJhDXwYwLQYDVR0RBCYwJIIRKi5nZW9iYXNpcy1ybHAu
|
||||
ZGWCD2dlb2Jhc2lzLXJscC5kZTANBgkqhkiG9w0BAQsFAAOCAYEAA4ebZa2YRg6v
|
||||
+mnkVqai8bBuSDb7I0Uh+HEVFW1dAMdDVswqJlv2C/a3zTaYeQqaaXCukq5ww4Fs
|
||||
m+gT5J1JzOv8rb+a9ajh0tl1HRG16NanUkJVrjfxaqOcmrTTgSstuYdq/lkP/hfj
|
||||
36YE+m42yBQ+J00H+jpA2MQnEa+U3+klzmVPlxWKd/E3coLRvAu/m8VNgg6Mvwgi
|
||||
eO6AM+26Mu9BveD7rNVRn4c7JcALzvO9mYmBhfaUpKdwXC8Vly8SeEkcX2L6onwa
|
||||
3m7elNT6Ark1nU5PJaRSB5BpDJ7tohWA1elV6cKtEKo3oh/K6Xxp5DQLxJ7PNtkR
|
||||
OjwQHImwr0GXhex53+bgLkha17LXxYvggAHvsmFcZT8oERMH3WeMRZDIOLmkOkhd
|
||||
4VElGzMSzm/25UnQvU+topittRQ0HnO3j8lERRKvuppK29GNPXX0c9RRHG7UOT+U
|
||||
EZoxlh5zgCMx9sLqZafFLv97NJgIwQKvTOmGdcBuoIg/KJVxpPqr
|
||||
-----END CERTIFICATE-----
|
||||
@@ -1,55 +0,0 @@
|
||||
{
|
||||
"raw": {
|
||||
"dgm1_dir": "raw/dgm1",
|
||||
"dop20_dir": "raw/dop20/jp2",
|
||||
"citygml_lod1_dir": "raw/citygml/lod1",
|
||||
"citygml_lod2_dir": "raw/citygml/lod2"
|
||||
},
|
||||
"archives": {
|
||||
"dgm1_dir": "archive/dgm1",
|
||||
"dop20_dir": "archive/dop20",
|
||||
"dop20_filelist": "archive/dop20/filelist.txt",
|
||||
"citygml_lod1_dir": "archive/citygml/lod1",
|
||||
"citygml_lod2_dir": "archive/citygml/lod2"
|
||||
},
|
||||
"work": {
|
||||
"work_dir": "work",
|
||||
"heightmap_vrt": "work/dgm.vrt",
|
||||
"ortho_vrt": "work/dop.vrt"
|
||||
},
|
||||
"export": {
|
||||
"heightmap_dir": "export_unity/height_png16",
|
||||
"ortho_dir": "export_unity/ortho_jpg",
|
||||
"manifest_path": "export_unity/tile_index.csv"
|
||||
},
|
||||
"heightmap": {
|
||||
"out_res": 1025,
|
||||
"resample": "bilinear",
|
||||
"tile_size_m": 1000
|
||||
},
|
||||
"ortho": {
|
||||
"out_res": 2048,
|
||||
"jpeg_quality": 90
|
||||
},
|
||||
"buildings": {
|
||||
"out_dir": "export_unity/buildings_tiles",
|
||||
"work_cityjson_dir": "work/cityjson_lod2",
|
||||
"work_rebased_dir": "work/cityjson_lod2_local",
|
||||
"work_glb_dir": "work/buildings_glb_tmp",
|
||||
"triangle_budget_min": 200000,
|
||||
"triangle_budget_max": 350000,
|
||||
"roof_unlit": true
|
||||
},
|
||||
"trees": {
|
||||
"csv_dir": "export_unity/trees",
|
||||
"glb_dir": "export_unity/trees_tiles",
|
||||
"proxy_library": "export_unity/tree_proxies.glb",
|
||||
"grid_res_m": 2.0,
|
||||
"min_height_m": 2.0,
|
||||
"max_trees": 5000,
|
||||
"chunk_grid": 4,
|
||||
"proxy_variants": 16,
|
||||
"proxy_min_tris": 120,
|
||||
"proxy_max_tris": 180
|
||||
}
|
||||
}
|
||||
54
geodata_config.example.toml
Normal file
54
geodata_config.example.toml
Normal file
@@ -0,0 +1,54 @@
|
||||
[raw]
|
||||
dgm1_dir = "raw/dgm1"
|
||||
dop20_dir = "raw/dop20/jp2"
|
||||
citygml_lod1_dir = "raw/citygml/lod1"
|
||||
citygml_lod2_dir = "raw/citygml/lod2"
|
||||
|
||||
[archives]
|
||||
dgm1_dir = "archive/dgm1"
|
||||
dop20_dir = "archive/dop20"
|
||||
dop20_filelist = "archive/dop20/filelist.txt"
|
||||
citygml_lod1_dir = "archive/citygml/lod1"
|
||||
citygml_lod2_dir = "archive/citygml/lod2"
|
||||
|
||||
[work]
|
||||
work_dir = "work"
|
||||
heightmap_vrt = "work/dgm.vrt"
|
||||
ortho_vrt = "work/dop.vrt"
|
||||
|
||||
[export]
|
||||
heightmap_dir = "export_unity/height_png16"
|
||||
ortho_dir = "export_unity/ortho_jpg"
|
||||
manifest_path = "export_unity/tile_index.csv"
|
||||
|
||||
[heightmap]
|
||||
out_res = 1025
|
||||
resample = "bilinear"
|
||||
tile_size_m = 1000
|
||||
|
||||
[ortho]
|
||||
out_res = 2048
|
||||
jpeg_quality = 90
|
||||
|
||||
[buildings]
|
||||
out_dir = "export_unity/buildings_tiles"
|
||||
work_cityjson_dir = "work/cityjson_lod2"
|
||||
work_rebased_dir = "work/cityjson_lod2_local"
|
||||
work_glb_dir = "work/buildings_glb_tmp"
|
||||
triangle_budget_min = 200000
|
||||
triangle_budget_max = 350000
|
||||
roof_unlit = true
|
||||
|
||||
[trees]
|
||||
csv_dir = "export_unity/trees"
|
||||
glb_dir = "export_unity/trees_tiles"
|
||||
proxy_library = "export_unity/tree_proxies.glb"
|
||||
grid_res_m = 2.0
|
||||
min_height_m = 2.0
|
||||
max_trees = 5000
|
||||
chunk_grid = 4
|
||||
proxy_variants = 16
|
||||
proxy_min_tris = 120
|
||||
proxy_max_tris = 180
|
||||
building_buffer_m = 1.5
|
||||
instancing = false
|
||||
53
geodata_config.toml
Normal file
53
geodata_config.toml
Normal file
@@ -0,0 +1,53 @@
|
||||
[raw]
|
||||
dgm1_dir = "raw/dgm1"
|
||||
dop20_dir = "raw/dop20/jp2"
|
||||
citygml_lod1_dir = "raw/citygml/lod1"
|
||||
citygml_lod2_dir = "raw/citygml/lod2"
|
||||
|
||||
[archives]
|
||||
dgm1_dir = "archive/dgm1"
|
||||
dop20_dir = "archive/dop20"
|
||||
citygml_lod1_dir = "archive/citygml/lod1"
|
||||
citygml_lod2_dir = "archive/citygml/lod2"
|
||||
|
||||
[work]
|
||||
work_dir = "work"
|
||||
heightmap_vrt = "work/dgm.vrt"
|
||||
ortho_vrt = "work/dop.vrt"
|
||||
|
||||
[export]
|
||||
heightmap_dir = "export_unity/height_png16"
|
||||
ortho_dir = "export_unity/ortho_jpg"
|
||||
manifest_path = "export_unity/tile_index.csv"
|
||||
|
||||
[heightmap]
|
||||
out_res = 1025
|
||||
resample = "bilinear"
|
||||
tile_size_m = 1000
|
||||
|
||||
[ortho]
|
||||
out_res = 2048
|
||||
jpeg_quality = 90
|
||||
|
||||
[buildings]
|
||||
out_dir = "export_unity/buildings_tiles"
|
||||
work_cityjson_dir = "work/cityjson_lod2"
|
||||
work_rebased_dir = "work/cityjson_lod2_local"
|
||||
work_glb_dir = "work/buildings_glb_tmp"
|
||||
triangle_budget_min = 200000
|
||||
triangle_budget_max = 350000
|
||||
roof_unlit = true
|
||||
|
||||
[trees]
|
||||
csv_dir = "export_unity/trees"
|
||||
glb_dir = "export_unity/trees_tiles"
|
||||
proxy_library = "export_unity/tree_proxies.glb"
|
||||
grid_res_m = 2.0
|
||||
min_height_m = 2.0
|
||||
max_trees = 5000
|
||||
chunk_grid = 4
|
||||
proxy_variants = 16
|
||||
proxy_min_tris = 120
|
||||
proxy_max_tris = 180
|
||||
building_buffer_m = 1.5
|
||||
instancing = false
|
||||
569
geodata_download.py
Normal file
569
geodata_download.py
Normal file
@@ -0,0 +1,569 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Download configured geodata tiles into raw/ based on a TOML config."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
import queue
|
||||
import threading
|
||||
from concurrent.futures import Future, as_completed
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Iterable, List, Optional, Tuple
|
||||
from urllib.parse import urlparse
|
||||
|
||||
try:
|
||||
import tomllib
|
||||
except ImportError: # pragma: no cover - tomllib is required
|
||||
raise SystemExit("tomllib is required (Python 3.11+).")
|
||||
|
||||
import requests
|
||||
|
||||
DEFAULT_CONFIG = "geodata_download.toml"
|
||||
DEFAULT_OUTPUT_DIR = "raw"
|
||||
|
||||
OUTPUT_SUBDIRS = {
|
||||
"dgm1": "dgm1",
|
||||
"dom1": "dom1",
|
||||
"dop20": "dop20",
|
||||
"geb3dlo": os.path.join("citygml", "lod2"),
|
||||
"citygml": os.path.join("citygml", "lod2"),
|
||||
"bdom20rgbi": "bdom20rgbi",
|
||||
"lpg": "lpolpg",
|
||||
"lpo": "lpolpg",
|
||||
"lpolpg": "lpolpg",
|
||||
}
|
||||
|
||||
FILE_TYPE_SUBDIRS = {
|
||||
"image": "jp2",
|
||||
"worldfile": "j2w",
|
||||
"metadata": "meta",
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DownloadTask:
|
||||
dataset: str
|
||||
url: str
|
||||
output_path: str
|
||||
|
||||
|
||||
class DownloadLogger:
|
||||
def __init__(
|
||||
self,
|
||||
log_file: Optional[str] = None,
|
||||
log_format: Optional[str] = None,
|
||||
report_progress: bool = True,
|
||||
) -> None:
|
||||
self._log_file = log_file
|
||||
self._log_format = log_format
|
||||
self._report_progress = report_progress
|
||||
|
||||
def log(self, level: str, message: str) -> None:
|
||||
line = self._format(level, message)
|
||||
if self._report_progress:
|
||||
print(line)
|
||||
if self._log_file:
|
||||
with open(self._log_file, "a", encoding="utf-8") as fh:
|
||||
fh.write(line + "\n")
|
||||
|
||||
def _format(self, level: str, message: str) -> str:
|
||||
if not self._log_format:
|
||||
return f"[{level}] {message}"
|
||||
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
try:
|
||||
return self._log_format.format(timestamp=timestamp, level=level, message=message)
|
||||
except Exception:
|
||||
return f"[{level}] {message}"
|
||||
|
||||
|
||||
class DownloadProgress:
|
||||
def __init__(self, total: int, enabled: bool) -> None:
|
||||
self._total = max(total, 1)
|
||||
self._enabled = enabled
|
||||
self._downloaded = 0
|
||||
self._missing = 0
|
||||
self._failed = 0
|
||||
self._bytes = 0
|
||||
self._lock = threading.Lock()
|
||||
self._start = time.time()
|
||||
self._last_label = ""
|
||||
self._last_render = 0.0
|
||||
self._min_interval = 0.25
|
||||
|
||||
def add_bytes(self, bytes_delta: int, label: Optional[str] = None) -> None:
|
||||
if not self._enabled or bytes_delta <= 0:
|
||||
return
|
||||
with self._lock:
|
||||
self._bytes += bytes_delta
|
||||
if label:
|
||||
self._last_label = os.path.basename(label)
|
||||
self._render_locked(force=False)
|
||||
|
||||
def set_counts(self, downloaded: int, missing: int, failed: int, label: Optional[str] = None) -> None:
|
||||
if not self._enabled:
|
||||
return
|
||||
with self._lock:
|
||||
self._downloaded = downloaded
|
||||
self._missing = missing
|
||||
self._failed = failed
|
||||
if label:
|
||||
self._last_label = os.path.basename(label)
|
||||
self._render_locked(force=True)
|
||||
|
||||
def finish(self) -> None:
|
||||
if not self._enabled:
|
||||
return
|
||||
sys.stderr.write("\n")
|
||||
sys.stderr.flush()
|
||||
|
||||
def _render_locked(self, force: bool) -> None:
|
||||
now = time.time()
|
||||
if not force and (now - self._last_render) < self._min_interval:
|
||||
return
|
||||
self._last_render = now
|
||||
elapsed = max(now - self._start, 0.001)
|
||||
done = self._downloaded + self._missing + self._failed
|
||||
rate = done / elapsed
|
||||
remaining = max(self._total - done, 0)
|
||||
eta = int(remaining / rate) if rate > 0 else 0
|
||||
bytes_mb = self._bytes / (1024 * 1024)
|
||||
bytes_gb = bytes_mb / 1024
|
||||
byte_rate = bytes_mb / elapsed
|
||||
width = 28
|
||||
filled = int(width * done / self._total)
|
||||
bar = "#" * filled + "-" * (width - filled)
|
||||
line = (
|
||||
f"\r[{bar}] {done}/{self._total} "
|
||||
f"{rate:.2f}/s eta {eta}s ok={self._downloaded} miss={self._missing} fail={self._failed} "
|
||||
f"{bytes_gb:.1f}GB {byte_rate:.1f}MB/s "
|
||||
f"{self._last_label}"
|
||||
)
|
||||
sys.stderr.write(line[:200])
|
||||
sys.stderr.flush()
|
||||
|
||||
|
||||
class DaemonThreadPool:
|
||||
"""Minimal daemon-thread pool that supports submit() + shutdown()."""
|
||||
|
||||
def __init__(self, max_workers: int) -> None:
|
||||
if max_workers <= 0:
|
||||
raise ValueError("max_workers must be greater than 0")
|
||||
self._tasks: queue.Queue = queue.Queue()
|
||||
self._threads: list[threading.Thread] = []
|
||||
self._shutdown = False
|
||||
for idx in range(max_workers):
|
||||
thread = threading.Thread(
|
||||
name=f"download-worker-{idx}",
|
||||
target=self._worker,
|
||||
daemon=True,
|
||||
)
|
||||
thread.start()
|
||||
self._threads.append(thread)
|
||||
|
||||
def submit(self, fn, *args, **kwargs) -> Future:
|
||||
if self._shutdown:
|
||||
raise RuntimeError("Thread pool already shutdown")
|
||||
future: Future = Future()
|
||||
self._tasks.put((future, fn, args, kwargs))
|
||||
return future
|
||||
|
||||
def shutdown(self, wait: bool = True, cancel_futures: bool = False) -> None:
|
||||
self._shutdown = True
|
||||
if cancel_futures:
|
||||
while True:
|
||||
try:
|
||||
item = self._tasks.get_nowait()
|
||||
except queue.Empty:
|
||||
break
|
||||
if item is None:
|
||||
continue
|
||||
future, _, _, _ = item
|
||||
future.cancel()
|
||||
for _ in self._threads:
|
||||
self._tasks.put(None)
|
||||
if wait:
|
||||
for thread in self._threads:
|
||||
thread.join()
|
||||
|
||||
def _worker(self) -> None:
|
||||
while True:
|
||||
item = self._tasks.get()
|
||||
if item is None:
|
||||
return
|
||||
future, fn, args, kwargs = item
|
||||
if not future.set_running_or_notify_cancel():
|
||||
continue
|
||||
try:
|
||||
result = fn(*args, **kwargs)
|
||||
except BaseException as exc:
|
||||
future.set_exception(exc)
|
||||
else:
|
||||
future.set_result(result)
|
||||
|
||||
|
||||
def _load_toml(path: str) -> dict:
|
||||
if not os.path.exists(path):
|
||||
raise SystemExit(f"Config not found: {path}")
|
||||
with open(path, "rb") as fh:
|
||||
return tomllib.load(fh)
|
||||
|
||||
|
||||
def _parse_tile_ranges(cfg: dict) -> Dict[str, dict]:
|
||||
ranges = cfg.get("tile_ranges", {})
|
||||
if not isinstance(ranges, dict):
|
||||
raise SystemExit("tile_ranges must be a table in the TOML config.")
|
||||
return ranges
|
||||
|
||||
|
||||
def _iter_tiles(range_cfg: dict) -> Iterable[Tuple[int, int]]:
|
||||
x_start = int(range_cfg["x_start"])
|
||||
x_end = int(range_cfg["x_end"])
|
||||
x_step = int(range_cfg.get("x_step", 1))
|
||||
y_start = int(range_cfg["y_start"])
|
||||
y_end = int(range_cfg["y_end"])
|
||||
y_step = int(range_cfg.get("y_step", 1))
|
||||
|
||||
for x in range(x_start, x_end + 1, x_step):
|
||||
for y in range(y_start, y_end + 1, y_step):
|
||||
yield x, y
|
||||
|
||||
|
||||
def _override_range(range_cfg: dict, start: Tuple[int, int], end: Tuple[int, int]) -> dict:
|
||||
new_range = dict(range_cfg)
|
||||
new_range["x_start"] = start[0]
|
||||
new_range["y_start"] = start[1]
|
||||
new_range["x_end"] = end[0]
|
||||
new_range["y_end"] = end[1]
|
||||
return new_range
|
||||
|
||||
|
||||
def _resolve_output_dir(dataset_key: str, dataset_cfg: dict) -> str:
|
||||
if dataset_key in OUTPUT_SUBDIRS:
|
||||
return OUTPUT_SUBDIRS[dataset_key]
|
||||
return dataset_cfg.get("output_subdir", dataset_key)
|
||||
|
||||
|
||||
def _format_url(
|
||||
template: str,
|
||||
base_url: str,
|
||||
x: int,
|
||||
y: int,
|
||||
extra: dict,
|
||||
) -> str:
|
||||
format_vars = {"base_url": base_url, "x": x, "y": y}
|
||||
for key, value in extra.items():
|
||||
if isinstance(value, (str, int, float)):
|
||||
format_vars[key] = value
|
||||
return template.format(**format_vars)
|
||||
|
||||
|
||||
def _build_tasks(
|
||||
cfg: dict,
|
||||
datasets: Dict[str, dict],
|
||||
tile_ranges: Dict[str, dict],
|
||||
base_output_dir: str,
|
||||
start_override: Optional[Tuple[int, int]],
|
||||
end_override: Optional[Tuple[int, int]],
|
||||
) -> List[DownloadTask]:
|
||||
tasks: List[DownloadTask] = []
|
||||
base_url = cfg.get("download", {}).get("base_url", "").rstrip("/")
|
||||
|
||||
for dataset_key, dataset_cfg in datasets.items():
|
||||
tile_range_key = dataset_cfg.get("tile_range")
|
||||
if not tile_range_key or tile_range_key not in tile_ranges:
|
||||
raise SystemExit(f"{dataset_key}: tile_range not found: {tile_range_key}")
|
||||
range_cfg = tile_ranges[tile_range_key]
|
||||
if start_override and end_override:
|
||||
range_cfg = _override_range(range_cfg, start_override, end_override)
|
||||
|
||||
base_subdir = _resolve_output_dir(dataset_key, dataset_cfg)
|
||||
dataset_out_dir = os.path.join(base_output_dir, base_subdir)
|
||||
|
||||
for x, y in _iter_tiles(range_cfg):
|
||||
if "files" in dataset_cfg:
|
||||
for file_cfg in dataset_cfg["files"]:
|
||||
file_type = file_cfg.get("type", "file")
|
||||
file_subdir = FILE_TYPE_SUBDIRS.get(file_type, file_type)
|
||||
out_dir = os.path.join(dataset_out_dir, file_subdir)
|
||||
url = _format_url(file_cfg["url_template"], base_url, x, y, file_cfg)
|
||||
filename = os.path.basename(urlparse(url).path)
|
||||
tasks.append(DownloadTask(dataset_key, url, os.path.join(out_dir, filename)))
|
||||
else:
|
||||
url = _format_url(dataset_cfg["url_template"], base_url, x, y, dataset_cfg)
|
||||
filename = os.path.basename(urlparse(url).path)
|
||||
tasks.append(DownloadTask(dataset_key, url, os.path.join(dataset_out_dir, filename)))
|
||||
|
||||
return tasks
|
||||
|
||||
|
||||
def _select_datasets(cfg: dict, requested: Optional[List[str]]) -> Dict[str, dict]:
|
||||
datasets = cfg.get("datasets", {})
|
||||
if not isinstance(datasets, dict) or not datasets:
|
||||
raise SystemExit("datasets must be defined in the TOML config.")
|
||||
|
||||
if requested:
|
||||
missing = [name for name in requested if name not in datasets]
|
||||
if missing:
|
||||
raise SystemExit(f"Unknown dataset(s): {', '.join(missing)}")
|
||||
selected = {name: datasets[name] for name in requested}
|
||||
else:
|
||||
selected = {name: ds for name, ds in datasets.items() if ds.get("enabled", True)}
|
||||
|
||||
if not selected:
|
||||
raise SystemExit("No datasets selected for download.")
|
||||
return selected
|
||||
|
||||
|
||||
def _safe_remove_dir(base_dir: str, rel_dir: str) -> None:
|
||||
target = os.path.abspath(os.path.join(base_dir, rel_dir))
|
||||
base = os.path.abspath(base_dir)
|
||||
if os.path.commonpath([target, base]) != base:
|
||||
raise SystemExit(f"Refusing to delete outside base dir: {target}")
|
||||
if target == base:
|
||||
raise SystemExit(f"Refusing to delete base dir: {target}")
|
||||
if os.path.exists(target):
|
||||
shutil.rmtree(target)
|
||||
|
||||
|
||||
def _download_task(
|
||||
session: requests.Session,
|
||||
task: DownloadTask,
|
||||
timeout: int,
|
||||
verify: bool | str,
|
||||
retries: int,
|
||||
stop_event: threading.Event,
|
||||
progress: DownloadProgress,
|
||||
) -> Tuple[str, DownloadTask, Optional[str]]:
|
||||
os.makedirs(os.path.dirname(task.output_path), exist_ok=True)
|
||||
tmp_path = f"{task.output_path}.part"
|
||||
|
||||
if stop_event.is_set():
|
||||
return "aborted", task, "Interrupted"
|
||||
|
||||
for attempt in range(retries + 1):
|
||||
if stop_event.is_set():
|
||||
return "aborted", task, "Interrupted"
|
||||
try:
|
||||
with session.get(task.url, stream=True, timeout=timeout, verify=verify) as resp:
|
||||
if resp.status_code in (404, 410):
|
||||
return "missing", task, f"HTTP {resp.status_code}"
|
||||
if resp.status_code >= 400:
|
||||
return "failed", task, f"HTTP {resp.status_code}"
|
||||
resp.raise_for_status()
|
||||
with open(tmp_path, "wb") as fh:
|
||||
for chunk in resp.iter_content(chunk_size=1024 * 1024):
|
||||
if stop_event.is_set():
|
||||
return "aborted", task, "Interrupted"
|
||||
if chunk:
|
||||
fh.write(chunk)
|
||||
progress.add_bytes(len(chunk), task.output_path)
|
||||
os.replace(tmp_path, task.output_path)
|
||||
return "downloaded", task, None
|
||||
except requests.RequestException as exc:
|
||||
if attempt >= retries:
|
||||
return "failed", task, str(exc)
|
||||
time.sleep(1.0 + attempt * 0.5)
|
||||
except OSError as exc:
|
||||
return "failed", task, str(exc)
|
||||
|
||||
if os.path.exists(tmp_path):
|
||||
try:
|
||||
os.remove(tmp_path)
|
||||
except OSError:
|
||||
pass
|
||||
return "failed", task, "Unknown error"
|
||||
|
||||
|
||||
def run_download(
|
||||
config_path: str,
|
||||
requested_datasets: Optional[List[str]] = None,
|
||||
start_override: Optional[Tuple[int, int]] = None,
|
||||
end_override: Optional[Tuple[int, int]] = None,
|
||||
clean_downloads: bool = False,
|
||||
ca_bundle_override: Optional[str] = None,
|
||||
) -> int:
|
||||
cfg = _load_toml(config_path)
|
||||
download_cfg = cfg.get("download", {})
|
||||
tile_ranges = _parse_tile_ranges(cfg)
|
||||
datasets = _select_datasets(cfg, requested_datasets)
|
||||
|
||||
logging_cfg = cfg.get("logging", {})
|
||||
progress_enabled = bool(logging_cfg.get("report_progress", True))
|
||||
logger = DownloadLogger(
|
||||
logging_cfg.get("log_file"),
|
||||
logging_cfg.get("log_format"),
|
||||
progress_enabled,
|
||||
)
|
||||
|
||||
configured_output_dir = download_cfg.get("output_directory")
|
||||
base_output_dir = DEFAULT_OUTPUT_DIR
|
||||
if configured_output_dir and os.path.normpath(configured_output_dir) != DEFAULT_OUTPUT_DIR:
|
||||
logger.log(
|
||||
"WARN",
|
||||
f"Ignoring download.output_directory={configured_output_dir}; using raw/ to match pipeline.",
|
||||
)
|
||||
|
||||
if clean_downloads:
|
||||
for dataset_key, dataset_cfg in datasets.items():
|
||||
_safe_remove_dir(base_output_dir, _resolve_output_dir(dataset_key, dataset_cfg))
|
||||
|
||||
tasks = _build_tasks(
|
||||
cfg,
|
||||
datasets,
|
||||
tile_ranges,
|
||||
base_output_dir,
|
||||
start_override,
|
||||
end_override,
|
||||
)
|
||||
|
||||
if not tasks:
|
||||
logger.log("INFO", "No download tasks generated.")
|
||||
return 0
|
||||
|
||||
skip_existing = not clean_downloads
|
||||
pending: List[DownloadTask] = []
|
||||
skipped = 0
|
||||
for task in tasks:
|
||||
if skip_existing and os.path.exists(task.output_path):
|
||||
skipped += 1
|
||||
continue
|
||||
pending.append(task)
|
||||
|
||||
if skipped:
|
||||
logger.log("INFO", f"Skipped {skipped} existing file(s).")
|
||||
|
||||
if not pending:
|
||||
logger.log("INFO", "Nothing to download after skipping existing files.")
|
||||
return 0
|
||||
|
||||
verify_ssl = download_cfg.get("verify_ssl", True)
|
||||
ca_bundle = ca_bundle_override or download_cfg.get("ca_bundle")
|
||||
if verify_ssl and ca_bundle:
|
||||
if os.path.exists(ca_bundle):
|
||||
verify = ca_bundle
|
||||
source = "CLI" if ca_bundle_override else "config"
|
||||
logger.log("INFO", f"Using CA bundle ({source}): {ca_bundle}")
|
||||
else:
|
||||
verify = True
|
||||
logger.log("WARN", f"CA bundle not found, using system trust: {ca_bundle}")
|
||||
else:
|
||||
verify = bool(verify_ssl)
|
||||
if not verify_ssl:
|
||||
logger.log("WARN", "TLS verification disabled by config.")
|
||||
|
||||
timeout = int(download_cfg.get("timeout_seconds", 300))
|
||||
retries = int(download_cfg.get("retry_attempts", 3))
|
||||
parallel = int(download_cfg.get("parallel_downloads", 4))
|
||||
user_agent = download_cfg.get("user_agent", "geodata-download/1.0")
|
||||
|
||||
downloaded = 0
|
||||
missing = 0
|
||||
failed = 0
|
||||
|
||||
progress = DownloadProgress(len(pending), progress_enabled)
|
||||
stop_event = threading.Event()
|
||||
interrupted = False
|
||||
|
||||
with requests.Session() as session:
|
||||
session.headers.update({"User-Agent": user_agent})
|
||||
executor = DaemonThreadPool(max_workers=parallel)
|
||||
futures = [
|
||||
executor.submit(_download_task, session, task, timeout, verify, retries, stop_event, progress)
|
||||
for task in pending
|
||||
]
|
||||
try:
|
||||
for future in as_completed(futures):
|
||||
status, task, detail = future.result()
|
||||
if status == "downloaded":
|
||||
downloaded += 1
|
||||
elif status == "missing":
|
||||
missing += 1
|
||||
logger.log("WARN", f"Missing tile: {task.url} ({detail})")
|
||||
elif status == "aborted":
|
||||
failed += 1
|
||||
else:
|
||||
failed += 1
|
||||
extra = f" ({detail})" if detail else ""
|
||||
logger.log("ERROR", f"Failed: {task.url}{extra}")
|
||||
progress.set_counts(downloaded, missing, failed, task.output_path)
|
||||
except KeyboardInterrupt:
|
||||
interrupted = True
|
||||
stop_event.set()
|
||||
logger.log("WARN", "Interrupted; stopping downloads.")
|
||||
finally:
|
||||
if interrupted:
|
||||
executor.shutdown(wait=False, cancel_futures=True)
|
||||
else:
|
||||
executor.shutdown(wait=True)
|
||||
|
||||
progress.finish()
|
||||
if interrupted:
|
||||
return 130
|
||||
|
||||
logger.log(
|
||||
"INFO",
|
||||
f"Done. Downloaded={downloaded}, Missing={missing}, Failed={failed}, Skipped={skipped}.",
|
||||
)
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
def parse_args(argv: Optional[Iterable[str]] = None) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Download geodata tiles from TOML config.")
|
||||
parser.add_argument(
|
||||
"--config",
|
||||
default=DEFAULT_CONFIG,
|
||||
help="Path to geodata_download.toml.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--datasets",
|
||||
help="Comma-separated dataset keys to download (default: enabled datasets).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--start",
|
||||
nargs=2,
|
||||
type=int,
|
||||
metavar=("X", "Y"),
|
||||
help="Override tile range start (x y).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--end",
|
||||
nargs=2,
|
||||
type=int,
|
||||
metavar=("X", "Y"),
|
||||
help="Override tile range end (x y).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--clean-downloads",
|
||||
action="store_true",
|
||||
help="Delete selected dataset folders before downloading.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ca-bundle",
|
||||
help="Path to a CA bundle file to override the config.",
|
||||
)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def main(argv: Optional[Iterable[str]] = None) -> int:
|
||||
args = parse_args(argv)
|
||||
datasets = [name.strip() for name in args.datasets.split(",")] if args.datasets else None
|
||||
start = tuple(args.start) if args.start else None
|
||||
end = tuple(args.end) if args.end else None
|
||||
if (start is None) != (end is None):
|
||||
raise SystemExit("--start and --end must be provided together.")
|
||||
|
||||
return run_download(
|
||||
config_path=args.config,
|
||||
requested_datasets=datasets,
|
||||
start_override=start,
|
||||
end_override=end,
|
||||
clean_downloads=args.clean_downloads,
|
||||
ca_bundle_override=args.ca_bundle,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
145
geodata_download.toml
Normal file
145
geodata_download.toml
Normal file
@@ -0,0 +1,145 @@
|
||||
# Geodata Download Configuration for Trier Digital Twin Project
|
||||
# Generated: 2026-01-19
|
||||
# Data Source: Geobasis Rheinland-Pfalz (geobasis-rlp.de)
|
||||
|
||||
[project]
|
||||
name = "Trier Digital Twin - Geodata Collection"
|
||||
description = "VR flood simulation and climate impact assessment"
|
||||
coordinate_system = "ETRS89/UTM32"
|
||||
vertical_datum = "DHHN2016"
|
||||
region = "Stadt Trier and surrounding area"
|
||||
|
||||
[download]
|
||||
base_url = "https://geobasis-rlp.de/data"
|
||||
output_directory = "raw"
|
||||
parallel_downloads = 4
|
||||
retry_attempts = 3
|
||||
timeout_seconds = 300
|
||||
verify_ssl = true
|
||||
user_agent = "TrierDigitalTwin/1.0"
|
||||
ca_bundle = "certs/geobasis-ca.pem"
|
||||
|
||||
# Define tile range presets that datasets inherit from
|
||||
[tile_ranges.range_1km]
|
||||
description = "1x1km tiles covering full area"
|
||||
x_start = 324
|
||||
x_end = 339
|
||||
x_step = 1
|
||||
y_start = 5506
|
||||
y_end = 5521
|
||||
y_step = 1
|
||||
|
||||
[tile_ranges.range_2km]
|
||||
description = "2x2km tiles covering full area"
|
||||
x_start = 324
|
||||
x_end = 338
|
||||
x_step = 2
|
||||
y_start = 5506
|
||||
y_end = 5520
|
||||
y_step = 2
|
||||
|
||||
# Digital Ground Model (bare earth terrain)
|
||||
[datasets.dgm1]
|
||||
enabled = true
|
||||
name = "Digital Ground Model"
|
||||
tile_range = "range_1km"
|
||||
resolution = "1m"
|
||||
format = "GeoTIFF"
|
||||
url_template = "{base_url}/dgm1/current/tif/dgm01_32_{x}_{y}_1_rp.tif"
|
||||
priority = 1
|
||||
output_subdir = "dgm1"
|
||||
|
||||
# Digital Surface Model (includes buildings/vegetation)
|
||||
[datasets.dom1]
|
||||
enabled = true
|
||||
name = "Digital Surface Model"
|
||||
tile_range = "range_1km"
|
||||
resolution = "1m"
|
||||
format = "GeoTIFF"
|
||||
year = 2020
|
||||
url_template = "{base_url}/dom1/current/tif/dom1_32_{x}_{y}_1_rp_2020.tif"
|
||||
priority = 2
|
||||
output_subdir = "dom1"
|
||||
|
||||
# 3D Building Models (CityGML LoD2)
|
||||
[datasets.geb3dlo]
|
||||
enabled = true
|
||||
name = "3D Buildings LoD2"
|
||||
tile_range = "range_2km"
|
||||
resolution = "LoD2"
|
||||
format = "CityGML"
|
||||
url_template = "{base_url}/geb3dlo/current/gml/LoD2_32_{x}_{y}_2_RP.gml"
|
||||
priority = 2
|
||||
output_subdir = "citygml/lod2"
|
||||
notes = "Semantically rich building models with roof structures"
|
||||
|
||||
# Orthophotos (aerial imagery)
|
||||
[datasets.dop20]
|
||||
enabled = true
|
||||
name = "Orthophotos 20cm"
|
||||
tile_range = "range_2km"
|
||||
resolution = "20cm"
|
||||
format = "JPEG2000"
|
||||
year = 2023
|
||||
priority = 3
|
||||
output_subdir = "dop20"
|
||||
|
||||
[[datasets.dop20.files]]
|
||||
type = "image"
|
||||
url_template = "{base_url}/dop20rgb/current/jp2/dop20rgb_32_{x}_{y}_2_rp_2023.jp2"
|
||||
|
||||
[[datasets.dop20.files]]
|
||||
type = "worldfile"
|
||||
url_template = "{base_url}/dop20rgb/current/jp2/dop20rgb_32_{x}_{y}_2_rp_2023.j2w"
|
||||
|
||||
[[datasets.dop20.files]]
|
||||
type = "metadata"
|
||||
url_template = "{base_url}/dop20rgb/current/metadata/dop20rgb_32_{x}_{y}_2_rp_2023_meta.xml"
|
||||
|
||||
# Building/Object Point Cloud (RGBI)
|
||||
[datasets.bdom20rgbi]
|
||||
enabled = true
|
||||
name = "Building/Object Point Cloud RGBI"
|
||||
tile_range = "range_2km"
|
||||
resolution = "20cm"
|
||||
format = "LAZ"
|
||||
url_template = "{base_url}/bdom20rgbi/current/las/bdom20rgbi_32_{x}_{y}_2_rp.laz"
|
||||
priority = 3
|
||||
output_subdir = "bdom20rgbi"
|
||||
notes = "RGB + Intensity point cloud for buildings and objects"
|
||||
|
||||
# Laser Point Ground/Object (combined LPO/LPG)
|
||||
[datasets.lpolpg]
|
||||
enabled = true
|
||||
name = "Laser Point Ground/Object"
|
||||
tile_range = "range_1km"
|
||||
resolution = "High density"
|
||||
format = "LAZ"
|
||||
url_template = "{base_url}/las/current/las/lpolpg_32_{x}_{y}_1_rp.laz"
|
||||
priority = 2
|
||||
output_subdir = "lpolpg"
|
||||
notes = "Combined point cloud (LPO/LPG) in one LAZ file"
|
||||
|
||||
[processing]
|
||||
convert_to_mesh = true
|
||||
generate_lods = true
|
||||
merge_tiles = false
|
||||
create_index = true
|
||||
|
||||
[processing.optimization]
|
||||
terrain_lod_levels = [1, 2, 5, 10, 20]
|
||||
point_cloud_decimation = [1.0, 0.5, 0.25, 0.1]
|
||||
building_simplification = true
|
||||
|
||||
[validation]
|
||||
check_file_size = true
|
||||
min_file_size_kb = 10
|
||||
verify_geotiff_tags = true
|
||||
verify_crs = "EPSG:25832"
|
||||
log_missing_tiles = true
|
||||
|
||||
[logging]
|
||||
level = "INFO"
|
||||
log_file = "./downloads.log"
|
||||
log_format = "[{timestamp}] {level}: {message}"
|
||||
report_progress = true
|
||||
@@ -11,6 +11,7 @@ from typing import Dict, Iterable, List, Tuple
|
||||
import numpy as np
|
||||
from osgeo import gdal
|
||||
|
||||
from .citygml_utils import find_citygml_lod2
|
||||
from .config import Config
|
||||
from .gdal_utils import ensure_dir
|
||||
|
||||
@@ -379,10 +380,9 @@ def _rebase_cityjson(path: str, bounds: Tuple[float, float, float, float], out_p
|
||||
|
||||
def _ensure_cityjson_for_tile(tile_id: str, bounds: Tuple[float, float, float, float], cfg: Config) -> str | None:
|
||||
"""Create CityJSON -> triangulated -> rebased file if missing. Returns rebased path."""
|
||||
suffix = _tile_suffix(tile_id)
|
||||
gml_path = os.path.join(cfg.raw.citygml_lod2_dir, f"LoD2_{suffix}.gml")
|
||||
if not os.path.exists(gml_path):
|
||||
print(f"[buildings] missing GML for {tile_id}: {gml_path}")
|
||||
gml_path = find_citygml_lod2(tile_id, cfg)
|
||||
if not gml_path:
|
||||
print(f"[buildings] missing GML for {tile_id} in {cfg.raw.citygml_lod2_dir}")
|
||||
return None
|
||||
|
||||
def resolve_cityjson(path: str) -> str | None:
|
||||
|
||||
@@ -23,11 +23,11 @@ from .gdal_utils import open_dataset
|
||||
from .pointcloud import (
|
||||
PointCloud,
|
||||
find_laz_file,
|
||||
find_xyz_file,
|
||||
find_pointcloud_file,
|
||||
has_bdom_data,
|
||||
has_lpo_data,
|
||||
read_laz_file,
|
||||
read_xyz_file,
|
||||
read_pointcloud_file,
|
||||
sample_rgb_average,
|
||||
)
|
||||
|
||||
@@ -239,14 +239,14 @@ def refine_building_with_lpo(
|
||||
Returns:
|
||||
Refined height or None if no LPO data
|
||||
"""
|
||||
lpo_file = find_xyz_file(cfg.pointcloud.lpo_dir, tile_id)
|
||||
lpo_file = find_pointcloud_file(cfg.pointcloud.lpo_dir, tile_id)
|
||||
if not lpo_file:
|
||||
return None
|
||||
|
||||
xmin, ymin, xmax, ymax = footprint_bounds
|
||||
|
||||
# Load LPO points in building bounds
|
||||
lpo = read_xyz_file(lpo_file, bounds=(xmin, ymin, xmax, ymax))
|
||||
lpo = read_pointcloud_file(lpo_file, bounds=(xmin, ymin, xmax, ymax))
|
||||
if len(lpo) < 10:
|
||||
return None
|
||||
|
||||
|
||||
71
geodata_pipeline/citygml_utils.py
Normal file
71
geodata_pipeline/citygml_utils.py
Normal file
@@ -0,0 +1,71 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import glob
|
||||
import os
|
||||
from typing import Iterable
|
||||
|
||||
from .config import Config
|
||||
|
||||
|
||||
def _tile_suffix(tile_id: str) -> str:
|
||||
parts = tile_id.split("_")
|
||||
return "_".join(parts[-3:]) if len(parts) >= 3 else tile_id
|
||||
|
||||
|
||||
def _parse_tile_xy(tile_id: str) -> tuple[int, int] | None:
|
||||
parts = tile_id.split("_")
|
||||
coords = [p for p in parts if p.isdigit() and len(p) >= 3]
|
||||
if len(coords) >= 2:
|
||||
return int(coords[-2]), int(coords[-1])
|
||||
return None
|
||||
|
||||
|
||||
def _unique(paths: Iterable[str]) -> list[str]:
|
||||
seen: set[str] = set()
|
||||
ordered: list[str] = []
|
||||
for path in paths:
|
||||
if path in seen:
|
||||
continue
|
||||
seen.add(path)
|
||||
ordered.append(path)
|
||||
return ordered
|
||||
|
||||
|
||||
def candidate_citygml_lod2_paths(tile_id: str, cfg: Config) -> list[str]:
|
||||
candidates: list[str] = []
|
||||
suffix = _tile_suffix(tile_id)
|
||||
candidates.append(os.path.join(cfg.raw.citygml_lod2_dir, f"LoD2_{suffix}.gml"))
|
||||
|
||||
xy = _parse_tile_xy(tile_id)
|
||||
if xy:
|
||||
x, y = xy
|
||||
candidates.append(os.path.join(cfg.raw.citygml_lod2_dir, f"LoD2_32_{x}_{y}_2_RP.gml"))
|
||||
x2 = x - (x % 2)
|
||||
y2 = y - (y % 2)
|
||||
candidates.append(os.path.join(cfg.raw.citygml_lod2_dir, f"LoD2_32_{x2}_{y2}_2_RP.gml"))
|
||||
|
||||
return _unique(candidates)
|
||||
|
||||
|
||||
def find_citygml_lod2(tile_id: str, cfg: Config) -> str | None:
|
||||
candidates = candidate_citygml_lod2_paths(tile_id, cfg)
|
||||
for path in candidates:
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
|
||||
suffix = _tile_suffix(tile_id)
|
||||
patterns: list[str] = [
|
||||
os.path.join(cfg.raw.citygml_lod2_dir, f"*{tile_id}*.gml"),
|
||||
os.path.join(cfg.raw.citygml_lod2_dir, f"*{suffix}*.gml"),
|
||||
]
|
||||
xy = _parse_tile_xy(tile_id)
|
||||
if xy:
|
||||
x, y = xy
|
||||
patterns.insert(0, os.path.join(cfg.raw.citygml_lod2_dir, f"LoD2_32_{x}_{y}_*.gml"))
|
||||
|
||||
for pattern in patterns:
|
||||
matches = sorted(glob.glob(pattern))
|
||||
if matches:
|
||||
return matches[0]
|
||||
|
||||
return None
|
||||
@@ -1,12 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from dataclasses import asdict, dataclass, field, replace
|
||||
from dataclasses import asdict, dataclass, field, fields, replace
|
||||
from typing import Any, Dict
|
||||
|
||||
import tomllib
|
||||
import tomli_w
|
||||
|
||||
DEFAULT_CONFIG_PATH = "geodata_config.json"
|
||||
|
||||
DEFAULT_CONFIG_PATH = "geodata_config.toml"
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -84,6 +86,7 @@ class PointCloudConfig:
|
||||
"""Configuration for point cloud data sources."""
|
||||
lpg_dir: str = "raw/lpg"
|
||||
lpo_dir: str = "raw/lpo"
|
||||
lpolpg_dir: str = "raw/lpolpg"
|
||||
bdom_dir: str = "raw/bdom20rgbi"
|
||||
dom1_dir: str = "raw/dom1"
|
||||
chunk_size: int = 5_000_000
|
||||
@@ -163,35 +166,48 @@ class Config:
|
||||
|
||||
@classmethod
|
||||
def load(cls, path: str = DEFAULT_CONFIG_PATH) -> "Config":
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
with open(path, "rb") as f:
|
||||
data = tomllib.load(f)
|
||||
return cls.from_dict(data)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "Config":
|
||||
return cls(
|
||||
raw=RawConfig(**data["raw"]),
|
||||
archives=ArchiveConfig(**data["archives"]),
|
||||
work=WorkConfig(**data["work"]),
|
||||
export=ExportConfig(**data["export"]),
|
||||
heightmap=HeightmapConfig(**data["heightmap"]),
|
||||
ortho=OrthoConfig(**data["ortho"]),
|
||||
buildings=BuildingConfig(**data.get("buildings", {})),
|
||||
trees=TreeConfig(**data.get("trees", {})),
|
||||
raw=RawConfig(**_filter_kwargs(RawConfig, data.get("raw", {}))),
|
||||
archives=ArchiveConfig(**_filter_kwargs(ArchiveConfig, data.get("archives", {}))),
|
||||
work=WorkConfig(**_filter_kwargs(WorkConfig, data.get("work", {}))),
|
||||
export=ExportConfig(**_filter_kwargs(ExportConfig, data.get("export", {}))),
|
||||
heightmap=HeightmapConfig(**_filter_kwargs(HeightmapConfig, data.get("heightmap", {}))),
|
||||
ortho=OrthoConfig(**_filter_kwargs(OrthoConfig, data.get("ortho", {}))),
|
||||
buildings=BuildingConfig(**_filter_kwargs(BuildingConfig, data.get("buildings", {}))),
|
||||
trees=TreeConfig(**_filter_kwargs(TreeConfig, data.get("trees", {}))),
|
||||
# Enhanced pipeline configs (with defaults for backward compat)
|
||||
pointcloud=PointCloudConfig(**data.get("pointcloud", {})),
|
||||
heightmap_enhanced=EnhancedHeightmapConfig(**data.get("heightmap_enhanced", {})),
|
||||
buildings_enhanced=EnhancedBuildingConfig(**data.get("buildings_enhanced", {})),
|
||||
street_furniture=StreetFurnitureConfig(**data.get("street_furniture", {})),
|
||||
trees_enhanced=EnhancedTreeConfig(**data.get("trees_enhanced", {})),
|
||||
pointcloud=PointCloudConfig(**_filter_kwargs(PointCloudConfig, data.get("pointcloud", {}))),
|
||||
heightmap_enhanced=EnhancedHeightmapConfig(**_filter_kwargs(
|
||||
EnhancedHeightmapConfig,
|
||||
data.get("heightmap_enhanced", {}),
|
||||
)),
|
||||
buildings_enhanced=EnhancedBuildingConfig(**_filter_kwargs(
|
||||
EnhancedBuildingConfig,
|
||||
data.get("buildings_enhanced", {}),
|
||||
)),
|
||||
street_furniture=StreetFurnitureConfig(**_filter_kwargs(
|
||||
StreetFurnitureConfig,
|
||||
_coerce_ranges(data.get("street_furniture", {})),
|
||||
)),
|
||||
trees_enhanced=EnhancedTreeConfig(**_filter_kwargs(
|
||||
EnhancedTreeConfig,
|
||||
data.get("trees_enhanced", {}),
|
||||
)),
|
||||
)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
def save(self, path: str = DEFAULT_CONFIG_PATH) -> None:
|
||||
payload = tomli_w.dumps(self.to_dict())
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(self.to_dict(), f, indent=2)
|
||||
f.write(payload)
|
||||
|
||||
def with_overrides(self, raw_dgm1_path: str | None = None, raw_dop20_path: str | None = None) -> "Config":
|
||||
cfg = self
|
||||
@@ -208,3 +224,21 @@ def ensure_default_config(path: str = DEFAULT_CONFIG_PATH) -> Config:
|
||||
cfg.save(path)
|
||||
return cfg
|
||||
return Config.load(path)
|
||||
|
||||
|
||||
def _filter_kwargs(cls: type, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if not isinstance(data, dict):
|
||||
return {}
|
||||
allowed = {field.name for field in fields(cls)}
|
||||
return {key: value for key, value in data.items() if key in allowed}
|
||||
|
||||
|
||||
def _coerce_ranges(data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if not isinstance(data, dict):
|
||||
return {}
|
||||
out = dict(data)
|
||||
for key in ("lamp_height_range", "bench_height_range", "sign_height_range", "bollard_height_range"):
|
||||
value = out.get(key)
|
||||
if isinstance(value, list):
|
||||
out[key] = tuple(value)
|
||||
return out
|
||||
|
||||
@@ -2,6 +2,8 @@ from __future__ import annotations
|
||||
|
||||
import glob
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import xml.etree.ElementTree as ET
|
||||
from typing import Iterable, Sequence
|
||||
|
||||
@@ -61,6 +63,33 @@ def _vrt_has_missing_sources(vrt_path: str) -> bool:
|
||||
return bool(missing)
|
||||
|
||||
|
||||
def _vrt_has_sources(vrt_path: str) -> bool:
|
||||
try:
|
||||
tree = ET.parse(vrt_path)
|
||||
except (ET.ParseError, OSError):
|
||||
return False
|
||||
return any(True for _ in tree.iterfind(".//SourceFilename"))
|
||||
|
||||
|
||||
def _build_vrt_cli(vrt_path: str, sources: Sequence[str]) -> None:
|
||||
ensure_parent(vrt_path)
|
||||
if os.path.exists(vrt_path):
|
||||
safe_remove(vrt_path)
|
||||
with tempfile.NamedTemporaryFile("w", delete=False, encoding="utf-8") as handle:
|
||||
list_path = handle.name
|
||||
for src in sources:
|
||||
handle.write(f"{src}\n")
|
||||
try:
|
||||
subprocess.run(
|
||||
["gdalbuildvrt", "-input_file_list", list_path, vrt_path],
|
||||
check=True,
|
||||
)
|
||||
except (OSError, subprocess.CalledProcessError) as exc:
|
||||
raise SystemExit(f"Could not build {vrt_path} using gdalbuildvrt: {exc}") from exc
|
||||
finally:
|
||||
safe_remove(list_path)
|
||||
|
||||
|
||||
def build_vrt(vrt_path: str, sources: Sequence[str], force: bool = False) -> bool:
|
||||
rebuild = force
|
||||
|
||||
@@ -81,9 +110,15 @@ def build_vrt(vrt_path: str, sources: Sequence[str], force: bool = False) -> boo
|
||||
ensure_parent(vrt_path)
|
||||
print(f"Building {vrt_path} from {len(sources)} files...")
|
||||
try:
|
||||
gdal.BuildVRT(vrt_path, list(sources))
|
||||
ds = gdal.BuildVRT(vrt_path, list(sources))
|
||||
if ds is not None:
|
||||
ds.FlushCache()
|
||||
ds = None
|
||||
except RuntimeError as exc:
|
||||
raise SystemExit(f"Could not build {vrt_path}: {exc}") from exc
|
||||
print(f"Warning: GDAL BuildVRT failed for {vrt_path}: {exc}")
|
||||
if not _vrt_has_sources(vrt_path):
|
||||
print(f"Warning: {vrt_path} missing sources after BuildVRT; retrying with gdalbuildvrt...")
|
||||
_build_vrt_cli(vrt_path, sources)
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ from osgeo import gdal
|
||||
from .config import Config
|
||||
from .gdal_utils import build_vrt, open_dataset
|
||||
from .heightmaps import export_heightmaps # Reuse existing export
|
||||
from .pointcloud import find_xyz_file, has_lpg_data, read_xyz_file
|
||||
from .pointcloud import find_pointcloud_file, read_pointcloud_file
|
||||
|
||||
|
||||
def validate_tile_with_lpg(
|
||||
@@ -45,7 +45,7 @@ def validate_tile_with_lpg(
|
||||
eh_cfg = cfg.heightmap_enhanced
|
||||
|
||||
# Check for LPG data
|
||||
lpg_file = find_xyz_file(pc_cfg.lpg_dir, tile_id)
|
||||
lpg_file = find_pointcloud_file(pc_cfg.lpg_dir, tile_id)
|
||||
if not lpg_file:
|
||||
return None
|
||||
|
||||
@@ -59,7 +59,7 @@ def validate_tile_with_lpg(
|
||||
nodata = dgm1_ds.GetRasterBand(1).GetNoDataValue() or -9999
|
||||
|
||||
# Load LPG points for tile
|
||||
lpg = read_xyz_file(lpg_file, bounds=(xmin, ymin, xmax, ymax))
|
||||
lpg = read_pointcloud_file(lpg_file, bounds=(xmin, ymin, xmax, ymax))
|
||||
if len(lpg) == 0:
|
||||
return {"point_count": 0, "valid": True}
|
||||
|
||||
@@ -122,7 +122,7 @@ def export_quality_map(
|
||||
pc_cfg = cfg.pointcloud
|
||||
eh_cfg = cfg.heightmap_enhanced
|
||||
|
||||
lpg_file = find_xyz_file(pc_cfg.lpg_dir, tile_id)
|
||||
lpg_file = find_pointcloud_file(pc_cfg.lpg_dir, tile_id)
|
||||
if not lpg_file:
|
||||
return None
|
||||
|
||||
@@ -135,7 +135,7 @@ def export_quality_map(
|
||||
nodata = dgm1_ds.GetRasterBand(1).GetNoDataValue() or -9999
|
||||
|
||||
# Load LPG points
|
||||
lpg = read_xyz_file(lpg_file, bounds=(xmin, ymin, xmax, ymax))
|
||||
lpg = read_pointcloud_file(lpg_file, bounds=(xmin, ymin, xmax, ymax))
|
||||
if len(lpg) == 0:
|
||||
return None
|
||||
|
||||
|
||||
143
geodata_pipeline/lpolpg_split.py
Normal file
143
geodata_pipeline/lpolpg_split.py
Normal file
@@ -0,0 +1,143 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from typing import Iterable
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .config import Config
|
||||
from .gdal_utils import ensure_dir
|
||||
|
||||
|
||||
@dataclass
|
||||
class SplitStats:
|
||||
ground_points: int = 0
|
||||
object_points: int = 0
|
||||
|
||||
@property
|
||||
def total(self) -> int:
|
||||
return self.ground_points + self.object_points
|
||||
|
||||
|
||||
def _parse_output_base(name: str) -> tuple[str, str]:
|
||||
base = os.path.splitext(name)[0]
|
||||
if base.startswith("lpolpg_"):
|
||||
suffix = base[len("lpolpg_") :]
|
||||
else:
|
||||
suffix = base
|
||||
return f"lpg_{suffix}", f"lpo_{suffix}"
|
||||
|
||||
|
||||
def _write_xyz(handle, points) -> int:
|
||||
if points is None or len(points) == 0:
|
||||
return 0
|
||||
coords = np.column_stack((points.x, points.y, points.z)).astype(np.float64, copy=False)
|
||||
np.savetxt(handle, coords, fmt="%.3f %.3f %.3f")
|
||||
return coords.shape[0]
|
||||
|
||||
|
||||
def split_lpolpg(
|
||||
cfg: Config,
|
||||
*,
|
||||
formats: Iterable[str] = ("laz", "xyz"),
|
||||
ground_classes: Iterable[int] = (2,),
|
||||
overwrite: bool = False,
|
||||
) -> int:
|
||||
try:
|
||||
import laspy
|
||||
except ImportError as exc:
|
||||
raise SystemExit("laspy is required to split lpolpg LAZ files.") from exc
|
||||
|
||||
input_dir = cfg.pointcloud.lpolpg_dir
|
||||
if not os.path.isdir(input_dir):
|
||||
raise SystemExit(f"lpolpg directory not found: {input_dir}")
|
||||
|
||||
format_set = {fmt.strip().lower() for fmt in formats if fmt}
|
||||
write_laz = "laz" in format_set or "las" in format_set
|
||||
write_xyz = "xyz" in format_set
|
||||
if not write_laz and not write_xyz:
|
||||
raise SystemExit("No output formats requested for lpolpg split.")
|
||||
|
||||
ensure_dir(cfg.pointcloud.lpg_dir)
|
||||
ensure_dir(cfg.pointcloud.lpo_dir)
|
||||
|
||||
ground_set = {int(v) for v in ground_classes}
|
||||
chunk_size = max(1_000, int(cfg.pointcloud.chunk_size))
|
||||
|
||||
files = sorted(
|
||||
name for name in os.listdir(input_dir)
|
||||
if name.lower().endswith((".laz", ".las"))
|
||||
)
|
||||
if not files:
|
||||
raise SystemExit(f"No LAZ/LAS files found in {input_dir}")
|
||||
|
||||
for name in files:
|
||||
in_path = os.path.join(input_dir, name)
|
||||
lpg_base, lpo_base = _parse_output_base(name)
|
||||
lpg_laz = os.path.join(cfg.pointcloud.lpg_dir, f"{lpg_base}.laz")
|
||||
lpo_laz = os.path.join(cfg.pointcloud.lpo_dir, f"{lpo_base}.laz")
|
||||
lpg_xyz = os.path.join(cfg.pointcloud.lpg_dir, f"{lpg_base}.xyz")
|
||||
lpo_xyz = os.path.join(cfg.pointcloud.lpo_dir, f"{lpo_base}.xyz")
|
||||
|
||||
outputs = []
|
||||
if write_laz:
|
||||
outputs.extend([lpg_laz, lpo_laz])
|
||||
if write_xyz:
|
||||
outputs.extend([lpg_xyz, lpo_xyz])
|
||||
if not overwrite and outputs and all(os.path.exists(p) for p in outputs):
|
||||
print(f"[lpolpg] skip {name}: outputs already exist")
|
||||
continue
|
||||
|
||||
print(f"[lpolpg] splitting {name}...")
|
||||
stats = SplitStats()
|
||||
|
||||
with laspy.open(in_path) as reader:
|
||||
lpg_writer = None
|
||||
lpo_writer = None
|
||||
lpg_xyz_handle = None
|
||||
lpo_xyz_handle = None
|
||||
|
||||
try:
|
||||
if write_laz:
|
||||
header_lpg = reader.header.copy()
|
||||
header_lpg.point_count = 0
|
||||
header_lpo = reader.header.copy()
|
||||
header_lpo.point_count = 0
|
||||
lpg_writer = laspy.open(lpg_laz, mode="w", header=header_lpg)
|
||||
lpo_writer = laspy.open(lpo_laz, mode="w", header=header_lpo)
|
||||
if write_xyz:
|
||||
lpg_xyz_handle = open(lpg_xyz, "w", encoding="utf-8")
|
||||
lpo_xyz_handle = open(lpo_xyz, "w", encoding="utf-8")
|
||||
|
||||
for points in reader.chunk_iterator(chunk_size):
|
||||
classes = np.array(points.classification)
|
||||
ground_mask = np.isin(classes, list(ground_set))
|
||||
if write_laz:
|
||||
if ground_mask.any():
|
||||
lpg_writer.write_points(points[ground_mask])
|
||||
if (~ground_mask).any():
|
||||
lpo_writer.write_points(points[~ground_mask])
|
||||
if write_xyz:
|
||||
stats.ground_points += _write_xyz(lpg_xyz_handle, points[ground_mask])
|
||||
stats.object_points += _write_xyz(lpo_xyz_handle, points[~ground_mask])
|
||||
|
||||
if write_laz:
|
||||
stats.ground_points = lpg_writer.header.point_count
|
||||
stats.object_points = lpo_writer.header.point_count
|
||||
finally:
|
||||
if lpg_writer is not None:
|
||||
lpg_writer.close()
|
||||
if lpo_writer is not None:
|
||||
lpo_writer.close()
|
||||
if lpg_xyz_handle is not None:
|
||||
lpg_xyz_handle.close()
|
||||
if lpo_xyz_handle is not None:
|
||||
lpo_xyz_handle.close()
|
||||
|
||||
print(
|
||||
f"[lpolpg] {name}: ground={stats.ground_points}, "
|
||||
f"objects={stats.object_points}, total={stats.total}"
|
||||
)
|
||||
|
||||
return 0
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Point cloud utilities for XYZ and LAZ file handling.
|
||||
|
||||
Provides unified reading for:
|
||||
- LPG (ground points) and LPO (object points) in XYZ format
|
||||
- LPG (ground points) and LPO (object points) in XYZ or LAZ format
|
||||
- BDOM20RGBI in LAZ format with RGB data
|
||||
"""
|
||||
|
||||
@@ -215,6 +215,20 @@ def read_laz_file(
|
||||
return pc
|
||||
|
||||
|
||||
def read_pointcloud_file(
|
||||
path: str,
|
||||
bounds: Optional[Tuple[float, float, float, float]] = None,
|
||||
chunk_size: int = 1_000_000,
|
||||
) -> PointCloud:
|
||||
"""Read XYZ/LAZ/LAS point cloud file with optional bounds filtering."""
|
||||
ext = os.path.splitext(path)[1].lower()
|
||||
if ext in (".laz", ".las"):
|
||||
return read_laz_file(path, bounds=bounds)
|
||||
if ext == ".xyz":
|
||||
return read_xyz_file(path, bounds=bounds, chunk_size=chunk_size)
|
||||
raise ValueError(f"Unsupported point cloud format: {path}")
|
||||
|
||||
|
||||
def _extract_tile_coords(tile_id: str) -> str:
|
||||
"""Extract tile coordinates from tile ID.
|
||||
|
||||
@@ -241,15 +255,26 @@ def find_xyz_file(directory: str, tile_id: str) -> Optional[str]:
|
||||
Returns:
|
||||
Path to XYZ file or None if not found
|
||||
"""
|
||||
return find_pointcloud_file(directory, tile_id, extensions=("xyz",))
|
||||
|
||||
|
||||
def find_pointcloud_file(
|
||||
directory: str,
|
||||
tile_id: str,
|
||||
extensions: Tuple[str, ...] = ("xyz", "laz", "las"),
|
||||
) -> Optional[str]:
|
||||
"""Find XYZ/LAZ/LAS file for a tile in directory."""
|
||||
# Extract coordinate suffix (e.g., '328_5511')
|
||||
coords = _extract_tile_coords(tile_id)
|
||||
|
||||
# Try common naming patterns
|
||||
patterns = [
|
||||
os.path.join(directory, f"*{coords}*.xyz"),
|
||||
os.path.join(directory, f"*{tile_id}*.xyz"),
|
||||
os.path.join(directory, f"*_{tile_id}.xyz"),
|
||||
]
|
||||
patterns = []
|
||||
for ext in extensions:
|
||||
patterns.extend([
|
||||
os.path.join(directory, f"*{coords}*.{ext}"),
|
||||
os.path.join(directory, f"*{tile_id}*.{ext}"),
|
||||
os.path.join(directory, f"*_{tile_id}.{ext}"),
|
||||
])
|
||||
|
||||
for pattern in patterns:
|
||||
matches = glob.glob(pattern)
|
||||
@@ -269,16 +294,14 @@ def find_laz_file(directory: str, tile_id: str) -> Optional[str]:
|
||||
Returns:
|
||||
Path to LAZ file or None if not found
|
||||
"""
|
||||
# Extract tile suffix for matching (e.g., '328_5511' from '32_328_5511')
|
||||
parts = tile_id.split("_")
|
||||
if len(parts) >= 3:
|
||||
tile_suffix = f"{parts[1]}_{parts[2]}"
|
||||
else:
|
||||
tile_suffix = tile_id
|
||||
|
||||
coords = _extract_tile_coords(tile_id)
|
||||
patterns = [
|
||||
os.path.join(directory, f"*{tile_suffix}*.laz"),
|
||||
os.path.join(directory, f"*{coords}*.laz"),
|
||||
os.path.join(directory, f"*{tile_id}*.laz"),
|
||||
os.path.join(directory, f"*_{tile_id}.laz"),
|
||||
os.path.join(directory, f"*{coords}*.las"),
|
||||
os.path.join(directory, f"*{tile_id}*.las"),
|
||||
os.path.join(directory, f"*_{tile_id}.las"),
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
@@ -291,12 +314,17 @@ def find_laz_file(directory: str, tile_id: str) -> Optional[str]:
|
||||
|
||||
def has_lpg_data(tile_id: str, lpg_dir: str = "raw/lpg") -> bool:
|
||||
"""Check if LPG (ground points) data exists for tile."""
|
||||
return find_xyz_file(lpg_dir, tile_id) is not None
|
||||
return find_pointcloud_file(lpg_dir, tile_id) is not None
|
||||
|
||||
|
||||
def has_lpo_data(tile_id: str, lpo_dir: str = "raw/lpo") -> bool:
|
||||
"""Check if LPO (object points) data exists for tile."""
|
||||
return find_xyz_file(lpo_dir, tile_id) is not None
|
||||
return find_pointcloud_file(lpo_dir, tile_id) is not None
|
||||
|
||||
|
||||
def has_lpolpg_data(tile_id: str, lpolpg_dir: str = "raw/lpolpg") -> bool:
|
||||
"""Check if combined LPO/LPG data exists for tile."""
|
||||
return find_pointcloud_file(lpolpg_dir, tile_id) is not None
|
||||
|
||||
|
||||
def has_bdom_data(tile_id: str, bdom_dir: str = "raw/bdom20rgbi") -> bool:
|
||||
|
||||
@@ -15,13 +15,14 @@ from typing import List, Optional, Tuple
|
||||
import numpy as np
|
||||
from osgeo import gdal, ogr
|
||||
|
||||
from .citygml_utils import find_citygml_lod2
|
||||
from .config import Config
|
||||
from .gdal_utils import open_dataset
|
||||
from .pointcloud import (
|
||||
PointCloud,
|
||||
find_xyz_file,
|
||||
find_pointcloud_file,
|
||||
has_lpo_data,
|
||||
read_xyz_file,
|
||||
read_pointcloud_file,
|
||||
)
|
||||
|
||||
|
||||
@@ -152,13 +153,9 @@ def _rasterize_citygml_footprints(
|
||||
Returns:
|
||||
Boolean mask where True = building footprint
|
||||
"""
|
||||
import glob
|
||||
|
||||
# Find CityGML file for tile
|
||||
citygml_pattern = os.path.join(cfg.raw.citygml_lod2_dir, f"*{tile_id}*.gml")
|
||||
citygml_files = glob.glob(citygml_pattern)
|
||||
|
||||
if not citygml_files:
|
||||
gml_path = find_citygml_lod2(tile_id, cfg)
|
||||
if not gml_path:
|
||||
return np.zeros(shape, dtype=bool)
|
||||
|
||||
# Create memory raster for rasterization
|
||||
@@ -168,7 +165,7 @@ def _rasterize_citygml_footprints(
|
||||
|
||||
# Open GML and rasterize
|
||||
try:
|
||||
gml_ds = ogr.Open(citygml_files[0])
|
||||
gml_ds = ogr.Open(gml_path)
|
||||
if gml_ds is None:
|
||||
return np.zeros(shape, dtype=bool)
|
||||
|
||||
@@ -354,12 +351,12 @@ def _refine_with_lpo(
|
||||
Returns:
|
||||
Refined detections with updated heights and confidence
|
||||
"""
|
||||
lpo_file = find_xyz_file(cfg.pointcloud.lpo_dir, tile_id)
|
||||
lpo_file = find_pointcloud_file(cfg.pointcloud.lpo_dir, tile_id)
|
||||
if not lpo_file:
|
||||
return detections
|
||||
|
||||
# Load LPO points for tile
|
||||
lpo = read_xyz_file(lpo_file, bounds=(xmin, ymin, xmax, ymax))
|
||||
lpo = read_pointcloud_file(lpo_file, bounds=(xmin, ymin, xmax, ymax))
|
||||
if len(lpo) == 0:
|
||||
return detections
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import json
|
||||
import math
|
||||
import os
|
||||
import struct
|
||||
import warnings
|
||||
from hashlib import blake2b
|
||||
from typing import Iterable, List, Tuple
|
||||
|
||||
@@ -13,8 +14,10 @@ import numpy as np
|
||||
from numpy.lib.stride_tricks import sliding_window_view
|
||||
from osgeo import gdal, ogr
|
||||
|
||||
from .citygml_utils import find_citygml_lod2
|
||||
from .config import Config
|
||||
from .gdal_utils import build_vrt, ensure_dir, ensure_parent, open_dataset
|
||||
from .pointcloud import has_bdom_data, has_lpo_data, has_lpolpg_data
|
||||
|
||||
gdal.UseExceptions()
|
||||
|
||||
@@ -24,20 +27,15 @@ def _hash_int(key: str, mod: int) -> int:
|
||||
return int.from_bytes(digest, "little") % mod
|
||||
|
||||
|
||||
def _tile_suffix(tile_id: str) -> str:
|
||||
parts = tile_id.split("_")
|
||||
return "_".join(parts[-3:]) if len(parts) >= 3 else tile_id
|
||||
|
||||
|
||||
def _ensure_dom_vrt(dom_dir: str, vrt_path: str) -> str:
|
||||
def _ensure_dom_vrt(dom_dir: str, vrt_path: str, *, force: bool = False) -> str:
|
||||
tif_paths = sorted(glob.glob(os.path.join(dom_dir, "*.tif")))
|
||||
build_vrt(vrt_path, tif_paths, force=False)
|
||||
build_vrt(vrt_path, tif_paths, force=force)
|
||||
return vrt_path
|
||||
|
||||
|
||||
def _ensure_dgm_vrt(cfg: Config) -> str:
|
||||
def _ensure_dgm_vrt(cfg: Config, *, force: bool = False) -> str:
|
||||
tif_paths = sorted(glob.glob(os.path.join(cfg.raw.dgm1_dir, "*.tif")))
|
||||
build_vrt(cfg.work.heightmap_vrt, tif_paths, force=False)
|
||||
build_vrt(cfg.work.heightmap_vrt, tif_paths, force=force)
|
||||
return cfg.work.heightmap_vrt
|
||||
|
||||
|
||||
@@ -54,14 +52,22 @@ def _warp_to_tile(src_path: str, bounds: Tuple[float, float, float, float], res:
|
||||
return gdal.Warp("", src_path, options=opts)
|
||||
|
||||
|
||||
def _building_mask(tile_id: str, bounds: Tuple[float, float, float, float], like_ds: gdal.Dataset) -> np.ndarray | None:
|
||||
suffix = _tile_suffix(tile_id)
|
||||
gml_path = os.path.join("raw", "citygml", "lod2", f"LoD2_{suffix}.gml")
|
||||
if not os.path.exists(gml_path):
|
||||
return None
|
||||
ds = ogr.Open(gml_path)
|
||||
if ds is None or ds.GetLayerCount() == 0:
|
||||
def _building_mask(
|
||||
tile_id: str,
|
||||
bounds: Tuple[float, float, float, float],
|
||||
like_ds: gdal.Dataset,
|
||||
cfg: Config,
|
||||
) -> np.ndarray | None:
|
||||
gml_path = find_citygml_lod2(tile_id, cfg)
|
||||
if not gml_path:
|
||||
return None
|
||||
gdal.PushErrorHandler("CPLQuietErrorHandler")
|
||||
try:
|
||||
ds = ogr.Open(gml_path)
|
||||
if ds is None or ds.GetLayerCount() == 0:
|
||||
return None
|
||||
finally:
|
||||
gdal.PopErrorHandler()
|
||||
driver = gdal.GetDriverByName("MEM")
|
||||
mask_ds = driver.Create("", like_ds.RasterXSize, like_ds.RasterYSize, 1, gdal.GDT_Byte)
|
||||
mask_ds.SetGeoTransform(like_ds.GetGeoTransform())
|
||||
@@ -70,10 +76,13 @@ def _building_mask(tile_id: str, bounds: Tuple[float, float, float, float], like
|
||||
band.Fill(0)
|
||||
for idx in range(ds.GetLayerCount()):
|
||||
layer = ds.GetLayer(idx)
|
||||
gdal.PushErrorHandler("CPLQuietErrorHandler")
|
||||
try:
|
||||
gdal.RasterizeLayer(mask_ds, [1], layer, burn_values=[1])
|
||||
except RuntimeError:
|
||||
continue
|
||||
finally:
|
||||
gdal.PopErrorHandler()
|
||||
return band.ReadAsArray()
|
||||
|
||||
|
||||
@@ -96,7 +105,9 @@ def _local_maxima(chm: np.ndarray, min_height: float, spacing_px: int) -> List[T
|
||||
pad = win // 2
|
||||
padded = np.pad(chm, pad_width=pad, mode="constant", constant_values=0.0)
|
||||
windows = sliding_window_view(padded, (win, win))
|
||||
local_max = windows.max(axis=(2, 3))
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings("ignore", message="All-NaN slice encountered")
|
||||
local_max = np.nanmax(windows, axis=(2, 3))
|
||||
mask = (chm >= local_max) & (chm >= min_height) & np.isfinite(chm)
|
||||
candidates = np.argwhere(mask)
|
||||
# Sort by height descending
|
||||
@@ -125,7 +136,9 @@ def _local_std(chm: np.ndarray, win: int = 3) -> np.ndarray:
|
||||
pad = win // 2
|
||||
padded = np.pad(chm, pad_width=pad, mode="constant", constant_values=np.nan)
|
||||
windows = sliding_window_view(padded, (win, win))
|
||||
std = np.nanstd(windows, axis=(2, 3))
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings("ignore", message="Degrees of freedom <= 0 for slice")
|
||||
std = np.nanstd(windows, axis=(2, 3))
|
||||
return std
|
||||
|
||||
|
||||
@@ -609,11 +622,12 @@ def export_trees(cfg: Config, *, force_vrt: bool = False) -> int:
|
||||
if not os.path.exists(cfg.export.manifest_path):
|
||||
raise SystemExit(f"Tile index missing: {cfg.export.manifest_path}. Run heightmap export first.")
|
||||
|
||||
dom_vrt_path = _ensure_dom_vrt("raw/dom1", os.path.join(cfg.work.work_dir, "dom1.vrt"))
|
||||
dgm_vrt_path = _ensure_dgm_vrt(cfg)
|
||||
dom_vrt_path = _ensure_dom_vrt(cfg.pointcloud.dom1_dir, os.path.join(cfg.work.work_dir, "dom1.vrt"), force=force_vrt)
|
||||
dgm_vrt_path = _ensure_dgm_vrt(cfg, force=force_vrt)
|
||||
|
||||
written = 0
|
||||
glb_written = 0
|
||||
no_tree_stats = {"no_valid_chm": 0, "below_min_height": 0, "no_local_maxima": 0}
|
||||
|
||||
with open(cfg.export.manifest_path, newline="", encoding="utf-8") as handle:
|
||||
reader = csv.DictReader(handle)
|
||||
@@ -638,15 +652,20 @@ def export_trees(cfg: Config, *, force_vrt: bool = False) -> int:
|
||||
|
||||
dtm = dtm_ds.ReadAsArray().astype(np.float32)
|
||||
dom = dom_ds.ReadAsArray().astype(np.float32)
|
||||
nodata = dtm_ds.GetRasterBand(1).GetNoDataValue()
|
||||
if nodata is None:
|
||||
nodata = -1e10
|
||||
mask = dtm == nodata
|
||||
nodata_dtm = dtm_ds.GetRasterBand(1).GetNoDataValue()
|
||||
nodata_dom = dom_ds.GetRasterBand(1).GetNoDataValue()
|
||||
dtm_valid = np.isfinite(dtm)
|
||||
dom_valid = np.isfinite(dom)
|
||||
if nodata_dtm is not None and math.isfinite(nodata_dtm):
|
||||
dtm_valid &= dtm != nodata_dtm
|
||||
if nodata_dom is not None and math.isfinite(nodata_dom):
|
||||
dom_valid &= dom != nodata_dom
|
||||
mask = ~(dtm_valid & dom_valid)
|
||||
chm = dom - dtm
|
||||
chm = np.where(mask, np.nan, chm)
|
||||
chm = np.where(chm < 0, np.nan, chm)
|
||||
|
||||
bmask = _building_mask(tile_id, bounds, dtm_ds)
|
||||
bmask = _building_mask(tile_id, bounds, dtm_ds, cfg)
|
||||
if bmask is not None:
|
||||
px_buffer = int(round(cfg.trees.building_buffer_m / max(cfg.trees.grid_res_m, 0.1)))
|
||||
bmask = _dilate_mask(bmask, px_buffer)
|
||||
@@ -655,11 +674,35 @@ def export_trees(cfg: Config, *, force_vrt: bool = False) -> int:
|
||||
spacing_px = max(2, int(math.ceil((cfg.trees.grid_res_m * 2.5) / cfg.trees.grid_res_m)))
|
||||
maxima = _local_maxima(chm, cfg.trees.min_height_m, spacing_px)
|
||||
if not maxima:
|
||||
print(f"[trees] no trees found for {tile_id}")
|
||||
valid_count = int(np.sum(dtm_valid & dom_valid))
|
||||
if valid_count == 0:
|
||||
no_tree_stats["no_valid_chm"] += 1
|
||||
print(
|
||||
f"[trees] no trees found for {tile_id} "
|
||||
f"(no valid CHM samples; dtm_valid={int(np.sum(dtm_valid))}, "
|
||||
f"dom_valid={int(np.sum(dom_valid))})"
|
||||
)
|
||||
else:
|
||||
max_chm = float(np.nanmax(chm))
|
||||
if max_chm < cfg.trees.min_height_m:
|
||||
no_tree_stats["below_min_height"] += 1
|
||||
print(
|
||||
f"[trees] no trees found for {tile_id} "
|
||||
f"(max_chm={max_chm:.2f}m < min_height={cfg.trees.min_height_m:.2f}m)"
|
||||
)
|
||||
else:
|
||||
no_tree_stats["no_local_maxima"] += 1
|
||||
print(
|
||||
f"[trees] no trees found for {tile_id} "
|
||||
f"(max_chm={max_chm:.2f}m, valid_samples={valid_count})"
|
||||
)
|
||||
continue
|
||||
# lightweight presence signals
|
||||
has_bdom = bool(list(glob.glob(os.path.join("raw", "bdom20rgbi", f"*{_tile_suffix(tile_id)}*.laz"))))
|
||||
has_lpo = bool(list(glob.glob(os.path.join("raw", "lpo", f"lpo_{_tile_suffix(tile_id)}.xyz"))))
|
||||
has_bdom = has_bdom_data(tile_id, cfg.pointcloud.bdom_dir)
|
||||
has_lpo = has_lpo_data(tile_id, cfg.pointcloud.lpo_dir)
|
||||
has_lpolpg = has_lpolpg_data(tile_id, cfg.pointcloud.lpolpg_dir)
|
||||
if has_lpolpg:
|
||||
has_lpo = True
|
||||
|
||||
# Sort by height desc and cap
|
||||
maxima.sort(key=lambda m: m[2], reverse=True)
|
||||
@@ -715,5 +758,10 @@ def export_trees(cfg: Config, *, force_vrt: bool = False) -> int:
|
||||
else:
|
||||
print(f"[trees] wrote {csv_path} (no GLB, empty chunks)")
|
||||
|
||||
print(f"[trees] Summary: wrote {written} CSV(s); wrote {glb_written} GLB(s).")
|
||||
print(
|
||||
f"[trees] Summary: wrote {written} CSV(s); wrote {glb_written} GLB(s). "
|
||||
f"No-trees: no_valid_chm={no_tree_stats['no_valid_chm']}, "
|
||||
f"below_min_height={no_tree_stats['below_min_height']}, "
|
||||
f"no_local_maxima={no_tree_stats['no_local_maxima']}."
|
||||
)
|
||||
return 0 if written else 1
|
||||
|
||||
@@ -19,7 +19,7 @@ from osgeo import gdal
|
||||
|
||||
from .config import Config
|
||||
from .gdal_utils import open_dataset
|
||||
from .pointcloud import find_xyz_file, has_lpo_data, read_xyz_file
|
||||
from .pointcloud import find_pointcloud_file, has_lpo_data, read_pointcloud_file
|
||||
from .street_furniture import load_furniture_detections
|
||||
|
||||
|
||||
@@ -270,9 +270,9 @@ def detect_trees_in_tile(
|
||||
# Load LPO for height refinement
|
||||
lpo = None
|
||||
if et_cfg.use_lpo_refinement and has_lpo_data(tile_id, pc_cfg.lpo_dir):
|
||||
lpo_file = find_xyz_file(pc_cfg.lpo_dir, tile_id)
|
||||
lpo_file = find_pointcloud_file(pc_cfg.lpo_dir, tile_id)
|
||||
if lpo_file:
|
||||
lpo = read_xyz_file(lpo_file, bounds=(xmin, ymin, xmax, ymax))
|
||||
lpo = read_pointcloud_file(lpo_file, bounds=(xmin, ymin, xmax, ymax))
|
||||
|
||||
trees = []
|
||||
pixel_size = abs(gt[1])
|
||||
|
||||
@@ -7,11 +7,14 @@ import os
|
||||
import sys
|
||||
from typing import Iterable
|
||||
|
||||
from geodata_download import run_download
|
||||
|
||||
from geodata_pipeline.config import Config, DEFAULT_CONFIG_PATH, ensure_default_config
|
||||
from geodata_pipeline.buildings import export_buildings
|
||||
from geodata_pipeline.buildings_enhanced import export_buildings_enhanced
|
||||
from geodata_pipeline.heightmaps import export_heightmaps
|
||||
from geodata_pipeline.heightmaps_enhanced import export_heightmaps_enhanced
|
||||
from geodata_pipeline.lpolpg_split import split_lpolpg
|
||||
from geodata_pipeline.orthophotos import export_orthophotos
|
||||
from geodata_pipeline.setup_helpers import ensure_directories, materialize_archives
|
||||
from geodata_pipeline.street_furniture import export_street_furniture
|
||||
@@ -24,7 +27,7 @@ def parse_args(argv: Iterable[str] | None = None) -> argparse.Namespace:
|
||||
parser.add_argument(
|
||||
"--config",
|
||||
default=DEFAULT_CONFIG_PATH,
|
||||
help="Path to config JSON (created on --setup if missing).",
|
||||
help="Path to config TOML (created on --setup if missing).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--export",
|
||||
@@ -53,6 +56,63 @@ def parse_args(argv: Iterable[str] | None = None) -> argparse.Namespace:
|
||||
action="store_true",
|
||||
help="Rebuild VRTs even if present (useful after moving raw data).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--download",
|
||||
action="store_true",
|
||||
help="Download raw datasets before export using geodata_download.toml.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--download-config",
|
||||
default="geodata_download.toml",
|
||||
help="Path to download config TOML.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--download-datasets",
|
||||
help="Comma-separated dataset keys to download (default: enabled datasets).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--download-start",
|
||||
nargs=2,
|
||||
type=int,
|
||||
metavar=("X", "Y"),
|
||||
help="Override tile range start for download (x y).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--download-end",
|
||||
nargs=2,
|
||||
type=int,
|
||||
metavar=("X", "Y"),
|
||||
help="Override tile range end for download (x y).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--clean-downloads",
|
||||
action="store_true",
|
||||
help="Delete selected dataset folders before downloading.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--download-ca-bundle",
|
||||
help="Override CA bundle path for downloads.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--split-lpolpg",
|
||||
action="store_true",
|
||||
help="Split combined lpolpg LAZ into LPG/LPO outputs.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--split-lpolpg-formats",
|
||||
default="laz,xyz",
|
||||
help="Comma-separated output formats for lpolpg split (laz,xyz).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--split-lpolpg-ground-classes",
|
||||
default="2",
|
||||
help="Comma-separated LAS classification codes treated as ground.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--split-lpolpg-overwrite",
|
||||
action="store_true",
|
||||
help="Overwrite existing LPG/LPO outputs when splitting lpolpg.",
|
||||
)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
@@ -72,19 +132,56 @@ def load_config(args: argparse.Namespace) -> Config:
|
||||
def main(argv: Iterable[str] | None = None) -> int:
|
||||
args = parse_args(argv)
|
||||
cfg = load_config(args)
|
||||
target_export = args.export or "all"
|
||||
target_export = None
|
||||
if args.export is not None or not (args.download or args.split_lpolpg):
|
||||
target_export = args.export or "all"
|
||||
|
||||
if args.setup:
|
||||
ensure_directories(cfg)
|
||||
print(f"Directories ensured. Config at {args.config}.")
|
||||
if args.build_from_archive:
|
||||
materialize_archives(cfg)
|
||||
if args.export is None:
|
||||
if args.export is None and not args.download:
|
||||
return 0
|
||||
|
||||
if args.build_from_archive and not args.setup:
|
||||
materialize_archives(cfg)
|
||||
|
||||
if args.download:
|
||||
datasets = (
|
||||
[name.strip() for name in args.download_datasets.split(",")]
|
||||
if args.download_datasets
|
||||
else None
|
||||
)
|
||||
start = tuple(args.download_start) if args.download_start else None
|
||||
end = tuple(args.download_end) if args.download_end else None
|
||||
if (start is None) != (end is None):
|
||||
raise SystemExit("--download-start and --download-end must be provided together.")
|
||||
download_exit = run_download(
|
||||
config_path=args.download_config,
|
||||
requested_datasets=datasets,
|
||||
start_override=start,
|
||||
end_override=end,
|
||||
clean_downloads=args.clean_downloads,
|
||||
ca_bundle_override=args.download_ca_bundle,
|
||||
)
|
||||
if download_exit != 0:
|
||||
return download_exit
|
||||
if args.split_lpolpg:
|
||||
formats = [fmt.strip() for fmt in args.split_lpolpg_formats.split(",") if fmt.strip()]
|
||||
ground = [int(val) for val in args.split_lpolpg_ground_classes.split(",") if val.strip()]
|
||||
split_exit = split_lpolpg(
|
||||
cfg,
|
||||
formats=formats,
|
||||
ground_classes=ground,
|
||||
overwrite=args.split_lpolpg_overwrite,
|
||||
)
|
||||
if split_exit != 0:
|
||||
return split_exit
|
||||
|
||||
if target_export is None:
|
||||
return 0
|
||||
|
||||
exit_codes = []
|
||||
|
||||
# Standard exports
|
||||
@@ -95,7 +192,7 @@ def main(argv: Iterable[str] | None = None) -> int:
|
||||
if target_export in ("buildings", "all"):
|
||||
exit_codes.append(export_buildings(cfg))
|
||||
if target_export in ("trees", "all"):
|
||||
exit_codes.append(export_trees(cfg))
|
||||
exit_codes.append(export_trees(cfg, force_vrt=args.force_vrt))
|
||||
|
||||
# Enhanced exports (use point cloud data)
|
||||
# Order matters: heightmap-enhanced creates tile_index.csv needed by others
|
||||
|
||||
@@ -3,7 +3,7 @@ name = "geodata-toolkit"
|
||||
version = "0.1.0"
|
||||
description = "Heightmap and orthophoto exporters using GDAL for Unity terrains."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10,<3.13"
|
||||
requires-python = ">=3.11,<3.13"
|
||||
dependencies = [
|
||||
"gdal>=3.4",
|
||||
"cjio[export,reproject]>=0.9",
|
||||
@@ -13,6 +13,8 @@ dependencies = [
|
||||
"shapely>=2.0",
|
||||
"numpy>=1.24",
|
||||
"trimesh>=4.0",
|
||||
"requests>=2.31",
|
||||
"tomli-w>=1.0",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
@@ -21,13 +23,13 @@ build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["geodata_pipeline"]
|
||||
force-include = { "geodata_to_unity.py" = "geodata_to_unity.py" }
|
||||
|
||||
force-include = { "geodata_to_unity.py" = "geodata_to_unity.py", "geodata_download.py" = "geodata_download.py" }
|
||||
[tool.hatch.build.targets.sdist]
|
||||
include = [
|
||||
"geodata_to_unity.py",
|
||||
"geodata_download.py",
|
||||
"geodata_pipeline/",
|
||||
"geodata_config.example.json",
|
||||
"geodata_config.example.toml",
|
||||
"scripts/",
|
||||
"README.md",
|
||||
"AGENTS.md",
|
||||
|
||||
232
uv.lock
generated
232
uv.lock
generated
@@ -1,10 +1,9 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.10, <3.13"
|
||||
requires-python = ">=3.11, <3.13"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12'",
|
||||
"python_full_version == '3.11.*'",
|
||||
"python_full_version < '3.11'",
|
||||
"python_full_version < '3.12'",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -16,6 +15,47 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cjio"
|
||||
version = "0.10.1"
|
||||
@@ -75,11 +115,11 @@ dependencies = [
|
||||
{ name = "gdal" },
|
||||
{ name = "laspy", extra = ["lazrs"] },
|
||||
{ name = "numpy" },
|
||||
{ name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||
{ name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||
{ name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||
{ name = "scipy", version = "1.16.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||
{ name = "requests" },
|
||||
{ name = "scikit-learn" },
|
||||
{ name = "scipy" },
|
||||
{ name = "shapely" },
|
||||
{ name = "tomli-w" },
|
||||
{ name = "trimesh" },
|
||||
]
|
||||
|
||||
@@ -89,12 +129,23 @@ requires-dist = [
|
||||
{ name = "gdal", specifier = ">=3.4" },
|
||||
{ name = "laspy", extras = ["lazrs"], specifier = ">=2.5" },
|
||||
{ name = "numpy", specifier = ">=1.24" },
|
||||
{ name = "requests", specifier = ">=2.31" },
|
||||
{ name = "scikit-learn", specifier = ">=1.3" },
|
||||
{ name = "scipy", specifier = ">=1.11" },
|
||||
{ name = "shapely", specifier = ">=2.0" },
|
||||
{ name = "tomli-w", specifier = ">=1.0" },
|
||||
{ name = "trimesh", specifier = ">=4.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.11"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "joblib"
|
||||
version = "1.5.3"
|
||||
@@ -127,12 +178,6 @@ version = "0.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9b/37/169d27b57af14b0f5992debf1477a1d7f1497d738432b9f383b271e739e4/lazrs-0.7.0.tar.gz", hash = "sha256:53191b351c1d9fa45f74471698384bf42bde14599309645fb9d4c353f0fb7f24", size = 10098, upload-time = "2025-06-06T15:58:13.026Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/9c/eab617a474e65421967faae0c3169f3f0ab0b10646feb6b95d5a5e8d2abe/lazrs-0.7.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:19d9897941c1a3c54198cb9db19526d7840bd7f82487e3542d28c5be80c6ded1", size = 569479, upload-time = "2025-06-06T15:57:36.872Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/e3/41aaebf1d31e9750d9f68d79e2aea245d97ee088a53375b42af62f232542/lazrs-0.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:834ef7f828044d8322fa53ee2f2b9bcd615f2bdf0c123504f622e54cb4f98836", size = 570906, upload-time = "2025-06-06T15:57:38.325Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/a5/1f2bc60b3ccb5e349333d2212af653d775ebb047d73ac47e71857d26a85c/lazrs-0.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e64dadfafcea7bf9df6f27d9439f5f75eae22a280399cc0de735abaa4bcb6f72", size = 633428, upload-time = "2025-06-06T15:57:39.747Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/28/d1dcc28c35c8a913821edb76b55d988b4f20ef50317304d5732eae869861/lazrs-0.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:376d8ca61cb00580a1a7e68aa1dc8a80d230953b255c7b0f16abae7f4edb4391", size = 639937, upload-time = "2025-06-06T15:57:41.228Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/60/ff29fc539824c9c2a5727639ec7dae12ce68bd9534b2166d1d935a7099b6/lazrs-0.7.0-cp310-cp310-win32.whl", hash = "sha256:f469ee4847214a3f901c8419213cd87b9ebeb22308af8253faad45bde8fc34b2", size = 407819, upload-time = "2025-06-06T15:57:42.607Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/4e/132051aa292353cf8929a2c0ce90681eb877e9fb2cdb7b7de523af380d38/lazrs-0.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:f1bd15a9ddec761a356d65b3ce819336ffdfcc3a79fd3b34272709f0d7c00ad5", size = 421002, upload-time = "2025-06-06T15:57:43.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/c6/506f7222db694f93384fee5fe420041062327d35247831e3ce458219fe41/lazrs-0.7.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4236ace28a55d8b1b6d181ea2390a4a57de896587b914ea21a7e887cd493e87f", size = 569443, upload-time = "2025-06-06T15:57:46.4Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/68/bc71647430e852070b6a2cf62117843daa1955ff10311ee5857eda3f3304/lazrs-0.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:42e943f7859c5f31067f28e5766f3a23237b3c3246b57c98ad4b765ccad69b11", size = 570900, upload-time = "2025-06-06T15:57:47.842Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/53/534844b7f7034dea47db0c445d5b6a6c3d40330e45db28a5faeab386ff7e/lazrs-0.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17a2dde6acbc77e13981f6d85b7c8e994b0004e63abb15602dc2b65d1301550d", size = 632368, upload-time = "2025-06-06T15:57:49.382Z" },
|
||||
@@ -156,14 +201,6 @@ dependencies = [
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bc/7b/bbf6b00488662be5d2eb7a188222c264b6f713bac10dc4a77bf37a4cb4b6/mapbox_earcut-2.0.0.tar.gz", hash = "sha256:81eab6b86cf99551deb698b98e3f7502c57900e5c479df15e1bdaf1a57f0f9d6", size = 39934, upload-time = "2025-11-16T18:41:27.251Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/d3/5222339a8fad091bf64f2e3041e48606d69d69f0609a7632ca17a8a05d5a/mapbox_earcut-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:c9a1dab7529f8e54bdb377f908e56f1e2b9a7e27ed168c64d3c7c38ed04ac201", size = 55920, upload-time = "2025-11-16T18:40:09.254Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/e4/88d06e83ab75db2f4ae140a1e03ad8f84b02ac8af585dd61108aba73b8ed/mapbox_earcut-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5e5953098ea198253c8a40e2f282ca5b04d50ec2b9661e20c4cd2b2be39f0bb0", size = 52557, upload-time = "2025-11-16T18:40:10.536Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/88/abefd244ea049e42334c5f7a9e3b58f4ec3c84d063119ba3c8d27ff31932/mapbox_earcut-2.0.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efe5fd5de409e3b6d13907e73f295c8f1d63bdb6b8ca155dde4c93865796eafe", size = 56950, upload-time = "2025-11-16T18:40:11.905Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/e2/11122fddd086b930502eb4a954735da0f75e9d658fdab2d9e5914b9ebd2a/mapbox_earcut-2.0.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd04da6edbca1dd68ddbfac2398a95c763f35d7317fed227fde5b3aff1253b18", size = 59618, upload-time = "2025-11-16T18:40:13.017Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/fd/e62195729daa3111fe95404a99c7a6b3aa174800373d10111b7e7278a789/mapbox_earcut-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8bdb8881e857d6d9277df696e9cfb8749c00d6162021d9359cba9da58dfdd4f5", size = 153021, upload-time = "2025-11-16T18:40:14.294Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/6a/d39ebaaa9010ea6c9f4d468f8812b1a1b31a40fba4f02ff29bc1bf321c30/mapbox_earcut-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6e2d1bf5af90d5857955775b4d8ea15b02e172f2a8f194bba50ff95f8ff3e80e", size = 157736, upload-time = "2025-11-16T18:40:16.344Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/00/6a59cdb8d8c1bf7e3cc92f0404f68fdb1a3cb0bbb0837af0dbb93d6290a6/mapbox_earcut-2.0.0-cp310-cp310-win32.whl", hash = "sha256:5b0aa63dd890d712343095b05eb7b60e071912ad3ced1fc4187d6a6a739677bc", size = 51564, upload-time = "2025-11-16T18:40:17.852Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/7b/af69669c959d8f7fd1bd49c15deace2360bf6a79dad7bf9f7a7f1c137da6/mapbox_earcut-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b1355f13af89ea815b32f59a5455db295c965d51ab501bde0459cddc010a7149", size = 56793, upload-time = "2025-11-16T18:40:18.953Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/9f/fbd15d9e348e75e986d6912c4eab99888106b7e5fb0a01e765422f7cd464/mapbox_earcut-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:9b5040e79e3783295e99c90277f31c1cbaddd3335297275331995ba5680e3649", size = 55773, upload-time = "2025-11-16T18:40:20.045Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/40/be761298704fbbaa81c5618bb306f1510fb068e482f6a1c8b3b6c1b31479/mapbox_earcut-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1cf43baafec3ef1e967319d9b5da96bc6ddf3dbb204b6f3535275eda4b519a72", size = 52444, upload-time = "2025-11-16T18:40:21.501Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/0b/0c0c08db9663238ffb82c48259582dc0047a3255d98c0ac83c48026b7544/mapbox_earcut-2.0.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a283531847f603dd9d69afb75b21bd009d385ca9485fcd3e5a7fa5db1ccd913", size = 56803, upload-time = "2025-11-16T18:40:22.891Z" },
|
||||
@@ -190,16 +227,6 @@ version = "2.2.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" },
|
||||
@@ -220,10 +247,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -238,13 +261,6 @@ dependencies = [
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", size = 11555763, upload-time = "2025-09-29T23:16:53.287Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", size = 10801217, upload-time = "2025-09-29T23:17:04.522Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", size = 12148791, upload-time = "2025-09-29T23:17:18.444Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", size = 12769373, upload-time = "2025-09-29T23:17:35.846Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444, upload-time = "2025-09-29T23:17:49.341Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459, upload-time = "2025-09-29T23:18:03.722Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086, upload-time = "2025-09-29T23:18:18.505Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" },
|
||||
@@ -270,14 +286,6 @@ dependencies = [
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/67/10/a8480ea27ea4bbe896c168808854d00f2a9b49f95c0319ddcbba693c8a90/pyproj-3.7.1.tar.gz", hash = "sha256:60d72facd7b6b79853f19744779abcd3f804c4e0d4fa8815469db20c9f640a47", size = 226339, upload-time = "2025-02-16T04:28:46.621Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/25/a3/c4cd4bba5b336075f145fe784fcaf4ef56ffbc979833303303e7a659dda2/pyproj-3.7.1-cp310-cp310-macosx_13_0_x86_64.whl", hash = "sha256:bf09dbeb333c34e9c546364e7df1ff40474f9fddf9e70657ecb0e4f670ff0b0e", size = 6262524, upload-time = "2025-02-16T04:27:19.725Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/45/4fdf18f4cc1995f1992771d2a51cf186a9d7a8ec973c9693f8453850c707/pyproj-3.7.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:6575b2e53cc9e3e461ad6f0692a5564b96e7782c28631c7771c668770915e169", size = 4665102, upload-time = "2025-02-16T04:27:24.428Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/d2/360eb127380106cee83569954ae696b88a891c804d7a93abe3fbc15f5976/pyproj-3.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8cb516ee35ed57789b46b96080edf4e503fdb62dbb2e3c6581e0d6c83fca014b", size = 9432667, upload-time = "2025-02-16T04:27:27.04Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/a5/c6e11b9a99ce146741fb4d184d5c468446c6d6015b183cae82ac822a6cfa/pyproj-3.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e47c4e93b88d99dd118875ee3ca0171932444cdc0b52d493371b5d98d0f30ee", size = 9259185, upload-time = "2025-02-16T04:27:30.35Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/56/a3c15c42145797a99363fa0fdb4e9805dccb8b4a76a6d7b2cdf36ebcc2a1/pyproj-3.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3e8d276caeae34fcbe4813855d0d97b9b825bab8d7a8b86d859c24a6213a5a0d", size = 10469103, upload-time = "2025-02-16T04:27:33.542Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/73/c9194c2802fefe2a4fd4230bdd5ab083e7604e93c64d0356fa49c363bad6/pyproj-3.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f173f851ee75e54acdaa053382b6825b400cb2085663a9bb073728a59c60aebb", size = 10401391, upload-time = "2025-02-16T04:27:36.051Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/1d/ce8bb5b9251b04d7c22d63619bb3db3d2397f79000a9ae05b3fd86a5837e/pyproj-3.7.1-cp310-cp310-win32.whl", hash = "sha256:f550281ed6e5ea88fcf04a7c6154e246d5714be495c50c9e8e6b12d3fb63e158", size = 5869997, upload-time = "2025-02-16T04:27:38.302Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/6a/ca145467fd2e5b21e3d5b8c2b9645dcfb3b68f08b62417699a1f5689008e/pyproj-3.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:3537668992a709a2e7f068069192138618c00d0ba113572fdd5ee5ffde8222f3", size = 6278581, upload-time = "2025-02-16T04:27:41.051Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/0d/63670fc527e664068b70b7cab599aa38b7420dd009bdc29ea257e7f3dfb3/pyproj-3.7.1-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:a94e26c1a4950cea40116775588a2ca7cf56f1f434ff54ee35a84718f3841a3d", size = 6264315, upload-time = "2025-02-16T04:27:44.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/9d/cbaf82cfb290d1f1fa42feb9ba9464013bb3891e40c4199f8072112e4589/pyproj-3.7.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:263b54ba5004b6b957d55757d846fc5081bc02980caa0279c4fc95fa0fff6067", size = 4666267, upload-time = "2025-02-16T04:27:47.019Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/53/24f9f9b8918c0550f3ff49ad5de4cf3f0688c9f91ff191476db8979146fe/pyproj-3.7.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6d6a2ccd5607cd15ef990c51e6f2dd27ec0a741e72069c387088bba3aab60fa", size = 9680510, upload-time = "2025-02-16T04:27:49.239Z" },
|
||||
@@ -318,50 +326,29 @@ wheels = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scikit-learn"
|
||||
version = "1.7.2"
|
||||
name = "requests"
|
||||
version = "2.32.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.11'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "joblib", marker = "python_full_version < '3.11'" },
|
||||
{ name = "numpy", marker = "python_full_version < '3.11'" },
|
||||
{ name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||
{ name = "threadpoolctl", marker = "python_full_version < '3.11'" },
|
||||
{ name = "certifi" },
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/3e/daed796fd69cce768b8788401cc464ea90b306fb196ae1ffed0b98182859/scikit_learn-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b33579c10a3081d076ab403df4a4190da4f4432d443521674637677dc91e61f", size = 9336221, upload-time = "2025-09-09T08:20:19.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/ce/af9d99533b24c55ff4e18d9b7b4d9919bbc6cd8f22fe7a7be01519a347d5/scikit_learn-1.7.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:36749fb62b3d961b1ce4fedf08fa57a1986cd409eff2d783bca5d4b9b5fce51c", size = 8653834, upload-time = "2025-09-09T08:20:22.073Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/0e/8c2a03d518fb6bd0b6b0d4b114c63d5f1db01ff0f9925d8eb10960d01c01/scikit_learn-1.7.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7a58814265dfc52b3295b1900cfb5701589d30a8bb026c7540f1e9d3499d5ec8", size = 9660938, upload-time = "2025-09-09T08:20:24.327Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/75/4311605069b5d220e7cf5adabb38535bd96f0079313cdbb04b291479b22a/scikit_learn-1.7.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a847fea807e278f821a0406ca01e387f97653e284ecbd9750e3ee7c90347f18", size = 9477818, upload-time = "2025-09-09T08:20:26.845Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/9b/87961813c34adbca21a6b3f6b2bea344c43b30217a6d24cc437c6147f3e8/scikit_learn-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:ca250e6836d10e6f402436d6463d6c0e4d8e0234cfb6a9a47835bd392b852ce5", size = 8886969, upload-time = "2025-09-09T08:20:29.329Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/83/564e141eef908a5863a54da8ca342a137f45a0bfb71d1d79704c9894c9d1/scikit_learn-1.7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7509693451651cd7361d30ce4e86a1347493554f172b1c72a39300fa2aea79e", size = 9331967, upload-time = "2025-09-09T08:20:32.421Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/d6/ba863a4171ac9d7314c4d3fc251f015704a2caeee41ced89f321c049ed83/scikit_learn-1.7.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:0486c8f827c2e7b64837c731c8feff72c0bd2b998067a8a9cbc10643c31f0fe1", size = 8648645, upload-time = "2025-09-09T08:20:34.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/0e/97dbca66347b8cf0ea8b529e6bb9367e337ba2e8be0ef5c1a545232abfde/scikit_learn-1.7.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89877e19a80c7b11a2891a27c21c4894fb18e2c2e077815bcade10d34287b20d", size = 9715424, upload-time = "2025-09-09T08:20:36.776Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/32/1f3b22e3207e1d2c883a7e09abb956362e7d1bd2f14458c7de258a26ac15/scikit_learn-1.7.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8da8bf89d4d79aaec192d2bda62f9b56ae4e5b4ef93b6a56b5de4977e375c1f1", size = 9509234, upload-time = "2025-09-09T08:20:38.957Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/71/34ddbd21f1da67c7a768146968b4d0220ee6831e4bcbad3e03dd3eae88b6/scikit_learn-1.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:9b7ed8d58725030568523e937c43e56bc01cadb478fc43c042a9aca1dacb3ba1", size = 8894244, upload-time = "2025-09-09T08:20:41.166Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/aa/3996e2196075689afb9fce0410ebdb4a09099d7964d061d7213700204409/scikit_learn-1.7.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8d91a97fa2b706943822398ab943cde71858a50245e31bc71dba62aab1d60a96", size = 9259818, upload-time = "2025-09-09T08:20:43.19Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/5d/779320063e88af9c4a7c2cf463ff11c21ac9c8bd730c4a294b0000b666c9/scikit_learn-1.7.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:acbc0f5fd2edd3432a22c69bed78e837c70cf896cd7993d71d51ba6708507476", size = 8636997, upload-time = "2025-09-09T08:20:45.468Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/d0/0c577d9325b05594fdd33aa970bf53fb673f051a45496842caee13cfd7fe/scikit_learn-1.7.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e5bf3d930aee75a65478df91ac1225ff89cd28e9ac7bd1196853a9229b6adb0b", size = 9478381, upload-time = "2025-09-09T08:20:47.982Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/70/8bf44b933837ba8494ca0fc9a9ab60f1c13b062ad0197f60a56e2fc4c43e/scikit_learn-1.7.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d6e9deed1a47aca9fe2f267ab8e8fe82ee20b4526b2c0cd9e135cea10feb44", size = 9300296, upload-time = "2025-09-09T08:20:50.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/99/ed35197a158f1fdc2fe7c3680e9c70d0128f662e1fee4ed495f4b5e13db0/scikit_learn-1.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:6088aa475f0785e01bcf8529f55280a3d7d298679f50c0bb70a2364a82d0b290", size = 8731256, upload-time = "2025-09-09T08:20:52.627Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scikit-learn"
|
||||
version = "1.8.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12'",
|
||||
"python_full_version == '3.11.*'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "joblib", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "numpy", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "scipy", version = "1.16.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||
{ name = "threadpoolctl", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "joblib" },
|
||||
{ name = "numpy" },
|
||||
{ name = "scipy" },
|
||||
{ name = "threadpoolctl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" }
|
||||
wheels = [
|
||||
@@ -379,57 +366,12 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/24/90/344a67811cfd561d7335c1b96ca21455e7e472d281c3c279c4d3f2300236/scikit_learn-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:8c497fff237d7b4e07e9ef1a640887fa4fb765647f86fbe00f969ff6280ce2bb", size = 7641898, upload-time = "2025-12-10T07:08:01.36Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scipy"
|
||||
version = "1.15.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.11'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "numpy", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770, upload-time = "2025-05-08T16:04:20.849Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511, upload-time = "2025-05-08T16:04:27.103Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151, upload-time = "2025-05-08T16:04:31.731Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732, upload-time = "2025-05-08T16:04:36.596Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617, upload-time = "2025-05-08T16:04:43.546Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964, upload-time = "2025-05-08T16:04:49.431Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749, upload-time = "2025-05-08T16:04:55.215Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383, upload-time = "2025-05-08T16:05:01.914Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201, upload-time = "2025-05-08T16:05:08.166Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scipy"
|
||||
version = "1.16.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12'",
|
||||
"python_full_version == '3.11.*'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "numpy", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "numpy" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0a/ca/d8ace4f98322d01abcd52d381134344bf7b431eba7ed8b42bdea5a3c2ac9/scipy-1.16.3.tar.gz", hash = "sha256:01e87659402762f43bd2fee13370553a17ada367d42e7487800bf2916535aecb", size = 30597883, upload-time = "2025-10-28T17:38:54.068Z" }
|
||||
wheels = [
|
||||
@@ -464,14 +406,6 @@ dependencies = [
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4d/bc/0989043118a27cccb4e906a46b7565ce36ca7b57f5a18b78f4f1b0f72d9d/shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9", size = 315489, upload-time = "2025-09-24T13:51:41.432Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/05/89/c3548aa9b9812a5d143986764dededfa48d817714e947398bdda87c77a72/shapely-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7ae48c236c0324b4e139bea88a306a04ca630f49be66741b340729d380d8f52f", size = 1825959, upload-time = "2025-09-24T13:50:00.682Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/8a/7ebc947080442edd614ceebe0ce2cdbd00c25e832c240e1d1de61d0e6b38/shapely-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eba6710407f1daa8e7602c347dfc94adc02205ec27ed956346190d66579eb9ea", size = 1629196, upload-time = "2025-09-24T13:50:03.447Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/86/c9c27881c20d00fc409e7e059de569d5ed0abfcec9c49548b124ebddea51/shapely-2.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef4a456cc8b7b3d50ccec29642aa4aeda959e9da2fe9540a92754770d5f0cf1f", size = 2951065, upload-time = "2025-09-24T13:50:05.266Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/8a/0ab1f7433a2a85d9e9aea5b1fbb333f3b09b309e7817309250b4b7b2cc7a/shapely-2.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e38a190442aacc67ff9f75ce60aec04893041f16f97d242209106d502486a142", size = 3058666, upload-time = "2025-09-24T13:50:06.872Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/c6/5a30ffac9c4f3ffd5b7113a7f5299ccec4713acd5ee44039778a7698224e/shapely-2.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:40d784101f5d06a1fd30b55fc11ea58a61be23f930d934d86f19a180909908a4", size = 3966905, upload-time = "2025-09-24T13:50:09.417Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/72/e92f3035ba43e53959007f928315a68fbcf2eeb4e5ededb6f0dc7ff1ecc3/shapely-2.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f6f6cd5819c50d9bcf921882784586aab34a4bd53e7553e175dece6db513a6f0", size = 4129260, upload-time = "2025-09-24T13:50:11.183Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/24/605901b73a3d9f65fa958e63c9211f4be23d584da8a1a7487382fac7fdc5/shapely-2.1.2-cp310-cp310-win32.whl", hash = "sha256:fe9627c39c59e553c90f5bc3128252cb85dc3b3be8189710666d2f8bc3a5503e", size = 1544301, upload-time = "2025-09-24T13:50:12.521Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/89/6db795b8dd3919851856bd2ddd13ce434a748072f6fdee42ff30cbd3afa3/shapely-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:1d0bfb4b8f661b3b4ec3565fa36c340bfb1cda82087199711f86a88647d26b2f", size = 1722074, upload-time = "2025-09-24T13:50:13.909Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/8d/1ff672dea9ec6a7b5d422eb6d095ed886e2e523733329f75fdcb14ee1149/shapely-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:91121757b0a36c9aac3427a651a7e6567110a4a67c97edf04f8d55d4765f6618", size = 1820038, upload-time = "2025-09-24T13:50:15.628Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/ce/28fab8c772ce5db23a0d86bf0adaee0c4c79d5ad1db766055fa3dab442e2/shapely-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:16a9c722ba774cf50b5d4541242b4cce05aafd44a015290c82ba8a16931ff63d", size = 1626039, upload-time = "2025-09-24T13:50:16.881Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/8b/868b7e3f4982f5006e9395c1e12343c66a8155c0374fdc07c0e6a1ab547d/shapely-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cc4f7397459b12c0b196c9efe1f9d7e92463cbba142632b4cc6d8bbbbd3e2b09", size = 3001519, upload-time = "2025-09-24T13:50:18.606Z" },
|
||||
@@ -508,6 +442,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomli-w"
|
||||
version = "1.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "triangle2"
|
||||
version = "20230923"
|
||||
@@ -516,12 +459,6 @@ dependencies = [
|
||||
{ name = "numpy" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/81/f5610bda7a92ce0ce2c284295ba11acfa429d4c2056c39e44ffd82db926d/triangle2-20230923-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d442d16e0ca0975c6c8f56f7b8e08639d31bcea2c985ebd0ee265ab9f3ad6bd4", size = 1457243, upload-time = "2024-02-26T14:39:41.088Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/dd/9c76c6f148a429d35b44d13fc5f49c5e5a99def61ab8a663fbd15d5815c6/triangle2-20230923-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b99780b93658ff93efbefd2dbf6fc54dde405930952fe40574c84dc4552cb73e", size = 1440863, upload-time = "2024-02-26T14:39:43.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/0c/3f060a6805c619674337478076003ddd9e63ff7fb58abb40f7b8f549a278/triangle2-20230923-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb8d14f04d871d978ff5356b247d185cd5129f027e1fd692d86a79b946a1b26c", size = 2054003, upload-time = "2024-02-26T14:39:45.061Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/2a/7bd73f5cc1509cbe12207bf8d5c12b06c78baea5ac2ba2d1e3ce6621984a/triangle2-20230923-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f758f99aca653e7d8bc4d2e14c034f09e7af6de1919d9792876d0d74dc6b4c49", size = 1977571, upload-time = "2024-02-26T14:39:46.643Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/2a/15bb0609c3c1a679533c02b97176a2967dd3c38275854e02c32b26778ca9/triangle2-20230923-cp310-cp310-win32.whl", hash = "sha256:c117a62965a64d07d5ec82b1880357a765c35f6b86820a7737170b3ec8c63725", size = 1406919, upload-time = "2024-02-26T14:39:48.228Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/6b/e6d963f0e6555a2626aa2954d9cee541b20f6a15c3bf80f4610bdbf4b727/triangle2-20230923-cp310-cp310-win_amd64.whl", hash = "sha256:eef547477e5dc0522b8504f1eba585ae938054016839df81d730b731f9e5b922", size = 1426296, upload-time = "2024-02-26T14:39:49.608Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/6d/90208d36e9cb2ba53414ee325cedf6aaff962f3577abbab2e0a73cc67225/triangle2-20230923-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e8ea0abc8e6d8bc08fec14541da0c564e5722b5d401f045d996ee04efac99a1e", size = 1457376, upload-time = "2024-02-26T14:39:51.289Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/24/f191d15419ae14d83e2cf4d0ca6f61340aeb39a0b74d6eed0cbd96e72522/triangle2-20230923-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4080e08adad70e882734f2f483e7985923e105fbd946998d222f7060ae7851ff", size = 1441037, upload-time = "2024-02-26T14:39:53.057Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/ae/ffc18956e59c9e5aad6d85e411f5522193c0c866cd331a35d662c9a454d2/triangle2-20230923-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3007191f44c3f37eb98d9e4751ff075a8f011ea359e14d7aae31f387de4edec", size = 2101785, upload-time = "2024-02-26T14:39:54.917Z" },
|
||||
@@ -556,3 +493,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf3
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.6.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user