Add downloader configs and update geodata pipeline

This commit is contained in:
2026-01-21 14:16:39 +01:00
parent d7023a57af
commit 8ef494f446
26 changed files with 5436 additions and 313 deletions

View File

@@ -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.

View File

@@ -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:

View File

@@ -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

File diff suppressed because it is too large Load Diff

Binary file not shown.

View 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
View 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-----

View File

@@ -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
}
}

View 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
View 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
View 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
View 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

View File

@@ -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:

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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])

View File

@@ -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

View File

@@ -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
View File

@@ -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" },
]