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