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
This commit is contained in:
legatt 2026-10-14 17:20:55 +03:00
parent 8c41f36e80
commit f1380dcef5
6 changed files with 494 additions and 221 deletions

View File

@ -16,8 +16,8 @@
"irtazlin": "#7F7B00", "irtazlin": "#7F7B00",
"akvaria": "#D8D10A", "akvaria": "#D8D10A",
"retorria": "#A5A008", "retorria": "#A5A008",
"north_rachia": "#BFB909", "north-rachia": "#BFB909",
"south_rachia": "#E5DE0B", "south-rachia": "#E5DE0B",
"tirandia": "#B2AC08", "tirandia": "#B2AC08",
"sverlopa": "#FFF719", "sverlopa": "#FFF719",
"strogskia": "#F2EA0C", "strogskia": "#F2EA0C",
@ -46,12 +46,73 @@
"paradizna": "#90B9F5", "paradizna": "#90B9F5",
"dalovburg": "#8A9AD8", "dalovburg": "#8A9AD8",
"koberec": "#59BBFA", "koberec": "#59BBFA",
"kichka_menrosh": "#4EDFFE", "kichka-menrosh": "#4EDFFE",
"chenec": "#44CAFC", "chenec": "#44CAFC",
"ishkivat": "#58DDF3", "ishkivat": "#58DDF3",
"samac": "#57B8F7", "samac": "#57B8F7",
"shkadozhiv": "#66D6DD", "shkadozhiv": "#66D6DD",
"zaknishce": "#5ACBE5", "zaknishce": "#5ACBE5",
"saranchet": "#51DFF1", "saranchet": "#51DFF1",
"nizhnegorsk": "#2AEAFE" "egrinda": "#EED459",
"chron": "#69BAFA",
"right-ribnica": "#57E2E6",
"headwaters": "#FDF651",
"drakat": "#FBC64B",
"doroshchegar": "#FCDA73",
"kontsk": "#ACCABE",
"karabshet": "#D7EBCF",
"port-belfa": "#B9E2C8",
"priozersk": "#90ECBC",
"zallesk": "#D2F69F",
"zelenov": "#2AEAFE",
"kzhechen": "#5AE4D9",
"nizhnesibursk": "#90F5D9",
"elnino": "#AE7ECE",
"labina": "#A5B0D7",
"gynska-sobota": "#31F8F0",
"sarabutolgo-gyt": "#6F9AF7",
"bakly": "#FB6B9C",
"hodeska": "#EE9185",
"zagorie": "#CE6FD8",
"sinitsyno": "#81E2FD",
"barkino": "#E47F93",
"tral": "#E58D92",
"enterba": "#F17B89",
"yuglave": "#FE5D9D",
"marak": "#E8868A",
"bakhansk": "#EE689F",
"castle-ulviun": "#F043C2",
"tornfkills": "#F3C246",
"newpillia": "#D98F96",
"dokbog": "#FAEF3F",
"pitiwos": "#D653D6",
"palave": "#FA27ED",
"orosbund": "#FBA78E",
"translat-edi": "#F96BE3",
"bardaslot": "#F151BB",
"prekaris": "#CE7FB1",
"fardont": "#EBC35A",
"steel-frontier": "#F67F95",
"doox-island": "#D2B58D",
"gigafacility": "#DD8497",
"praeter": "#DF81A0",
"dvadechreta": "#D893E1",
"kechukmagna": "#D76DDD",
"ksiplovkur": "#EA80AF",
"rahadon": "#FE97A4",
"erashdipidipibund": "#FE888E",
"labanav": "#DE68C2",
"redriver": "#ED2BEA",
"dookland": "#F2F7A9",
"bergsland": "#F5B352",
"bargental": "#81C1ED",
"bretesse": "#40F0C9",
"bloydorp": "#5EB7E0",
"karlsborg": "#46BBF6",
"gekelen": "#59C3F5",
"kilborg": "#969EF7",
"paulkerhen": "#A0BDCD",
"tordheim": "#57C0FE",
"sankt-mikhel": "#6A9DF9",
"nederborn": "#22EDFC"
} }

View File

@ -3,7 +3,7 @@
"subjects": [ "subjects": [
{ {
"tag": "UCB", "tag": "UCB",
"color": "#F7EF00", "color": "#E5EA5D",
"regions": [ "regions": [
"dineria", "dineria",
"minderia", "minderia",
@ -22,20 +22,21 @@
"irtazlin", "irtazlin",
"akvaria", "akvaria",
"retorria", "retorria",
"north_rachia", "north-rachia",
"south_rachia", "south-rachia",
"tirandia", "tirandia",
"sverlopa", "sverlopa",
"strogskia", "strogskia",
"fillirand", "fillirand",
"mgahvatia", "mgahvatia",
"glednar", "glednar",
"kima" "kima",
"egrinda"
] ]
}, },
{ {
"tag": "FSC", "tag": "FSC",
"color": "#AB5177", "color": "#D88CC9",
"regions": [ "regions": [
"alende", "alende",
"pepene", "pepene",
@ -46,38 +47,112 @@
"scarna", "scarna",
"dabreg", "dabreg",
"scenra", "scenra",
"prakirol", "prakirol"
"illeton"
] ]
}, },
{ {
"tag": "TRP", "tag": "TRP",
"color": "#F77D93", "color": "#C8ABCC",
"regions": [ "regions": [
"barlakir", "barlakir",
"assental", "assental",
"castle-viktor", "castle-viktor",
"sector-kaya", "sector-kaya",
"castle-avens" "castle-avens",
"castle-ulviun"
] ]
}, },
{ {
"tag": "SFL", "tag": "ISH",
"color": "#392986", "color": "#EADCC5",
"regions": [ "regions": [
"langat",
"illeton",
"nanec", "nanec",
"paradizna", "paradizna",
"dalovburg", "dalovburg",
"koberec", "koberec",
"kichka_menrosh", "kichka-menrosh",
"chenec", "chenec",
"ishkivat", "ishkivat",
"samac", "samac",
"shkadozhiv", "shkadozhiv",
"zaknishce", "zaknishce",
"saranchet" "saranchet",
"chron",
"right-ribnica",
"headwaters",
"drakat",
"doroshchegar",
"kontsk",
"karabshet",
"port-belfa",
"priozersk",
"zallesk",
"zelenov",
"kzhechen",
"nizhnesibursk",
"elnino",
"labina",
"gynska-sobota",
"sarabutolgo-gyt",
"bakly",
"hodeska",
"zagorie",
"sinitsyno",
"barkino",
"tral",
"enterba",
"yuglave",
"marak",
"bakhansk"
]
},
{
"tag": "IID",
"color": "#AD3832",
"regions": [
"tornfkills",
"newpillia",
"dokbog",
"pitiwos",
"palave",
"orosbund",
"translat-edi",
"bardaslot",
"prekaris",
"fardont",
"steel-frontier",
"doox-island",
"gigafacility",
"praeter",
"dvadechreta",
"kechukmagna",
"ksiplovkur",
"rahadon",
"erashdipidipibund",
"labanav",
"redriver",
"dookland",
"bergsland"
]
},
{
"tag": "KAR",
"color": "#A4B5D8",
"regions": [
"bargental",
"bretesse",
"bloydorp",
"karlsborg",
"gekelen",
"kilborg",
"paulkerhen",
"tordheim",
"sankt-mikhel",
"nederborn"
] ]
} }
] ]
} }
} }

View File

@ -1,4 +1,4 @@
#version 330 #version 450
#extension GL_ARB_separate_shader_objects : enable #extension GL_ARB_separate_shader_objects : enable
#extension GL_ARB_shading_language_420pack : enable #extension GL_ARB_shading_language_420pack : enable
@ -9,7 +9,28 @@ layout (location = 1) in vec4 vColor;
layout (location = 0) out vec4 outColor; layout (location = 0) out vec4 outColor;
// RmlUi works in sRGB with premultiplied alpha: vertex colours from the styles and generated textures
// (font glyphs) are premultiplied sRGB values. The swapchain is sRGB, so the GPU expects linear output
// and encodes it to sRGB on write; without conversion colours would be encoded twice and look washed out.
vec3 srgbToLinear(vec3 c) {
return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(vec3(0.04045), c));
}
vec3 linearToSrgb(vec3 c) {
return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(vec3(0.0031308), c));
}
void main() { void main() {
// Textures are uploaded as R8G8B8A8_SRGB, so sampling has already decoded them to linear;
// bring them back to the sRGB values RmlUi generated before combining with the vertex colour.
vec4 texColor = texture(Texture, vUV); vec4 texColor = texture(Texture, vUV);
outColor = vColor * texColor; texColor.rgb = linearToSrgb(texColor.rgb);
}
vec4 color = vColor * texColor; // premultiplied sRGB
if (color.a <= 0.0) discard;
// The pipeline blends with SRC_ALPHA / ONE_MINUS_SRC_ALPHA, so output straight (non-premultiplied) linear colour.
vec3 straight = clamp(color.rgb / color.a, 0.0, 1.0);
outColor = vec4(srgbToLinear(straight), color.a);
}

View File

@ -1,3 +1,3 @@
(@@GQ?HKÔł÷ďÔăČp«QwE\ľů!÷}“U,ł ł×9)†Y\fö˙ęňfŃŘÝĺf¸żĹĚf¬˛ Ąf“™‡Św K@@gQHKÔłĺę])ÜăČpŘŚÉYTľů!Č«Ěe4 $¸ęÜĹ1iD¸.ő.­82±Ľ<C4BD>ÁÜѤµŘŮTfö˙ęňfŃŘÝĺf¸żĹĚf¬˛ Ąf“™‡Św
盘噷w{敊v{ 盘噷w{敊v{
ŃŘw Ą ążw Ţ嬲w÷˙ ęňw ö˙ÎŘwŰĺęňw´}ďę0ďw×_Íň0ęw ńžŞůŽwŻÓvoí¦w{ú¨Đ±zčÁ|wÎEďękűwç‹ýȉ˝­‰Ýwü¤iőą<C591>wŘšŠú»YwţßNüĘDwóÝX÷¸WwÝÖfĺËZńßQ ŃŘw Ą ążw Ţ嬲w÷˙ ęňw ö˙ÎŘwŰĺęňYÔîw´}ďę0ďw×_Íň0ęw ńžŞůŽwŻÓvoí¦w{ú¨Đ±zwÎEďękűwç‹ýȉ˝w­‰ÝÂCđwµűŇčÁ|wü¤iőą<C591>wŘšŠú»YwţßNüĘDwóÝX÷¸WwÝÖfĺËZwńßQúşiwćâWQöýwKĆűsÚüwľĘ¬Ďë×wČ⹼ě<C4BD>wźöŇţę*wŮäZŮő<C5AE>wÎ~®×°Ąwđř1÷šowśkű…‘îwŘoÎýâ<C3BD>w“ä’Ťĺw‰{ńť]ţwІčźhîwFÂó–ŹŮw?ďúÖSÖwí'úާűwăků»Qńw±ÎZĂëw•öŤµŇw—„Ý <C39D>ßwá“ŘÝm×w݀椗ţwŽ<77>ţÂhŢwę+í©÷ňRłőwíÁ<C3AD>Éđ@wŕ·^ö»FwőĂY÷ž–wÍ˝ ţŔWwůťjüí"

Binary file not shown.

512
nudge.py
View File

@ -1,65 +1,49 @@
# -*- coding: utf-8 -*- # -- coding: utf-8 --
""" """
Region Mapper — большая цветовая карта -> JSON {region: "#RRGGBB"}. Region Mapper — работа с двумя JSON: регионы (цвета) и страны (группы).
Зависимости: pip install PyQt5 Pillow Зависимости: pip install PyQt5 Pillow
Запуск: python nudge.py map.bmp Запуск: python nudge.py map.bmp
""" """
import sys import sys
import os import os
import json import json
import math import math
from PyQt5.QtCore import Qt, QPoint, QPointF, QRect, QRectF, QTimer, pyqtSignal from PyQt5.QtCore import Qt, QPoint, QPointF, QRect, QRectF, QTimer, pyqtSignal
from PyQt5.QtGui import QImage, QPixmap, QPainter, QColor, QPen, QKeySequence from PyQt5.QtGui import QImage, QPixmap, QPainter, QColor, QPen, QKeySequence
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QHBoxLayout, QVBoxLayout, QApplication, QMainWindow, QWidget, QHBoxLayout, QVBoxLayout,
QLabel, QLineEdit, QPushButton, QListWidget, QListWidgetItem, QLabel, QLineEdit, QPushButton, QListWidget, QListWidgetItem,
QSplitter, QFileDialog, QMessageBox, QInputDialog, QShortcut, QSplitter, QFileDialog, QMessageBox, QInputDialog, QShortcut,
QFormLayout
) )
from PIL import Image from PIL import Image
Image.MAX_IMAGE_PIXELS = None Image.MAX_IMAGE_PIXELS = None
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
# Утилиты # Утилиты
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
def rgb_to_hex(rgb): def rgb_to_hex(rgb):
return "#{:02X}{:02X}{:02X}".format(*rgb) return "#{:02X}{:02X}{:02X}".format(*rgb)
def hex_to_rgb(s): def hex_to_rgb(s):
s = s.lstrip("#") s = s.lstrip("#")
return int(s[0:2], 16), int(s[2:4], 16), int(s[4:6], 16) return int(s[0:2], 16), int(s[2:4], 16), int(s[4:6], 16)
def load_map(path): def load_map(path):
"""Возвращает (QImage, buffer). buffer нужно держать живым во втором случае."""
# Штатный загрузчик Qt: нет лимита Pillow, нет лишних копий памяти.
img = QImage(path) img = QImage(path)
if not img.isNull(): if not img.isNull():
return img, None return img, None
# Запасной путь через Pillow.
with Image.open(path) as f: with Image.open(path) as f:
pil = f.convert("RGB") pil = f.convert("RGB")
w, h = pil.size w, h = pil.size
raw = pil.tobytes() raw = pil.tobytes()
del pil del pil
# ВАЖНО: этот конструктор QImage НЕ копирует данные.
# Буфер raw обязан жить столько же, сколько живёт QImage.
img = QImage(raw, w, h, w * 3, QImage.Format_RGB888) img = QImage(raw, w, h, w * 3, QImage.Format_RGB888)
if img.isNull(): if img.isNull():
raise RuntimeError("Не удалось декодировать изображение") raise RuntimeError("Не удалось декодировать изображение")
return img, raw return img, raw
def make_overview(img, max_side=4096): def make_overview(img, max_side=4096):
"""Маленькая копия для режима «вся карта на экране»."""
w, h = img.width(), img.height() w, h = img.width(), img.height()
k = min(1.0, max_side / float(max(w, h))) k = min(1.0, max_side / float(max(w, h)))
if k >= 1.0: if k >= 1.0:
@ -69,64 +53,54 @@ def make_overview(img, max_side=4096):
Qt.IgnoreAspectRatio, Qt.SmoothTransformation) Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
return QPixmap.fromImage(small) return QPixmap.fromImage(small)
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
# Просмотр карты # Просмотр карты
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
class MapViewer(QWidget): class MapViewer(QWidget):
pixelPicked = pyqtSignal(int, int, tuple) # x, y, (r, g, b) pixelPicked = pyqtSignal(int, int, tuple)
zoomChanged = pyqtSignal(float) zoomChanged = pyqtSignal(float)
cursorMoved = pyqtSignal(int, int) cursorMoved = pyqtSignal(int, int)
LOD_SWITCH = 0.25 # ниже этого масштаба рисуем уменьшенную копию LOD_SWITCH = 0.25
MAX_SCALE = 64.0 MAX_SCALE = 64.0
def __init__(self, image, raw_buffer, parent=None): def __init__(self, image, raw_buffer, parent=None):
super().__init__(parent) super().__init__(parent)
self.image = image self.image = image
self._raw = raw_buffer # держим буфер Pillow живым self._raw = raw_buffer
self.W = image.width() self.W = image.width()
self.H = image.height() self.H = image.height()
print("Размер карты: {} x {} пикселей".format(self.W, self.H)) print("Размер карты: {} x {} пикселей".format(self.W, self.H))
print("Создаю уменьшенную копию...") print("Создаю уменьшенную копию...")
self.overview = make_overview(image) self.overview = make_overview(image)
self.scale = 1.0 # экранных пикселей на пиксель карты self.scale = 1.0
self.offset = QPointF(0, 0) # куда смещён левый верхний угол карты self.offset = QPointF(0, 0)
self._fit_scale = 1.0 self._fit_scale = 1.0
self._fitted = False self._fitted = False
self._panning = False self._panning = False
self._pan_last = QPoint() self._pan_last = QPoint()
self._press_pos = None self._press_pos = None
self._press_offset = QPointF() self._press_offset = QPointF()
self._dragging = False self._dragging = False
self.marker = None self.marker = None
self.setMouseTracking(True) self.setMouseTracking(True)
self.setFocusPolicy(Qt.StrongFocus) self.setFocusPolicy(Qt.StrongFocus)
self.setMinimumSize(200, 120) self.setMinimumSize(200, 120)
# ---------- координаты ----------
def _to_image(self, pos): def _to_image(self, pos):
x = math.floor((pos.x() - self.offset.x()) / self.scale) x = math.floor((pos.x() - self.offset.x()) / self.scale)
y = math.floor((pos.y() - self.offset.y()) / self.scale) y = math.floor((pos.y() - self.offset.y()) / self.scale)
return x, y return x, y
# ---------- масштаб ----------
def fitToWindow(self): def fitToWindow(self):
vw, vh = self.width(), self.height() vw, vh = self.width(), self.height()
if vw < 2 or vh < 2: if vw < 2 or vh < 2: return
return
s = min(vw / self.W, vh / self.H) s = min(vw / self.W, vh / self.H)
self._fit_scale = s self._fit_scale = s
self.scale = s self.scale = s
self.offset = QPointF((vw - self.W * s) / 2.0, self.offset = QPointF((vw - self.W * s) / 2.0, (vh - self.H * s) / 2.0)
(vh - self.H * s) / 2.0)
self._fitted = True self._fitted = True
self.update() self.update()
self.zoomChanged.emit(s) self.zoomChanged.emit(s)
@ -142,61 +116,46 @@ class MapViewer(QWidget):
def _zoom_at(self, point, new_scale): def _zoom_at(self, point, new_scale):
new_scale = max(self._fit_scale * 0.25, min(self.MAX_SCALE, new_scale)) new_scale = max(self._fit_scale * 0.25, min(self.MAX_SCALE, new_scale))
# Точка карты под курсором должна остаться под курсором.
ix = (point.x() - self.offset.x()) / self.scale ix = (point.x() - self.offset.x()) / self.scale
iy = (point.y() - self.offset.y()) / self.scale iy = (point.y() - self.offset.y()) / self.scale
self.scale = new_scale self.scale = new_scale
self.offset = QPointF(point.x() - ix * new_scale, self.offset = QPointF(point.x() - ix * new_scale, point.y() - iy * new_scale)
point.y() - iy * new_scale)
self.update() self.update()
self.zoomChanged.emit(new_scale) self.zoomChanged.emit(new_scale)
def resizeEvent(self, event): def resizeEvent(self, event):
super().resizeEvent(event) super().resizeEvent(event)
if not self._fitted: if not self._fitted: self.fitToWindow()
self.fitToWindow()
# ---------- отрисовка ----------
def paintEvent(self, event): def paintEvent(self, event):
p = QPainter(self) p = QPainter(self)
p.fillRect(self.rect(), QColor(45, 45, 48)) p.fillRect(self.rect(), QColor(45, 45, 48))
p.setRenderHint(QPainter.SmoothPixmapTransform, self.scale < 1.0) p.setRenderHint(QPainter.SmoothPixmapTransform, self.scale < 1.0)
s = self.scale s = self.scale
ox, oy = self.offset.x(), self.offset.y() ox, oy = self.offset.x(), self.offset.y()
if s < self.LOD_SWITCH: if s < self.LOD_SWITCH:
# Далеко: рисуем маленькую копию, Qt сам обрежет по виджету.
target = QRectF(ox, oy, self.W * s, self.H * s) target = QRectF(ox, oy, self.W * s, self.H * s)
p.drawPixmap(target, self.overview, p.drawPixmap(target, self.overview, QRectF(0, 0, self.overview.width(), self.overview.height()))
QRectF(0, 0, self.overview.width(), self.overview.height()))
else: else:
# Близко: берём из полного разрешения ТОЛЬКО видимый прямоугольник.
x0 = max(0, int(math.floor(-ox / s))) x0 = max(0, int(math.floor(-ox / s)))
y0 = max(0, int(math.floor(-oy / s))) y0 = max(0, int(math.floor(-oy / s)))
x1 = min(self.W, int(math.ceil((self.width() - ox) / s))) x1 = min(self.W, int(math.ceil((self.width() - ox) / s)))
y1 = min(self.H, int(math.ceil((self.height() - oy) / s))) y1 = min(self.H, int(math.ceil((self.height() - oy) / s)))
if x1 > x0 and y1 > y0: if x1 > x0 and y1 > y0:
src = QRect(x0, y0, x1 - x0, y1 - y0) src = QRect(x0, y0, x1 - x0, y1 - y0)
target = QRectF(ox + x0 * s, oy + y0 * s, target = QRectF(ox + x0 * s, oy + y0 * s, (x1 - x0) * s, (y1 - y0) * s)
(x1 - x0) * s, (y1 - y0) * s)
p.drawImage(target, self.image, QRectF(src)) p.drawImage(target, self.image, QRectF(src))
# Рамка карты
p.setPen(QPen(QColor(95, 95, 95), 1)) p.setPen(QPen(QColor(95, 95, 95), 1))
p.drawRect(QRectF(ox, oy, self.W * s, self.H * s)) p.drawRect(QRectF(ox, oy, self.W * s, self.H * s))
# Метка последнего выбранного пикселя
if self.marker is not None and s >= 4: if self.marker is not None and s >= 4:
mx, my = self.marker mx, my = self.marker
p.setPen(QPen(QColor(255, 255, 255), 1)) p.setPen(QPen(QColor(255, 255, 255), 1))
p.drawRect(QRectF(ox + mx * s, oy + my * s, s, s)) p.drawRect(QRectF(ox + mx * s, oy + my * s, s, s))
p.end() p.end()
# ---------- мышь ----------
def wheelEvent(self, event): def wheelEvent(self, event):
d = event.angleDelta().y() d = event.angleDelta().y()
if d: if d:
@ -217,31 +176,25 @@ class MapViewer(QWidget):
if self._panning: if self._panning:
d = event.pos() - self._pan_last d = event.pos() - self._pan_last
self._pan_last = event.pos() self._pan_last = event.pos()
self.offset = QPointF(self.offset.x() + d.x(), self.offset = QPointF(self.offset.x() + d.x(), self.offset.y() + d.y())
self.offset.y() + d.y())
self.update() self.update()
elif self._press_pos is not None: elif self._press_pos is not None:
d = event.pos() - self._press_pos d = event.pos() - self._press_pos
if d.manhattanLength() > 4: if d.manhattanLength() > 4: self._dragging = True
self._dragging = True
if self._dragging: if self._dragging:
self.offset = QPointF(self._press_offset.x() + d.x(), self.offset = QPointF(self._press_offset.x() + d.x(), self._press_offset.y() + d.y())
self._press_offset.y() + d.y())
self.update() self.update()
else: else:
x, y = self._to_image(event.pos()) x, y = self._to_image(event.pos())
if 0 <= x < self.W and 0 <= y < self.H: if 0 <= x < self.W and 0 <= y < self.H: self.cursorMoved.emit(x, y)
self.cursorMoved.emit(x, y) else: self.cursorMoved.emit(-1, -1)
else:
self.cursorMoved.emit(-1, -1)
def mouseReleaseEvent(self, event): def mouseReleaseEvent(self, event):
if event.button() in (Qt.RightButton, Qt.MiddleButton): if event.button() in (Qt.RightButton, Qt.MiddleButton):
self._panning = False self._panning = False
self.setCursor(Qt.ArrowCursor) self.setCursor(Qt.ArrowCursor)
elif event.button() == Qt.LeftButton and self._press_pos is not None: elif event.button() == Qt.LeftButton and self._press_pos is not None:
if not self._dragging: if not self._dragging: self._pick(event.pos())
self._pick(event.pos())
self._press_pos = None self._press_pos = None
self._dragging = False self._dragging = False
@ -254,49 +207,43 @@ class MapViewer(QWidget):
self.pixelPicked.emit(x, y, (c.red(), c.green(), c.blue())) self.pixelPicked.emit(x, y, (c.red(), c.green(), c.blue()))
def keyPressEvent(self, event): def keyPressEvent(self, event):
if event.key() in (Qt.Key_0, Qt.Key_F): if event.key() in (Qt.Key_0, Qt.Key_F): self.fitToWindow()
self.fitToWindow() elif event.key() == Qt.Key_1: self.actualSize()
elif event.key() == Qt.Key_1: elif event.key() in (Qt.Key_Plus, Qt.Key_Equal): self.zoomIn()
self.actualSize() elif event.key() == Qt.Key_Minus: self.zoomOut()
elif event.key() in (Qt.Key_Plus, Qt.Key_Equal): else: super().keyPressEvent(event)
self.zoomIn()
elif event.key() == Qt.Key_Minus:
self.zoomOut()
else:
super().keyPressEvent(event)
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
# Окно # Окно
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
class MainWindow(QMainWindow): class MainWindow(QMainWindow):
def __init__(self, path): def __init__(self, path):
super().__init__() super().__init__()
self.setWindowTitle("Region Mapper — " + os.path.basename(path)) self.setWindowTitle("Region Mapper — " + os.path.basename(path))
self.resize(1400, 850) self.resize(1400, 850)
# Обычный dict: в Python 3.7+ сохраняет порядок вставки. # --- Файл 1: Регионы (плоский словарь) ---
self.mapping = {} # name -> "#RRGGBB" self.mapping = {} # name -> "#RRGGBB"
self.by_color = {} # (r, g, b) -> name self.by_color = {} # (r, g, b) -> name
# --- Файл 2: Страны (вложенная структура) ---
self.subjects_list = [] # список тегов для сохранения порядка
self.subjects = {} # tag -> {"color": str, "regions": list}
self.current_rgb = None self.current_rgb = None
self._shown_once = False self._shown_once = False
image, raw = load_map(path) image, raw = load_map(path)
self.viewer = MapViewer(image, raw) self.viewer = MapViewer(image, raw)
self.viewer.pixelPicked.connect(self.on_pixel) self.viewer.pixelPicked.connect(self.on_pixel)
self.viewer.zoomChanged.connect( self.viewer.zoomChanged.connect(lambda s: self.zoom_label.setText("Масштаб: {:.4f}×".format(s)))
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.viewer.cursorMoved.connect(
lambda x, y: self.coord_label.setText("X: {} Y: {}".format(x, y)))
self._build_ui() self._build_ui()
def showEvent(self, event): def showEvent(self, event):
super().showEvent(event) super().showEvent(event)
if not self._shown_once: if not self._shown_once:
self._shown_once = True self._shown_once = True
# Финальный фит уже после того, как layout раздал размеры.
QTimer.singleShot(0, self.viewer.fitToWindow) QTimer.singleShot(0, self.viewer.fitToWindow)
def _build_ui(self): def _build_ui(self):
@ -306,7 +253,7 @@ class MainWindow(QMainWindow):
root.setContentsMargins(6, 6, 6, 6) root.setContentsMargins(6, 6, 6, 6)
splitter = QSplitter(Qt.Horizontal) splitter = QSplitter(Qt.Horizontal)
# --- слева: тулбар + карта --- # --- слева: карта ---
left = QWidget() left = QWidget()
lv = QVBoxLayout(left) lv = QVBoxLayout(left)
lv.setContentsMargins(0, 0, 0, 0) lv.setContentsMargins(0, 0, 0, 0)
@ -325,12 +272,13 @@ class MainWindow(QMainWindow):
lv.addWidget(self.viewer, 1) lv.addWidget(self.viewer, 1)
splitter.addWidget(left) splitter.addWidget(left)
# --- справа: панель разметки --- # --- справа: панель управления ---
panel = QWidget() panel = QWidget()
pv = QVBoxLayout(panel) pv = QVBoxLayout(panel)
pv.setContentsMargins(8, 8, 8, 8) pv.setContentsMargins(8, 8, 8, 8)
pv.setSpacing(6) pv.setSpacing(6)
# Блок текущего пикселя
pv.addWidget(QLabel("Текущий пиксель:")) pv.addWidget(QLabel("Текущий пиксель:"))
row = QHBoxLayout() row = QHBoxLayout()
self.swatch = QLabel() self.swatch = QLabel()
@ -341,25 +289,23 @@ class MainWindow(QMainWindow):
self.color_info.setWordWrap(True) self.color_info.setWordWrap(True)
row.addWidget(self.color_info, 1) row.addWidget(self.color_info, 1)
pv.addLayout(row) pv.addLayout(row)
# ================= БЛОК РЕГИОНОВ (ФАЙЛ 1) =================
pv.addSpacing(10) pv.addSpacing(10)
pv.addWidget(QLabel("Название региона:")) pv.addWidget(QLabel("1. Регионы (Название и цвет):"))
self.name_input = QLineEdit() self.name_input = QLineEdit()
self.name_input.setPlaceholderText("Например: pepene") self.name_input.setPlaceholderText("Например: pepene")
self.name_input.returnPressed.connect(self.commit) self.name_input.returnPressed.connect(self.commit)
pv.addWidget(self.name_input) pv.addWidget(self.name_input)
btn = QPushButton("Назначить / обновить цвет [Enter]") btn = QPushButton("Назначить / обновить цвет [Enter]")
btn.clicked.connect(self.commit) btn.clicked.connect(self.commit)
pv.addWidget(btn) pv.addWidget(btn)
pv.addSpacing(12)
pv.addWidget(QLabel("Регионы (порядок = порядок в JSON):"))
self.list_widget = QListWidget() self.list_widget = QListWidget()
self.list_widget.itemDoubleClicked.connect( self.list_widget.itemDoubleClicked.connect(lambda it: self.rename_region(it.data(Qt.UserRole)))
lambda it: self.rename_region(it.data(Qt.UserRole)))
pv.addWidget(self.list_widget, 1) pv.addWidget(self.list_widget, 1)
row2 = QHBoxLayout() row2 = QHBoxLayout()
b1 = QPushButton("Переименовать") b1 = QPushButton("Переименовать")
b1.clicked.connect(lambda: self.rename_region()) b1.clicked.connect(lambda: self.rename_region())
@ -369,54 +315,107 @@ class MainWindow(QMainWindow):
row2.addWidget(b2) row2.addWidget(b2)
pv.addLayout(row2) pv.addLayout(row2)
# ================= БЛОК СТРАН (ФАЙЛ 2) =================
pv.addSpacing(12) pv.addSpacing(12)
b_open = QPushButton("Открыть существующий JSON") pv.addWidget(QLabel("2. Страны (Теги и группы регионов):"))
b_open.clicked.connect(self.load_json)
pv.addWidget(b_open) 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)
b_save = QPushButton("Сохранить JSON [Ctrl+S]") # ================= КНОПКИ ФАЙЛОВ =================
b_save.setStyleSheet("background:#2e7d32; color:white; " pv.addSpacing(12)
"font-weight:bold; padding:6px;") pv.addWidget(QLabel("--- Файл регионов (JSON 1) ---"))
b_save.clicked.connect(self.save_json) row_files1 = QHBoxLayout()
pv.addWidget(b_save) 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.addWidget(panel)
splitter.setStretchFactor(0, 1) splitter.setStretchFactor(0, 1)
splitter.setStretchFactor(1, 0) splitter.setStretchFactor(1, 0)
splitter.setSizes([1000, 330]) splitter.setSizes([1000, 500])
root.addWidget(splitter) root.addWidget(splitter)
self.coord_label = QLabel("X: — Y: —") self.coord_label = QLabel("X: — Y: —")
self.statusBar().addPermanentWidget(self.coord_label) self.statusBar().addPermanentWidget(self.coord_label)
self.statusBar().showMessage( self.statusBar().showMessage("ЛКМ — пиксель; ПКМ/ЛКМ-драг — панорама; колесо — зум")
"ЛКМ — выбрать пиксель; ЛКМ-драг / ПКМ — панорама; "
"колесо — зум; 0 — вписать; 1 — 1:1")
QShortcut(QKeySequence("Ctrl+S"), self, self.save_json)
# ---------- выбор пикселя ----------
# ---------- Логика пикселя и регионов ----------
def on_pixel(self, x, y, rgb): def on_pixel(self, x, y, rgb):
self.current_rgb = rgb self.current_rgb = rgb
hexv = rgb_to_hex(rgb) hexv = rgb_to_hex(rgb)
self.swatch.setStyleSheet("background:{}; border:1px solid #555;".format(hexv)) self.swatch.setStyleSheet("background:{}; border:1px solid #555;".format(hexv))
text = "{}\nRGB ({}, {}, {})\nПиксель ({}, {})".format(hexv, rgb[0], rgb[1], rgb[2], x, y)
text = "{}\nRGB ({}, {}, {})\nПиксель ({}, {})".format(
hexv, rgb[0], rgb[1], rgb[2], x, y)
owner = self.by_color.get(rgb) owner = self.by_color.get(rgb)
if owner: if owner:
text += "\nУже назначен: " + owner text += "\nУже назначен: " + owner
self.name_input.setText(owner) # сразу готово к правке self.name_input.setText(owner)
self._select_in_list(owner) self._select_in_list(owner)
else: else:
self.name_input.clear() self.name_input.clear()
self.color_info.setText(text) self.color_info.setText(text)
self.name_input.setFocus() self.name_input.setFocus()
self.name_input.selectAll() 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): def commit(self):
name = self.name_input.text().strip() name = self.name_input.text().strip()
@ -426,36 +425,33 @@ class MainWindow(QMainWindow):
if self.current_rgb is None: if self.current_rgb is None:
QMessageBox.warning(self, "Цвет", "Сначала кликните по пикселю на карте.") QMessageBox.warning(self, "Цвет", "Сначала кликните по пикселю на карте.")
return return
rgb = self.current_rgb rgb = self.current_rgb
hexv = rgb_to_hex(rgb) hexv = rgb_to_hex(rgb)
# Цвет уже занят другим регионом?
owner = self.by_color.get(rgb) owner = self.by_color.get(rgb)
if owner is not None and owner != name: if owner is not None and owner != name:
ret = QMessageBox.question( ret = QMessageBox.question(self, "Цвет занят",
self, "Цвет занят", "Цвет {} уже назначен региону «{}».\nПереназначить его региону «{}»?".format(hexv, owner, name),
"Цвет {} уже назначен региону «{}».\n"
"Переназначить его региону «{}»?".format(hexv, owner, name),
QMessageBox.Yes | QMessageBox.No, QMessageBox.No) QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if ret != QMessageBox.Yes: if ret != QMessageBox.Yes: return
return
self.mapping.pop(owner, None) self.mapping.pop(owner, None)
self.by_color.pop(rgb, None) self.by_color.pop(rgb, None)
# Освобождаем старый цвет, если регион редактируется.
if name in self.mapping: if name in self.mapping:
old_rgb = hex_to_rgb(self.mapping[name]) old_rgb = hex_to_rgb(self.mapping[name])
if self.by_color.get(old_rgb) == name: if self.by_color.get(old_rgb) == name:
self.by_color.pop(old_rgb, None) self.by_color.pop(old_rgb, None)
# Присваивание СУЩЕСТВУЮЩЕМУ ключу не меняет его позицию в dict.
self.mapping[name] = hexv self.mapping[name] = hexv
self.by_color[rgb] = name self.by_color[rgb] = name
self._rebuild_list(select=name) self._rebuild_list(select=name)
self.name_input.clear() self.name_input.clear()
self.name_input.setFocus() 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): def _rebuild_list(self, select=None):
self.list_widget.clear() self.list_widget.clear()
@ -465,11 +461,9 @@ class MainWindow(QMainWindow):
it.setBackground(QColor(hexv)) it.setBackground(QColor(hexv))
c = QColor(hexv) c = QColor(hexv)
bright = 0.299 * c.red() + 0.587 * c.green() + 0.114 * c.blue() bright = 0.299 * c.red() + 0.587 * c.green() + 0.114 * c.blue()
it.setForeground(QColor(20, 20, 20) if bright > 140 it.setForeground(QColor(20, 20, 20) if bright > 140 else QColor(245, 245, 245))
else QColor(245, 245, 245))
self.list_widget.addItem(it) self.list_widget.addItem(it)
if select is not None: if select is not None: self._select_in_list(select)
self._select_in_list(select)
def _select_in_list(self, name): def _select_in_list(self, name):
for i in range(self.list_widget.count()): for i in range(self.list_widget.count()):
@ -480,125 +474,247 @@ class MainWindow(QMainWindow):
def rename_region(self, name=None): def rename_region(self, name=None):
if name is None: if name is None:
it = self.list_widget.currentItem() it = self.list_widget.currentItem()
if it is None: if it is None: return
return
name = it.data(Qt.UserRole) name = it.data(Qt.UserRole)
new, ok = QInputDialog.getText(self, "Переименовать регион", "Новое название:", QLineEdit.Normal, name)
new, ok = QInputDialog.getText(self, "Переименовать регион", if not ok: return
"Новое название:", QLineEdit.Normal, name)
if not ok:
return
new = new.strip() new = new.strip()
if not new or new == name: if not new or new == name: return
return
if new in self.mapping: if new in self.mapping:
QMessageBox.warning(self, "Дубликат", "Регион с таким названием уже есть.") QMessageBox.warning(self, "Дубликат", "Регион с таким названием уже есть.")
return return
# Обновляем имя во всех странах
for tag in self.subjects_list:
regions = self.subjects[tag]["regions"]
if name in regions:
idx = regions.index(name)
regions[idx] = new
# Пересобираем dict, сохраняя позицию переименованного элемента.
reordered = {} reordered = {}
for k, v in self.mapping.items(): for k, v in self.mapping.items():
reordered[new if k == name else k] = v reordered[new if k == name else k] = v
self.mapping = reordered self.mapping = reordered
rgb = hex_to_rgb(self.mapping[new]) rgb = hex_to_rgb(self.mapping[new])
self.by_color.pop(rgb, None) self.by_color.pop(rgb, None)
self.by_color[rgb] = new self.by_color[rgb] = new
self._rebuild_list(select=new) self._rebuild_list(select=new)
if self.subject_list.currentItem(): self._update_subject_regions()
def delete_region(self): def delete_region(self):
it = self.list_widget.currentItem() it = self.list_widget.currentItem()
if it is None: if it is None: return
return
name = it.data(Qt.UserRole) name = it.data(Qt.UserRole)
hexv = self.mapping.pop(name, None) hexv = self.mapping.pop(name, None)
if hexv is not None: if hexv is not None:
rgb = hex_to_rgb(hexv) rgb = hex_to_rgb(hexv)
if self.by_color.get(rgb) == name: if self.by_color.get(rgb) == name: self.by_color.pop(rgb, None)
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() self._rebuild_list()
if self.subject_list.currentItem(): self._update_subject_regions()
# ---------- JSON ---------- # ---------- Логика Стран ----------
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 save_json(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: if not self.mapping:
QMessageBox.information(self, "Пусто", "Сначала добавьте регион.") QMessageBox.information(self, "Пусто", "Сначала добавьте регионы.")
return
path, _ = QFileDialog.getSaveFileName(self, "Сохранить маппинг",
"regions.json", "JSON (*.json)")
if not path:
return return
path, _ = QFileDialog.getSaveFileName(self, "Сохранить регионы", "regions.json", "JSON (*.json)")
if not path: return
with open(path, "w", encoding="utf-8") as f: with open(path, "w", encoding="utf-8") as f:
json.dump(self.mapping, f, ensure_ascii=False, indent=2) json.dump(self.mapping, f, ensure_ascii=False, indent=2)
f.write("\n") f.write("\n")
self.statusBar().showMessage( self.statusBar().showMessage("Сохранено регионов: {} → {}".format(len(self.mapping), path), 8000)
"Сохранено регионов: {} → {}".format(len(self.mapping), path), 8000)
def load_json(self): def load_regions_json(self):
path, _ = QFileDialog.getOpenFileName(self, "Открыть маппинг", path, _ = QFileDialog.getOpenFileName(self, "Открыть регионы", "", "JSON (*.json)")
"", "JSON (*.json)") if not path: return
if not path:
return
try: try:
with open(path, encoding="utf-8") as f: with open(path, encoding="utf-8") as f:
data = json.load(f) data = json.load(f)
if not isinstance(data, dict): if not isinstance(data, dict): raise ValueError("Ожидался JSON-объект (словарь)")
raise ValueError("Ожидался JSON-объект")
mapping, by_color = {}, {} mapping, by_color = {}, {}
for name, value in data.items(): for name, value in data.items():
if not isinstance(value, str): if not isinstance(value, str): continue
continue v = value.strip().upper()
v = value.strip() if len(v) != 7 or not v.startswith("#"): continue
if len(v) != 7 or not v.startswith("#"): try: rgb = hex_to_rgb(v)
continue except ValueError: continue
try:
rgb = hex_to_rgb(v)
except ValueError:
continue
name = str(name) name = str(name)
if rgb in by_color: if rgb in by_color: continue
continue
mapping[name] = rgb_to_hex(rgb) mapping[name] = rgb_to_hex(rgb)
by_color[rgb] = name by_color[rgb] = name
except Exception as e: except Exception as e:
QMessageBox.critical(self, "Ошибка чтения", str(e)) QMessageBox.critical(self, "Ошибка чтения", str(e))
return return
self.mapping = mapping self.mapping = mapping
self.by_color = by_color self.by_color = by_color
self._rebuild_list() self._rebuild_list()
self.statusBar().showMessage("Загружено регионов: {}".format(len(mapping)), 5000) 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(): def main():
app = QApplication(sys.argv) app = QApplication(sys.argv)
app.setApplicationName("Region Mapper") app.setApplicationName("Region Mapper")
if len(sys.argv) > 1: if len(sys.argv) > 1:
path = sys.argv[1] path = sys.argv[1]
else: else:
path, _ = QFileDialog.getOpenFileName( path, _ = QFileDialog.getOpenFileName(
None, "Открыть карту", "", None, "Открыть карту", "",
"Изображения (*.bmp *.png *.jpg *.jpeg *.tif *.tiff);;Все файлы (*)") "Изображения (*.bmp *.png *.jpg *.jpeg *.tif *.tiff);;Все файлы (*)")
if not path: if not path: return
return
if not os.path.isfile(path): if not os.path.isfile(path):
QMessageBox.critical(None, "Файл не найден", path) QMessageBox.critical(None, "Файл не найден", path)
return return
try: try:
win = MainWindow(path) win = MainWindow(path)
except Exception as exc: except Exception as exc:
QMessageBox.critical(None, "Не удалось открыть карту", repr(exc)) QMessageBox.critical(None, "Не удалось открыть карту", repr(exc))
raise raise
win.show() win.show()
sys.exit(app.exec_()) sys.exit(app.exec_())
if __name__ == "__main__": if __name__ == "__main__":
main() main()