Flatten Arma tiles over callback provider

This commit is contained in:
2026-07-25 18:19:00 -03:00
parent dd987de699
commit dd759dba74
4 changed files with 43 additions and 32 deletions
+38 -13
View File
@@ -15,8 +15,10 @@ import tempfile
import uuid
import zipfile
from datetime import UTC, datetime
from io import BytesIO
from pathlib import Path
from urllib.parse import unquote, urlparse
from urllib.request import Request, urlopen
from xml.etree import ElementTree as ET
from PIL import Image
@@ -346,7 +348,34 @@ def alpha_is_empty(image: Image.Image) -> bool:
return image.getchannel("A").getbbox() is None
def compose_layer(source_tiles: Path, final_tiles: Path) -> None:
def callback_background(relative: Path, provider, cache_root: Path) -> Image.Image:
"""Return a cached callback tile, or an opaque white tile when disabled."""
if provider is None:
return Image.new("RGBA", (256, 256), (255, 255, 255, 255))
z, x, filename = relative.parts
y = filename.removesuffix(".png")
cached = cache_root / z / x / filename
if cached.exists():
with Image.open(cached) as opened:
image = opened.convert("RGBA")
image.load()
return image
url = provider.origin + provider.path_template.replace("{z}", z).replace("{x}", x).replace("{y}", y)
try:
request = Request(url, headers={"User-Agent": "arma-tiles-builder/1.0"})
with urlopen(request, timeout=30) as response:
payload = response.read()
with Image.open(BytesIO(payload)) as opened:
image = opened.convert("RGBA")
image.load()
except Exception as error:
raise SourceError(f"callback provider falhou para {z}/{x}/{y}: {error}") from error
cached.parent.mkdir(parents=True, exist_ok=True)
image.save(cached, format="PNG", optimize=False)
return image
def compose_layer(source_tiles: Path, final_tiles: Path, provider, callback_cache: Path) -> None:
for tile in sorted(source_tiles.rglob("*.png")):
relative = tile.relative_to(source_tiles)
destination = final_tiles / relative
@@ -360,21 +389,17 @@ def compose_layer(source_tiles: Path, final_tiles: Path) -> None:
with Image.open(destination) as opened:
base = opened.convert("RGBA")
base.load()
base.alpha_composite(layer)
result = base
else:
result = layer
if alpha_is_empty(result):
destination.unlink(missing_ok=True)
else:
result.save(destination, format="PNG", optimize=False)
base = callback_background(relative, provider, callback_cache)
base.alpha_composite(layer)
base.save(destination, format="PNG", optimize=False)
def write_empty_tile(tiles_dir: Path) -> None:
Image.new("RGBA", (256, 256), (255, 255, 255, 255)).save(tiles_dir / "empty.png", format="PNG", optimize=False)
def tile_overlay(overlay: dict, work_root: Path, final_tiles: Path, processes: int) -> None:
def tile_overlay(overlay: dict, work_root: Path, final_tiles: Path, processes: int, provider, callback_cache: Path) -> None:
layer_root = work_root / f"layer-{overlay['priority']:04d}-{overlay['index']}"
help_result = subprocess.run(["gdal2tiles.py", "--help"], text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
legacy = ["--legacy"] if "--legacy" in help_result.stdout else []
@@ -382,7 +407,7 @@ def tile_overlay(overlay: dict, work_root: Path, final_tiles: Path, processes: i
"gdal2tiles.py", *legacy, "-q", "--profile=mercator", "--xyz", "--exclude", "--webviewer=none", "--resampling=bilinear",
f"--zoom={overlay['minzoom']}-{overlay['maxzoom']}", f"--processes={processes}", overlay["warped"], str(layer_root),
])
compose_layer(layer_root, final_tiles)
compose_layer(layer_root, final_tiles, provider, callback_cache)
shutil.rmtree(layer_root, ignore_errors=True)
@@ -431,7 +456,7 @@ def source_record(descriptor: dict, config: dict) -> dict:
def build(arguments: argparse.Namespace) -> None:
# Validate before reading or warping any source, so a malformed provider never wastes a full build.
validate_callback_provider(os.environ.get("CALLBACK_PROVIDER"))
provider = validate_callback_provider(os.environ.get("CALLBACK_PROVIDER"))
source_dir = Path(arguments.source_dir).resolve()
data_dir = Path(arguments.data_dir).resolve()
tiles_dir = Path(arguments.tiles_dir).resolve()
@@ -476,7 +501,7 @@ def build(arguments: argparse.Namespace) -> None:
# alphabetical winner is painted last.
for overlay in sorted(overlays, key=lambda item: (item["area_m2"], item["source"], item["index"]), reverse=True):
try:
tile_overlay(overlay, work_root, stage, arguments.processes)
tile_overlay(overlay, work_root, stage, arguments.processes, provider, cache_root / "callback-provider")
except SourceError as error:
errors.append({"source": overlay["source"], "overlay": overlay["index"], "stage": "tile", "reason": str(error)})
global_min = min((item["minzoom"] for item in overlays), default=None)
@@ -486,7 +511,7 @@ def build(arguments: argparse.Namespace) -> None:
"generated_at": datetime.now(UTC).isoformat(),
"minzoom": global_min,
"maxzoom": global_max,
"tile_background": "transparent",
"tile_background": "callback_provider" if provider else "white",
"processed_sources": records,
"sources_with_error": errors,
"source_count": len(records),