2026-06-19 17:04:03 +08:00
|
|
|
"""
|
|
|
|
|
地图生成主流程控制器。
|
|
|
|
|
协调模板加载、图层过滤、缩放、文本更新、导出。
|
|
|
|
|
"""
|
|
|
|
|
import os
|
|
|
|
|
import time
|
|
|
|
|
|
|
|
|
|
from qgis.core import QgsProject, QgsDataSourceUri
|
|
|
|
|
|
2026-06-20 15:50:24 +08:00
|
|
|
from app.config.paths import get_logger
|
|
|
|
|
from app.config.qgis_mappings import TABLE_RENAMES, SCHEMA_REPLACEMENTS
|
2026-06-19 17:04:03 +08:00
|
|
|
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)}")
|
2026-06-20 17:50:17 +08:00
|
|
|
# 直接读原始模板,不做文件级修改(避免 ZIP 兼容性问题)
|
|
|
|
|
project.read(template_path)
|
|
|
|
|
|
|
|
|
|
# 更新图层连接 + GPKG/SRID/表名修正(仅首次加载)
|
2026-06-19 17:04:03 +08:00
|
|
|
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:
|
2026-06-20 17:50:17 +08:00
|
|
|
"""更新图层连接 + SRID修正 + GPKG静态层替换"""
|
2026-06-19 17:04:03 +08:00
|
|
|
db_config = self.config["db"]
|
|
|
|
|
override = self.config.get("template_override", {})
|
|
|
|
|
actual_schema = override.get("actual", {}).get("schema", "qgis")
|
2026-06-20 17:50:17 +08:00
|
|
|
static_config = self.config.get("static_layers", {})
|
|
|
|
|
static_enabled = static_config.get("enabled", False)
|
|
|
|
|
gpkg_dir = static_config.get("gpkg_dir", "")
|
|
|
|
|
static_layers_map = static_config.get("layers", {})
|
|
|
|
|
|
2026-06-19 17:04:03 +08:00
|
|
|
static_count = 0
|
|
|
|
|
|
|
|
|
|
for layer in project.mapLayers().values():
|
2026-06-20 17:50:17 +08:00
|
|
|
provider = layer.providerType()
|
|
|
|
|
|
|
|
|
|
# GPKG 静态层替换:postgres 表 → 本地 GPKG 文件
|
|
|
|
|
if provider == "postgres" and static_enabled:
|
|
|
|
|
uri_str = layer.dataProvider().uri().uri()
|
|
|
|
|
for name, info in static_layers_map.items():
|
|
|
|
|
table_key = info["table"]
|
|
|
|
|
schema, table = table_key.split(".", 1)
|
|
|
|
|
if f'table="{schema}"."{table}"' in uri_str:
|
|
|
|
|
gpkg_path = os.path.join(gpkg_dir, info["file"]).replace("\\", "/")
|
|
|
|
|
layer.setDataSource(gpkg_path, name, "ogr")
|
|
|
|
|
static_count += 1
|
|
|
|
|
logger.debug(f"静态图层 {name} → GPKG")
|
|
|
|
|
break
|
|
|
|
|
else:
|
|
|
|
|
# 没匹配到静态层,继续处理为 postgres
|
|
|
|
|
self._fix_postgres_layer(layer, db_config, actual_schema)
|
2026-06-19 17:04:03 +08:00
|
|
|
continue
|
|
|
|
|
|
2026-06-20 17:50:17 +08:00
|
|
|
if provider == "ogr":
|
|
|
|
|
static_count += 1
|
2026-06-19 17:04:03 +08:00
|
|
|
continue
|
|
|
|
|
|
2026-06-20 17:50:17 +08:00
|
|
|
if provider == "postgres":
|
|
|
|
|
self._fix_postgres_layer(layer, db_config, actual_schema)
|
2026-06-19 17:04:03 +08:00
|
|
|
|
|
|
|
|
if static_count:
|
2026-06-20 17:50:17 +08:00
|
|
|
logger.info(f"静态底图已本地化: {static_count} 个图层")
|
|
|
|
|
|
|
|
|
|
@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"],
|
|
|
|
|
)
|
|
|
|
|
# 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 = 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}"'
|
|
|
|
|
full_new = f'table="{actual_schema}"."{new_name}"'
|
|
|
|
|
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():
|
|
|
|
|
fc = layer.featureCount()
|
|
|
|
|
logger.info(f"图层 {layer.name()} 连接更新成功 ({fc} features)")
|
|
|
|
|
else:
|
|
|
|
|
logger.error(f"图层 {layer.name()} 更新后仍无效")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"更新图层 {layer.name()} 连接失败: {e}")
|