Fix gpsmap.py to correctly use output file path
This commit is contained in:
parent
68301c6d23
commit
819a53a159
1 changed files with 82 additions and 31 deletions
111
bin/gpsmap.py
111
bin/gpsmap.py
|
|
@ -15,7 +15,7 @@ from urllib.request import Request, urlopen
|
|||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
|
||||
VERSION = "2026-05-31-infographic-v4-date-stats-centered-counts"
|
||||
VERSION = "2026-05-31-infographic-v5-single-location-footer-fix"
|
||||
|
||||
MAP_WIDTH = 906
|
||||
MAP_HEIGHT = 510
|
||||
|
|
@ -23,6 +23,8 @@ TILE_SIZE = 256
|
|||
MIN_ZOOM = 3
|
||||
MAX_ZOOM = 13
|
||||
CLUSTER_RADIUS_KM = 2.0
|
||||
FOOTER_HEIGHT = 118
|
||||
FOOTER_MARGIN = 14
|
||||
USER_AGENT = "gpsmap-infographic-script/1.0"
|
||||
|
||||
PHOTO_EXTENSIONS = {
|
||||
|
|
@ -244,6 +246,9 @@ def reverse_geocode_parts(lat, lon):
|
|||
address.get("city")
|
||||
or address.get("town")
|
||||
or address.get("village")
|
||||
or address.get("island")
|
||||
or address.get("suburb")
|
||||
or address.get("hamlet")
|
||||
or address.get("municipality")
|
||||
or ""
|
||||
)
|
||||
|
|
@ -261,10 +266,41 @@ def reverse_geocode_parts(lat, lon):
|
|||
return {"city": "", "region": "", "country": ""}
|
||||
|
||||
|
||||
def make_map_label(items):
|
||||
def clean_place_name(value):
|
||||
value = (value or "").strip()
|
||||
|
||||
# Shorter Danish names look much better in the footer.
|
||||
replacements = {
|
||||
"Region Syddanmark": "Syddanmark",
|
||||
"Region Midtjylland": "Midtjylland",
|
||||
"Region Nordjylland": "Nordjylland",
|
||||
"Region Sjælland": "Sjælland",
|
||||
"Region Hovedstaden": "Hovedstaden",
|
||||
}
|
||||
|
||||
return replacements.get(value, value)
|
||||
|
||||
|
||||
def make_map_label(items, clusters=None):
|
||||
lats = [i.lat for i in items]
|
||||
lons = [i.lon for i in items]
|
||||
|
||||
# If this is a one-location map, use the center point and keep the label short.
|
||||
# Example: Rømø DANMARK instead of Tønder Kommune Region Syddanmark DANMARK.
|
||||
if clusters is not None and len(clusters) == 1:
|
||||
lat = sum(lats) / len(lats)
|
||||
lon = sum(lons) / len(lons)
|
||||
info = reverse_geocode_parts(lat, lon)
|
||||
city = clean_place_name(info.get("city", ""))
|
||||
region = clean_place_name(info.get("region", ""))
|
||||
country = clean_place_name(info.get("country", ""))
|
||||
|
||||
if city and country:
|
||||
return f"{city} {country}"
|
||||
if region and country:
|
||||
return f"{region} {country}"
|
||||
return country
|
||||
|
||||
sample_points = [
|
||||
(min(lats), min(lons)),
|
||||
(max(lats), max(lons)),
|
||||
|
|
@ -275,12 +311,16 @@ def make_map_label(items):
|
|||
|
||||
for lat, lon in sample_points:
|
||||
info = reverse_geocode_parts(lat, lon)
|
||||
if info["city"]:
|
||||
cities.add(info["city"])
|
||||
if info["region"]:
|
||||
regions.add(info["region"])
|
||||
if info["country"]:
|
||||
countries.add(info["country"])
|
||||
city = clean_place_name(info.get("city", ""))
|
||||
region = clean_place_name(info.get("region", ""))
|
||||
country = clean_place_name(info.get("country", ""))
|
||||
|
||||
if city:
|
||||
cities.add(city)
|
||||
if region:
|
||||
regions.add(region)
|
||||
if country:
|
||||
countries.add(country)
|
||||
|
||||
if len(countries) > 1:
|
||||
return " • ".join(sorted(countries))
|
||||
|
|
@ -303,7 +343,6 @@ def make_map_label(items):
|
|||
return f"{region} {country}"
|
||||
return country
|
||||
|
||||
|
||||
def format_date_range(items):
|
||||
dates = [i.taken_at for i in items if i.taken_at]
|
||||
if not dates:
|
||||
|
|
@ -330,10 +369,9 @@ def latlon_to_pixel(lat, lon, zoom):
|
|||
|
||||
|
||||
def choose_zoom(points, width, height):
|
||||
# Leave room for bottom title/stat text.
|
||||
pad_x = 80
|
||||
pad_top = 55
|
||||
pad_bottom = 125
|
||||
pad_x = 70
|
||||
pad_top = 45
|
||||
pad_bottom = FOOTER_HEIGHT + FOOTER_MARGIN
|
||||
usable_width = width - 2 * pad_x
|
||||
usable_height = height - pad_top - pad_bottom
|
||||
|
||||
|
|
@ -350,22 +388,25 @@ def choose_zoom(points, width, height):
|
|||
|
||||
return MIN_ZOOM
|
||||
|
||||
|
||||
def map_view(points, zoom):
|
||||
pixels = [latlon_to_pixel(lat, lon, zoom) for lat, lon in points]
|
||||
xs = [p[0] for p in pixels]
|
||||
ys = [p[1] for p in pixels]
|
||||
|
||||
# Center route slightly above image center so title/stat text has room at bottom.
|
||||
center_x = (min(xs) + max(xs)) / 2
|
||||
center_y = (min(ys) + max(ys)) / 2 + 25
|
||||
center_y = (min(ys) + max(ys)) / 2
|
||||
|
||||
top_left_x = center_x - MAP_WIDTH / 2
|
||||
top_left_y = center_y - MAP_HEIGHT / 2
|
||||
# Keep the route/markers in the upper map area and reserve the lower footer
|
||||
# for date, statistics and location. This prevents single-location maps from
|
||||
# having the marker collide with the footer.
|
||||
usable_center_x = MAP_WIDTH / 2
|
||||
usable_center_y = (MAP_HEIGHT - FOOTER_HEIGHT) / 2
|
||||
|
||||
top_left_x = center_x - usable_center_x
|
||||
top_left_y = center_y - usable_center_y
|
||||
|
||||
return top_left_x, top_left_y
|
||||
|
||||
|
||||
def download_tile(x, y, z):
|
||||
url = f"https://tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
request = Request(url, headers={"User-Agent": USER_AGENT})
|
||||
|
|
@ -491,10 +532,21 @@ def draw_centered_text(draw, y, text, font, fill="black"):
|
|||
draw.text(((MAP_WIDTH - w) / 2, y), text, font=font, fill=fill)
|
||||
|
||||
|
||||
def fit_font(draw, text, font_path, start_size, max_width, min_size=12):
|
||||
for size in range(start_size, min_size - 1, -1):
|
||||
font = load_font(font_path, size)
|
||||
bbox = draw.textbbox((0, 0), text, font=font)
|
||||
if bbox[2] - bbox[0] <= max_width:
|
||||
return font
|
||||
return load_font(font_path, min_size)
|
||||
|
||||
|
||||
def draw_bottom_title(draw, label, stats, date_text=""):
|
||||
title_font = load_font(FONT_BOLD, 38)
|
||||
stats_font = load_font(FONT_REGULAR, 30)
|
||||
date_font = load_font(FONT_REGULAR, 22)
|
||||
max_text_width = MAP_WIDTH - 28
|
||||
|
||||
title_font = fit_font(draw, label, FONT_BOLD, 26, max_text_width, min_size=20)
|
||||
stats_font = fit_font(draw, stats, FONT_REGULAR, 26, max_text_width, min_size=16)
|
||||
date_font = fit_font(draw, date_text, FONT_REGULAR, 22, max_text_width, min_size=14) if date_text else None
|
||||
|
||||
title_bbox = draw.textbbox((0, 0), label, font=title_font)
|
||||
stats_bbox = draw.textbbox((0, 0), stats, font=stats_font)
|
||||
|
|
@ -504,8 +556,8 @@ def draw_bottom_title(draw, label, stats, date_text=""):
|
|||
stats_h = stats_bbox[3] - stats_bbox[1]
|
||||
date_h = date_bbox[3] - date_bbox[1] if date_text else 0
|
||||
|
||||
gap = 4
|
||||
bottom_margin = 20
|
||||
gap = 3
|
||||
bottom_margin = 14
|
||||
|
||||
y_title = MAP_HEIGHT - title_h - bottom_margin
|
||||
y_stats = y_title - stats_h - gap
|
||||
|
|
@ -516,7 +568,6 @@ def draw_bottom_title(draw, label, stats, date_text=""):
|
|||
draw_centered_text(draw, y_stats, stats, stats_font, fill=(30, 30, 30))
|
||||
draw_centered_text(draw, y_title, label, title_font, fill="black")
|
||||
|
||||
|
||||
def main():
|
||||
print(f"gpsmap version: {VERSION}")
|
||||
|
||||
|
|
@ -525,19 +576,18 @@ def main():
|
|||
sys.exit(1)
|
||||
|
||||
media_dir = Path(sys.argv[1])
|
||||
output_dir = Path(sys.argv[2])
|
||||
output_arg = Path(sys.argv[2])
|
||||
|
||||
if not media_dir.is_dir():
|
||||
print(f"Media directory not found: {media_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
output_arg = output_dir
|
||||
if output_arg.suffix == ".png":
|
||||
output_png = output_arg
|
||||
output_png.parent.mkdir(parents=True, exist_ok=True)
|
||||
else:
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
output_png = output_dir / f"{media_dir.resolve().name}.png"
|
||||
output_arg.mkdir(parents=True, exist_ok=True)
|
||||
output_png = output_arg / f"{media_dir.resolve().name}.png"
|
||||
|
||||
items = collect_media(media_dir)
|
||||
if not items:
|
||||
|
|
@ -550,13 +600,14 @@ def main():
|
|||
zoom = choose_zoom(points, MAP_WIDTH, MAP_HEIGHT)
|
||||
top_left_x, top_left_y = map_view(points, zoom)
|
||||
|
||||
label = make_map_label(items)
|
||||
label = make_map_label(items, clusters)
|
||||
photo_count = sum(1 for i in items if i.kind == "photo")
|
||||
video_count = sum(1 for i in items if i.kind == "video")
|
||||
distance = round(route_distance_km(clusters))
|
||||
stats_parts = [plural(photo_count, "photo")]
|
||||
if video_count:
|
||||
stats_parts.append(plural(video_count, "video"))
|
||||
if len(clusters) > 1 and distance > 0:
|
||||
stats_parts.append(f"{distance} km route")
|
||||
stats = " • ".join(stats_parts)
|
||||
date_text = format_date_range(items)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue