初始化集成qgis
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
from app.services.qgis.map_service import MapService
|
||||
from app.services.qgis.map_exporter import MapExporter
|
||||
from app.services.qgis.template_modifier import TemplateModifier
|
||||
from app.services.qgis.template_cache import TemplateCache
|
||||
from app.services.qgis.layer_filter import LayerFilter
|
||||
|
||||
__all__ = [
|
||||
'MapService',
|
||||
'MapExporter',
|
||||
'TemplateModifier',
|
||||
'TemplateCache',
|
||||
'LayerFilter',
|
||||
]
|
||||
@@ -0,0 +1,31 @@
|
||||
"""
|
||||
图层过滤模块。按 event 和 eqqueue_id 筛选要素。
|
||||
"""
|
||||
from app.config.paths import get_logger
|
||||
|
||||
logger = get_logger("qgis.filter")
|
||||
|
||||
EVENT_LAYERS = ["eqcenter", "震中"]
|
||||
QUEUE_LAYERS = [
|
||||
"intensity", "intensity_mian",
|
||||
"dz_ryss", "dz_jjss", "dz_rysw", "dz_jzph", "dz_xzjl",
|
||||
]
|
||||
|
||||
|
||||
class LayerFilter:
|
||||
def apply(self, project, model: dict) -> None:
|
||||
"""对项目中的图层应用过滤条件"""
|
||||
event = model.get("event", "")
|
||||
queue_id = model.get("queueId", "")
|
||||
|
||||
logger.info(f"图层过滤: event='{event}', queueId='{queue_id}'")
|
||||
|
||||
for name in EVENT_LAYERS:
|
||||
layers = project.mapLayersByName(name)
|
||||
if layers:
|
||||
layers[0].setSubsetString(f"event = '{event}'")
|
||||
|
||||
for name in QUEUE_LAYERS:
|
||||
layers = project.mapLayersByName(name)
|
||||
if layers:
|
||||
layers[0].setSubsetString(f"eqqueue_id = '{queue_id}'")
|
||||
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
地图导出模块。布局文本更新、比例尺调整、图片导出。
|
||||
"""
|
||||
from qgis.core import QgsLayoutExporter, QgsScaleBarSettings
|
||||
|
||||
from app.config.paths import get_logger
|
||||
|
||||
logger = get_logger("qgis.exporter")
|
||||
|
||||
|
||||
class MapExporter:
|
||||
def __init__(self, config: dict, layout):
|
||||
self.config = config
|
||||
self.layout = layout
|
||||
|
||||
def update_texts(self, model: dict) -> None:
|
||||
"""更新布局中的文本标签"""
|
||||
for key in ["mapTitle", "mapTime", "mapUnit", "info"]:
|
||||
label = self.layout.itemById(key)
|
||||
if label is not None:
|
||||
label.setText(model[key])
|
||||
|
||||
def update_scale_bar(self) -> None:
|
||||
"""调整比例尺为自适应宽度模式"""
|
||||
scale_bar = self.layout.itemById("ScaleBar")
|
||||
if scale_bar is None:
|
||||
logger.warning("比例尺控件不存在")
|
||||
return
|
||||
|
||||
scale_bar.setSegmentSizeMode(
|
||||
QgsScaleBarSettings.SegmentSizeMode.SegmentSizeFitWidth
|
||||
)
|
||||
scale_bar.setMaximumBarWidth(70)
|
||||
scale_bar.setMinimumBarWidth(40)
|
||||
|
||||
def export(self, path: str) -> None:
|
||||
"""导出布局为图片"""
|
||||
dpi = self.config["qgis"]["exportDpi"]
|
||||
|
||||
settings = QgsLayoutExporter.ImageExportSettings()
|
||||
settings.dpi = dpi
|
||||
|
||||
exporter = QgsLayoutExporter(self.layout)
|
||||
result = exporter.exportToImage(path, settings)
|
||||
|
||||
if result != QgsLayoutExporter.Success:
|
||||
raise RuntimeError(f"图片导出失败: {path}")
|
||||
|
||||
logger.info(f"图片已导出: {path}")
|
||||
@@ -0,0 +1,135 @@
|
||||
"""
|
||||
地图生成主流程控制器。
|
||||
协调模板加载、图层过滤、缩放、文本更新、导出。
|
||||
"""
|
||||
import os
|
||||
import time
|
||||
|
||||
from qgis.core import QgsProject, QgsDataSourceUri
|
||||
|
||||
from .template_cache import TemplateCache
|
||||
from .template_modifier import TemplateModifier
|
||||
from .layer_filter import LayerFilter
|
||||
from .map_exporter import MapExporter
|
||||
from app.utils.map_zoom import MapZoom
|
||||
from app.config.paths import get_logger
|
||||
|
||||
logger = get_logger("qgis.service")
|
||||
|
||||
# 全局模板缓存(跨请求复用)
|
||||
template_cache = TemplateCache()
|
||||
|
||||
|
||||
class MapService:
|
||||
def __init__(self, config: dict):
|
||||
self.config = config
|
||||
|
||||
def generate(self, model: dict) -> str:
|
||||
"""
|
||||
执行完整的地图生成流程。
|
||||
|
||||
Args:
|
||||
model: 包含地图参数的字典
|
||||
|
||||
Returns:
|
||||
地图名称
|
||||
"""
|
||||
t_start = time.time()
|
||||
template_path = model["path"]
|
||||
project = QgsProject.instance()
|
||||
is_cache_hit = template_cache.is_loaded(template_path)
|
||||
|
||||
# 加载/恢复模板
|
||||
if is_cache_hit:
|
||||
logger.info(f"[缓存命中] {os.path.basename(template_path)}")
|
||||
project, texts, extent = template_cache.restore_template(template_path)
|
||||
template_cache.reset_project_state(project, texts, extent)
|
||||
else:
|
||||
logger.info(f"[首次加载] {os.path.basename(template_path)}")
|
||||
modifier = TemplateModifier(self.config)
|
||||
actual_path = modifier.modify(template_path)
|
||||
template_cache.load_template(actual_path, layout_name=model.get("mapLayout", "A4"))
|
||||
if actual_path != template_path:
|
||||
try:
|
||||
os.remove(actual_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# 更新 PostgreSQL 图层连接(仅首次加载)
|
||||
if not is_cache_hit:
|
||||
self._update_db_connections(project)
|
||||
|
||||
# 图层过滤
|
||||
LayerFilter().apply(project, model)
|
||||
|
||||
# 地图缩放
|
||||
layout = project.layoutManager().layoutByName(model["mapLayout"])
|
||||
if layout is None:
|
||||
available = [l.name() for l in project.layoutManager().layouts()]
|
||||
raise RuntimeError(
|
||||
f"模板中未找到布局 '{model['mapLayout']}',可用布局:{available}"
|
||||
)
|
||||
|
||||
map_item = layout.itemById("Map")
|
||||
zoom = MapZoom(project, layout, map_item)
|
||||
zoom.execute(model["zoomRule"], {
|
||||
"X": model["centerX"],
|
||||
"Y": model["centerY"],
|
||||
"value": model["zoomValue"],
|
||||
})
|
||||
|
||||
# 文本更新 + 比例尺 + 导出
|
||||
exporter = MapExporter(self.config, layout)
|
||||
exporter.update_texts(model)
|
||||
exporter.update_scale_bar()
|
||||
exporter.export(model["outFile"])
|
||||
|
||||
elapsed = time.time() - t_start
|
||||
logger.info(
|
||||
f"{'[缓存命中]' if is_cache_hit else '[首次加载]'} "
|
||||
f"导出完成: {model['name']},耗时 {elapsed:.1f}s"
|
||||
)
|
||||
return model["name"]
|
||||
|
||||
def _update_db_connections(self, project: QgsProject) -> None:
|
||||
"""更新所有 PostgreSQL 图层的数据库连接参数"""
|
||||
db_config = self.config["db"]
|
||||
override = self.config.get("template_override", {})
|
||||
actual_schema = override.get("actual", {}).get("schema", "qgis")
|
||||
static_count = 0
|
||||
|
||||
for layer in project.mapLayers().values():
|
||||
if layer.providerType() == "ogr":
|
||||
static_count += 1
|
||||
continue
|
||||
|
||||
if layer.providerType() != "postgres":
|
||||
continue
|
||||
|
||||
try:
|
||||
uri = layer.dataProvider().uri()
|
||||
uri.setConnection(
|
||||
db_config["host"],
|
||||
str(db_config["port"]),
|
||||
db_config["database"],
|
||||
db_config["username"],
|
||||
db_config["password"],
|
||||
)
|
||||
# 更新 schema(table="base"."xxx" → table="qgis"."xxx")
|
||||
old_uri = uri.uri()
|
||||
if f'table="{actual_schema}".' not in old_uri:
|
||||
new_uri = old_uri.replace('table="base".', f'table="{actual_schema}".')
|
||||
if new_uri != old_uri:
|
||||
uri = QgsDataSourceUri(new_uri)
|
||||
|
||||
layer.setDataSource(uri.uri(), layer.name(), "postgres")
|
||||
|
||||
if layer.isValid():
|
||||
logger.info(f"图层 {layer.name()} 连接更新成功")
|
||||
else:
|
||||
logger.error(f"图层 {layer.name()} 更新后仍无效")
|
||||
except Exception as e:
|
||||
logger.error(f"更新图层 {layer.name()} 连接失败: {e}")
|
||||
|
||||
if static_count:
|
||||
logger.info(f"静态底图已本地化: {static_count} 个 GPKG 图层跳过远程连接")
|
||||
@@ -0,0 +1,266 @@
|
||||
"""
|
||||
QGIS 环境初始化模块(跨平台:Windows / Linux)。
|
||||
在 server.py lifespan 启动阶段调用 init_qgis_env(),
|
||||
完成共享库注入、环境变量设置、QgsApplication 初始化。
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import platform
|
||||
from pathlib import Path
|
||||
|
||||
from app.config.paths import get_logger
|
||||
|
||||
logger = get_logger("qgis.env")
|
||||
|
||||
_qgs_app = None
|
||||
_initialized = False
|
||||
_IS_WINDOWS = platform.system() == "Windows"
|
||||
|
||||
|
||||
def init_qgis_env(qgis_root: str) -> None:
|
||||
"""
|
||||
初始化 QGIS 运行环境。整个应用生命周期只需调用一次。
|
||||
|
||||
Args:
|
||||
qgis_root: QGIS 安装根目录
|
||||
Windows: "D:/QGIS"
|
||||
Linux: "/usr" 或 "/opt/QGIS"
|
||||
"""
|
||||
global _qgs_app, _initialized
|
||||
if _initialized:
|
||||
return
|
||||
|
||||
qgis_app_dir = _find_qgis_app_dir(qgis_root)
|
||||
|
||||
# 共享库搜索路径(平台相关)
|
||||
# Windows: os.add_dll_directory() 显式注册 DLL 目录
|
||||
# Linux: LD_LIBRARY_PATH 追加 .so 搜索目录
|
||||
_add_library_paths(qgis_root, qgis_app_dir)
|
||||
|
||||
# 环境变量
|
||||
_set_environment(qgis_root, qgis_app_dir)
|
||||
|
||||
# Python 模块路径
|
||||
_add_python_paths(qgis_root, qgis_app_dir)
|
||||
|
||||
# 初始化 QgsApplication
|
||||
_init_qgs_application(qgis_app_dir)
|
||||
|
||||
_initialized = True
|
||||
logger.info(f"QGIS 环境初始化完成 ({platform.system()})")
|
||||
|
||||
|
||||
def cleanup_qgis_env() -> None:
|
||||
"""清理 QGIS 资源(应用退出时调用)"""
|
||||
global _qgs_app, _initialized
|
||||
if _qgs_app is not None:
|
||||
_qgs_app.exitQgis()
|
||||
_qgs_app = None
|
||||
_initialized = False
|
||||
logger.info("QGIS 资源已清理")
|
||||
|
||||
|
||||
def is_qgis_ready() -> bool:
|
||||
"""检查 QGIS 是否已初始化"""
|
||||
return _initialized
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# 平台检测
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
|
||||
def _find_qgis_app_dir(root: str) -> str:
|
||||
"""
|
||||
自动检测 QGIS 应用目录。
|
||||
|
||||
Windows(OSGeo4W 安装): {root}/apps/qgis-ltr/
|
||||
Linux(包管理器安装): {root}/share/qgis/(Python 在 {root}/share/qgis/python)
|
||||
"""
|
||||
if _IS_WINDOWS:
|
||||
return _find_qgis_app_dir_windows(root)
|
||||
else:
|
||||
return _find_qgis_app_dir_linux(root)
|
||||
|
||||
|
||||
def _find_qgis_app_dir_windows(root: str) -> str:
|
||||
"""Windows: 在 {root}/apps/ 下查找 qgis* 目录"""
|
||||
apps_dir = Path(root) / "apps"
|
||||
if apps_dir.is_dir():
|
||||
for name in sorted(apps_dir.iterdir()):
|
||||
if name.name.startswith("qgis") and name.is_dir():
|
||||
logger.info(f"检测到 QGIS 应用目录: {name}")
|
||||
return str(name)
|
||||
fallback = str(apps_dir / "qgis")
|
||||
logger.warning(f"未检测到 qgis* 目录,使用默认路径: {fallback}")
|
||||
return fallback
|
||||
|
||||
|
||||
def _find_qgis_app_dir_linux(root: str) -> str:
|
||||
"""
|
||||
Linux: 返回 QGIS_PREFIX_PATH。
|
||||
包管理器安装的标准路径:
|
||||
Debian/Ubuntu: /usr (qgis-core 在 /usr/lib/qgis/)
|
||||
RHEL/CentOS: /usr
|
||||
QGIS.org 官方: /usr 或 /opt/QGIS
|
||||
"""
|
||||
candidates = [
|
||||
Path(root) / "share" / "qgis", # /usr/share/qgis
|
||||
Path(root) / "lib" / "qgis", # /usr/lib/qgis
|
||||
Path(root) / "share" / "qgis-ltr", # qgis-ltr 变体
|
||||
]
|
||||
for c in candidates:
|
||||
if c.is_dir():
|
||||
logger.info(f"检测到 QGIS 应用目录: {c}")
|
||||
return str(c)
|
||||
|
||||
# 回退:用 root 本身(/usr),由 QgsApplication 自行探测
|
||||
logger.warning(f"未找到 QGIS 标准目录,使用 root: {root}")
|
||||
return str(root)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# 共享库注入
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
|
||||
def _add_library_paths(root: str, qgis_app: str) -> None:
|
||||
"""
|
||||
注入共享库搜索路径。
|
||||
|
||||
Windows: 调用 os.add_dll_directory() 显式注册每个 DLL 目录,
|
||||
Python 3.8+ 不再自动搜索 PATH 中的 DLL。
|
||||
Linux: 追加 LD_LIBRARY_PATH,让动态链接器 (ld-linux) 找到 .so。
|
||||
"""
|
||||
if _IS_WINDOWS:
|
||||
_add_dll_directories_windows(root, qgis_app)
|
||||
else:
|
||||
_add_ld_library_path_linux(root, qgis_app)
|
||||
|
||||
|
||||
def _add_dll_directories_windows(root: str, qgis_app: str) -> None:
|
||||
"""Windows: 显式注册 DLL 搜索目录"""
|
||||
if not hasattr(os, "add_dll_directory"):
|
||||
return # Python < 3.8
|
||||
|
||||
dll_dirs = [
|
||||
os.path.join(root, "apps", "Qt5", "bin"),
|
||||
os.path.join(qgis_app, "bin"),
|
||||
os.path.join(root, "bin"),
|
||||
os.path.join(root, "apps", "gdal", "bin"),
|
||||
os.path.join(root, "apps", "gdal", "lib"),
|
||||
]
|
||||
for d in dll_dirs:
|
||||
if os.path.isdir(d):
|
||||
os.add_dll_directory(d)
|
||||
logger.debug(f"注册 DLL 目录: {d}")
|
||||
|
||||
|
||||
def _add_ld_library_path_linux(root: str, qgis_app: str) -> None:
|
||||
"""
|
||||
Linux: 追加 LD_LIBRARY_PATH,使动态链接器找到 QGIS/GDAL 的 .so。
|
||||
|
||||
QGIS 包管理器安装时的标准 .so 路径:
|
||||
/usr/lib/ — libqgis_core.so, libqgis_analysis.so
|
||||
/usr/lib/qgis/ — 插件 .so
|
||||
/usr/lib/qgis/plugins/ — provider .so
|
||||
/usr/share/qgis/python/ — Python 模块
|
||||
"""
|
||||
ld_dirs = [
|
||||
os.path.join(root, "lib"), # /usr/lib
|
||||
os.path.join(root, "lib", "qgis"), # /usr/lib/qgis
|
||||
os.path.join(root, "lib", "qgis", "plugins"), # /usr/lib/qgis/plugins
|
||||
os.path.join(root, "share", "qgis", "lib"), # 某些安装方式
|
||||
os.path.join(root, "apps", "gdal", "lib"), # OSGeo4W 风格
|
||||
]
|
||||
existing = os.environ.get("LD_LIBRARY_PATH", "")
|
||||
new_dirs = [d for d in ld_dirs if os.path.isdir(d) and d not in existing]
|
||||
if new_dirs:
|
||||
joined = ":".join(new_dirs)
|
||||
os.environ["LD_LIBRARY_PATH"] = f"{joined}:{existing}" if existing else joined
|
||||
logger.info(f"LD_LIBRARY_PATH 追加: {new_dirs}")
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# 环境变量
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
|
||||
def _set_environment(root: str, qgis_app: str) -> None:
|
||||
"""设置 QGIS 和 GDAL 相关环境变量(平台自适应)"""
|
||||
env_vars = {
|
||||
"QGIS_PREFIX_PATH": qgis_app,
|
||||
"PYTHONUTF8": "1",
|
||||
"GDAL_FILENAME_IS_UTF8": "YES",
|
||||
"VSI_CACHE": "TRUE",
|
||||
"VSI_CACHE_SIZE": "1000000",
|
||||
}
|
||||
|
||||
if _IS_WINDOWS:
|
||||
env_vars["QT_PLUGIN_PATH"] = (
|
||||
f"{os.path.join(qgis_app, 'qtplugins')};"
|
||||
f"{os.path.join(root, 'apps', 'Qt5', 'plugins')}"
|
||||
)
|
||||
env_vars["GDAL_DATA"] = os.path.join(root, "apps", "gdal", "share", "gdal")
|
||||
else:
|
||||
# Linux: GDAL 数据通常在系统路径 /usr/share/gdal/ 下
|
||||
gdal_candidates = [
|
||||
os.path.join(root, "share", "gdal"),
|
||||
os.path.join(root, "share", "qgis", "resources"),
|
||||
]
|
||||
for p in gdal_candidates:
|
||||
if os.path.isdir(p):
|
||||
env_vars["GDAL_DATA"] = p
|
||||
break
|
||||
|
||||
# QGIS 插件路径(Linux 标准)
|
||||
qt_plugin = os.path.join(root, "lib", "qt", "plugins")
|
||||
if os.path.isdir(qt_plugin):
|
||||
env_vars["QT_PLUGIN_PATH"] = qt_plugin
|
||||
|
||||
for key, value in env_vars.items():
|
||||
os.environ[key] = value
|
||||
logger.debug(f"环境变量: {key}={value}")
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Python 模块路径
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
|
||||
def _add_python_paths(root: str, qgis_app: str) -> None:
|
||||
"""将 QGIS Python 模块路径加入 sys.path(平台自适应)"""
|
||||
if _IS_WINDOWS:
|
||||
python_paths = [
|
||||
os.path.join(qgis_app, "python"),
|
||||
os.path.join(root, "apps", "Python312", "Lib", "site-packages"),
|
||||
]
|
||||
else:
|
||||
python_paths = [
|
||||
os.path.join(qgis_app, "python"), # /usr/share/qgis/python
|
||||
os.path.join(root, "lib", "python3", "dist-packages"), # Debian/Ubuntu
|
||||
os.path.join(root, "lib", "python3.10", "site-packages"), # 通用
|
||||
]
|
||||
|
||||
for p in python_paths:
|
||||
if os.path.isdir(p) and p not in sys.path:
|
||||
sys.path.insert(0, p)
|
||||
logger.info(f"添加 Python 路径: {p}")
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# QgsApplication 初始化
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
|
||||
def _init_qgs_application(qgis_app: str) -> None:
|
||||
"""创建并初始化 QgsApplication 实例"""
|
||||
global _qgs_app
|
||||
|
||||
from qgis.core import QgsApplication, QgsSettings
|
||||
|
||||
QgsApplication.setPrefixPath(qgis_app, True)
|
||||
_qgs_app = QgsApplication([], False) # False = 不启动 GUI
|
||||
|
||||
settings = QgsSettings()
|
||||
settings.setValue("/qgis/render_decorations", False)
|
||||
settings.setValue("/qgis/parallel_rendering", True)
|
||||
settings.setValue("/qgis/use_spatial_index", True)
|
||||
|
||||
_qgs_app.initQgis()
|
||||
logger.info("QgsApplication 初始化完成")
|
||||
@@ -0,0 +1,120 @@
|
||||
"""
|
||||
模板缓存引擎。解决 QgsProject 单例问题。
|
||||
|
||||
流程:
|
||||
1. 首次请求:project.read() 加载模板(慢,仅一次)
|
||||
2. 加载后 project.write() 保存到临时文件
|
||||
3. 后续同模板请求:从临时文件恢复(快,连接复用)
|
||||
4. 手动恢复文本/过滤/缩放(毫秒级)
|
||||
"""
|
||||
import os
|
||||
import time
|
||||
import tempfile
|
||||
|
||||
from qgis.core import QgsProject, QgsRectangle
|
||||
|
||||
from app.config.paths import get_logger
|
||||
|
||||
logger = get_logger("qgis.cache")
|
||||
|
||||
|
||||
class TemplateCache:
|
||||
def __init__(self):
|
||||
self._cache: dict[str, dict] = {}
|
||||
|
||||
def is_loaded(self, template_path: str) -> bool:
|
||||
return template_path in self._cache
|
||||
|
||||
def load_template(self, template_path: str, layout_name: str = "A4") -> None:
|
||||
"""首次加载模板"""
|
||||
start = time.time()
|
||||
project = QgsProject.instance()
|
||||
|
||||
logger.info(f"首次加载: {os.path.basename(template_path)}")
|
||||
project.read(template_path)
|
||||
logger.info(f"project.read() 耗时: {time.time() - start:.1f}s")
|
||||
|
||||
# 保存到临时文件
|
||||
tmp_file = tempfile.NamedTemporaryFile(
|
||||
suffix=".qgz", delete=False,
|
||||
dir=os.path.dirname(template_path),
|
||||
)
|
||||
tmp_path = tmp_file.name
|
||||
tmp_file.close()
|
||||
|
||||
t_save = time.time()
|
||||
project.write(tmp_path)
|
||||
logger.info(f"项目保存耗时: {time.time() - t_save:.1f}s")
|
||||
|
||||
# 记录初始状态
|
||||
texts = {}
|
||||
extent = None
|
||||
layout = project.layoutManager().layoutByName(layout_name)
|
||||
if layout:
|
||||
for item_id in ["mapTitle", "mapTime", "mapUnit", "info"]:
|
||||
item = layout.itemById(item_id)
|
||||
if item:
|
||||
texts[item_id] = item.text()
|
||||
map_item = layout.itemById("Map")
|
||||
if map_item:
|
||||
extent = QgsRectangle(map_item.extent())
|
||||
|
||||
self._cache[template_path] = {
|
||||
"file": tmp_path,
|
||||
"texts": texts,
|
||||
"extent": extent,
|
||||
"layout": layout_name,
|
||||
}
|
||||
logger.info(f"模板加载完成,总耗时: {time.time() - start:.1f}s")
|
||||
|
||||
def restore_template(self, template_path: str) -> tuple:
|
||||
"""从缓存恢复模板"""
|
||||
cached = self._cache.get(template_path)
|
||||
if not cached:
|
||||
raise RuntimeError(f"模板未缓存: {template_path}")
|
||||
|
||||
start = time.time()
|
||||
project = QgsProject.instance()
|
||||
|
||||
logger.info(f"恢复模板: {os.path.basename(template_path)}")
|
||||
project.read(cached["file"])
|
||||
logger.info(f"project.read() 耗时: {time.time() - start:.1f}s")
|
||||
|
||||
return project, cached["texts"], cached["extent"]
|
||||
|
||||
def reset_project_state(self, project: QgsProject, texts: dict, extent) -> None:
|
||||
"""重置项目到干净状态"""
|
||||
start = time.time()
|
||||
|
||||
for layer in project.mapLayers().values():
|
||||
if layer.subsetString():
|
||||
layer.setSubsetString("")
|
||||
|
||||
# 获取 layout 名称(从缓存中)
|
||||
layout_name = "A4"
|
||||
for cached in self._cache.values():
|
||||
layout_name = cached.get("layout", "A4")
|
||||
break
|
||||
|
||||
layout = project.layoutManager().layoutByName(layout_name)
|
||||
if layout:
|
||||
for item_id, text in texts.items():
|
||||
item = layout.itemById(item_id)
|
||||
if item:
|
||||
item.setText(text)
|
||||
if extent:
|
||||
map_item = layout.itemById("Map")
|
||||
if map_item:
|
||||
map_item.zoomToExtent(extent)
|
||||
|
||||
logger.info(f"状态重置耗时: {time.time() - start:.3f}s")
|
||||
|
||||
def cleanup(self) -> None:
|
||||
"""清理所有临时文件"""
|
||||
for cached in self._cache.values():
|
||||
try:
|
||||
os.remove(cached["file"])
|
||||
except OSError:
|
||||
pass
|
||||
self._cache.clear()
|
||||
logger.info("已清理所有缓存")
|
||||
@@ -0,0 +1,111 @@
|
||||
"""
|
||||
模板 XML 修改模块。在 project.read() 之前:
|
||||
1. 替换 PostgreSQL 连接参数(host/port/dbname/schema)
|
||||
2. 将静态底图的 datasource 替换为本地 GeoPackage 路径
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import zipfile
|
||||
import tempfile
|
||||
|
||||
from app.config.paths import get_logger
|
||||
|
||||
logger = get_logger("qgis.modifier")
|
||||
|
||||
|
||||
class TemplateModifier:
|
||||
def __init__(self, config: dict):
|
||||
self.config = config
|
||||
self._static_map = self._build_static_map()
|
||||
|
||||
def _build_static_map(self) -> dict[str, str]:
|
||||
"""构建 table_key → gpkg_abs_path 的映射"""
|
||||
sl = self.config.get("static_layers")
|
||||
if not sl or not sl.get("enabled", False):
|
||||
return {}
|
||||
|
||||
gpkg_dir = sl["gpkg_dir"]
|
||||
mapping = {}
|
||||
for info in sl["layers"].values():
|
||||
schema, table = info["table"].split(".", 1)
|
||||
xml_key = f'table="{schema}"."{table}"'
|
||||
gpkg_path = os.path.join(gpkg_dir, info["file"]).replace("\\", "/")
|
||||
mapping[xml_key] = gpkg_path
|
||||
return mapping
|
||||
|
||||
def modify(self, template_path: str) -> str:
|
||||
"""修改模板文件,返回修改后的临时 .qgz 路径"""
|
||||
override = self.config.get("template_override")
|
||||
has_override = override and override.get("enabled", False)
|
||||
has_static = bool(self._static_map)
|
||||
|
||||
if not has_override and not has_static:
|
||||
return template_path
|
||||
|
||||
orig = override["original"] if has_override else None
|
||||
actual = override["actual"] if has_override else None
|
||||
|
||||
try:
|
||||
tmp = tempfile.NamedTemporaryFile(
|
||||
suffix=".qgz", delete=False,
|
||||
dir=os.path.dirname(template_path),
|
||||
)
|
||||
tmp_path = tmp.name
|
||||
tmp.close()
|
||||
|
||||
datasource_re = re.compile(r"(<datasource>)(.*?)(</datasource>)", re.DOTALL)
|
||||
table_re = re.compile(r'table="(\w+)"\."(\w+)"')
|
||||
|
||||
with zipfile.ZipFile(template_path, "r") as zin, \
|
||||
zipfile.ZipFile(tmp_path, "w", zipfile.ZIP_DEFLATED) as zout:
|
||||
|
||||
for item in zin.infolist():
|
||||
data = zin.read(item.filename)
|
||||
|
||||
if item.filename.endswith(".qgs"):
|
||||
content = data.decode("utf-8")
|
||||
|
||||
# 替换 host/port/dbname/schema
|
||||
if orig and actual:
|
||||
content = re.sub(
|
||||
rf"(host\s*=\s*['\"])({re.escape(orig['host'])})(['\"])",
|
||||
rf"\g<1>{actual['host']}\3",
|
||||
content,
|
||||
)
|
||||
content = re.sub(
|
||||
rf"(port\s*=\s*['\"])({re.escape(str(orig['port']))})(['\"])",
|
||||
rf"\g<1>{actual['port']}\3",
|
||||
content,
|
||||
)
|
||||
content = re.sub(
|
||||
rf"(dbname\s*=\s*['\"])({re.escape(orig['dbname'])})(['\"])",
|
||||
rf"\g<1>{actual['dbname']}\3",
|
||||
content,
|
||||
)
|
||||
content = content.replace(
|
||||
f'table="{orig["schema"]}".',
|
||||
f'table="{actual["schema"]}".',
|
||||
)
|
||||
|
||||
# 替换静态底图 datasource
|
||||
if self._static_map:
|
||||
def _replace(m):
|
||||
tbl = table_re.search(m.group(2))
|
||||
if tbl:
|
||||
key = f'table="{tbl.group(1)}"."{tbl.group(2)}"'
|
||||
if key in self._static_map:
|
||||
return f"{m.group(1)}{self._static_map[key]}{m.group(3)}"
|
||||
return m.group(0)
|
||||
|
||||
content = datasource_re.sub(_replace, content)
|
||||
|
||||
data = content.encode("utf-8")
|
||||
|
||||
zout.writestr(item, data)
|
||||
|
||||
logger.info(f"模板已修改并写入: {tmp_path}")
|
||||
return tmp_path
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"模板修改失败,将使用原始模板: {e}")
|
||||
return template_path
|
||||
Reference in New Issue
Block a user