Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ This source requires creating an AW3D30 account on https://www.eorc.jaxa.jp/ALOS

For ubuntu-like system:

## Ligther deployment (without GeoTiff support)
## Lighter deployment (without GeoTiff support)

GDAL dependency is required only to add GeoTiff support, and is quite painful to install.
If you don't need GeoTiff support, simply install the default version of pyhgtmap:
Expand Down
27 changes: 13 additions & 14 deletions pyhgtmap/NASASRTMUtil.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,19 @@
if TYPE_CHECKING:
from collections.abc import Iterable

from pyhgtmap import PolygonsList
from pyhgtmap import BBox, PolygonsList


IntBBox: TypeAlias = tuple[int, int, int, int]


def calc_bbox(area: str, corrx: float = 0.0, corry: float = 0.0) -> IntBBox:
"""Parse bounding box string and calculates the appropriate bounding box for the needed files"""
min_lon, min_lat, max_lon, max_lat = [
float(value) - inc
for value, inc in zip(
area.split(":"), [corrx, corry, corrx, corry], strict=True
)
]
def calc_bbox(area: BBox, corrx: float = 0.0, corry: float = 0.0) -> IntBBox:
"""Convert a BBox to integer bounding box coordinates for the needed files."""
min_lon = area.min_lon - corrx
min_lat = area.min_lat - corry
max_lon = area.max_lon - corrx
max_lat = area.max_lat - corry

if min_lon < 0:
bbox_min_lon = int(min_lon) if min_lon % 1 == 0 else int(min_lon) - 1
else:
Expand Down Expand Up @@ -235,7 +234,7 @@ def get_file(self, area: str, source: str):


def get_files(
area: str,
area: BBox,
polygons: PolygonsList | None,
corrx: float,
corry: float,
Expand All @@ -247,14 +246,14 @@ def get_files(
files: list[tuple[str, bool]] = []
sources_pool = SourcesPool(configuration)

for area, check_poly in area_prefixes:
for area_prefix, check_poly in area_prefixes:
for source in sources:
print(f"{area:s}: trying {source:s} ...")
save_filename = sources_pool.get_file(area, source)
print(f"{area_prefix:s}: trying {source:s} ...")
save_filename = sources_pool.get_file(area_prefix, source)
if save_filename:
files.append((save_filename, check_poly))
break
else:
print(f"{area:s}: no file found on server.")
print(f"{area_prefix:s}: no file found on server.")
continue
return files
17 changes: 14 additions & 3 deletions pyhgtmap/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from configargparse import ArgumentDefaultsHelpFormatter, ArgumentParser

from pyhgtmap import __version__
from pyhgtmap import BBox, __version__
from pyhgtmap.configuration import CONFIG_FILENAME, Configuration, NestedConfig
from pyhgtmap.hgt.file import parse_polygons_file
from pyhgtmap.sources.pool import ALL_SUPPORTED_SOURCES, Pool
Expand All @@ -15,6 +15,14 @@
from pyhgtmap.sources import Source


def _str_to_bbox(area_str: str) -> BBox:
"""Convert area string format 'left:bottom:right:top' to BBox."""
parts = area_str.split(":")
if len(parts) != 4:
raise ValueError(f"Invalid area format: {area_str}")
return BBox(*[float(x) for x in parts])


def build_common_parser() -> ArgumentParser:
"""Build the common argument parser for pyhgtmap."""
default_config = Configuration()
Expand Down Expand Up @@ -53,6 +61,7 @@ def build_common_parser() -> ArgumentParser:
metavar="LEFT:BOTTOM:RIGHT:TOP",
action="store",
default=default_config.area,
type=_str_to_bbox,
)
parser.add_argument(
"--polygon",
Expand Down Expand Up @@ -401,8 +410,10 @@ def parse_command_line(sys_args: list[str]) -> tuple[Configuration, list[str]]:
if not os.path.isfile(opts.polygon_file):
print(f"Polygon file '{opts.polygon_file:s}' is not a regular file")
sys.exit(1)
opts.area, opts.polygons = parse_polygons_file(opts.polygon_file)
elif opts.downloadOnly and not opts.area:
area_str, opts.polygons = parse_polygons_file(opts.polygon_file)
opts.area = _str_to_bbox(area_str)

if not opts.area and opts.downloadOnly:
# no area, no polygon, so nothing to download
sys.stderr.write(
"Nothing to download. Combine the --download-only option with"
Expand Down
4 changes: 2 additions & 2 deletions pyhgtmap/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
from pyhgtmap import PolygonsList
from pyhgtmap import BBox, PolygonsList

CONFIG_DIR = str(Path.home() / ".pyhgtmap")
CONFIG_FILENAME = str(Path(CONFIG_DIR, "config.yaml"))
Expand Down Expand Up @@ -48,7 +48,7 @@ def __init__(self, *args, **kwargs) -> None:
# providing typing.
# Sadly some parts have to be duplicated...

area: str | None = None
area: BBox | None = None
polygon_file: str | None = None
polygons: PolygonsList | None = None
downloadOnly: bool = False
Expand Down
7 changes: 5 additions & 2 deletions pyhgtmap/hgt/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -569,15 +569,18 @@ def make_tiles(self, opts: Configuration) -> list[HgtTile]:
step = int(opts.contourStepSize) or 20

def truncate_data(
area: str | None, inputData: numpy.ma.masked_array
area: BBox | None, inputData: numpy.ma.masked_array
) -> tuple[BBox, numpy.ma.masked_array]:
"""truncates a numpy array.
returns (<min lon>, <min lat>, <max lon>, <max lat>) and an array of the
truncated height data.
"""
if area:
bboxMinLon, bboxMinLat, bboxMaxLon, bboxMaxLat = (
float(bound) for bound in area.split(":")
area.min_lon,
area.min_lat,
area.max_lon,
area.max_lat,
)
if self.reverseTransform is not None:
bboxMinLon, bboxMinLat, bboxMaxLon, bboxMaxLat = transform_lon_lats(
Expand Down
5 changes: 1 addition & 4 deletions pyhgtmap/hgt/processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,10 +277,7 @@ def process_files(self, files: list[tuple[str, bool]]) -> None:
raise ValueError("self.options.area is not defined")
self.get_osm_output(
[file_tuple[0] for file_tuple in files],
cast(
"BBox",
[float(b) for b in self.options.area.split(":")],
),
self.options.area,
)

# import objgraph
Expand Down
9 changes: 2 additions & 7 deletions pyhgtmap/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,7 @@ def main_internal(sys_args: list[str]) -> None:
for arg in args
if os.path.splitext(arg)[1].lower() in (".hgt", ".tif", ".tiff", ".vrt")
]
opts.area = ":".join(
[
str(i)
for i in calc_hgt_area(hgtDataFiles, opts.srtmCorrx, opts.srtmCorry)
],
)
opts.area = calc_hgt_area(hgtDataFiles, opts.srtmCorrx, opts.srtmCorry)
# sources are not used in this case
opts.dataSources = []
else:
Expand All @@ -53,7 +48,7 @@ def main_internal(sys_args: list[str]) -> None:
opts,
)
if len(hgtDataFiles) == 0:
print(f"No files for this area {opts.area:s} from desired source(s).")
print(f"No files for this area {opts.area} from desired source(s).")
sys.exit(0)
elif opts.downloadOnly:
sys.exit(0)
Expand Down
8 changes: 2 additions & 6 deletions tests/hgt/test_file.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
from __future__ import annotations

import os
from typing import TYPE_CHECKING

import numpy
import pytest

from pyhgtmap import Coordinates, Polygon, PolygonsList, hgt
from pyhgtmap import BBox, Coordinates, Polygon, PolygonsList, hgt
from pyhgtmap.configuration import Configuration
from pyhgtmap.hgt.file import (
HgtFile,
Expand All @@ -19,9 +18,6 @@
from tests import TEST_DATA_PATH
from tests.hgt import handle_optional_geotiff_support

if TYPE_CHECKING:
from pyhgtmap import BBox

HGT_SIZE: int = 1201


Expand Down Expand Up @@ -98,7 +94,7 @@ def test_make_tiles_chopped() -> None:
def test_make_tiles_chopped_with_area() -> None:
"""Tiles chopped due to nodes threshold and area."""
custom_options = Configuration(
area="6.2:43.1:7.1:43.8",
area=BBox(6.2, 43.1, 7.1, 43.8),
maxNodesPerTile=500000,
contourStepSize=20,
)
Expand Down
32 changes: 31 additions & 1 deletion tests/hgt/test_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ def _test_process_files_single_output(nb_jobs: int, options) -> None:
(os.path.join(TEST_DATA_PATH, "N43E007.hgt"), False),
]
# This is usually done by main() (could be improved)
options.area = "6:43:8:44"
options.area = BBox(6.0, 43.0, 8.0, 44.0)
# Increase step size to speed up test case
options.contourStepSize = 500
# Instrument method without changing its behavior
Expand Down Expand Up @@ -331,6 +331,36 @@ def test_get_osm_output(default_options: Configuration) -> None:
)
assert output1 is not output2

@staticmethod
def test_process_files_single_output_bbox_type(
default_options: Configuration,
) -> None:
"""Regression test: process_files must pass a BBox (not a plain list) to
get_osm_output in single output mode.

Previously, typing.cast() was used instead of BBox(), leaving the area
string parsed as a plain list at runtime, which caused AttributeError
when make_bounds_tag() accessed bbox.min_lat (XML / o5m output paths).
"""
default_options.maxNodesPerTile = 0
default_options.area = BBox(6.0, 43.0, 8.0, 44.0)
processor = HgtFilesProcessor(
1,
node_start_id=100,
way_start_id=200,
options=default_options,
)
with mock.patch("pyhgtmap.hgt.processor.get_osm_output") as get_osm_output_mock:
get_osm_output_mock.return_value = Mock()
processor.process_files([]) # empty file list — only initializes output

get_osm_output_mock.assert_called_once()
bbox_arg = get_osm_output_mock.call_args[0][2]
assert isinstance(bbox_arg, BBox), f"Expected BBox, got {type(bbox_arg)}"
assert bbox_arg == BBox(
min_lon=6.0, min_lat=43.0, max_lon=8.0, max_lat=44.0
)

@staticmethod
def test_get_osm_output_single_output(default_options: Configuration) -> None:
# Enable single output mode
Expand Down
Loading
Loading