#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
sas_layer.py — слой с дачами верхней Оки для SAS.Планеты.

Берёт нанесённое в атласе (контуры дач, метки селений, линии рек и дорог),
рисует поверх прозрачного фона тайловую пирамиду и создаёт запись в меню
карт SAS.Планеты. После запуска слой появится в «Карты → ПГМ → Дачи».

    python sas_layer.py --sas "C:\\Карта\\СасПланет 260426"

Что делает:
  1. качает с сервера нанесённое (atlas_state.json) и реестр (индекс);
  2. рисует тайлы 256×256 в проекции Web Mercator — только там, где
     есть объекты, пустые квадраты не создаются;
  3. кладёт их в <SAS>/cache_gmt/<имя>/z{z}/{y}/{x}.png — это формат
     GlobalMapper, он же CacheType=4;
  4. пишет <SAS>/Maps/PGM/<имя>.zmp/params.txt в UTF-8 с BOM
     (без BOM кириллица в меню превратится в «РџР“Рњ»);
  5. рядом кладёт .kml — его можно вместо этого загрузить в «Метки»,
     если нужен не слой, а редактируемые объекты.

Слой прозрачный и накладывается поверх любой карты: asLayer=1.
"""
import argparse
import json
import math
import os
import shutil
import sys
import urllib.request

try:
    from PIL import Image, ImageDraw, ImageFont
except ImportError:
    sys.exit("Нужна библиотека Pillow:  pip install pillow")

SERVER = "https://pgm.24stroybot.ru"
TILE = 256

# ------------------------------------------------------------------ цвета
COL_POLY_LINE = (150, 60, 30, 235)
COL_POLY_FILL = (200, 120, 60, 46)
COL_ZONE_LINE = (200, 140, 20, 220)
COL_PT = {
    "green": (46, 125, 50), "purple": (123, 82, 168), "red": (192, 57, 43),
    "orange": (219, 124, 30), "blue": (47, 111, 159), "teal": (0, 131, 143),
    "brown": (141, 85, 36), "pink": (194, 60, 140), "yellow": (216, 161, 26),
    "gray": (130, 122, 106), "black": (60, 54, 44),
}
HALO = (255, 255, 255, 225)


def deg2num(lat, lon, z):
    n = 2 ** z
    x = (lon + 180.0) / 360.0 * n
    la = math.radians(lat)
    y = (1 - math.log(math.tan(la) + 1 / math.cos(la)) / math.pi) / 2 * n
    return x, y


def fetch(path):
    with urllib.request.urlopen(SERVER + path, timeout=90) as r:
        return json.load(r)


def find_font(size):
    cands = [
        r"C:\Windows\Fonts\arial.ttf", r"C:\Windows\Fonts\segoeui.ttf",
        "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
        "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
    ]
    for p in cands:
        if os.path.exists(p):
            try:
                return ImageFont.truetype(p, size)
            except Exception:
                pass
    return ImageFont.load_default()


def text_halo(dr, xy, txt, font, fill=(30, 26, 20, 255)):
    """Подпись с белой обводкой — иначе на пёстрой карте её не прочитать."""
    x, y = xy
    for dx in (-2, -1, 0, 1, 2):
        for dy in (-2, -1, 0, 1, 2):
            if dx or dy:
                dr.text((x + dx, y + dy), txt, font=font, fill=HALO)
    dr.text((x, y), txt, font=font, fill=fill)


# ------------------------------------------------------------ сбор объектов
def collect(state, index):
    idx = {r["id"]: r for r in index}
    objs = {"polys": [], "points": [], "lines": []}
    for g in state.get("polys", []):
        r = idx.get(g.get("id")) or {}
        num = r.get("n")
        objs["polys"].append({
            "ll": g["latlngs"],
            "zone": bool(g.get("zone")),
            "label": g.get("label") or (r.get("nm") or ""),
            "num": ("№%d" % round(num)) if num else "",
        })
    for p in state.get("points", []):
        r = idx.get(p.get("id")) or {}
        num = r.get("n")
        objs["points"].append({
            "lat": p["lat"], "lng": p["lng"],
            "label": p.get("label") or r.get("nm") or "",
            "num": ("№%d" % round(num)) if num else "",
            "color": COL_PT.get(p.get("color"), COL_PT["purple"]),
        })
    for l in state.get("lines", []):
        objs["lines"].append({"ll": l["latlngs"],
                              "color": COL_PT.get(l.get("color"), COL_PT["blue"]),
                              "w": int(l.get("width") or 2)})
    return objs


def tiles_needed(objs, z, pad=1):
    """Только те квадраты, где что-то есть, плюс поля под подписи."""
    need = set()
    def add(lat, lng):
        x, y = deg2num(lat, lng, z)
        for dx in range(-pad, pad + 1):
            for dy in range(-pad, pad + 1):
                need.add((int(x) + dx, int(y) + dy))
    for g in objs["polys"]:
        for lat, lng in g["ll"]:
            add(lat, lng)
    for l in objs["lines"]:
        for lat, lng in l["ll"]:
            add(lat, lng)
    for p in objs["points"]:
        add(p["lat"], p["lng"])
    return sorted(need)


def draw_tile(objs, z, tx, ty, f_num, f_nm, labels, labels_mode='both'):
    im = Image.new("RGBA", (TILE, TILE), (0, 0, 0, 0))
    dr = ImageDraw.Draw(im, "RGBA")
    ox, oy = tx * TILE, ty * TILE

    def pt(lat, lng):
        x, y = deg2num(lat, lng, z)
        return x * TILE - ox, y * TILE - oy

    empty = True
    for l in objs["lines"]:
        xy = [pt(a, b) for a, b in l["ll"]]
        if any(-TILE < x < 2 * TILE and -TILE < y < 2 * TILE for x, y in xy):
            dr.line(xy, fill=l["color"] + (210,), width=l["w"], joint="curve")
            empty = False
    for g in objs["polys"]:
        xy = [pt(a, b) for a, b in g["ll"]]
        if not any(-TILE < x < 2 * TILE and -TILE < y < 2 * TILE for x, y in xy):
            continue
        empty = False
        line = COL_ZONE_LINE if g["zone"] else COL_POLY_LINE
        if len(xy) > 2:
            dr.polygon(xy, fill=COL_POLY_FILL)
            dr.line(xy + [xy[0]], fill=line, width=3)
        if labels and g["label"]:
            cx = sum(p[0] for p in xy) / len(xy)
            cy = sum(p[1] for p in xy) / len(xy)
            if -60 < cx < TILE + 60 and -40 < cy < TILE + 40:
                cap = (g["num"] + " " + g["label"]).strip()
                if labels_mode == "num": cap = g["num"] or g["label"]
                elif labels_mode == "name": cap = g["label"] or g["num"]
                text_halo(dr, (cx - 4 * len(cap) // 2, cy - 7), cap, f_nm)
    for p in objs["points"]:
        x, y = pt(p["lat"], p["lng"])
        if not (-40 < x < TILE + 40 and -40 < y < TILE + 40):
            continue
        empty = False
        r = 5
        dr.ellipse([x - r - 2, y - r - 2, x + r + 2, y + r + 2], fill=HALO)
        dr.ellipse([x - r, y - r, x + r, y + r], fill=p["color"] + (255,))
        if labels:
            # номер и название вместе: «№63 Мышьборъ». Номер впереди —
            # так он читается, даже если хвост подписи налезет на соседнюю
            cap = (" ".join(x for x in (p["num"], p["label"]) if x)).strip()
            if labels_mode == "num":
                cap = p["num"] or p["label"]
            elif labels_mode == "name":
                cap = p["label"] or p["num"]
            if cap:
                text_halo(dr, (x + 8, y - 8), cap, f_num)
    return None if empty else im


# --------------------------------------------------------------- params.txt
PARAMS = """[PARAMS]
GUID={{{guid}}}
ParentSubMenu=ПГМ
ParentSubMenu_ru=ПГМ
name={name}
name_ru={name_ru}
NameInCache={cache}
CacheType=4
Ext=.png
DefaultContentType=image/png
ContentType="image/png"
IgnoreContentType=1
projection=1
sradiusa=6378137
sradiusb=6378137
asLayer=1
MaxZoom={maxz}
UseDwn=0
UseStick=1
Sleep=0
MaxConnectToServerCount=1

[ViewInfo]
MapCenter={lon:.6f},{lat:.6f},{vz}
"""


def write_zmp(sas, name, cache, maxz, center, guid):
    d = os.path.join(sas, "Maps", "PGM", cache + ".zmp")
    os.makedirs(d, exist_ok=True)
    txt = PARAMS.format(guid=guid, name=name, name_ru=name, cache=cache,
                        maxz=maxz + 1,          # у SAS нумерация с единицы
                        lon=center[1], lat=center[0], vz=min(maxz + 1, 14))
    # UTF-8 с BOM: без него кириллица в меню карт превращается в «РџР“Рњ»
    with open(os.path.join(d, "params.txt"), "wb") as f:
        f.write(b"\xef\xbb\xbf" + txt.encode("utf-8"))
    # значки берём у любой готовой карты, чтобы пункт меню не был пустым
    for ico in ("18.bmp", "24.bmp"):
        if os.path.exists(os.path.join(d, ico)):
            continue
        for root, _, files in os.walk(os.path.join(sas, "Maps")):
            if ico in files and ".zmp" in root and cache + ".zmp" not in root:
                try:
                    shutil.copy(os.path.join(root, ico), os.path.join(d, ico))
                except Exception:
                    pass
                break
    return d


def write_kml(path, objs):
    def coords(ll):
        return " ".join("%.6f,%.6f,0" % (b, a) for a, b in ll)
    esc = lambda s: (s or "").replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
    body = []
    for g in objs["polys"]:
        ring = g["ll"] + [g["ll"][0]] if g["ll"] else []
        body.append("<Placemark><name>%s</name><Style><LineStyle><color>ff1e3c96</color>"
                    "<width>2</width></LineStyle><PolyStyle><color>3c3c96c8</color></PolyStyle></Style>"
                    "<Polygon><outerBoundaryIs><LinearRing><coordinates>%s</coordinates>"
                    "</LinearRing></outerBoundaryIs></Polygon></Placemark>"
                    % (esc((g["num"] + " " + g["label"]).strip()), coords(ring)))
    for p in objs["points"]:
        body.append("<Placemark><name>%s</name><Point><coordinates>%.6f,%.6f,0</coordinates>"
                    "</Point></Placemark>"
                    % (esc((p["num"] + " " + p["label"]).strip()), p["lng"], p["lat"]))
    for l in objs["lines"]:
        body.append("<Placemark><LineString><tessellate>1</tessellate><coordinates>%s"
                    "</coordinates></LineString></Placemark>" % coords(l["ll"]))
    xml = ('<?xml version="1.0" encoding="UTF-8"?>\n<kml xmlns="http://www.opengis.net/kml/2.2">'
           '<Document><name>Дачи верхней Оки</name>%s</Document></kml>' % "".join(body))
    with open(path, "w", encoding="utf-8") as f:
        f.write(xml)


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--sas", required=True, help="папка SAS.Планеты")
    ap.add_argument("--cache", default="PGM_dachi", help="имя папки кэша (латиницей)")
    ap.add_argument("--name", default="Дачи верхней Оки", help="название в меню")
    ap.add_argument("--minz", type=int, default=11)
    ap.add_argument("--maxz", type=int, default=16)
    ap.add_argument("--labels-from", type=int, default=13,
                    help="с какого зума подписывать")
    ap.add_argument("--labels", choices=["both", "name", "num"], default="both",
                    help="что писать: номер и название, только название или только номер")
    a = ap.parse_args()

    if not os.path.isdir(a.sas):
        sys.exit("Не нашёл папку SAS.Планеты: " + a.sas)

    print("Качаю нанесённое с сервера…")
    state = fetch("/data/atlas_state.json")
    index = fetch("/data/registry_index.json")
    objs = collect(state, index)
    print("  контуров: %d, меток: %d, линий: %d"
          % (len(objs["polys"]), len(objs["points"]), len(objs["lines"])))
    if not any(objs.values()):
        sys.exit("Нечего выгружать.")

    lats = [p["lat"] for p in objs["points"]] + [a2 for g in objs["polys"] for a2, _ in g["ll"]]
    lngs = [p["lng"] for p in objs["points"]] + [b for g in objs["polys"] for _, b in g["ll"]]
    center = (sum(lats) / len(lats), sum(lngs) / len(lngs))

    f_num = find_font(13)
    f_nm = find_font(12)
    root = os.path.join(a.sas, "cache_gmt", a.cache)
    total = 0
    for z in range(a.minz, a.maxz + 1):
        need = tiles_needed(objs, z)
        made = 0
        for tx, ty in need:
            im = draw_tile(objs, z, tx, ty, f_num, f_nm, z >= a.labels_from, a.labels)
            if im is None:
                continue
            d = os.path.join(root, "z%d" % z, str(ty))
            os.makedirs(d, exist_ok=True)
            im.save(os.path.join(d, "%d.png" % tx))
            made += 1
        total += made
        print("  зум %2d: %d тайлов" % (z, made))
    print("всего тайлов: %d" % total)

    guid = "B7D3%04X-1F2A-4C6E-9A81-%012X" % (total % 65536, abs(hash(a.cache)) % (16 ** 12))
    d = write_zmp(a.sas, a.name, a.cache, a.maxz, center, guid)
    print("запись в меню: " + d)

    kml = os.path.join(a.sas, a.cache + ".kml")
    write_kml(kml, objs)
    print("файл для «Меток»: " + kml)
    print("\nГотово. Перезапустите SAS.Планету — слой появится в «Карты → ПГМ».")
    print("Он прозрачный (asLayer=1), включается поверх любой основы.")


if __name__ == "__main__":
    main()
