提升效率并删除不必要的文件
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"""
|
||||
地图生成主流程控制器。
|
||||
协调模板加载、图层过滤、缩放、文本更新、导出。
|
||||
模板加载 → 图层过滤 → 缩放 → 文本更新 → 导出。
|
||||
"""
|
||||
import os
|
||||
import time
|
||||
@@ -9,7 +9,6 @@ from qgis.core import QgsProject, QgsDataSourceUri
|
||||
|
||||
from app.config.paths import get_logger
|
||||
from app.config.qgis_mappings import TABLE_RENAMES, SCHEMA_REPLACEMENTS
|
||||
from .template_cache import TemplateCache
|
||||
from .template_modifier import TemplateModifier
|
||||
from .layer_filter import LayerFilter
|
||||
from .map_exporter import MapExporter
|
||||
@@ -17,56 +16,37 @@ from app.utils.map_zoom import MapZoom
|
||||
|
||||
logger = get_logger("qgis.service")
|
||||
|
||||
# 全局模板缓存(跨请求复用)
|
||||
template_cache = TemplateCache()
|
||||
|
||||
|
||||
class MapService:
|
||||
def __init__(self, config: dict):
|
||||
self.config = config
|
||||
|
||||
def generate(self, model: dict) -> str:
|
||||
"""
|
||||
执行完整的地图生成流程。
|
||||
"""
|
||||
_timing = {} # {step: seconds}
|
||||
_timing = {}
|
||||
t_total = time.time()
|
||||
|
||||
template_path = model["path"]
|
||||
template_name = os.path.basename(template_path)
|
||||
project = QgsProject.instance()
|
||||
is_cache_hit = template_cache.is_loaded(template_path)
|
||||
|
||||
# ── 步骤 1:加载/恢复模板 ──
|
||||
# ── 步骤 1:加载模板 ──
|
||||
t0 = time.time()
|
||||
if is_cache_hit:
|
||||
logger.info(f"[缓存命中] {template_name}")
|
||||
project.clear()
|
||||
project, texts, extent = template_cache.restore_template(template_path)
|
||||
template_cache.reset_project_state(project, texts, extent)
|
||||
else:
|
||||
logger.info(f"[首次加载] {template_name}")
|
||||
project.clear()
|
||||
modifier = TemplateModifier(self.config)
|
||||
actual_path = modifier.modify(template_path)
|
||||
project.read(actual_path)
|
||||
if actual_path != template_path:
|
||||
try:
|
||||
os.remove(actual_path)
|
||||
except OSError:
|
||||
pass
|
||||
project.clear()
|
||||
modifier = TemplateModifier(self.config)
|
||||
actual_path = modifier.modify(template_path)
|
||||
project.read(actual_path)
|
||||
if actual_path != template_path:
|
||||
try:
|
||||
os.remove(actual_path)
|
||||
except OSError:
|
||||
pass
|
||||
_timing["1.load"] = time.time() - t0
|
||||
|
||||
# ── 步骤 2:图层连接修正 ──
|
||||
t0 = time.time()
|
||||
if not is_cache_hit:
|
||||
self._fix_invalid_layers(project)
|
||||
self._fix_invalid_layers(project)
|
||||
_timing["2.connections"] = time.time() - t0
|
||||
|
||||
# ★ 首次加载完成后再保存缓存(确保图层已修正)
|
||||
if not is_cache_hit:
|
||||
template_cache.save_current(template_path, model.get("mapLayout", "A3"))
|
||||
|
||||
# ── 步骤 3:图层过滤 ──
|
||||
t0 = time.time()
|
||||
LayerFilter().apply(project, model)
|
||||
@@ -77,15 +57,11 @@ class MapService:
|
||||
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}"
|
||||
)
|
||||
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"],
|
||||
"X": model["centerX"], "Y": model["centerY"], "value": model["zoomValue"],
|
||||
})
|
||||
_timing["4.zoom"] = time.time() - t0
|
||||
|
||||
@@ -99,18 +75,14 @@ class MapService:
|
||||
|
||||
total = time.time() - t_total
|
||||
steps = ", ".join(f"{k}={v:.1f}s" for k, v in _timing.items())
|
||||
logger.info(
|
||||
f"{'[缓存命中]' if is_cache_hit else '[首次加载]'} "
|
||||
f"{template_name} → {total:.1f}s ({steps})"
|
||||
)
|
||||
logger.info(f"{template_name} → {total:.1f}s ({steps})")
|
||||
return model["name"]
|
||||
|
||||
def _fix_invalid_layers(self, project: QgsProject) -> None:
|
||||
"""只修复 TemplateModifier 处理后仍无效的图层(TemplateModifier 已修正大部分连接)"""
|
||||
"""只修复 TemplateModifier 处理后仍无效的图层"""
|
||||
t0 = time.time()
|
||||
db_config = self.config["db"]
|
||||
override = self.config.get("template_override", {})
|
||||
actual_schema = override.get("actual", {}).get("schema", "qgis")
|
||||
actual_schema = "qgis"
|
||||
|
||||
fixed = 0
|
||||
failed = 0
|
||||
@@ -120,7 +92,7 @@ class MapService:
|
||||
continue
|
||||
total += 1
|
||||
if layer.isValid():
|
||||
continue # TemplateModifier 已修正,跳过
|
||||
continue
|
||||
try:
|
||||
self._fix_postgres_layer(layer, db_config, actual_schema)
|
||||
if layer.isValid():
|
||||
@@ -138,42 +110,21 @@ class MapService:
|
||||
f"修复{fixed}, 仍失败{failed}, 耗时{elapsed:.1f}s"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _resolve_postgres_layers(project: QgsProject) -> None:
|
||||
"""FlagDontResolveLayers 跳过了数据库连接,重新 setDataSource 触发"""
|
||||
for layer in project.mapLayers().values():
|
||||
if layer.providerType() != "postgres":
|
||||
continue
|
||||
try:
|
||||
source = layer.source()
|
||||
if source:
|
||||
layer.setDataSource(source, layer.name(), "postgres")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _fix_postgres_layer(layer, db_config, actual_schema):
|
||||
"""修正单个 PostgreSQL 图层的连接参数"""
|
||||
try:
|
||||
uri = layer.dataProvider().uri()
|
||||
uri.setConnection(
|
||||
db_config["host"],
|
||||
str(db_config["port"]),
|
||||
db_config["database"],
|
||||
db_config["username"],
|
||||
db_config["password"],
|
||||
db_config["host"], str(db_config["port"]),
|
||||
db_config["database"], db_config["username"], db_config["password"],
|
||||
)
|
||||
# Schema 替换
|
||||
uri_str = uri.uri()
|
||||
for old_schema in SCHEMA_REPLACEMENTS:
|
||||
if f'table="{old_schema}".' in uri_str:
|
||||
uri_str = uri_str.replace(
|
||||
f'table="{old_schema}".',
|
||||
f'table="{actual_schema}".',
|
||||
)
|
||||
uri_str = uri_str.replace(f'table="{old_schema}".', f'table="{actual_schema}".')
|
||||
uri = QgsDataSourceUri(uri_str)
|
||||
break
|
||||
# 表名映射
|
||||
uri_str = uri.uri()
|
||||
for old_name, new_name in TABLE_RENAMES.items():
|
||||
full_old = f'table="{actual_schema}"."{old_name}"'
|
||||
@@ -181,17 +132,14 @@ class MapService:
|
||||
if full_old in uri_str:
|
||||
uri_str = uri_str.replace(full_old, full_new)
|
||||
uri = QgsDataSourceUri(uri_str)
|
||||
# SRID 修正
|
||||
uri_str = uri.uri()
|
||||
if " srid=0 " in uri_str:
|
||||
uri_str = uri_str.replace(" srid=0 ", " srid=4326 ")
|
||||
uri = QgsDataSourceUri(uri_str)
|
||||
|
||||
layer.setDataSource(uri.uri(), layer.name(), "postgres")
|
||||
|
||||
if layer.isValid():
|
||||
logger.debug(f" 图层 {layer.name()} 连接更新成功")
|
||||
else:
|
||||
logger.error(f" 图层 {layer.name()} 更新后仍无效")
|
||||
except Exception as e:
|
||||
logger.error(f" 更新图层 {layer.name()} 连接失败: {e}")
|
||||
logger.error(f" 更新图层 {layer.name()} 连接失败: {e}")
|
||||
|
||||
@@ -115,18 +115,10 @@ def main():
|
||||
qgs_app = QgsApplication([], False)
|
||||
qgs_app.initQgis()
|
||||
|
||||
from app.services.qgis.map_service import MapService, template_cache
|
||||
from app.services.qgis.template_modifier import TemplateModifier
|
||||
from app.services.qgis.map_service import MapService
|
||||
from app.config.qgis_mappings import build_static_layers_config, get_gpkg_dir
|
||||
|
||||
# 从磁盘恢复已缓存的模板
|
||||
cached_count = template_cache.load_persistent_cache()
|
||||
print(f"[qgis_daemon] QGIS 已启动, 磁盘缓存: {cached_count} 个模板",
|
||||
file=sys.stderr, flush=True)
|
||||
|
||||
total_cached = len(template_cache._cache)
|
||||
print(f"[qgis_daemon] 就绪: 缓存共{total_cached}个模板",
|
||||
file=sys.stderr, flush=True)
|
||||
print("[qgis_daemon] QGIS 已启动", file=sys.stderr, flush=True)
|
||||
|
||||
# ── 请求处理循环 ──
|
||||
print("[qgis_daemon] 等待请求...", file=sys.stderr, flush=True)
|
||||
@@ -197,7 +189,6 @@ def main():
|
||||
# 清理
|
||||
project = QgsProject.instance()
|
||||
project.clear()
|
||||
template_cache.cleanup()
|
||||
print("[qgis_daemon] 已退出", file=sys.stderr, flush=True)
|
||||
|
||||
|
||||
|
||||
@@ -138,12 +138,6 @@ def main():
|
||||
# 初始化 QgsApplication(只做一次)
|
||||
qgs_app = _init_qgis()
|
||||
|
||||
# 从磁盘恢复已缓存的模板(跨进程加速)
|
||||
from app.services.qgis.map_service import template_cache
|
||||
cached_count = template_cache.load_persistent_cache()
|
||||
if cached_count > 0:
|
||||
print(f"[qgis_runner] 磁盘缓存命中: {cached_count} 个模板", file=sys.stderr)
|
||||
|
||||
try:
|
||||
from app.services.qgis.map_service import MapService
|
||||
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
"""
|
||||
模板缓存引擎(支持持久化)。
|
||||
|
||||
流程:
|
||||
1. 首次加载:TemplateModifier 修改模板 → project.read() → 保存到磁盘缓存
|
||||
2. 同进程内缓存命中:从内存恢复(~1s)
|
||||
3. 跨进程/重启:从磁盘缓存恢复(首次 ~5s,后续 ~1s)
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import time
|
||||
import tempfile
|
||||
|
||||
from qgis.core import QgsProject, QgsRectangle
|
||||
|
||||
from app.config.paths import get_logger
|
||||
|
||||
logger = get_logger("qgis.cache")
|
||||
|
||||
# 持久化缓存目录(项目根目录下)
|
||||
_CACHE_DIR = os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))),
|
||||
"app", "data", "cache", "qgis_templates",
|
||||
)
|
||||
_INDEX_FILE = os.path.join(_CACHE_DIR, "index.json")
|
||||
|
||||
|
||||
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 save_current(self, template_path: str, layout_name: str = "A4") -> None:
|
||||
"""将当前已加载的项目保存到内存缓存 + 磁盘缓存"""
|
||||
project = QgsProject.instance()
|
||||
|
||||
# ── 保存项目到持久化缓存目录 ──
|
||||
os.makedirs(_CACHE_DIR, exist_ok=True)
|
||||
key = self._path_key(template_path)
|
||||
cache_file = os.path.join(_CACHE_DIR, f"{key}.qgz")
|
||||
|
||||
t_save = time.time()
|
||||
project.write(cache_file)
|
||||
logger.info(f" 缓存写入磁盘: {os.path.basename(cache_file)} ({time.time() - t_save:.1f}s)")
|
||||
|
||||
# ── 记录状态 ──
|
||||
texts = {}
|
||||
extent = None
|
||||
layout = project.layoutManager().layoutByName(layout_name)
|
||||
if layout:
|
||||
for item_id in ["mapTitle", "mapTime", "mapUint", "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())
|
||||
|
||||
entry = {
|
||||
"file": cache_file,
|
||||
"texts": texts,
|
||||
"extent": extent,
|
||||
"layout": layout_name,
|
||||
}
|
||||
self._cache[template_path] = entry
|
||||
|
||||
# ── 更新索引文件 ──
|
||||
self._update_index(template_path, entry)
|
||||
logger.info(f" 模板已缓存: {os.path.basename(template_path)} ({len(self._cache)} 个)")
|
||||
|
||||
def restore_template(self, template_path: str) -> tuple:
|
||||
"""从缓存恢复模板"""
|
||||
cached = self._cache.get(template_path)
|
||||
if not cached:
|
||||
raise RuntimeError(f"模板未缓存: {template_path}")
|
||||
|
||||
project = QgsProject.instance()
|
||||
t0 = time.time()
|
||||
|
||||
# 大部分图层已 GPKG 本地化,不需要 FlagDontResolveLayers
|
||||
project.read(cached["file"])
|
||||
|
||||
logger.debug(f" 恢复耗时: {time.time() - t0:.1f}s")
|
||||
return project, cached["texts"], cached["extent"]
|
||||
|
||||
def reset_project_state(self, project: QgsProject, texts: dict, extent) -> None:
|
||||
"""重置项目到干净状态"""
|
||||
for layer in project.mapLayers().values():
|
||||
if layer.subsetString():
|
||||
layer.setSubsetString("")
|
||||
|
||||
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)
|
||||
|
||||
def load_persistent_cache(self) -> int:
|
||||
"""从磁盘恢复所有缓存到内存(qgis_runner 启动时调用)"""
|
||||
if not os.path.isfile(_INDEX_FILE):
|
||||
logger.info(" 磁盘缓存为空,首次启动需要加载模板")
|
||||
return 0
|
||||
|
||||
try:
|
||||
with open(_INDEX_FILE, "r", encoding="utf-8") as f:
|
||||
index = json.load(f)
|
||||
except (json.JSONDecodeError, OSError) as e:
|
||||
logger.warning(f" 缓存索引损坏: {e}")
|
||||
return 0
|
||||
|
||||
count = 0
|
||||
for template_path, entry in index.get("templates", {}).items():
|
||||
cache_file = entry.get("file", "")
|
||||
if not cache_file or not os.path.isfile(cache_file):
|
||||
logger.debug(f" 跳过无效缓存: {os.path.basename(template_path)}")
|
||||
continue
|
||||
texts = entry.get("texts", {})
|
||||
extent = entry.get("extent")
|
||||
# 反序列化 extent
|
||||
if extent and isinstance(extent, dict):
|
||||
extent = QgsRectangle(
|
||||
extent.get("xmin", 0), extent.get("ymin", 0),
|
||||
extent.get("xmax", 0), extent.get("ymax", 0),
|
||||
)
|
||||
else:
|
||||
extent = None
|
||||
self._cache[template_path] = {
|
||||
"file": cache_file,
|
||||
"texts": texts,
|
||||
"extent": extent,
|
||||
"layout": entry.get("layout", "A4"),
|
||||
}
|
||||
count += 1
|
||||
|
||||
logger.info(f" 磁盘缓存加载: {count} 个模板")
|
||||
return count
|
||||
|
||||
def cleanup(self) -> None:
|
||||
"""清理内存缓存(磁盘缓存保留)"""
|
||||
self._cache.clear()
|
||||
logger.info("已清理内存缓存")
|
||||
|
||||
# ── 内部方法 ──
|
||||
|
||||
@staticmethod
|
||||
def _path_key(template_path: str) -> str:
|
||||
"""模板路径 → 缓存文件名 key"""
|
||||
return hashlib.md5(template_path.encode()).hexdigest()[:16]
|
||||
|
||||
def _update_index(self, template_path: str, entry: dict) -> None:
|
||||
"""更新磁盘索引文件"""
|
||||
index = {}
|
||||
if os.path.isfile(_INDEX_FILE):
|
||||
try:
|
||||
with open(_INDEX_FILE, "r", encoding="utf-8") as f:
|
||||
index = json.load(f)
|
||||
except (json.JSONDecodeError, OSError):
|
||||
pass
|
||||
|
||||
index.setdefault("templates", {})[template_path] = {
|
||||
"file": entry["file"],
|
||||
"layout": entry.get("layout", "A4"),
|
||||
"texts": entry.get("texts", {}),
|
||||
"extent": (
|
||||
{
|
||||
"xmin": entry["extent"].xMinimum(),
|
||||
"ymin": entry["extent"].yMinimum(),
|
||||
"xmax": entry["extent"].xMaximum(),
|
||||
"ymax": entry["extent"].yMaximum(),
|
||||
}
|
||||
if entry.get("extent") else None
|
||||
),
|
||||
}
|
||||
|
||||
with open(_INDEX_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(index, f, ensure_ascii=False, indent=2)
|
||||
@@ -57,14 +57,13 @@ class TemplateModifier:
|
||||
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:
|
||||
if not override and not has_static:
|
||||
return template_path
|
||||
|
||||
orig = override["original"] if has_override else None
|
||||
actual = override["actual"] if has_override else None
|
||||
orig = override.get("original") if override else None
|
||||
actual = override.get("actual") if override else None
|
||||
|
||||
try:
|
||||
tmp = tempfile.NamedTemporaryFile(
|
||||
|
||||
Reference in New Issue
Block a user