#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ ohm_pack.py — добор срезов «машины времени» (/istoriya/) из OpenHistoricalMap (CC0). Конвейер: год → Overpass (overpass-api.openhistoricalmap.org) → реляции boundary=administrative + admin_level=2, живые на 1 июля года (start_date/end_date, лексикографическое сравнение ISO-дат) → сборка колец из ways → мультиполигоны (outer+inner) → упрощение Дуглас-Пекером (eps подбирается автоматически под лимит размера файла) → формат срезов data/geo/history/<год>.json: {n,s,p,b,g}. НАМОТКА КОЛЕЦ (грабли d3, КАРТА-ПРАВОК 2026-07-08): RFC7946 CCW-внешние кольца на сферическом d3 читаются как «планета минус страна». Здесь внешние кольца мотаются так, чтобы сферическая площадь (алгоритм d3.geoArea) была < 2π, внутренние — наоборот; клиентский rewind() в history-map.js остаётся страховкой. Использование (из build/): python3 tools/ohm_pack.py list 1861 1871 ... # списки живых реляций (тэги) → кэш python3 tools/ohm_pack.py fetch # геометрия всех реляций из списков → кэш python3 tools/ohm_pack.py build 1861 ... [--emit] # сборка срезов; --emit пишет в # data/geo/history/ + index.json + ru.json python3 tools/ohm_pack.py all 1861 ... [--emit] # всё сразу Кэш (--cache, по умолчанию ./ohm_cache) можно удалять — перекачается. Вежливость к API: пауза 3с между запросами, ретраи на 429/5xx с бэкоффом. """ import argparse, gzip, json, math, os, subprocess, sys, time API = "https://overpass-api.openhistoricalmap.org/api/interpreter" UA = "real-stats.ru /istoriya/ pipeline (ohm_pack.py)" SLEEP = 3.0 # пауза между запросами к Overpass BATCH = 12 # реляций на один geom-запрос PRE_EPS = 0.005 # предварительное упрощение при кэшировании (градусы) EPS_LADDER = [0.02, 0.03, 0.045, 0.07, 0.1, 0.15] # подбор под лимит размера MAX_KB = 1150 # целевой потолок размера среза DATE_SUFFIX = "-07-01" # «живые на середину года» # ---------------------------------------------------------------- Overpass def overpass(query, tries=6): """POST к Overpass через curl (системные сертификаты), gzip, ретраи. Возвращает dict или None после исчерпания ретраев (тяжёлый батч дробим выше).""" for attempt in range(tries): time.sleep(SLEEP) p = subprocess.run( ["curl", "-sS", "--compressed", "--http1.1", "-m", "900", "-A", UA, "--data-urlencode", "data=" + query, API], capture_output=True) out = p.stdout if p.returncode == 0 and out[:1] == b"{": try: return json.loads(out) except json.JSONDecodeError: pass wait = 20 * (attempt + 1) sys.stderr.write(" overpass retry %d (rc=%s, %dB): %s | жду %dс\n" % (attempt + 1, p.returncode, len(out), out[:160].decode("utf-8", "replace").replace("\n", " "), wait)) time.sleep(wait) return None def year_query(year): d = "%d%s" % (year, DATE_SUFFIX) return ('[out:json][timeout:300];\n' 'relation["boundary"="administrative"]["admin_level"="2"]\n' ' (if: (!is_tag("start_date") || t["start_date"] <= "%s")' ' && (!is_tag("end_date") || t["end_date"] > "%s"));\n' 'out tags;' % (d, d)) # ---------------------------------------------------------------- геометрия def dp_simplify(pts, eps): """Дуглас-Пекер, итеративно (без рекурсии). pts: [[lon,lat],...].""" n = len(pts) if n < 3: return pts[:] keep = bytearray(n); keep[0] = keep[-1] = 1 e2 = eps * eps stack = [(0, n - 1)] while stack: i, j = stack.pop() if j <= i + 1: continue ax, ay = pts[i]; bx, by = pts[j] dx = bx - ax; dy = by - ay den = dx * dx + dy * dy dmax = -1.0; k = -1 for m in range(i + 1, j): px, py = pts[m] if den == 0.0: d = (px - ax) ** 2 + (py - ay) ** 2 else: t = ((px - ax) * dx + (py - ay) * dy) / den if t < 0.0: t = 0.0 elif t > 1.0: t = 1.0 ex = ax + t * dx - px; ey = ay + t * dy - py d = ex * ex + ey * ey if d > dmax: dmax = d; k = m if dmax > e2: keep[k] = 1 stack.append((i, k)); stack.append((k, j)) return [pts[m] for m in range(n) if keep[m]] def ring_area_sph(ring): """Сферическая площадь кольца, эквивалент d3.geoArea({Polygon,[ring]}) ∈ [0,4π). Внешнее кольцо для d3 обязано давать < 2π (иначе «залит весь глобус»).""" pts = ring[:-1] if len(ring) > 1 and ring[0] == ring[-1] else ring if len(pts) < 3: return 0.0 qp = math.pi / 4 s = 0.0 lam0 = math.radians(pts[-1][0]); phi0 = math.radians(pts[-1][1]) / 2 + qp cos0, sin0 = math.cos(phi0), math.sin(phi0) for lon, lat in pts: lam = math.radians(lon); phi = math.radians(lat) / 2 + qp dl = lam - lam0 sd = 1.0 if dl >= 0 else -1.0 ad = sd * dl cosp, sinp = math.cos(phi), math.sin(phi) k = sin0 * sinp u = cos0 * cosp + k * math.cos(ad) v = k * sd * math.sin(ad) s += math.atan2(v, u) lam0, cos0, sin0 = lam, cosp, sinp if s < 0: s += 2 * math.pi return 2 * s def point_in_ring(pt, ring): """Чёт-нечет по плоским lon/lat (для привязки дырок к оболочкам).""" x, y = pt; inside = False n = len(ring) for i in range(n - 1): x1, y1 = ring[i]; x2, y2 = ring[i + 1] if (y1 > y) != (y2 > y): xi = x1 + (y - y1) * (x2 - x1) / (y2 - y1) if xi > x: inside = not inside return inside def stitch(ways): """Сшивка сегментов-ways в замкнутые кольца по совпадающим концам.""" segs = [w for w in ways if len(w) >= 2] used = [False] * len(segs) key = lambda p: (round(p[0], 7), round(p[1], 7)) ends = {} for i, s in enumerate(segs): for p in (s[0], s[-1]): ends.setdefault(key(p), []).append(i) rings, open_left = [], 0 for i in range(len(segs)): if used[i]: continue used[i] = True ring = list(segs[i]) while key(ring[0]) != key(ring[-1]): k = key(ring[-1]); nxt = None for j in ends.get(k, ()): if not used[j]: nxt = j; break if nxt is None: break used[nxt] = True s = segs[nxt] ring.extend(s[1:] if key(s[0]) == k else s[-2::-1]) if key(ring[0]) != key(ring[-1]): # разрыв в данных: замыкаем принудительно, если зазор мал — иначе тоже # замыкаем (иначе фича не отрисуется), но считаем как дефект open_left += 1 ring.append(list(ring[0])) if len(ring) >= 4: rings.append(ring) return rings, open_left def assemble(rel): """Реляция OHM (out geom) → {outer:[кольца], inner:[кольца], open:n}.""" outers, inners = [], [] for m in rel.get("members", ()): if m.get("type") != "way" or "geometry" not in m: continue pts = [[g["lon"], g["lat"]] for g in m["geometry"]] role = m.get("role", "") (inners if role == "inner" else outers).append(pts) o_rings, o_open = stitch(outers) i_rings, i_open = stitch(inners) return {"outer": o_rings, "inner": i_rings, "open": o_open + i_open} # ---------------------------------------------------------------- кэш def cache_paths(cache): os.makedirs(os.path.join(cache, "rels"), exist_ok=True) os.makedirs(os.path.join(cache, "years"), exist_ok=True) return cache def save_gz(path, obj): with gzip.open(path, "wt", encoding="utf-8") as f: json.dump(obj, f, ensure_ascii=False, separators=(",", ":")) def load_gz(path): with gzip.open(path, "rt", encoding="utf-8") as f: return json.load(f) # ---------------------------------------------------------------- шаги def step_list(years, cache): for y in years: out = os.path.join(cache, "years", "%d.json.gz" % y) if os.path.exists(out): print("список %d уже в кэше" % y); continue print("Overpass: список реляций, живых на %d%s ..." % (y, DATE_SUFFIX)) d = overpass(year_query(y)) if d is None: raise RuntimeError("Overpass не отдал список за %d" % y) els = [{"id": e["id"], "tags": e.get("tags", {})} for e in d.get("elements", ())] save_gz(out, els) print(" %d: %d реляций" % (y, len(els))) def step_fetch(cache, shard=None): need = set() ydir = os.path.join(cache, "years") for fn in os.listdir(ydir): for e in load_gz(os.path.join(ydir, fn)): need.add(e["id"]) have = {int(fn.split(".")[0]) for fn in os.listdir(os.path.join(cache, "rels"))} todo = sorted(need - have) print("геометрия: нужно %d реляций, в кэше %d, не хватает %d" % (len(need), len(need) - len(todo), len(todo))) if shard: # "k/n" — параллельные воркеры с непересекающимися долями id k, n = map(int, shard.split("/")) todo = [t for i, t in enumerate(todo) if i % n == k] print(" доля воркера %s: %d реляций" % (shard, len(todo))) def fetch_ids(ids): """Батч с адаптивным дроблением: тяжёлые империи валят сервер — бисекция.""" q = "[out:json][timeout:900];relation(id:%s);out geom;" % ",".join(map(str, ids)) d = overpass(q, tries=3 if len(ids) > 1 else 6) if d is None: if len(ids) == 1: # заглушку НЕ пишем: иначе отравим кэш и добор её не перекачает print(" реляция %d: сервер так и не отдал — пропуск, докачается потом" % ids[0]) return 0 mid = len(ids) // 2 print(" батч из %d не влез — дроблю пополам" % len(ids)) return fetch_ids(ids[:mid]) + fetch_ids(ids[mid:]) got = set() for rel in d.get("elements", ()): if rel.get("type") != "relation": continue a = assemble(rel) # предварительное упрощение — кэш в десятки раз легче, точность с запасом a["outer"] = [dp_simplify(r, PRE_EPS) for r in a["outer"]] a["inner"] = [dp_simplify(r, PRE_EPS) for r in a["inner"]] a["tags"] = rel.get("tags", {}) save_gz(os.path.join(cache, "rels", "%d.json.gz" % rel["id"]), a) got.add(rel["id"]) for mid_ in set(ids) - got: # реляция без геометрии — заглушка, чтобы не перекачивать save_gz(os.path.join(cache, "rels", "%d.json.gz" % mid_), {"outer": [], "inner": [], "open": 0, "tags": {}}) return len(got) for b in range(0, len(todo), BATCH): ids = todo[b:b + BATCH] n = fetch_ids(ids) print(" батч %d-%d: получено %d" % (b + 1, b + len(ids), n)) def rings_to_feature(a, eps): """Кольца реляции → мультиполигон формата среза (упрощение, намотка, округление).""" shells = [] for r in a["outer"]: s = dp_simplify(r, eps) s = [[round(x, 3), round(y, 3)] for x, y in s] s = [p for i, p in enumerate(s) if i == 0 or p != s[i - 1]] if len(s) < 3: continue if s[0] != s[-1]: s.append(list(s[0])) if len(s) < 4: continue # намотка: внешнее кольцо обязано давать сферическую площадь < 2π (d3) if ring_area_sph(s) > 2 * math.pi: s.reverse() shells.append([s]) # [оболочка, дырки...] for r in a["inner"]: s = dp_simplify(r, eps) s = [[round(x, 3), round(y, 3)] for x, y in s] s = [p for i, p in enumerate(s) if i == 0 or p != s[i - 1]] if len(s) < 3: continue if s[0] != s[-1]: s.append(list(s[0])) if len(s) < 4: continue if ring_area_sph(s) < 2 * math.pi: # дырка мотается противоположно оболочке s.reverse() for poly in shells: # дырку — в содержащую оболочку if point_in_ring(s[0], poly[0]): poly.append(s) break return shells def build_year(y, cache, src_note): alive = load_gz(os.path.join(cache, "years", "%d.json.gz" % y)) rels = [] for e in alive: p = os.path.join(cache, "rels", "%d.json.gz" % e["id"]) if not os.path.exists(p): continue a = load_gz(p) if a["outer"]: rels.append((e, a)) for eps in EPS_LADDER: feats = [] for e, a in rels: t = e["tags"] name = (t.get("name:en") or t.get("name") or "").strip() if not name: continue # линейные реляции-«границы» (не территории) — не рисуем if name.startswith(("Boundary", "Граница")): continue g = rings_to_feature(a, eps) if not g: continue feats.append({"n": name, "s": name, "p": name, "b": 3, "g": g}) feats.sort(key=lambda f: f["n"]) rec = {"year": y, "src": src_note, "features": feats} blob = json.dumps(rec, ensure_ascii=False, separators=(",", ":")) kb = len(blob.encode("utf-8")) // 1024 if kb <= MAX_KB: return rec, blob, kb, eps return rec, blob, kb, eps # даже максимальный eps не влез — вернём как есть def validate(rec, kb): """Санити-чек среза: население планеты и ключевые державы на месте.""" names = " | ".join(f["n"] for f in rec["features"]) y = rec["year"]; probs = [] n = len(rec["features"]) if y >= 1800 and n < 150: probs.append("мало стран: %d < 150" % n) checks = [("Россия", ("Russian Empire", "Soviet Union", "Russia", "USSR")), ("Франция", ("France", "French Empire", "French Republic", "French State")), ("Китай", ("Qing", "China", "Republic of China", "People's Republic"))] for label, keys in checks: if not any(k in names for k in keys): probs.append("нет ключевой державы: %s" % label) if kb > MAX_KB: probs.append("размер %dКБ > %d" % (kb, MAX_KB)) return probs def step_build(years, cache, out_dir, emit): src_note = "OpenHistoricalMap contributors, CC0 (overpass-api.openhistoricalmap.org)" results, ru_add = [], {} for y in years: rec, blob, kb, eps = build_year(y, cache, src_note) probs = validate(rec, kb) status = "OK" if not probs else "ПРОБЛЕМЫ: " + "; ".join(probs) print("%d: %d фич, %dКБ, eps=%.3f — %s" % (y, len(rec["features"]), kb, eps, status)) results.append((y, rec, blob, kb, probs)) # русские имена из OHM для словаря for e in load_gz(os.path.join(cache, "years", "%d.json.gz" % y)): t = e["tags"] en = (t.get("name:en") or t.get("name") or "").strip() ru = (t.get("name:ru") or "").strip() if en and ru and en != ru: ru_add[en] = ru for k, v in OHM_RU.items(): # кураторский словарь — только в пробелы ru_add.setdefault(k, v) if not emit: print("(сухой прогон: --emit не задан, файлы не пишутся)") return idx_path = os.path.join(out_dir, "index.json") idx = json.load(open(idx_path, encoding="utf-8")) for y, rec, blob, kb, probs in results: if probs: print("%d: НЕ записан (не прошёл валидацию)" % y) continue fn = "%d.json" % y open(os.path.join(out_dir, fn), "w", encoding="utf-8").write(blob) idx["years"] = [e for e in idx["years"] if e["y"] != y] idx["years"].append({"y": y, "f": fn, "n": len(rec["features"]), "kb": kb}) idx["years"].sort(key=lambda e: e["y"]) idx["src"] = ("https://github.com/aourednik/historical-basemaps (GPL-3.0) + " "https://www.openhistoricalmap.org (CC0)") idx["license"] = "GPL-3.0 (basemaps) / CC0 (OHM); срез помечен полем src" json.dump(idx, open(idx_path, "w", encoding="utf-8"), ensure_ascii=False, separators=(",", ":")) print("index.json: %d срезов" % len(idx["years"])) ru_path = os.path.join(out_dir, "ru.json") ru = json.load(open(ru_path, encoding="utf-8")) new = {k: v for k, v in ru_add.items() if k not in ru} if new: ru.update(new) json.dump(dict(sorted(ru.items())), open(ru_path, "w", encoding="utf-8"), ensure_ascii=False, separators=(",", ":")) print("ru.json: +%d новых имён (из name:ru OHM)" % len(new)) # ---------------------------------------------------------------- русские имена # Дополнение к name:ru из OHM: устоявшиеся русские названия для имён, у которых в OHM # нет name:ru и которых нет в ru.json. Только однозначные переводы; спорные оставлены # по-английски сознательно (лучше английское имя, чем выдуманное русское). OHM_RU = { "Alawi Sultanate": "Алауитский султанат (Марокко)", "All-Palestine Government": "Всепалестинское правительство", "Antigua-Barbuda-Montserrat Colony": "Колония Антигуа, Барбуда и Монтсеррат", "Argentine Confederation": "Аргентинская конфедерация", "Ashanti Colony": "Колония Ашанти", "Austrian Empire": "Австрийская империя", "Bahawalpur Kingdom": "Бахавалпур", "Barotziland": "Баротселенд", "British Arctic Territories": "Британские арктические территории", "British Bechuanaland": "Британский Бечуаналенд", "British Belize": "Британский Белиз", "British Ceylon": "Британский Цейлон", "British Concession of Jiujiang": "Британская концессия в Цзюцзяне", "British Eritrea": "Британская Эритрея", "British Guiana": "Британская Гвиана", "British Honduras Colony": "Колония Британский Гондурас", "British Hong Kong": "Британский Гонконг", "British Mauritius": "Британский Маврикий", "British Military Administration of Somaliland": "Британская администрация Сомалиленда", "British Raj": "Британская Индия", "British Settlements on the Bay of Honduras": "Британские поселения в Гондурасском заливе", "British Solomon Islands Protectorate": "Британские Соломоновы острова", "British Windward Islands": "Британские Наветренные острова", "British concession of Hankou": "Британская концессия в Ханькоу", "British occupation zone in Germany": "Британская зона оккупации Германии", "Cape Colony": "Капская колония", "Captaincy General of Cuba": "Генерал-капитанство Куба", "Captaincy General of Puerto Rico": "Генерал-капитанство Пуэрто-Рико", "Captaincy General of Santo Domingo": "Генерал-капитанство Санто-Доминго", "Captaincy General of the Philippines": "Генерал-капитанство Филиппины", "Captaincy-General of Mozambique, Sofala and Rivers of Sena": "Генерал-капитанство Мозамбик", "Cayman Islands": "Каймановы острова", "Chan Santa Cruz": "Чан-Санта-Крус", "Cochinchina (1697-1757)": "Кохинхина", "Colonia Trinidad": "Колония Тринидад", "Colonie de la Dominique": "Колония Доминика", "Colony of Acadia": "Акадия", "Colony of Antigua and Barbuda": "Колония Антигуа и Барбуда", "Colony of Barbados": "Колония Барбадос", "Colony of Berbice": "Колония Бербис", "Colony of Bermuda": "Бермуды", "Colony of British Columbia": "Колония Британская Колумбия", "Colony of Cochinchina": "Колония Кохинхина", "Colony of Connecticut": "Колония Коннектикут", "Colony of Demerara-Essequibo": "Колония Демерара-Эссекибо", "Colony of Dominica": "Колония Доминика", "Colony of Grenada": "Колония Гренада", "Colony of Guadelupe": "Колония Гваделупа", "Colony of Jamaica": "Колония Ямайка", "Colony of Labuan": "Колония Лабуан", "Colony of Liberia": "Колония Либерия", "Colony of Maryland": "Колония Мэриленд", "Colony of Middle Congo": "Среднее Конго", "Colony of Mozambique": "Колония Мозамбик", "Colony of Natal": "Колония Наталь", "Colony of New Brunswick": "Колония Нью-Брансуик", "Colony of New South Wales": "Колония Новый Южный Уэльс", "Colony of New York": "Колония Нью-Йорк", "Colony of New Zealand": "Колония Новая Зеландия", "Colony of Nova Scotia": "Колония Новая Шотландия", "Colony of Queensland": "Колония Квинсленд", "Colony of Rhode Island and Providence Plantations": "Колония Род-Айленд", "Colony of Saint Lucia": "Колония Сент-Люсия", "Colony of Seychelles": "Колония Сейшелы", "Colony of Sierra Leone": "Колония Сьерра-Леоне", "Colony of South Australia": "Колония Южная Австралия", "Colony of St Vincent and the Grenadines": "Колония Сент-Винсент и Гренадины", "Colony of Tasmania": "Колония Тасмания", "Colony of Tobago": "Колония Тобаго", "Colony of Trinidad": "Колония Тринидад", "Colony of Van Diemen's Land": "Земля Ван-Димена", "Colony of Vancouver Island": "Колония Остров Ванкувер", "Colony of Victoria": "Колония Виктория", "Colony of Western Australia": "Колония Западная Австралия", "Colony of the Bahamas": "Колония Багамы", "Colony of the Turks and Caicos Islands": "Тёркс и Кайкос", "Comancheria": "Команчерия", "Company Raj": "Владения Ост-Индской компании", "Confederacy of Independent Kingdoms of Fiji": "Конфедерация Фиджи", "Confederate States": "Конфедеративные Штаты Америки", "Confederate States of America": "Конфедеративные Штаты Америки", "Confederate States of Lanao": "Конфедерация Ланао", "Couto Misto": "Коуто-Мишто", "Crown Colony of Malta": "Коронная колония Мальта", "Crown Colony of Newfoundland": "Ньюфаундленд", "Crown Colony of Sarawak": "Коронная колония Саравак", "Czechoslovak Socialist Republic": "Чехословакия", "Danish West Indies": "Датская Вест-Индия", "Duchy of Anhalt": "Ангальт", "Duchy of Anhalt-Bernburg": "Ангальт-Бернбург", "Duchy of Anhalt-Dessau": "Ангальт-Дессау", "Duchy of Anhalt-Köthen": "Ангальт-Кётен", "Duchy of Lucca": "Герцогство Лукка", "Duchy of Massa and Carrara": "Масса и Каррара", "Duchy of Schleswig-Holstein-Gottorp": "Шлезвиг-Гольштейн-Готторп", "Dutch East Indies": "Нидерландская Ост-Индия", "Electorate of Hesse": "Кургессен", "Emirate of Afghanistan": "Афганский эмират", "Emirate of Bukhara (1785-1868)": "Бухарский эмират", "Emirate of Kabul": "Кабульский эмират", "Empire of Brazil": "Бразильская империя", "Establishments in Oceania": "Французские владения в Океании", "First Nigerian Republic": "Нигерия", "Frankfur": "Вольный город Франкфурт", "Free State of the Three Leagues": "Республика Трёх лиг", "Free Territory of Trieste (1947-1954)": "Свободная территория Триест", "Free and Hanseatic City of Hamburg": "Вольный город Гамбург", "Free and Hanseatic City of Lübeck": "Вольный город Любек", "French Establishments in Oceania": "Французские владения в Океании", "French Louisiana": "Французская Луизиана", "French Nosy Be": "Французский Нуси-Бе", "French Republic": "Франция", "Gambia Colony and Protectorate": "Колония Гамбия", "German East Africa": "Германская Восточная Африка", "German Protectorate of the Marshall Islands": "Германские Маршалловы острова", "Germany-Luxembourg condominium": "Кондоминиум Германии и Люксембурга", "Gilbert and Ellice Islands Colony": "Колония острова Гилберта и Эллис", "Gilbert and Ellice Islands Protectorate": "Протекторат острова Гилберта и Эллис", "Gold Coast Colony": "Колония Золотой Берег", "Governorate of Chiloe": "Губернаторство Чилоэ", "Granadine Confederation": "Гранадская конфедерация", "Grand Duchy of Hesse": "Великое герцогство Гессен", "Grand Duchy of Mecklenburg-Schwerin": "Мекленбург-Шверин", "Grand Duchy of Mecklenburg-Strelitz": "Мекленбург-Стрелиц", "Guernsey": "Гернси", "Hanover": "Ганновер", "Haushabi Sultanate": "Султанат Хаушаби", "Hawaiian Kingdom": "Гавайское королевство", "Heligoland": "Гельголанд", "Hesse-Homburg": "Гессен-Гомбург", "Hohenzollern-Hechingen": "Гогенцоллерн-Гехинген", "Hohenzollern-Sigmaringen": "Гогенцоллерн-Зигмаринген", "Hunza Kingdom": "Хунза", "Indochinese Union": "Французский Индокитай", "Intendency of Yucatán": "Интендантство Юкатан", "Isle Bourbon": "Остров Бурбон", "Isle Saint-Jean": "Остров Сен-Жан", "Isle de France": "Иль-де-Франс (Маврикий)", "Isle of Man": "Остров Мэн", "Israeli administration of Gaza": "Сектор Газа", "Japan (1947-1952)": "Япония", "Jersey": "Джерси", "Joseon": "Чосон", "Kedah Protectorate": "Протекторат Кедах", "Kelantan Protectorate": "Протекторат Келантан", "Khanate of Kalat": "Калатское ханство", "Khanate of Khiva": "Хивинское ханство", "Khanate of Kokand": "Кокандское ханство", "Kiautschou Bay Leased Territory": "Цзяочжоу (германская аренда)", "Kingdom of Araucanía and Patagonia": "Араукания и Патагония", "Kingdom of Bahawalpur": "Бахавалпур", "Kingdom of Baol": "Баол", "Kingdom of Bavaria": "Королевство Бавария", "Kingdom of Burundi": "Королевство Бурунди", "Kingdom of Cayor": "Кайор", "Kingdom of Coorg": "Кург", "Kingdom of Dahomey": "Дагомея", "Kingdom of France": "Королевство Франция", "Kingdom of Greece": "Королевство Греция", "Kingdom of Imerina": "Имерина", "Kingdom of Jolof": "Джолоф", "Kingdom of Kakongo": "Каконго", "Kingdom of Mexico": "Королевство Мексика", "Kingdom of Ngoyo": "Нгойо", "Kingdom of Niani": "Ниани", "Kingdom of Poland": "Царство Польское", "Kingdom of Portugal": "Королевство Португалия", "Kingdom of Prussia": "Королевство Пруссия", "Kingdom of Rwanda": "Королевство Руанда", "Kingdom of Saloum": "Салум", "Kingdom of Saxony": "Королевство Саксония", "Kingdom of Serbs, Croats and Slovenes": "Королевство сербов, хорватов и словенцев", "Kingdom of Sine": "Син", "Kingdom of Travancore": "Траванкор", "Kingdom of Waalo": "Вало", "Kingdom of Wuli": "Вули", "Kingdom of the Netherlands": "Нидерланды", "La Réunion": "Реюньон", "Lagos Colony": "Колония Лагос", "Lippe": "Липпе", "Majeerteen Sultanate": "Султанат Маджиртин", "Malagasy Protectorate": "Протекторат Мадагаскар", "Manipur Kingdom": "Манипур", "Mayotte": "Майотта", "Mayotte and Dependencies": "Майотта", "Miskito Kingdom": "Королевство Мискито", "Monaco": "Монако", "Monastic State of the Order of Malta": "Мальтийский орден", "Mosquito Kingdom": "Королевство Мискито", "Mosquito Reserve": "Резервация Мискито", "Mueang Chiang Mai (1727-1763)": "Чиангмай", "Nassau": "Нассау", "Nepal": "Непал", "New France": "Новая Франция", "New Granada": "Новая Гранада", "New Hebrides / Nouvelles-Hébrides": "Новые Гебриды", "New Spain (1739-1742)": "Новая Испания", "New Spain (1819-1821)": "Новая Испания", "New Spain (1823-1854)": "Новая Испания", "New Spain (1861-1865)": "Новая Испания", "New Spain (1865-1899)": "Новая Испания", "North Yemen": "Северный Йемен", "North-West Territories": "Северо-Западные территории", "Nosy Boraha": "Нуси-Бураха", "Obock Colony": "Колония Обок", "Obock and Protectorates of the Somali Coast and Dankils Countries": "Обок и протектораты Сомалийского берега", "Oldenburg": "Ольденбург", "Omani Empire": "Оманская империя", "Orange Free State": "Оранжевое Свободное государство", "Pahang Protectorate": "Протекторат Паханг", "Papal States": "Папская область", "People's Republic of China": "КНР", "Perak Protectorate": "Протекторат Перак", "Perlis Protectorate": "Протекторат Перлис", "Persia": "Персия", "Pitcairn Islands": "Острова Питкэрн", "Poland-Lithuania": "Речь Посполитая", "Portuguese Goa": "Португальский Гоа", "Portuguese Guinea": "Португальская Гвинея", "Portuguese India": "Португальская Индия", "Portuguese Macau": "Португальский Макао", "Portuguese São Tomé and Príncipe": "Португальские Сан-Томе и Принсипи", "Portuguese Timor": "Португальский Тимор", "Prince Edward Island Colony": "Колония Остров Принца Эдуарда", "Principality of Lamphun": "Княжество Лампхун", "Principality of Montenegro": "Княжество Черногория", "Principality of Phrae": "Княжество Пхрэ", "Principality of Reuss-Gera": "Рейсс-Гера", "Principality of Reuss-Greiz": "Рейсс-Грейц", "Principality of Schwarzburg-Rudolstadt": "Шварцбург-Рудольштадт", "Principality of Schwarzburg-Sondershausen": "Шварцбург-Зондерсхаузен", "Protectorate of Afghanistan": "Афганистан (протекторат)", "Protectorate of Bahrain": "Протекторат Бахрейн", "Protectorate of Kenya": "Протекторат Кения", "Protectorate of Kuwait": "Протекторат Кувейт", "Protectorate of Qatar": "Протекторат Катар", "Protectorate of Tonga": "Протекторат Тонга", "Province of Caracas": "Провинция Каракас", "Province of Mozambique": "Провинция Мозамбик", "Provisional Revolutionary Government of the Republic of South Vietnam": "Южный Вьетнам (ВРП)", "Provisional government of Milan": "Временное правительство Милана", "Pyrmont": "Пирмонт", "Qing": "Империя Цин", "Rattanakosin Kingdom Mandala": "Сиам (Раттанакосин)", "Red River Colony": "Колония Ред-Ривер", "Republic of Cospaia": "Республика Коспая", "Republic of Geneva": "Женевская республика", "Republic of Haiti": "Гаити", "Republic of Lucca": "Республика Лукка", "Republic of Mulhouse": "Республика Мюлуз", "Republic of Noli": "Республика Ноли", "Republic of Venice": "Венецианская республика", "Republic of the Seven United Netherlands": "Республика Соединённых провинций", "Rupert's Land": "Земля Руперта", "Russian America": "Русская Америка", "Ryukyu Kingdom": "Королевство Рюкю", "SFR Yugoslavia": "Югославия", "Saint Helena Crown Colony": "Остров Святой Елены", "Saint Pierre and Miquelon": "Сен-Пьер и Микелон", "Sarawak": "Саравак", "Satara State": "Сатара", "Saxe-Altenburg": "Саксен-Альтенбург", "Saxe-Coburg and Gotha": "Саксен-Кобург-Гота", "Saxe-Meiningen": "Саксен-Мейнинген", "Saxe-Weimar-Eisenach": "Саксен-Веймар-Эйзенах", "Schaumburg-Lippe": "Шаумбург-Липпе", "Second French Empire": "Франция (Вторая империя)", "Selangor Protectorate": "Протекторат Селангор", "Settlement of British Honduras": "Британский Гондурас", "Shilluk Kingdom": "Королевство Шиллук", "Sierra Leone Protectorate": "Протекторат Сьерра-Леоне", "Sind Sultanate": "Синд", "Sindh Sultanate": "Синд", "Sokoto Caliphate": "Халифат Сокото", "South African Republic": "Трансвааль", "Sovereign Duchy of Bouillon": "Герцогство Бульон", "Spanish Bay Islands": "Испанские острова Бэй", "Spanish Philippines": "Испанские Филиппины", "Spanish Posessions on the Gulf of Guinea": "Испанская Гвинея", "Spanish Possessions on the Gulf of Guinea": "Испанская Гвинея", "Spanish West Africa": "Испанская Западная Африка", "Spanish protectorate in Morocco": "Испанское Марокко", "St Vincent and the Grenadines Territory": "Сент-Винсент и Гренадины", "State of Buenos Aires": "Буэнос-Айрес", "State of Ecuador": "Эквадор", "State of Grão-Pará and Rio Negro": "Гран-Пара и Риу-Негру", "State of Maranhão": "Мараньян", "State of Venezuela": "Венесуэла", "Sultanate of Banjar": "Султанат Банджар", "Sultanate of Dar al Kuti": "Султанат Дар аль-Кути", "Sultanate of Lahej": "Султанат Лахедж", "Sultanate of Maguindanao": "Султанат Магинданао", "Sultanate of Maore": "Султанат Маоре", "Sultanate of Muscat and Oman": "Маскат и Оман", "Sultanate of Mwali": "Султанат Мвали", "Sultanate of Ndzuwani": "Султанат Ндзуани", "Sultanate of Qishn and Socotra": "Султанат Кишн и Сокотра", "Sultanate of Sulu": "Султанат Сулу", "Sultanate of Zanzibar": "Султанат Занзибар", "Sultanate of the Maldive Islands": "Мальдивский султанат", "Swan River Colony": "Колония Суон-Ривер", "Tahiti Protectorate": "Протекторат Таити", "Tahuatu Protectorate": "Протекторат Тахуата", "Terengganu Protectorate": "Протекторат Тренгану", "Territory of Ifni": "Ифни", "Togoland Protectorate": "Тоголенд", "Tokelau Islands Protectorate": "Протекторат Токелау", "Tokugawa Shogunate": "Сёгунат Токугава", "Transvaal Colony": "Колония Трансвааль", "Trinidad and Tobago Colony": "Колония Тринидад и Тобаго", "Trucial States": "Договорный Оман", "Turco-Egyptian Sudan": "Турецко-египетский Судан", "Tuvan People's Republic": "Тувинская Народная Республика", "U.S. Naval Base Subic Bay": "База США Субик-Бей", "US Philippines": "Американские Филиппины", "United Kingdom of Great Britain and Ireland": "Великобритания", "United Province of Canada": "Провинция Канада", "United States of the Ionian Islands": "Соединённые Штаты Ионических островов", "Upper Senegal and Niger": "Верхний Сенегал и Нигер", "Viceroyalty of Peru": "Вице-королевство Перу", "Viceroyalty of Peru (1817-1820)": "Вице-королевство Перу", "Waldeck-Pyrmont": "Вальдек-Пирмонт", "Wallis and Futuna Colony": "Уоллис и Футуна", "Walvis Bay Colony": "Уолфиш-Бей", "Württemberg": "Вюртемберг", "Đại Nam": "Дайнам", "Đại Việt - Chúa Trịnh": "Дайвьет (Чинь)", } # ---------------------------------------------------------------- CLI def main(): ap = argparse.ArgumentParser(description="Срезы машины времени из OpenHistoricalMap") ap.add_argument("cmd", choices=["list", "fetch", "build", "all"]) ap.add_argument("years", nargs="*", type=int) ap.add_argument("--cache", default="ohm_cache") ap.add_argument("--out", default="data/geo/history") ap.add_argument("--emit", action="store_true", help="записать срезы + index.json + ru.json (иначе сухой прогон)") ap.add_argument("--shard", default=None, help='доля воркера "k/n" для параллельной скачки (напр. 0/2 и 1/2)') ap.add_argument("--batch", type=int, default=None, help="реляций на один geom-запрос (по умолчанию %d)" % BATCH) a = ap.parse_args() if a.batch: globals()["BATCH"] = a.batch cache_paths(a.cache) if a.cmd in ("list", "all"): step_list(a.years, a.cache) if a.cmd in ("fetch", "all"): step_fetch(a.cache, a.shard) if a.cmd in ("build", "all"): step_build(a.years, a.cache, a.out, a.emit) if __name__ == "__main__": main()