""" 地图生成主流程控制器。 协调模板加载、图层过滤、缩放、文本更新、导出。 """ 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 图层跳过远程连接")