From 819a53a1591e2939bc7edf6d5832890a6f144345 Mon Sep 17 00:00:00 2001 From: hbrain Date: Sun, 31 May 2026 01:46:29 +0200 Subject: [PATCH] Fix gpsmap.py to correctly use output file path --- bin/gpsmap.py | 113 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 82 insertions(+), 31 deletions(-) diff --git a/bin/gpsmap.py b/bin/gpsmap.py index f18820d..15171f6 100644 --- a/bin/gpsmap.py +++ b/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,14 +600,15 @@ 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")) - stats_parts.append(f"{distance} km route") + if len(clusters) > 1 and distance > 0: + stats_parts.append(f"{distance} km route") stats = " • ".join(stats_parts) date_text = format_date_range(items) print(f"Map label: {label}")