#!/usr/bin/env python3.12
# -*- coding: utf-8 -*-
"""
Genere la carte pleine page du carnet 2 (Japon entier + encarts Kansai / Kanto)
et la re-injecte dans carnets/carnet-2-roadbook.html.

    python3.12 carnets/outils/carte-japon.py

Fond de carte : contours officiels des prefectures japonaises (domaine public),
telecharges au 1er lancement dans carnets/outils/.cache/ (13 Mo, non versionne).
Source : https://github.com/dataofjapan/land
"""
import json, math, os, io, urllib.request

HERE  = os.path.dirname(os.path.abspath(__file__))
CACHE = os.path.join(HERE, ".cache")
GEO   = os.path.join(CACHE, "japan.geojson")
URL   = "https://raw.githubusercontent.com/dataofjapan/land/master/japan.geojson"
HTML  = os.path.normpath(os.path.join(HERE, "..", "carnet-2-roadbook.html"))

# ---- emprise : archipel principal (exclut Okinawa, Ogasawara, Minamitorishima)
LON0, LON1, LAT0, LAT1 = 128.3, 146.3, 30.8, 45.7
VBW = 1000.0
TOL, MIN_AREA = 1.0, 6.0            # simplification Douglas-Peucker / seuil micro-iles

C = {"osaka":"#d33b2c","koya":"#6b4a8f","hiro":"#2f6f8f","kyoto":"#b0472e",
     "kana":"#2d7d6b","hako":"#37619e","tokyo":"#444a54"}
INK, SOFT, SEA = "#1a1c1f", "#4a4f57", "#2f6f8f"
POI = {"osaka":(34.666,135.501),"koyasan":(34.213,135.584),"hiroshima":(34.397,132.459),
       "kyoto":(35.011,135.768),"kanazawa":(36.578,136.648),"hakone":(35.243,139.043),
       "tokyo":(35.712,139.777),"nara":(34.685,135.805),"miyajima":(34.296,132.320),
       "shirakawago":(36.257,136.906),"nikko":(36.758,139.599),"fuji":(35.361,138.728),
       "kix":(34.432,135.230),"hnd":(35.549,139.780)}
ETAPES = [(1,"osaka","osaka"),(2,"koyasan","koya"),(3,"hiroshima","hiro"),(4,"kyoto","kyoto"),
          (5,"kanazawa","kana"),(6,"hakone","hako"),(7,"tokyo","tokyo")]
ORDER  = [e[1] for e in ETAPES]

# =============================================================== projection
def merc(lon, lat):
    return math.radians(lon), math.log(math.tan(math.pi/4 + math.radians(lat)/2))
X0, Y0 = merc(LON0, LAT0); X1, Y1 = merc(LON1, LAT1)
SCALE  = VBW / (X1 - X0); VBH = (Y1 - Y0) * SCALE
def proj(lon, lat):
    x, y = merc(lon, lat); return ((x - X0) * SCALE, (Y1 - y) * SCALE)

def dp(pts, tol):
    if len(pts) < 3: return pts
    keep = [False]*len(pts); keep[0] = keep[-1] = True; stack = [(0, len(pts)-1)]
    while stack:
        i, j = stack.pop()
        if j <= i+1: continue
        ax, ay = pts[i]; bx, by = pts[j]; dx, dy = bx-ax, by-ay
        nrm = math.hypot(dx, dy); best, bi = -1.0, -1
        for k in range(i+1, j):
            px, py = pts[k]
            d = math.hypot(px-ax, py-ay) if nrm == 0 else abs(dy*px - dx*py + bx*ay - by*ax)/nrm
            if d > best: best, bi = d, k
        if best > tol:
            keep[bi] = True; stack += [(i, bi), (bi, j)]
    return [p for p, k in zip(pts, keep) if k]

def ring_area(r):
    return abs(sum(r[i][0]*r[i+1][1] - r[i+1][0]*r[i][1] for i in range(len(r)-1)))/2

def land_path():
    if not os.path.exists(GEO):
        os.makedirs(CACHE, exist_ok=True)
        print("telechargement du fond de carte (13 Mo)...")
        urllib.request.urlretrieve(URL, GEO)
    d = json.load(open(GEO)); out = []
    for f in d["features"]:
        if f["properties"]["id"] == 47: continue          # Okinawa
        g = f["geometry"]
        polys = g["coordinates"] if g["type"] == "MultiPolygon" else [g["coordinates"]]
        for poly in polys:
            ring = poly[0]
            cx = sum(p[0] for p in ring)/len(ring); cy = sum(p[1] for p in ring)/len(ring)
            if not (LON0 <= cx <= LON1 and LAT0 <= cy <= LAT1): continue
            pr = [proj(lo, la) for lo, la in ring]
            if ring_area(pr) < MIN_AREA: continue
            s = dp(pr, TOL)
            if len(s) >= 4:
                out.append("M%.1f %.1f"%s[0] + "".join("L%.1f %.1f"%p for p in s[1:]) + "Z")
    return "".join(out), len(out)

LAND, NISL = land_path()
P = {k: proj(lo, la) for k, (la, lo) in POI.items()}

# =============================================================== dessin
out = []; A = out.append
def lab(x, y, t, size=15, anchor="middle", fill=INK, weight="600", style="", cls="lb"):
    A('<text class="%s" x="%.1f" y="%.1f" text-anchor="%s" font-size="%.1f" fill="%s" font-weight="%s"%s>%s</text>'
      % (cls, x, y, anchor, size, fill, weight,
         (' font-style="%s"' % style) if style else "", t.replace("&", "&amp;").replace("<", "&lt;")))
def leg(a, b, bow):
    ax, ay = P[a]; bx, by = P[b]; mx, my = (ax+bx)/2, (ay+by)/2
    dx, dy = bx-ax, by-ay; L = math.hypot(dx, dy) or 1
    return "M%.1f %.1f Q%.1f %.1f %.1f %.1f" % (ax, ay, mx-dy/L*bow, my+dx/L*bow, bx, by)
ROUTE = [leg(ORDER[i], ORDER[i+1], b) for i, b in enumerate([0, 34, -30, 10, 14, 0])]

def route_paths(k=1.0, op=1.0):
    for d in ROUTE:
        A('<path d="%s" fill="none" stroke="#fff" stroke-width="%.2f" stroke-linecap="round" opacity=".9"/>' % (d, 7/k))
    for i, d in enumerate(ROUTE):
        A('<path d="%s" fill="none" stroke="#b8443a" stroke-width="%.2f" stroke-linecap="round" opacity="%.2f"%s/>'
          % (d, 3/k, op, ' marker-end="url(#ar)"' if (i in (1, 2, 4) and k == 1.0) else ''))
def pastille(x, y, n, col, r=13):
    A('<circle cx="%.1f" cy="%.1f" r="%.1f" fill="#fff"/>' % (x, y, r+2.5))
    A('<circle cx="%.1f" cy="%.1f" r="%.1f" fill="%s"/>' % (x, y, r, col))
    A('<text x="%.1f" y="%.1f" text-anchor="middle" font-size="%.1f" fill="#fff" font-weight="700"'
      ' font-family="Avenir Next,Segoe UI,system-ui,Helvetica,Arial,sans-serif">%d</text>' % (x, y+r*.36, r*1.15, n))
def petit(x, y, col="#5d646d", r=6):
    A('<circle cx="%.1f" cy="%.1f" r="%.1f" fill="#fff"/>' % (x, y, r+2.2))
    A('<circle cx="%.1f" cy="%.1f" r="%.1f" fill="%s"/>' % (x, y, r, col))
def leader(x0, y0, x1, y1):
    A('<path d="M%.1f %.1f L%.1f %.1f" fill="none" stroke="%s" stroke-width="1.3" opacity=".8"/>' % (x0, y0, x1, y1, SOFT))

A('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 %.1f %.1f" role="img"'
  ' aria-label="Carte du Japon : itineraire Osaka vers Tokyo en 7 etapes">' % (VBW, VBH))
A('<defs><path id="jp" d="%s"/>' % LAND)
A('<clipPath id="ck"><rect x="45" y="128" width="386" height="432" rx="8"/></clipPath>')
A('<clipPath id="cq"><rect x="628" y="818" width="348" height="228" rx="8"/></clipPath>')
A('<marker id="ar" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="6" markerHeight="6" orient="auto">'
  '<path d="M0.5,1 L9,5 L0.5,9" fill="none" stroke="#b8443a" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></marker></defs>')
A('<style>.lb{font-family:"Avenir Next",Avenir,"Segoe UI",system-ui,-apple-system,Helvetica,Arial,sans-serif;'
  'paint-order:stroke fill;stroke:#fff;stroke-width:3.4;stroke-linejoin:round}'
  '.sm{stroke-width:2.6}.sea{font-family:Georgia,"Times New Roman",serif;letter-spacing:2px;stroke-width:0}</style>')
A('<rect width="%.1f" height="%.1f" fill="#eef4f7"/>' % (VBW, VBH))
A('<use href="#jp" fill="#e7e4da" stroke="#b9b5a8" stroke-width="1.1" stroke-linejoin="round"/>')
lab(196, 262, "MER DU JAPON", 19, "middle", "#9db8c6", "400", cls="lb sea")
lab(838, 632, "OCÉAN PACIFIQUE", 19, "middle", "#9db8c6", "400", cls="lb sea")

def zbox(keys, pad=16):
    xs = [P[k][0] for k in keys]; ys = [P[k][1] for k in keys]
    x0, y0, x1, y1 = min(xs)-pad, min(ys)-pad, max(xs)+pad, max(ys)+pad
    A('<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" rx="5" fill="none" stroke="%s"'
      ' stroke-width="2" stroke-dasharray="7 5"/>' % (x0, y0, x1-x0, y1-y0, SEA))
    return x0, y0, x1, y1
def funnel(a, b):
    A('<path d="M%.1f %.1f L%.1f %.1f" fill="none" stroke="%s" stroke-width="1.3" stroke-dasharray="6 5" opacity=".7"/>'
      % (a[0], a[1], b[0], b[1], SEA))
KX0, KY0, KX1, KY1 = zbox(["kix","osaka","nara","koyasan","kyoto"])
funnel((KX0, KY0), (45, 560)); funnel((KX1, KY0), (431, 560))
QX0, QY0, QX1, QY1 = zbox(["fuji","hakone","tokyo","hnd"])
funnel((QX0, QY1), (628, 818)); funnel((QX1, QY1), (976, 818))
route_paths()

hx, hy = P["hiroshima"]; pastille(hx, hy, 3, C["hiro"])
leader(hx-14, hy+5, hx-70, hy+42)
lab(hx-74, hy+46, "Hiroshima", 17, "end", C["hiro"], "700")
lab(hx-74, hy+66, "+ Miyajima · 24 oct", 13, "end", SOFT, "500", cls="lb sm")
kx, ky = P["kanazawa"]; pastille(kx, ky, 5, C["kana"])
leader(kx-14, ky-6, kx-84, ky-46); lab(kx-88, ky-50, "Kanazawa", 17, "end", C["kana"], "700")
sx, sy = P["shirakawago"]; petit(sx, sy, C["kana"], 6)
leader(sx+8, sy+5, sx+50, sy+36)
lab(sx+54, sy+40, "Shirakawa-gō", 13, "start", SOFT, "600", cls="lb sm")
lab(sx+54, sy+58, "29 oct", 12, "start", SOFT, "400", "italic", cls="lb sm")
nx, ny = P["nikko"]; petit(nx, ny, C["tokyo"], 6)
leader(nx+8, ny-5, nx+56, ny-36)
lab(nx+60, ny-40, "Nikkō", 13, "start", SOFT, "600", cls="lb sm")
lab(nx+60, ny-22, "3 nov", 12, "start", SOFT, "400", "italic", cls="lb sm")
for n, k, ck in ETAPES:
    if k in ("osaka","koyasan","kyoto","hakone","tokyo"):
        x, y = P[k]; pastille(x, y, n, C[ck], 10)

def encart(fx, fy, fw, fh, clip, keys, titre, sous, padx, pady, draw):
    xs = [P[k][0] for k in keys]; ys = [P[k][1] for k in keys]
    bw, bh = max(xs)-min(xs), max(ys)-min(ys)
    k = min((fw-2*padx)/max(bw, 1), (fh-2*pady)/max(bh, 1))
    cx, cy = (max(xs)+min(xs))/2, (max(ys)+min(ys))/2
    tx, ty = fx+fw/2-cx*k, fy+fh/2-cy*k
    T = lambda key: (P[key][0]*k+tx, P[key][1]*k+ty)
    A('<g clip-path="url(#%s)">' % clip)
    A('<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" fill="#f5f9fb"/>' % (fx, fy, fw, fh))
    A('<g transform="translate(%.2f %.2f) scale(%.4f)">' % (tx, ty, k))
    A('<use href="#jp" fill="#e7e4da" stroke="#b9b5a8" stroke-width="%.3f" stroke-linejoin="round"/>' % (1.1/k))
    route_paths(k, .9); A('</g>'); draw(T); A('</g>')
    A('<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" rx="8" fill="none" stroke="%s" stroke-width="2"/>' % (fx, fy, fw, fh, SEA))
    A('<path d="M%.1f %.1f h%.1f v18 a8 8 0 0 1 -8 8 h-%.1f a8 8 0 0 1 -8 -8 Z" fill="%s"/>' % (fx+8, fy, fw-16, fw-32, SEA))
    A('<rect x="%.1f" y="%.1f" width="%.1f" height="18" rx="8" fill="%s"/>' % (fx, fy, fw, SEA))
    lab(fx+12, fy+19, titre, 15, "start", "#fff", "700")
    lab(fx+fw-12, fy+18, sous, 12, "end", "#cfe3ec", "500")
    return k

def kansai(T):
    x, y = T("kix");     petit(x, y, "#5d646d", 7)
    lab(x, y+32, "✈ KIX", 14, "middle", SOFT, "600", cls="lb sm")
    lab(x, y+49, "19 oct", 12, "middle", SOFT, "400", "italic", cls="lb sm")
    x, y = T("kyoto");   pastille(x, y, 4, C["kyoto"], 15); lab(x+27, y+7, "Kyoto", 19, "start", C["kyoto"], "700")
    x, y = T("nara");    petit(x, y, "#5d646d", 7)
    lab(x+17, y+1, "Nara", 14, "start", SOFT, "600", cls="lb sm")
    lab(x+17, y+18, "21 oct", 12, "start", SOFT, "400", "italic", cls="lb sm")
    x, y = T("osaka");   pastille(x, y, 1, C["osaka"], 15); lab(x-27, y+7, "Osaka", 19, "end", C["osaka"], "700")
    x, y = T("koyasan"); pastille(x, y, 2, C["koya"], 15);  lab(x, y+40, "Koyasan", 19, "middle", C["koya"], "700")
def kanto(T):
    x, y = T("fuji")
    A('<path d="M%.1f %.1f l-14 24 h28 Z" fill="#fff" stroke="#fff" stroke-width="5" stroke-linejoin="round"/>' % (x, y-12))
    A('<path d="M%.1f %.1f l-14 24 h28 Z" fill="#cdd1d6" stroke="#8e96a0" stroke-width="1.4" stroke-linejoin="round"/>' % (x, y-12))
    lab(x, y+40, "Fuji", 14, "middle", SOFT, "600", cls="lb sm")
    x, y = T("hakone"); pastille(x, y, 6, C["hako"], 15); lab(x, y+42, "Hakone", 19, "middle", C["hako"], "700")
    x, y = T("tokyo");  pastille(x, y, 7, C["tokyo"], 15); lab(x-27, y+7, "Tokyo", 19, "end", C["tokyo"], "700")
    x, y = T("hnd");    petit(x, y, "#5d646d", 7)
    lab(x+18, y+2, "✈ HND", 14, "start", SOFT, "600", cls="lb sm")
    lab(x+18, y+19, "6 nov", 12, "start", SOFT, "400", "italic", cls="lb sm")

kk = encart(45, 128, 386, 432, "ck", ["kix","osaka","nara","koyasan","kyoto"], "KANSAI", "étapes 1 · 2 · 4", 104, 58, kansai)
qk = encart(628, 818, 348, 228, "cq", ["fuji","hakone","tokyo","hnd"], "KANTŌ", "étapes 6 · 7", 78, 36, kanto)

KMU = 111.320*math.cos(math.radians(35))/(1000/math.radians(LON1-LON0)*math.radians(1)); BAR = 200/KMU
A('<g transform="translate(62,1012)">')
A('<rect width="%.1f" height="7" fill="%s"/>' % (BAR/2, INK))
A('<rect x="%.1f" width="%.1f" height="7" fill="none" stroke="%s" stroke-width="1.5"/>' % (BAR/2, BAR/2, INK))
lab(0, -8, "0", 12, "middle", SOFT, "500", cls="lb sm"); lab(BAR, -8, "200 km", 12, "middle", SOFT, "500", cls="lb sm")
A('</g>')
lab(62, 1046, "Projection Mercator · côtes réelles · échelle valable vers 35° N", 12, "start", "#93a7b1", "400", "italic", cls="lb sm")
A('</svg>')
svg = "\n".join(out)

# =============================================================== re-injection
h = io.open(HTML, encoding="utf-8").read()
a = h.index('<div class="mapfig">'); b = h.index("</svg>", a) + 6
h = h[:a] + '<div class="mapfig">\n' + svg + h[b:]
io.open(HTML, "w", encoding="utf-8").write(h)
print("carte regeneree : %d iles · %.1f Ko de SVG · zoom Kansai x%.2f / Kanto x%.2f"
      % (NISL, len(svg)/1024, kk, qk))
print("-> %s (%.1f Ko)" % (HTML, len(h.encode())/1024))
