xian_qgis
This commit is contained in:
Generated
+8
@@ -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
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AgentMigrationStateService">
|
||||
<option name="migrationStatus" value="COMPLETED" />
|
||||
</component>
|
||||
</project>
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="ignoredPackages">
|
||||
<value>
|
||||
<list size="1">
|
||||
<item index="0" class="java.lang.String" itemvalue="psycopg2-binary" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<option name="ignoredErrors">
|
||||
<list>
|
||||
<option value="N806" />
|
||||
<option value="N803" />
|
||||
<option value="N802" />
|
||||
</list>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
Generated
+7
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.12 (qgis-make)" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (qgis-py)" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/qgis-xian.iml" filepath="$PROJECT_DIR$/.idea/qgis-xian.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+10
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/venv" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Python 3.12 (qgis-py)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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"] + "'")
|
||||
@@ -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%范围后缩放
|
||||
@@ -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"]
|
||||
+169
@@ -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"
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
# 作为 qgis-api的第三方调用接口
|
||||
@@ -0,0 +1,4 @@
|
||||
fastapi>=0.100.0
|
||||
uvicorn>=0.23.0
|
||||
pydantic>=2.0.0
|
||||
pyyaml>=6.0
|
||||
Reference in New Issue
Block a user