diff --git a/skills/aaif-create-chapter/scripts/create_chapter.py b/skills/aaif-create-chapter/scripts/create_chapter.py index f938203..7d1974a 100755 --- a/skills/aaif-create-chapter/scripts/create_chapter.py +++ b/skills/aaif-create-chapter/scripts/create_chapter.py @@ -215,8 +215,12 @@ def lat2y(lat): # are dragged WEST to nearly Tokyo's x, so a separable lon/lat projection can't # place East-Asia/Oceania. (Tokyo itself IS placed correctly by the linear # formula and needs no override — it's only the landmark showing how far west the -# others land.) Keep a per-city pixel override table for the cities that need it. -PIXEL_OVERRIDES = {"Seoul": (822, 305), "Sydney": (870, 512), "Melbourne": (836, 543)} +# others land.) Separately, Shanghai (~121°E) IS placed almost correctly by the +# linear formula but lands ~8px offshore; its override only nudges it west onto the +# Chinese coast — a small cosmetic fix, not the gross Oceania distortion above. +# Keep a per-city pixel override table for the cities that need it. +PIXEL_OVERRIDES = {"Seoul": (822, 305), "Sydney": (870, 512), "Melbourne": (836, 543), + "Shanghai": (833, 330)} def project_city(name, lat, lon): """Map a city to (x, y) pixels on image18.png. Overridden cities take their @@ -338,6 +342,16 @@ def resolve_latlon(name, lat, lon): "value and geocoding %r instead." % name) return geocode_city(name) +def map_dot_latlon(name, lat, lon): + """Coordinates to feed the slide-5 map-dot placement, or None to leave the dot + at San Francisco. Fixed-pixel-override cities are placed by NAME (project_city + ignores lat/lon for them), so they're placeable WITHOUT geocoding: return a + sentinel instead of gating them on a network lookup that can't change where the + dot lands. Explicit --lat/--lon still short-circuit to resolve_latlon.""" + if name in PIXEL_OVERRIDES and not (lat is not None and lon is not None): + return (0.0, 0.0) # value unused by project_city; just marks the dot placeable + return resolve_latlon(name, lat, lon) + # ---------------------------------------------------------------------------- # Drive helpers (via the gws CLI) # ---------------------------------------------------------------------------- @@ -478,11 +492,20 @@ def main(): print("Upper: %s" % upper) print("Slug : aaif-%s -> https://luma.com/aaif-%s" % (slug, slug)) - # Coordinates for the slide-5 network-map dot (override -> geocode -> none). - latlon = resolve_latlon(name, a.lat, a.lon) - if latlon: + # Coordinates for the slide-5 network-map dot (explicit -> pixel-override + # sentinel -> geocode -> none). Fixed-pixel cities place by name, so they never + # get the "stays at San Francisco" warning just because geocoding was skipped. + pixel_override = name in PIXEL_OVERRIDES + latlon = map_dot_latlon(name, a.lat, a.lon) + if pixel_override and not (a.lat is not None and a.lon is not None): + print("Coords: -- (fixed pixel override for %s; lat/lon not needed)" % name) + elif latlon: src = "override" if (a.lat is not None and a.lon is not None) else "geocoded" print("Coords: %.4f, %.4f (%s)" % (latlon[0], latlon[1], src)) + if pixel_override: + print("Note: %s has a fixed pixel override for the slide-5 map dot; the " + "coordinates above do NOT position it (--lat/--lon ignored for " + "placement)." % name) else: print("Coords: --") print("WARNING: could not resolve coordinates for %r; the slide-5 map dot " diff --git a/skills/aaif-create-chapter/scripts/test_create_chapter.py b/skills/aaif-create-chapter/scripts/test_create_chapter.py index dd6641d..3eef8c8 100644 --- a/skills/aaif-create-chapter/scripts/test_create_chapter.py +++ b/skills/aaif-create-chapter/scripts/test_create_chapter.py @@ -102,6 +102,14 @@ def test_pixel_override_wins_over_formula(self): self.assertEqual(cc.project_city("Seoul", 0.0, 0.0), (822, 305)) self.assertEqual(cc.project_city("Sydney", 99.0, 99.0), (870, 512)) + def test_every_override_pixel_is_within_the_map(self): + # A transposed or fat-fingered override (e.g. (3833, 330)) would place the + # dot off-canvas and be placed silently. Guard the whole table at once. + w, h = cc.MAP_PX + for city, (x, y) in cc.PIXEL_OVERRIDES.items(): + self.assertTrue(0 <= x <= w, "%s x=%d out of 0..%d" % (city, x, w)) + self.assertTrue(0 <= y <= h, "%s y=%d out of 0..%d" % (city, y, h)) + def test_lat2y_hits_every_anchor_exactly(self): for lat, y in cc.LAT_ANCHORS: self.assertAlmostEqual(cc.lat2y(lat), y, places=6, msg="anchor %s" % lat) @@ -230,6 +238,33 @@ def test_ungeocodable_returns_none(self): cc.geocode_city = orig +class TestMapDotLatlon(unittest.TestCase): + def test_override_city_is_placeable_without_geocoding(self): + # An override city (placed by name) must stay placeable even if geocoding + # would fail — and must NOT hit the network to decide that. + calls = [] + orig = cc.geocode_city + cc.geocode_city = lambda name, **kw: calls.append(name) or None + try: + self.assertIsNotNone(cc.map_dot_latlon("Shanghai", None, None)) + self.assertEqual(calls, []) # no geocode call for a fixed-pixel city + finally: + cc.geocode_city = orig + + def test_non_override_ungeocodable_still_none(self): + orig = cc.geocode_city + cc.geocode_city = lambda name, **kw: None + try: + self.assertIsNone(cc.map_dot_latlon("Tatooine", None, None)) + finally: + cc.geocode_city = orig + + def test_explicit_latlon_wins_even_for_override_city(self): + # Passing both coords short-circuits to resolve_latlon (no network), so the + # user's values flow through unchanged (project_city still ignores them). + self.assertEqual(cc.map_dot_latlon("Shanghai", 31.23, 121.47), (31.23, 121.47)) + + class _FakeResp: def __init__(self, body): self._body = body.encode("utf-8") if isinstance(body, str) else body