Flatten Arma tiles over callback provider
This commit is contained in:
+38
-13
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user