commit 9813d6d1645b7514618a3692387949002e68c89f
Author: zzw <2029503428@qq.com>
Date: Thu Jun 18 10:12:15 2026 +0800
xian_qgis
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..13566b8
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/copilot.data.migration.agent.xml b/.idea/copilot.data.migration.agent.xml
new file mode 100644
index 0000000..4ea72a9
--- /dev/null
+++ b/.idea/copilot.data.migration.agent.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..ebe624a
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..c307b09
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..3b59e4c
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/qgis-xian.iml b/.idea/qgis-xian.iml
new file mode 100644
index 0000000..52e57a8
--- /dev/null
+++ b/.idea/qgis-xian.iml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/apps/config/application.yml b/apps/config/application.yml
new file mode 100644
index 0000000..c335999
--- /dev/null
+++ b/apps/config/application.yml
@@ -0,0 +1,20 @@
+# 配置端口
+web:
+ host: localhost
+ port: 18998
+ allowOrigin: ['*']
+
+# qgis 配置
+qgis:
+ # qgis路径
+ root: "D:/QGIS"
+ # 设置dpi
+ exportDpi: 300
+
+# 连接到自己的数据库
+db:
+ host: 127.0.0.1
+ port: 5432
+ database: yjzyk_xian
+ username: postgres
+ password: Zzw.0401
\ No newline at end of file
diff --git a/apps/core/log.py b/apps/core/log.py
new file mode 100644
index 0000000..81f56ac
--- /dev/null
+++ b/apps/core/log.py
@@ -0,0 +1,59 @@
+import logging # Python内置日志模块
+import os # 文件路径处理模块
+import datetime # 时间处理模块
+
+def info(str):
+ """记录INFO级别的日志到info_日期.log文件"""
+ script_path = os.path.split(os.path.realpath(__file__))[0] # 获取当前脚本所在目录
+ nowTime = datetime.datetime.now().strftime('%Y%m%d') # 获取当前日期(年月日)
+ path = os.path.join(script_path+r'\logs', 'info_' +nowTime+".log") # 日志文件路径(logs子目录下)
+ isExists = os.path.exists(script_path+r'\logs') # 检查logs目录是否存在
+ if not isExists:
+ os.makedirs(script_path+r'\logs') # 不存在则创建logs目录
+ logger = logging.getLogger("run") # 创建名为"run"的日志器
+ logger.setLevel(level=logging.INFO) # 设置日志级别为INFO
+ handler = logging.FileHandler(path) # 创建文件处理器(指定日志文件)
+ handler.setLevel(logging.INFO) # 处理器日志级别为INFO
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s => %(message)s') # 日志格式
+ handler.setFormatter(formatter) # 绑定格式到处理器
+ logger.addHandler(handler) # 日志器添加处理器
+ logger.info(str) # 写入INFO日志
+ logger.removeHandler(handler) # 移除处理器(避免重复输出)
+
+def warning(str):
+ """记录WARNING级别的日志到info_日期.log文件(与INFO共用文件)"""
+ # 逻辑与info()基本一致,区别在于日志级别为WARNING
+ script_path = os.path.split(os.path.realpath(__file__))[0]
+ nowTime = datetime.datetime.now().strftime('%Y%m%d')
+ path = os.path.join(script_path+r'\logs', 'info_' +nowTime+".log")
+ isExists = os.path.exists(script_path+r'\logs')
+ if not isExists:
+ os.makedirs(script_path+r'\logs')
+ logger = logging.getLogger("run")
+ logger.setLevel(level=logging.WARNING)
+ handler = logging.FileHandler(path)
+ handler.setLevel(logging.WARNING)
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s => %(message)s')
+ handler.setFormatter(formatter)
+ logger.addHandler(handler)
+ logger.warning(str)
+ logger.removeHandler(handler)
+
+def error(str):
+ """记录ERROR级别的日志到error_日期.log文件"""
+ # 逻辑与info()基本一致,区别在于日志级别为ERROR,且日志文件前缀为error_
+ script_path = os.path.split(os.path.realpath(__file__))[0]
+ nowTime = datetime.datetime.now().strftime('%Y%m%d')
+ path = os.path.join(script_path+r'\logs', 'error_' +nowTime+".log")
+ isExists = os.path.exists(script_path+r'\logs')
+ if not isExists:
+ os.makedirs(script_path+r'\logs')
+ logger = logging.getLogger("run")
+ logger.setLevel(level=logging.ERROR)
+ handler = logging.FileHandler(path)
+ handler.setLevel(logging.ERROR)
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s => %(message)s')
+ handler.setFormatter(formatter)
+ logger.addHandler(handler)
+ logger.error(str)
+ logger.removeHandler(handler)
\ No newline at end of file
diff --git a/apps/core/map_filter.py b/apps/core/map_filter.py
new file mode 100644
index 0000000..30a54d4
--- /dev/null
+++ b/apps/core/map_filter.py
@@ -0,0 +1,34 @@
+import logging
+
+def Filter(project, model):
+ """
+ 图层过滤,按照不同的规则进行条件过滤(根据event和queueId筛选要素)
+ """
+ if model == None:
+ logging.warning("过滤参数为空!!!")
+ return
+
+ logging.info("图层过滤")
+ # 需要按event过滤的图层名称
+ eqid = ["eqcenter", "震中"]
+ # 需要按eqqueue_id过滤的图层名称(人员伤亡、经济损失等相关图层)
+ eqidAndBatch = ["intensity", "intensity_mian", "dz_ryss", "dz_jjss", "dz_rysw", "dz_jzph", "dz_xzjl"]
+
+ # 按event字段过滤图层(震中相关图层)
+ logging.info("图层过滤--" + "event = '" + model["event"] + "'")
+
+ for i in eqid:
+ layers = project.mapLayersByName(i) # 获取图层
+ if len(layers) > 0:
+ layer = project.mapLayersByName(i)[0]
+ # 设置图层子集字符串(过滤条件)
+ layer.setSubsetString("event = '" + model["event"] + "'")
+
+ # 按eqqueue_id字段过滤图层(灾害相关图层)
+ logging.info("图层过滤--" + "eqqueue_id = '" + model["queueId"] + "'")
+
+ for i in eqidAndBatch:
+ layers = project.mapLayersByName(i)
+ if len(layers) > 0:
+ layer = project.mapLayersByName(i)[0]
+ layer.setSubsetString("eqqueue_id = '" + model["queueId"] + "'")
diff --git a/apps/core/map_utils.py b/apps/core/map_utils.py
new file mode 100644
index 0000000..e79a20b
--- /dev/null
+++ b/apps/core/map_utils.py
@@ -0,0 +1,197 @@
+import logging # 导入自定义日志工具
+from qgis._core import QgsScaleBarSettings, QgsLayoutExporter, QgsCoordinateTransform, QgsCoordinateReferenceSystem, \
+ QgsPointXY, QgsGeometry, QgsRectangle
+
+
+class MapUtils:
+ """出图工具类(封装地图缩放、文本更新、比例尺调整、导出等功能)"""
+
+ def __init__(self, config, project, layout):
+ self.config = config # 配置参数
+ self.project = project # QGIS项目对象
+ self.layout = layout # 布局对象(用于地图排版)
+ self.imap = layout.itemById("Map") # 获取ID为"Map"的地图项
+
+ # 缩放
+ def Zoom(self, rule, data):
+ logging.info(data) # 记录缩放参数日志
+ zoom = MapZoom(self.project, self.layout, self.imap) # 创建缩放操作对象
+ # 2024 Zoom转换(将数字规则映射为方法名)
+ # NO("10", "不缩放"),
+ # PAN("11", "平移"),
+ # LAYER("12", "单图层"),
+ # M_LAYER("13", "多图层"),
+ # DISTANCE("14", "距离"),
+ # M_LAYER2("15", "按图层合并缩放");
+ if rule == "11":
+ rule = "FlatToCenter" # 平移至中心点
+ elif rule == "12":
+ rule = "Layer" # 单图层缩放
+ elif rule == "13":
+ rule = "LayerIntersect" # 多图层相交缩放
+ elif rule == "14":
+ rule = "CenterDistance" # 中心距离缩放
+ elif rule == "15":
+ rule = "LayerMerged" # 多图层合并缩放
+ else:
+ rule = "FlatToCenter" # 默认平移至中心点
+ eval("zoom." + rule + "(data)") # 动态调用对应缩放方法
+
+ # 文本更新
+ def Update(self, model, key):
+ label = self.layout.itemById(key) # 获取布局中ID为key的文本标签
+ if (label != None):
+ label.setText(model[key]) # 更新标签文本为model中key对应的值
+
+ # 比例尺更新
+ def UpdateScale(self):
+ ScaleBar = self.layout.itemById("ScaleBar") # 获取ID为"ScaleBar"的比例尺控件
+ if ScaleBar == None:
+ logging.warning("比例尺不存在或控件标识不等于 ScaleBar") # 日志警告
+ return
+ # 设置比例尺段大小模式为"适应宽度"
+ ScaleBar.setSegmentSizeMode(QgsScaleBarSettings.SegmentSizeMode.SegmentSizeFitWidth)
+ ScaleBar.setMaximumBarWidth(70) # 最大宽度
+ ScaleBar.setMinimumBarWidth(40) # 最小宽度
+
+ # 布局设置器
+ def QgsLayoutSettings(self):
+ dpi = self.config["qgis"]["exportDpi"] # 设置dpi
+ # 设置多个导出格式
+ settings = {
+ 'PDF': QgsLayoutExporter.PdfExportSettings(),
+ 'PNG': QgsLayoutExporter.ImageExportSettings(),
+ 'JPG': QgsLayoutExporter.ImageExportSettings(),
+ 'SVG': QgsLayoutExporter.SvgExportSettings()
+ }
+
+ for img_format in ['PNG', 'JPG']:
+ settings[img_format].dpi = dpi
+ settings['JPG'].jpegQuality = 85
+
+ return settings['PNG']
+
+ # 导出图片
+ def Export(self, path):
+ try:
+ qle = QgsLayoutExporter(self.layout) # 创建布局导出器
+ # 布局设置
+ setting = self.QgsLayoutSettings()
+ # 导出为图片
+ res = qle.exportToImage(path, setting)
+ except Exception as e:
+ raise Exception(f"导出失败... {str(e)}")
+
+
+class MapZoom:
+ """地图缩放操作类(封装多种缩放逻辑)"""
+
+ def __init__(self, project, layout, imap):
+ self.project = project # QGIS项目对象
+ self.layout = layout # 布局对象
+ self.imap = imap # 地图项对象
+
+ # 设置坐标系转换
+ def SetSrc(self, crs):
+ qct = QgsCoordinateTransform() # 坐标系转换对象
+ qct.setDestinationCrs(self.imap.crs()) # 目标坐标系为地图当前坐标系
+ if crs == None:
+ qct.setSourceCrs(QgsCoordinateReferenceSystem("EPSG:4326")) # 源坐标系默认WGS84(经纬度)
+ else:
+ qct.setSourceCrs(crs) # 自定义源坐标系
+ return qct
+
+ # 平移至中心点
+ def FlatToCenter(self, data):
+ logging.info("平移至中心点")
+ qct = self.SetSrc(None) # 源坐标系为WGS84
+ point = QgsPointXY(float(data["X"]), float(data["Y"])) # 解析中心点坐标(经纬度)
+ qgm = QgsGeometry.fromWkt(point.asWkt()) # 转换为QGIS几何对象
+ # 坐标转换(从源坐标系到地图坐标系)
+ qgm.transform(qct, QgsCoordinateTransform.TransformDirection.ForwardTransform)
+ cpoint = qgm.asPoint() # 转换后的中心点
+ # 保持当前地图宽度和高度,以新中心点创建矩形范围
+ qr = QgsRectangle.fromCenterAndSize(cpoint, self.imap.extent().width(), self.imap.extent().height())
+ self.imap.zoomToExtent(qr) # 缩放至该范围(实现平移)
+
+ # 中心距离缩放(以中心点为圆心,指定距离为半径缩放)
+ def CenterDistance(self, data):
+ logging.info("中心距离")
+ qct = self.SetSrc(None)
+ point = QgsPointXY(float(data["X"]), float(data["Y"])) # 中心点坐标
+ qgm = QgsGeometry.fromWkt(point.asWkt())
+ qgm.transform(qct, QgsCoordinateTransform.TransformDirection.ForwardTransform)
+ # 缓冲距离(单位:公里,转换为米后缓冲),并扩大10%范围
+ box = qgm.buffer(float(data["value"]) / 1000, 100).boundingBox() # 平面 4326
+ # box = qgm.buffer(float(data["value"])/111,100).boundingBox()#投影坐标系下的缓冲计算
+ self.imap.zoomToExtent(box.buffered(box.width() * 0.1))
+
+ # 按单图层缩放(缩放至指定图层范围)
+ def Layer(self, data):
+ logging.info("按图层缩放")
+ layers = self.project.mapLayersByName(data["value"]) # 根据名称获取图层
+ if len(layers) == 0:
+ logging.warning("图层不存在:" + data["value"]) # 图层不存在时警告
+ return
+ layer = layers[0]
+ if layer.featureCount() == 0.0: # 图层无要素时,默认平移至中心点
+ self.FlatToCenter(data)
+ return
+
+ qct = self.SetSrc(layer.crs()) # 源坐标系为图层坐标系
+ # 将图层范围转换为几何对象
+ qgm = QgsGeometry.fromWkt(layer.extent().asWktPolygon())
+ qgm.transform(qct, QgsCoordinateTransform.TransformDirection.ForwardTransform) # 转换到地图坐标系
+ box = qgm.boundingBox() # 获取范围矩形
+ self.imap.zoomToExtent(box.buffered(box.width() * 0.1)) # 扩大10%范围后缩放
+
+ # 多图层合并缩放(缩放至多个图层的合并范围)
+ def LayerMerged(self, data):
+ logging.info("多图层合并")
+ layers = data["value"].split(",") # 图层名称以逗号分隔
+ box = None
+ for lay in layers:
+ temp = self.project.mapLayersByName(lay)
+ if len(temp) == 0:
+ logging.warning("图层不存在:" + lay)
+ else:
+ layer = temp[0]
+ qct = self.SetSrc(layer.crs())
+ qgm = QgsGeometry.fromWkt(layer.extent().asWktPolygon())
+ qgm.transform(qct, QgsCoordinateTransform.TransformDirection.ForwardTransform)
+ if box == None:
+ box = qgm.boundingBox() # 初始化范围
+ else:
+ box.combineExtentWith(qgm.boundingBox()) # 合并范围
+ if box != None:
+ self.imap.zoomToExtent(box.buffered(box.width() * 0.1)) # 扩大10%范围后缩放
+
+ # 多图层相交缩放(缩放至多个图层的相交范围)
+ def LayerIntersect(self, data):
+ logging.info("多图层相交")
+ layers = data["value"].split(",") # 图层名称以逗号分隔
+ box = None
+ for lay in layers:
+ temp = self.project.mapLayersByName(lay)
+ if len(temp) == 0:
+ logging.warning("图层不存在:" + lay)
+ else:
+ layer = temp[0]
+ qct = self.SetSrc(layer.crs())
+ layer.selectAll() # 选中图层所有要素
+ geom = None
+ # 合并选中要素的几何范围
+ for feature in layer.selectedFeatureIds():
+ if geom == '':
+ geom = layer.getGeometry(feature)
+ else:
+ geom = geom.combine(layer.getGeometry(feature))
+ qgm = QgsGeometry.fromWkt(geom.asWkt())
+ qgm.transform(qct, QgsCoordinateTransform.TransformDirection.ForwardTransform)
+ if box == None:
+ box = qgm.boundingBox() # 初始化范围
+ else:
+ box = box.intersection(qgm) # 计算相交范围
+ if box != None:
+ box = box.boundingBox()
+ self.imap.zoomToExtent(box.buffered(box.width() * 0.1)) # 扩大10%范围后缩放
diff --git a/apps/core/maps.py b/apps/core/maps.py
new file mode 100644
index 0000000..6d7e6bd
--- /dev/null
+++ b/apps/core/maps.py
@@ -0,0 +1,105 @@
+import os
+import importlib # 动态导入模块
+import logging
+import sys
+from qgis._core import QgsProject
+from .map_utils import MapUtils
+
+
+class Maps:
+ def __init__(self, model, data, config):
+ self.model = model # 震情基本数据
+ self.data = data # 额外数据
+ self.config = config # 配置参数
+
+ def load(self):
+ # ---加载地图模板---
+ project = QgsProject.instance() # 获取QGIS项目实例
+ project.read(self.model["path"]) # 读取地图模板(.qgs或.qgz文件)
+
+ db_config = self.config["db"] # 需在application.yml中添加db配置
+
+ # 遍历所有图层,更新PostgreSQL图层的连接
+ for layer in project.mapLayers().values():
+ if layer.providerType() == "postgres": # 仅处理 PostgreSQL 图层
+ try:
+ # 检查配置项是否完整
+ required_keys = ["host", "port", "database", "username", "password"]
+ missing_keys = [k for k in required_keys if k not in db_config]
+ if missing_keys:
+ logging.error(f"数据库配置缺失键:{missing_keys},跳过图层 {layer.name()}")
+ continue
+
+ # 获取当前图层的数据源 URI
+ uri = layer.dataProvider().uri()
+
+
+ # 使用位置参数传递,而非关键字参数
+ uri.setConnection(
+ db_config["host"], # 主机
+ str(db_config["port"]), # 端口转换为字符串
+ db_config["database"], # 数据库名
+ db_config["username"], # 用户名
+ db_config["password"] # 密码
+ )
+
+ # 重新设置数据源,刷新连接
+ layer.setDataSource(uri.uri(), layer.name(), "postgres")
+
+ # 验证图层是否有效
+ if layer.isValid():
+ logging.info(f"图层 {layer.name()} 数据库连接更新成功")
+ else:
+ logging.error(f"图层 {layer.name()} 更新连接后仍无效,请检查配置")
+ except Exception as e:
+ logging.error(f"更新图层 {layer.name()} 连接失败:{str(e)}")
+
+ logging.info("读取project完成,模板路径:" + self.model["path"] + " 画幅:" + self.model["mapLayout"])
+ # logging.info("图层--" + str(len(project.mapLayersByName('eqcenter')))) # 日志记录特定图层数量
+ qLayout = project.layoutManager().layoutByName(self.model["mapLayout"]) # 获取指定名称的布局
+ imap = qLayout.itemById("Map") # 获取地图项
+ mapUtils = MapUtils(self.config, project, qLayout) # 创建地图工具实例
+
+ # 设置坐标系(当前注释掉,未启用)
+ # kid = 32000 + (700 if float(self.model["centerY"]) < 0 else 600) + (int(float(self.model["centerX"])/6)+31)
+ # logging.info("设置坐标系:" + str(kid))
+ # imap.setCrs(QgsCoordinateReferenceSystem(kid,QgsCoordinateReferenceSystem.CrsType.EpsgCrsId))
+
+ # 修改格网
+ # logging.info("修改格网")
+ # grid = imap.grid()
+ # grid.setLineSymbol(None)
+ # grid.setAnnotationDisplay(QgsLayoutItemMapGrid.DisplayMode.HideAll,QgsLayoutItemMapGrid.BorderSide.Left)
+ # grid.setAnnotationDisplay(QgsLayoutItemMapGrid.DisplayMode.HideAll, QgsLayoutItemMapGrid.BorderSide.Right)
+ # grid.setAnnotationDisplay(QgsLayoutItemMapGrid.DisplayMode.HideAll, QgsLayoutItemMapGrid.BorderSide.Bottom)
+ # grid.setAnnotationDisplay(QgsLayoutItemMapGrid.DisplayMode.HideAll, QgsLayoutItemMapGrid.BorderSide.Top)
+
+ # 按照过滤图层,避免以前的数据干扰(动态加载map_filter.py)
+ if os.path.exists(os.path.join(os.path.split(os.path.realpath(__file__))[0], 'map_filter.py')):
+ # 添加模块所在目录到 Python 搜索路径
+ sys.path.insert(0, os.path.split(os.path.realpath(__file__))[0])
+ importlib.import_module('map_filter').Filter(project, self.model)
+
+ # 缩放地图
+ logging.info("缩放地图")
+ # 调用缩放方法(参数:缩放规则、中心点坐标、缩放值)
+ mapUtils.Zoom(self.model["zoomRule"],
+ {'X': self.model["centerX"], 'Y': self.model["centerY"], 'value': self.model["zoomValue"]})
+
+ # 修改制图时间、单位、地图名称等文本
+ logging.info("正在修改地震信息...")
+ mapUtils.Update(self.model, "mapTitle") # 更新地图标题
+ mapUtils.Update(self.model, "mapTime") # 更新制图时间
+ mapUtils.Update(self.model, "mapUnit") # 更新单位
+ mapUtils.Update(self.model, "info") # 更新地震信息
+
+ # 修改比例尺
+ logging.info("修改比例尺")
+ mapUtils.UpdateScale()
+
+ # 导出图片
+ logging.info("导出图片")
+ mapUtils.Export(self.model["outFile"]) # 导出至指定路径
+
+ logging.info(self.model["event"] + "导出完成") # 记录导出完成日志
+ return self.model["name"]
diff --git a/apps/main.py b/apps/main.py
new file mode 100644
index 0000000..bf04584
--- /dev/null
+++ b/apps/main.py
@@ -0,0 +1,169 @@
+import os
+import sys
+from pathlib import Path
+import logging
+import json
+from pydantic import BaseModel, Field
+from qgis._core import QgsApplication, QgsSettings
+from core.maps import Maps
+import yaml
+from fastapi import FastAPI, Request
+import uvicorn
+import atexit
+
+# 配置日志
+logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
+
+app = FastAPI(
+ title="QGIS 专题图导出",
+ description="基于 pyqgis 的专题图制作与导出,用于生成暴雨、地震灾害类分布图",
+ version="1.0.0"
+)
+
+# 全局QGIS应用实例
+qgs_app = None
+config = None
+
+
+# 模型参数
+class MapModel(BaseModel):
+ centerX: float = Field(..., description="地图中心点X坐标(经度)")
+ centerY: float = Field(..., description="地图中心点Y坐标(纬度)")
+ info: str = Field(..., description="信息文本")
+ event: str = Field(..., description="事件ID(用于图层过滤)")
+ mapLayout: str = Field(..., description="QGIS布局名称(如A3)")
+ mapTime: str = Field(..., description="制图时间文本")
+ mapTitle: str = Field(..., description="地图标题")
+ mapUint: str = Field(..., description="制图单位文本")
+ name: str = Field(..., description="地图名称")
+ outFile: str = Field(..., description="导出文件路径(含文件名)")
+ path: str = Field(..., description="QGIS模板文件路径(.qgs/.qgz)")
+ queueId: str = Field(..., description="队列ID(用于图层过滤)")
+ zoomRule: str = Field(default="1", description="缩放规则")
+ zoomValue: str = Field(default="", description="缩放值")
+
+
+def load_config(config_path=None):
+ # 默认配置文件路径
+ if config_path is None:
+ # 获取当前脚本所在目录的父目录,拼接配置文件路径
+ current_dir = Path(__file__).resolve().parent
+ config_path = current_dir / "config" / "application.yml"
+
+ # 转换为字符串路径
+ config_path_str = str(config_path)
+
+ try:
+ # 检查文件是否存在
+ if not Path(config_path_str).exists():
+ raise FileNotFoundError(f"配置文件不存在:{config_path_str}")
+
+ # 读取并解析 YAML 文件
+ with open(config_path_str, 'r', encoding='utf-8') as f:
+ config_data = yaml.safe_load(f)
+ return config_data
+
+ except Exception as e:
+ logging.error(f"加载配置文件失败:{str(e)}", exc_info=True)
+ print(f"加载配置文件失败:{str(e)}", exc_info=True)
+ raise # 抛出异常,终止程序执行
+
+
+def init_qgis():
+ """
+ 初始化QGIS应用
+ """
+ global qgs_app, config
+
+ # 加载配置
+ if config is None:
+ config = load_config()
+
+ # 初始化QGIS(只初始化一次)
+ if qgs_app is None:
+ root = config['qgis']['root']
+ QgsApplication.setPrefixPath(root, True)
+
+ # 设置环境变量
+ os.environ['QGIS_PREFIX_PATH'] = os.path.join(root, "apps", "qgis")
+ os.environ['PATH'] = os.path.join(root, "bin") + ";" + os.environ["PATH"]
+ os.environ['PATH'] = os.path.join(root, "apps", "qgis", "bin") + ";" + os.environ["PATH"]
+ os.environ['PATH'] = os.path.join(root, "apps", "Python312", "lib") + ";" + os.environ["PATH"]
+
+ # 把QGIS的Python路径加入系统
+ sys.path.insert(0, os.path.join(root, "apps", "qgis", "python"))
+ sys.path.insert(0, os.path.join(root, "apps", "Python312", "Lib", "site-packages"))
+
+ # 创建QgsApplication实例(禁用GUI)
+ qgs_app = QgsApplication([], False)
+
+ # 配置QGIS参数
+ settings = QgsSettings()
+ settings.setValue("/qgis/render_decorations", False)
+ settings.setValue("/qgis/parallel_rendering", True)
+ settings.setValue("/qgis/use_spatial_index", True)
+
+ # 加载QGIS提供者
+ qgs_app.initQgis()
+ logging.info("QGIS初始化完成")
+
+ # 注册程序退出时的清理函数
+ atexit.register(cleanup_qgis)
+
+
+def cleanup_qgis():
+ """
+ 清理QGIS资源
+ """
+ global qgs_app
+ if qgs_app is not None:
+ qgs_app.exitQgis()
+ qgs_app = None
+ logging.info("QGIS资源已清理")
+
+
+def run(model, data):
+ """
+ 执行地图生成逻辑
+ """
+ try:
+ # 核心代码,地图操作
+ mp = Maps(model, data, config)
+ mapName = mp.load() # 执行地图加载、处理、导出流程
+ logging.info(f"地图生成成功:{mapName}")
+ return mapName
+
+ except Exception as e:
+ logging.error(f"地图生成失败:{str(e)}", exc_info=True)
+ print(f"地图生成失败:{str(e)}", exc_info=True)
+ raise # 抛出异常让FastAPI返回错误响应
+
+
+@app.post("/qgis/make/map", summary="地图导出接口")
+async def start(request: Request, model: MapModel):
+ # 打印原始请求体
+ raw_body = await request.body()
+ logging.info(f"原始请求体:{raw_body.decode('utf-8')}")
+
+ logging.info("接收到地图导出请求")
+ # 确保QGIS已初始化
+ init_qgis()
+
+ # 转换请求参数
+ req = model.dict()
+ logging.info(f"解析后的参数:{json.dumps(req, ensure_ascii=False, indent=2)}")
+ # 执行制图逻辑
+ mapName = run(req, None)
+
+ return mapName
+
+
+if __name__ == "__main__":
+ # 启动FastAPI服务
+ uvicorn.run(
+ app="main:app",
+ host="0.0.0.0",
+ port=18998,
+ reload=False, # 生产环境必须关闭reload!reload会导致重复初始化QGIS
+ log_level="info"
+ )
diff --git a/apps/readme.md b/apps/readme.md
new file mode 100644
index 0000000..aaea5b5
--- /dev/null
+++ b/apps/readme.md
@@ -0,0 +1 @@
+# 作为 qgis-api的第三方调用接口
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..64f160c
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,4 @@
+fastapi>=0.100.0
+uvicorn>=0.23.0
+pydantic>=2.0.0
+pyyaml>=6.0
\ No newline at end of file