EtrediaEmpires/nudge.py
legatt f1380dcef5 Fix RmlUi colors and map content
- Prevent RmlUi shader from second convertion to SRGB format
- Doox Empire regions
- ESH regions
- Kartilia regions
- Country colors are changed
2026-10-14 17:20:55 +03:00

720 lines
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -- coding: utf-8 --
"""
Region Mapper — работа с двумя JSON: регионы (цвета) и страны (группы).
Зависимости: pip install PyQt5 Pillow
Запуск: python nudge.py map.bmp
"""
import sys
import os
import json
import math
from PyQt5.QtCore import Qt, QPoint, QPointF, QRect, QRectF, QTimer, pyqtSignal
from PyQt5.QtGui import QImage, QPixmap, QPainter, QColor, QPen, QKeySequence
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QHBoxLayout, QVBoxLayout,
QLabel, QLineEdit, QPushButton, QListWidget, QListWidgetItem,
QSplitter, QFileDialog, QMessageBox, QInputDialog, QShortcut,
QFormLayout
)
from PIL import Image
Image.MAX_IMAGE_PIXELS = None
# ----------------------------------------------------------------------
# Утилиты
# ----------------------------------------------------------------------
def rgb_to_hex(rgb):
return "#{:02X}{:02X}{:02X}".format(*rgb)
def hex_to_rgb(s):
s = s.lstrip("#")
return int(s[0:2], 16), int(s[2:4], 16), int(s[4:6], 16)
def load_map(path):
img = QImage(path)
if not img.isNull():
return img, None
with Image.open(path) as f:
pil = f.convert("RGB")
w, h = pil.size
raw = pil.tobytes()
del pil
img = QImage(raw, w, h, w * 3, QImage.Format_RGB888)
if img.isNull():
raise RuntimeError("Не удалось декодировать изображение")
return img, raw
def make_overview(img, max_side=4096):
w, h = img.width(), img.height()
k = min(1.0, max_side / float(max(w, h)))
if k >= 1.0:
small = img
else:
small = img.scaled(max(1, int(w * k)), max(1, int(h * k)),
Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
return QPixmap.fromImage(small)
# ----------------------------------------------------------------------
# Просмотр карты
# ----------------------------------------------------------------------
class MapViewer(QWidget):
pixelPicked = pyqtSignal(int, int, tuple)
zoomChanged = pyqtSignal(float)
cursorMoved = pyqtSignal(int, int)
LOD_SWITCH = 0.25
MAX_SCALE = 64.0
def __init__(self, image, raw_buffer, parent=None):
super().__init__(parent)
self.image = image
self._raw = raw_buffer
self.W = image.width()
self.H = image.height()
print("Размер карты: {} x {} пикселей".format(self.W, self.H))
print("Создаю уменьшенную копию...")
self.overview = make_overview(image)
self.scale = 1.0
self.offset = QPointF(0, 0)
self._fit_scale = 1.0
self._fitted = False
self._panning = False
self._pan_last = QPoint()
self._press_pos = None
self._press_offset = QPointF()
self._dragging = False
self.marker = None
self.setMouseTracking(True)
self.setFocusPolicy(Qt.StrongFocus)
self.setMinimumSize(200, 120)
def _to_image(self, pos):
x = math.floor((pos.x() - self.offset.x()) / self.scale)
y = math.floor((pos.y() - self.offset.y()) / self.scale)
return x, y
def fitToWindow(self):
vw, vh = self.width(), self.height()
if vw < 2 or vh < 2: return
s = min(vw / self.W, vh / self.H)
self._fit_scale = s
self.scale = s
self.offset = QPointF((vw - self.W * s) / 2.0, (vh - self.H * s) / 2.0)
self._fitted = True
self.update()
self.zoomChanged.emit(s)
def actualSize(self):
self._zoom_at(QPointF(self.width() / 2, self.height() / 2), 1.0)
def zoomIn(self):
self._zoom_at(QPointF(self.width() / 2, self.height() / 2), self.scale * 1.5)
def zoomOut(self):
self._zoom_at(QPointF(self.width() / 2, self.height() / 2), self.scale / 1.5)
def _zoom_at(self, point, new_scale):
new_scale = max(self._fit_scale * 0.25, min(self.MAX_SCALE, new_scale))
ix = (point.x() - self.offset.x()) / self.scale
iy = (point.y() - self.offset.y()) / self.scale
self.scale = new_scale
self.offset = QPointF(point.x() - ix * new_scale, point.y() - iy * new_scale)
self.update()
self.zoomChanged.emit(new_scale)
def resizeEvent(self, event):
super().resizeEvent(event)
if not self._fitted: self.fitToWindow()
def paintEvent(self, event):
p = QPainter(self)
p.fillRect(self.rect(), QColor(45, 45, 48))
p.setRenderHint(QPainter.SmoothPixmapTransform, self.scale < 1.0)
s = self.scale
ox, oy = self.offset.x(), self.offset.y()
if s < self.LOD_SWITCH:
target = QRectF(ox, oy, self.W * s, self.H * s)
p.drawPixmap(target, self.overview, QRectF(0, 0, self.overview.width(), self.overview.height()))
else:
x0 = max(0, int(math.floor(-ox / s)))
y0 = max(0, int(math.floor(-oy / s)))
x1 = min(self.W, int(math.ceil((self.width() - ox) / s)))
y1 = min(self.H, int(math.ceil((self.height() - oy) / s)))
if x1 > x0 and y1 > y0:
src = QRect(x0, y0, x1 - x0, y1 - y0)
target = QRectF(ox + x0 * s, oy + y0 * s, (x1 - x0) * s, (y1 - y0) * s)
p.drawImage(target, self.image, QRectF(src))
p.setPen(QPen(QColor(95, 95, 95), 1))
p.drawRect(QRectF(ox, oy, self.W * s, self.H * s))
if self.marker is not None and s >= 4:
mx, my = self.marker
p.setPen(QPen(QColor(255, 255, 255), 1))
p.drawRect(QRectF(ox + mx * s, oy + my * s, s, s))
p.end()
def wheelEvent(self, event):
d = event.angleDelta().y()
if d:
factor = 1.25 if d > 0 else 1.0 / 1.25
self._zoom_at(QPointF(event.pos()), self.scale * factor)
def mousePressEvent(self, event):
if event.button() in (Qt.RightButton, Qt.MiddleButton):
self._panning = True
self._pan_last = event.pos()
self.setCursor(Qt.ClosedHandCursor)
elif event.button() == Qt.LeftButton:
self._press_pos = event.pos()
self._press_offset = QPointF(self.offset)
self._dragging = False
def mouseMoveEvent(self, event):
if self._panning:
d = event.pos() - self._pan_last
self._pan_last = event.pos()
self.offset = QPointF(self.offset.x() + d.x(), self.offset.y() + d.y())
self.update()
elif self._press_pos is not None:
d = event.pos() - self._press_pos
if d.manhattanLength() > 4: self._dragging = True
if self._dragging:
self.offset = QPointF(self._press_offset.x() + d.x(), self._press_offset.y() + d.y())
self.update()
else:
x, y = self._to_image(event.pos())
if 0 <= x < self.W and 0 <= y < self.H: self.cursorMoved.emit(x, y)
else: self.cursorMoved.emit(-1, -1)
def mouseReleaseEvent(self, event):
if event.button() in (Qt.RightButton, Qt.MiddleButton):
self._panning = False
self.setCursor(Qt.ArrowCursor)
elif event.button() == Qt.LeftButton and self._press_pos is not None:
if not self._dragging: self._pick(event.pos())
self._press_pos = None
self._dragging = False
def _pick(self, pos):
x, y = self._to_image(pos)
if 0 <= x < self.W and 0 <= y < self.H:
c = self.image.pixelColor(x, y)
self.marker = (x, y)
self.update()
self.pixelPicked.emit(x, y, (c.red(), c.green(), c.blue()))
def keyPressEvent(self, event):
if event.key() in (Qt.Key_0, Qt.Key_F): self.fitToWindow()
elif event.key() == Qt.Key_1: self.actualSize()
elif event.key() in (Qt.Key_Plus, Qt.Key_Equal): self.zoomIn()
elif event.key() == Qt.Key_Minus: self.zoomOut()
else: super().keyPressEvent(event)
# ----------------------------------------------------------------------
# Окно
# ----------------------------------------------------------------------
class MainWindow(QMainWindow):
def __init__(self, path):
super().__init__()
self.setWindowTitle("Region Mapper — " + os.path.basename(path))
self.resize(1400, 850)
# --- Файл 1: Регионы (плоский словарь) ---
self.mapping = {} # name -> "#RRGGBB"
self.by_color = {} # (r, g, b) -> name
# --- Файл 2: Страны (вложенная структура) ---
self.subjects_list = [] # список тегов для сохранения порядка
self.subjects = {} # tag -> {"color": str, "regions": list}
self.current_rgb = None
self._shown_once = False
image, raw = load_map(path)
self.viewer = MapViewer(image, raw)
self.viewer.pixelPicked.connect(self.on_pixel)
self.viewer.zoomChanged.connect(lambda s: self.zoom_label.setText("Масштаб: {:.4f}×".format(s)))
self.viewer.cursorMoved.connect(lambda x, y: self.coord_label.setText("X: {} Y: {}".format(x, y)))
self._build_ui()
def showEvent(self, event):
super().showEvent(event)
if not self._shown_once:
self._shown_once = True
QTimer.singleShot(0, self.viewer.fitToWindow)
def _build_ui(self):
central = QWidget()
self.setCentralWidget(central)
root = QHBoxLayout(central)
root.setContentsMargins(6, 6, 6, 6)
splitter = QSplitter(Qt.Horizontal)
# --- слева: карта ---
left = QWidget()
lv = QVBoxLayout(left)
lv.setContentsMargins(0, 0, 0, 0)
tb = QHBoxLayout()
for text, slot in (("Вписать (0)", self.viewer.fitToWindow),
("1:1", self.viewer.actualSize),
("+", self.viewer.zoomIn),
("−", self.viewer.zoomOut)):
b = QPushButton(text)
b.clicked.connect(slot)
tb.addWidget(b)
self.zoom_label = QLabel("Масштаб: —")
tb.addWidget(self.zoom_label)
tb.addStretch()
lv.addLayout(tb)
lv.addWidget(self.viewer, 1)
splitter.addWidget(left)
# --- справа: панель управления ---
panel = QWidget()
pv = QVBoxLayout(panel)
pv.setContentsMargins(8, 8, 8, 8)
pv.setSpacing(6)
# Блок текущего пикселя
pv.addWidget(QLabel("Текущий пиксель:"))
row = QHBoxLayout()
self.swatch = QLabel()
self.swatch.setFixedSize(64, 44)
self.swatch.setStyleSheet("background:#808080; border:1px solid #555;")
row.addWidget(self.swatch)
self.color_info = QLabel("Кликните ЛКМ по карте")
self.color_info.setWordWrap(True)
row.addWidget(self.color_info, 1)
pv.addLayout(row)
# ================= БЛОК РЕГИОНОВ (ФАЙЛ 1) =================
pv.addSpacing(10)
pv.addWidget(QLabel("1. Регионы (Название и цвет):"))
self.name_input = QLineEdit()
self.name_input.setPlaceholderText("Например: pepene")
self.name_input.returnPressed.connect(self.commit)
pv.addWidget(self.name_input)
btn = QPushButton("Назначить / обновить цвет [Enter]")
btn.clicked.connect(self.commit)
pv.addWidget(btn)
self.list_widget = QListWidget()
self.list_widget.itemDoubleClicked.connect(lambda it: self.rename_region(it.data(Qt.UserRole)))
pv.addWidget(self.list_widget, 1)
row2 = QHBoxLayout()
b1 = QPushButton("Переименовать")
b1.clicked.connect(lambda: self.rename_region())
b2 = QPushButton("Удалить")
b2.clicked.connect(self.delete_region)
row2.addWidget(b1)
row2.addWidget(b2)
pv.addLayout(row2)
# ================= БЛОК СТРАН (ФАЙЛ 2) =================
pv.addSpacing(12)
pv.addWidget(QLabel("2. Страны (Теги и группы регионов):"))
subj_form = QFormLayout()
subj_form.setContentsMargins(0, 0, 0, 0)
subj_form.setSpacing(4)
self.subject_tag_input = QLineEdit()
self.subject_tag_input.setPlaceholderText("Тег (например, UCB)")
subj_form.addRow("Тег:", self.subject_tag_input)
self.subject_color_input = QLineEdit()
self.subject_color_input.setPlaceholderText("Цвет (например, #E5EA5D)")
subj_form.addRow("Цвет:", self.subject_color_input)
pv.addLayout(subj_form)
btn_use_color = QPushButton("Взять цвет текущего пикселя")
btn_use_color.clicked.connect(self.use_current_color_for_subject)
pv.addWidget(btn_use_color)
btn_save_subject = QPushButton("Создать / Обновить страну")
btn_save_subject.clicked.connect(self.commit_subject)
pv.addWidget(btn_save_subject)
self.subject_list = QListWidget()
self.subject_list.currentItemChanged.connect(self.on_subject_selected)
pv.addWidget(self.subject_list)
pv.addWidget(QLabel("Регионы в выбранной стране:"))
self.subject_regions_list = QListWidget()
pv.addWidget(self.subject_regions_list)
row_subj = QHBoxLayout()
b_rem_reg = QPushButton("Удалить из страны")
b_rem_reg.clicked.connect(self.remove_region_from_subject)
b_del_subj = QPushButton("Удалить страну")
b_del_subj.clicked.connect(self.delete_subject)
row_subj.addWidget(b_rem_reg)
row_subj.addWidget(b_del_subj)
pv.addLayout(row_subj)
# ================= КНОПКИ ФАЙЛОВ =================
pv.addSpacing(12)
pv.addWidget(QLabel("--- Файл регионов (JSON 1) ---"))
row_files1 = QHBoxLayout()
b_open_reg = QPushButton("Открыть регионы")
b_open_reg.clicked.connect(self.load_regions_json)
b_save_reg = QPushButton("Сохранить регионы")
b_save_reg.setStyleSheet("background:#1565c0; color:white; font-weight:bold; padding:4px;")
b_save_reg.clicked.connect(self.save_regions_json)
row_files1.addWidget(b_open_reg)
row_files1.addWidget(b_save_reg)
pv.addLayout(row_files1)
pv.addSpacing(6)
pv.addWidget(QLabel("--- Файл стран (JSON 2) ---"))
row_files2 = QHBoxLayout()
b_open_subj = QPushButton("Открыть страны")
b_open_subj.clicked.connect(self.load_subjects_json)
b_save_subj = QPushButton("Сохранить страны")
b_save_subj.setStyleSheet("background:#2e7d32; color:white; font-weight:bold; padding:4px;")
b_save_subj.clicked.connect(self.save_subjects_json)
row_files2.addWidget(b_open_subj)
row_files2.addWidget(b_save_subj)
pv.addLayout(row_files2)
splitter.addWidget(panel)
splitter.setStretchFactor(0, 1)
splitter.setStretchFactor(1, 0)
splitter.setSizes([1000, 500])
root.addWidget(splitter)
self.coord_label = QLabel("X: — Y: —")
self.statusBar().addPermanentWidget(self.coord_label)
self.statusBar().showMessage("ЛКМ — пиксель; ПКМ/ЛКМ-драг — панорама; колесо — зум")
# ---------- Логика пикселя и регионов ----------
def on_pixel(self, x, y, rgb):
self.current_rgb = rgb
hexv = rgb_to_hex(rgb)
self.swatch.setStyleSheet("background:{}; border:1px solid #555;".format(hexv))
text = "{}\nRGB ({}, {}, {})\nПиксель ({}, {})".format(hexv, rgb[0], rgb[1], rgb[2], x, y)
owner = self.by_color.get(rgb)
if owner:
text += "\nУже назначен: " + owner
self.name_input.setText(owner)
self._select_in_list(owner)
else:
self.name_input.clear()
self.color_info.setText(text)
self.name_input.setFocus()
self.name_input.selectAll()
# Автодобавление в выбранную страну при клике
current_subject_item = self.subject_list.currentItem()
if current_subject_item:
tag = current_subject_item.data(Qt.UserRole)
region_name = self.by_color.get(rgb)
if region_name:
self.add_region_to_subject(tag, region_name)
def commit(self):
name = self.name_input.text().strip()
if not name:
QMessageBox.warning(self, "Название", "Введите название региона.")
return
if self.current_rgb is None:
QMessageBox.warning(self, "Цвет", "Сначала кликните по пикселю на карте.")
return
rgb = self.current_rgb
hexv = rgb_to_hex(rgb)
owner = self.by_color.get(rgb)
if owner is not None and owner != name:
ret = QMessageBox.question(self, "Цвет занят",
"Цвет {} уже назначен региону «{}».\nПереназначить его региону «{}»?".format(hexv, owner, name),
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if ret != QMessageBox.Yes: return
self.mapping.pop(owner, None)
self.by_color.pop(rgb, None)
if name in self.mapping:
old_rgb = hex_to_rgb(self.mapping[name])
if self.by_color.get(old_rgb) == name:
self.by_color.pop(old_rgb, None)
self.mapping[name] = hexv
self.by_color[rgb] = name
self._rebuild_list(select=name)
self.name_input.clear()
self.name_input.setFocus()
current_subject_item = self.subject_list.currentItem()
if current_subject_item:
tag = current_subject_item.data(Qt.UserRole)
self.add_region_to_subject(tag, name)
def _rebuild_list(self, select=None):
self.list_widget.clear()
for i, (name, hexv) in enumerate(self.mapping.items(), 1):
it = QListWidgetItem("{:>3}. {} {}".format(i, name, hexv))
it.setData(Qt.UserRole, name)
it.setBackground(QColor(hexv))
c = QColor(hexv)
bright = 0.299 * c.red() + 0.587 * c.green() + 0.114 * c.blue()
it.setForeground(QColor(20, 20, 20) if bright > 140 else QColor(245, 245, 245))
self.list_widget.addItem(it)
if select is not None: self._select_in_list(select)
def _select_in_list(self, name):
for i in range(self.list_widget.count()):
if self.list_widget.item(i).data(Qt.UserRole) == name:
self.list_widget.setCurrentRow(i)
return
def rename_region(self, name=None):
if name is None:
it = self.list_widget.currentItem()
if it is None: return
name = it.data(Qt.UserRole)
new, ok = QInputDialog.getText(self, "Переименовать регион", "Новое название:", QLineEdit.Normal, name)
if not ok: return
new = new.strip()
if not new or new == name: return
if new in self.mapping:
QMessageBox.warning(self, "Дубликат", "Регион с таким названием уже есть.")
return
# Обновляем имя во всех странах
for tag in self.subjects_list:
regions = self.subjects[tag]["regions"]
if name in regions:
idx = regions.index(name)
regions[idx] = new
reordered = {}
for k, v in self.mapping.items():
reordered[new if k == name else k] = v
self.mapping = reordered
rgb = hex_to_rgb(self.mapping[new])
self.by_color.pop(rgb, None)
self.by_color[rgb] = new
self._rebuild_list(select=new)
if self.subject_list.currentItem(): self._update_subject_regions()
def delete_region(self):
it = self.list_widget.currentItem()
if it is None: return
name = it.data(Qt.UserRole)
hexv = self.mapping.pop(name, None)
if hexv is not None:
rgb = hex_to_rgb(hexv)
if self.by_color.get(rgb) == name: self.by_color.pop(rgb, None)
# Удаляем из всех стран
for tag in self.subjects_list:
regions = self.subjects[tag]["regions"]
if name in regions: regions.remove(name)
self._rebuild_list()
if self.subject_list.currentItem(): self._update_subject_regions()
# ---------- Логика Стран ----------
def use_current_color_for_subject(self):
if self.current_rgb:
self.subject_color_input.setText(rgb_to_hex(self.current_rgb))
else:
QMessageBox.information(self, "Цвет", "Сначала кликните по пикселю на карте.")
def commit_subject(self):
tag = self.subject_tag_input.text().strip()
color = self.subject_color_input.text().strip().upper()
if not tag:
QMessageBox.warning(self, "Тег", "Введите тег страны.")
return
if not color.startswith("#") or len(color) != 7:
QMessageBox.warning(self, "Цвет", "Введите корректный HEX цвет (например, #E5EA5D).")
return
if tag in self.subjects:
self.subjects[tag]["color"] = color
else:
self.subjects_list.append(tag)
self.subjects[tag] = {"color": color, "regions": []}
self._rebuild_subject_list(select=tag)
self.subject_tag_input.clear()
self.subject_color_input.clear()
def _rebuild_subject_list(self, select=None):
self.subject_list.clear()
for tag in self.subjects_list:
data = self.subjects[tag]
color = data["color"]
text = "{} ({}) [{} рег.]".format(tag, color, len(data["regions"]))
it = QListWidgetItem(text)
it.setData(Qt.UserRole, tag)
it.setBackground(QColor(color))
c = QColor(color)
bright = 0.299 * c.red() + 0.587 * c.green() + 0.114 * c.blue()
it.setForeground(QColor(20, 20, 20) if bright > 140 else QColor(245, 245, 245))
self.subject_list.addItem(it)
if select:
for i in range(self.subject_list.count()):
if self.subject_list.item(i).data(Qt.UserRole) == select:
self.subject_list.setCurrentRow(i)
break
self._update_subject_regions()
def on_subject_selected(self, current, previous):
self._update_subject_regions()
def _update_subject_regions(self):
self.subject_regions_list.clear()
item = self.subject_list.currentItem()
if not item: return
tag = item.data(Qt.UserRole)
regions = self.subjects[tag]["regions"]
for r in regions:
self.subject_regions_list.addItem(r)
def add_region_to_subject(self, tag, region_name):
if tag not in self.subjects: return
if region_name not in self.subjects[tag]["regions"]:
self.subjects[tag]["regions"].append(region_name)
if self.subject_list.currentItem() and self.subject_list.currentItem().data(Qt.UserRole) == tag:
self._update_subject_regions()
self._rebuild_subject_list(select=tag)
def remove_region_from_subject(self):
item = self.subject_list.currentItem()
reg_item = self.subject_regions_list.currentItem()
if not item or not reg_item: return
tag = item.data(Qt.UserRole)
region_name = reg_item.text()
if region_name in self.subjects[tag]["regions"]:
self.subjects[tag]["regions"].remove(region_name)
self._update_subject_regions()
self._rebuild_subject_list(select=tag)
def delete_subject(self):
item = self.subject_list.currentItem()
if not item: return
tag = item.data(Qt.UserRole)
if tag in self.subjects_list: self.subjects_list.remove(tag)
if tag in self.subjects: del self.subjects[tag]
self._rebuild_subject_list()
# ---------- Работа с ФАЙЛОМ 1 (Регионы) ----------
def save_regions_json(self):
if not self.mapping:
QMessageBox.information(self, "Пусто", "Сначала добавьте регионы.")
return
path, _ = QFileDialog.getSaveFileName(self, "Сохранить регионы", "regions.json", "JSON (*.json)")
if not path: return
with open(path, "w", encoding="utf-8") as f:
json.dump(self.mapping, f, ensure_ascii=False, indent=2)
f.write("\n")
self.statusBar().showMessage("Сохранено регионов: {} → {}".format(len(self.mapping), path), 8000)
def load_regions_json(self):
path, _ = QFileDialog.getOpenFileName(self, "Открыть регионы", "", "JSON (*.json)")
if not path: return
try:
with open(path, encoding="utf-8") as f:
data = json.load(f)
if not isinstance(data, dict): raise ValueError("Ожидался JSON-объект (словарь)")
mapping, by_color = {}, {}
for name, value in data.items():
if not isinstance(value, str): continue
v = value.strip().upper()
if len(v) != 7 or not v.startswith("#"): continue
try: rgb = hex_to_rgb(v)
except ValueError: continue
name = str(name)
if rgb in by_color: continue
mapping[name] = rgb_to_hex(rgb)
by_color[rgb] = name
except Exception as e:
QMessageBox.critical(self, "Ошибка чтения", str(e))
return
self.mapping = mapping
self.by_color = by_color
self._rebuild_list()
self.statusBar().showMessage("Загружено регионов: {}".format(len(mapping)), 5000)
# ---------- Работа с ФАЙЛОМ 2 (Страны) ----------
def save_subjects_json(self):
if not self.subjects_list:
QMessageBox.information(self, "Пусто", "Сначала добавьте страны.")
return
path, _ = QFileDialog.getSaveFileName(self, "Сохранить страны", "subjects.json", "JSON (*.json)")
if not path: return
data = {"content": {"subjects": []}}
for tag in self.subjects_list:
subj = self.subjects[tag]
data["content"]["subjects"].append({
"tag": tag,
"color": subj["color"],
"regions": subj["regions"]
})
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
f.write("\n")
self.statusBar().showMessage("Сохранено стран: {} → {}".format(len(self.subjects_list), path), 8000)
def load_subjects_json(self):
path, _ = QFileDialog.getOpenFileName(self, "Открыть страны", "", "JSON (*.json)")
if not path: return
try:
with open(path, encoding="utf-8") as f:
data = json.load(f)
if "content" not in data or "subjects" not in data["content"]:
raise ValueError("Ожидался формат с 'content' -> 'subjects'")
self.subjects_list = []
self.subjects = {}
for subj in data["content"]["subjects"]:
tag = str(subj.get("tag", ""))
color = str(subj.get("color", "")).strip().upper()
regions = [str(r) for r in subj.get("regions", [])]
if tag and color.startswith("#") and len(color) == 7:
self.subjects_list.append(tag)
self.subjects[tag] = {"color": color, "regions": regions}
except Exception as e:
QMessageBox.critical(self, "Ошибка чтения", str(e))
return
self._rebuild_subject_list()
self.statusBar().showMessage("Загружено стран: {}".format(len(self.subjects_list)), 5000)
# ----------------------------------------------------------------------
def main():
app = QApplication(sys.argv)
app.setApplicationName("Region Mapper")
if len(sys.argv) > 1:
path = sys.argv[1]
else:
path, _ = QFileDialog.getOpenFileName(
None, "Открыть карту", "",
"Изображения (*.bmp *.png *.jpg *.jpeg *.tif *.tiff);;Все файлы (*)")
if not path: return
if not os.path.isfile(path):
QMessageBox.critical(None, "Файл не найден", path)
return
try:
win = MainWindow(path)
except Exception as exc:
QMessageBox.critical(None, "Не удалось открыть карту", repr(exc))
raise
win.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()