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:
parent
8c41f36e80
commit
f1380dcef5
@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@ -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
512
nudge.py
@ -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()
|
||||||
Loading…
x
Reference in New Issue
Block a user