QGIS提升速度

This commit is contained in:
wzy-warehouse
2026-06-21 13:29:19 +08:00
parent fe3ccd005d
commit 5169ed2f33
28 changed files with 444 additions and 394 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+244
View File
@@ -0,0 +1,244 @@
{
"templates": {
"F:/project/xian/xian_algorithm_new/app/data/template/rainfall/暴雨内涝潜在隐患点及人口分布图.qgz": {
"file": "F:\\project\\xian\\xian_algorithm_new\\app\\data\\cache\\qgis_templates\\4c40d3feeddf4567.qgz",
"layout": "A3",
"texts": {
"mapTitle": "四川长宁6.0级地震震中附近乡镇距离分布图",
"mapTime": "制图时间:2020年11月",
"mapUint": "制图单位:四川省地震应急分服务中心",
"info": "时间:2019年06月17日22点55分\n震级:6.0级\n位置:四川省宜宾市长宁县双河镇"
},
"extent": {
"xmin": 107.95707951802657,
"ymin": 32.9127480757085,
"xmax": 109.03945634967837,
"ymax": 33.622777448228966
}
},
"F:/project/xian/xian_algorithm_new/app/data/template/rainfall/暴雨内涝潜在隐患点及农作物分布图.qgz": {
"file": "F:\\project\\xian\\xian_algorithm_new\\app\\data\\cache\\qgis_templates\\19470628504c856d.qgz",
"layout": "A3",
"texts": {
"mapTitle": "四川长宁6.0级地震震中附近乡镇距离分布图",
"mapTime": "制图时间:2020年11月",
"mapUint": "制图单位:四川省地震应急分服务中心",
"info": "时间:2019年06月17日22点55分\n震级:6.0级\n位置:四川省宜宾市长宁县双河镇"
},
"extent": {
"xmin": 107.95707951802657,
"ymin": 32.9127480757085,
"xmax": 109.03945634967837,
"ymax": 33.622777448228966
}
},
"F:/project/xian/xian_algorithm_new/app/data/template/rainfall/暴雨地灾风险区分布图.qgz": {
"file": "F:\\project\\xian\\xian_algorithm_new\\app\\data\\cache\\qgis_templates\\41f47cbdd9c9c8d0.qgz",
"layout": "A3",
"texts": {
"mapTitle": "四川长宁6.0级地震震中附近乡镇距离分布图",
"mapTime": "制图时间:2020年11月",
"mapUint": "制图单位:四川省地震应急分服务中心",
"info": "时间:2019年06月17日22点55分\n震级:6.0级\n位置:四川省宜宾市长宁县双河镇"
},
"extent": {
"xmin": 107.87768948431949,
"ymin": 32.972841920667406,
"xmax": 108.96006631597123,
"ymax": 33.68287129318787
}
},
"F:/project/xian/xian_algorithm_new/app/data/template/rainfall/暴雨城市生命线工程分布图.qgz": {
"file": "F:\\project\\xian\\xian_algorithm_new\\app\\data\\cache\\qgis_templates\\82ba30aacf8a53dd.qgz",
"layout": "A3",
"texts": {
"mapTitle": "四川长宁6.0级地震震中附近乡镇距离分布图",
"mapTime": "制图时间:2020年11月",
"mapUint": "制图单位:四川省地震应急分服务中心",
"info": "时间:2019年06月17日22点55分\n震级:6.0级\n位置:四川省宜宾市长宁县双河镇"
},
"extent": {
"xmin": 107.46472706539424,
"ymin": 32.921385746843555,
"xmax": 108.51635924537791,
"ymax": 33.61124690706137
}
},
"F:/project/xian/xian_algorithm_new/app/data/template/rainfall/暴雨山洪潜在隐患点及人口分布图.qgz": {
"file": "F:\\project\\xian\\xian_algorithm_new\\app\\data\\cache\\qgis_templates\\66a88abaa976d72a.qgz",
"layout": "A3",
"texts": {
"mapTitle": "四川长宁6.0级地震震中附近乡镇距离分布图",
"mapTime": "制图时间:2020年11月",
"mapUint": "制图单位:四川省地震应急分服务中心",
"info": "时间:2019年06月17日22点55分\n震级:6.0级\n位置:四川省宜宾市长宁县双河镇"
},
"extent": {
"xmin": 107.95707951802657,
"ymin": 32.9127480757085,
"xmax": 109.03945634967837,
"ymax": 33.622777448228966
}
},
"F:/project/xian/xian_algorithm_new/app/data/template/rainfall/暴雨山洪潜在隐患点及农作物分布图.qgz": {
"file": "F:\\project\\xian\\xian_algorithm_new\\app\\data\\cache\\qgis_templates\\3bac2f2636b197e9.qgz",
"layout": "A3",
"texts": {
"mapTitle": "四川长宁6.0级地震震中附近乡镇距离分布图",
"mapTime": "制图时间:2020年11月",
"mapUint": "制图单位:四川省地震应急分服务中心",
"info": "时间:2019年06月17日22点55分\n震级:6.0级\n位置:四川省宜宾市长宁县双河镇"
},
"extent": {
"xmin": 107.95707951802657,
"ymin": 32.9127480757085,
"xmax": 109.03945634967837,
"ymax": 33.622777448228966
}
},
"F:/project/xian/xian_algorithm_new/app/data/template/rainfall/暴雨救援队伍分布图.qgz": {
"file": "F:\\project\\xian\\xian_algorithm_new\\app\\data\\cache\\qgis_templates\\d8c1e99a6640a0fa.qgz",
"layout": "A3",
"texts": {
"mapTitle": "四川长宁6.0级地震震中附近乡镇距离分布图",
"mapTime": "制图时间:2020年11月",
"mapUint": "制图单位:四川省地震应急分服务中心",
"info": "时间:2019年06月17日22点55分\n震级:6.0级\n位置:四川省宜宾市长宁县双河镇"
},
"extent": {
"xmin": 107.95707951802657,
"ymin": 32.9127480757085,
"xmax": 109.03945634967837,
"ymax": 33.622777448228966
}
},
"F:/project/xian/xian_algorithm_new/app/data/template/rainfall/暴雨泥石流潜在隐患点及人口分布图.qgz": {
"file": "F:\\project\\xian\\xian_algorithm_new\\app\\data\\cache\\qgis_templates\\5a480fbeb8b2c66c.qgz",
"layout": "A3",
"texts": {
"mapTitle": "四川长宁6.0级地震震中附近乡镇距离分布图",
"mapTime": "制图时间:2020年11月",
"mapUint": "制图单位:四川省地震应急分服务中心",
"info": "时间:2019年06月17日22点55分\n震级:6.0级\n位置:四川省宜宾市长宁县双河镇"
},
"extent": {
"xmin": 107.95707951802657,
"ymin": 32.9127480757085,
"xmax": 109.03945634967837,
"ymax": 33.622777448228966
}
},
"F:/project/xian/xian_algorithm_new/app/data/template/rainfall/暴雨泥石流潜在隐患点及农作物分布图.qgz": {
"file": "F:\\project\\xian\\xian_algorithm_new\\app\\data\\cache\\qgis_templates\\32dc5df99342a7da.qgz",
"layout": "A3",
"texts": {
"mapTitle": "四川长宁6.0级地震震中附近乡镇距离分布图",
"mapTime": "制图时间:2020年11月",
"mapUint": "制图单位:四川省地震应急分服务中心",
"info": "时间:2019年06月17日22点55分\n震级:6.0级\n位置:四川省宜宾市长宁县双河镇"
},
"extent": {
"xmin": 107.95707951802657,
"ymin": 32.9127480757085,
"xmax": 109.03945634967837,
"ymax": 33.622777448228966
}
},
"F:/project/xian/xian_algorithm_new/app/data/template/rainfall/暴雨滑坡潜在隐患点及人口分布图.qgz": {
"file": "F:\\project\\xian\\xian_algorithm_new\\app\\data\\cache\\qgis_templates\\98c5467c5904eee5.qgz",
"layout": "A3",
"texts": {
"mapTitle": "四川长宁6.0级地震震中附近乡镇距离分布图",
"mapTime": "制图时间:2020年11月",
"mapUint": "制图单位:四川省地震应急分服务中心",
"info": "时间:2019年06月17日22点55分\n震级:6.0级\n位置:四川省宜宾市长宁县双河镇"
},
"extent": {
"xmin": 107.95707951802657,
"ymin": 32.9127480757085,
"xmax": 109.03945634967837,
"ymax": 33.622777448228966
}
},
"F:/project/xian/xian_algorithm_new/app/data/template/rainfall/暴雨滑坡潜在隐患点及农作物分布图.qgz": {
"file": "F:\\project\\xian\\xian_algorithm_new\\app\\data\\cache\\qgis_templates\\bc46a5234f68e54e.qgz",
"layout": "A3",
"texts": {
"mapTitle": "四川长宁6.0级地震震中附近乡镇距离分布图",
"mapTime": "制图时间:2020年11月",
"mapUint": "制图单位:四川省地震应急分服务中心",
"info": "时间:2019年06月17日22点55分\n震级:6.0级\n位置:四川省宜宾市长宁县双河镇"
},
"extent": {
"xmin": 107.95707951802657,
"ymin": 32.9127480757085,
"xmax": 109.03945634967837,
"ymax": 33.622777448228966
}
},
"F:/project/xian/xian_algorithm_new/app/data/template/rainfall/暴雨避难场所分布图.qgz": {
"file": "F:\\project\\xian\\xian_algorithm_new\\app\\data\\cache\\qgis_templates\\d94f0d54dd607cbf.qgz",
"layout": "A3",
"texts": {
"mapTitle": "四川长宁6.0级地震震中附近乡镇距离分布图",
"mapTime": "制图时间:2020年11月",
"mapUint": "制图单位:四川省地震应急分服务中心",
"info": "时间:2019年06月17日22点55分\n震级:6.0级\n位置:四川省宜宾市长宁县双河镇"
},
"extent": {
"xmin": 107.46472706539424,
"ymin": 32.921385746843555,
"xmax": 108.51635924537791,
"ymax": 33.61124690706137
}
},
"F:/project/xian/xian_algorithm_new/app/data/template/rainfall/暴雨防汛物资分布图.qgz": {
"file": "F:\\project\\xian\\xian_algorithm_new\\app\\data\\cache\\qgis_templates\\f0cf04a82b3bfb58.qgz",
"layout": "A3",
"texts": {
"mapTitle": "四川长宁6.0级地震震中附近乡镇距离分布图",
"mapTime": "制图时间:2020年11月",
"mapUint": "制图单位:四川省地震应急分服务中心",
"info": "时间:2019年06月17日22点55分\n震级:6.0级\n位置:四川省宜宾市长宁县双河镇"
},
"extent": {
"xmin": 107.95707951802657,
"ymin": 32.9127480757085,
"xmax": 109.03945634967837,
"ymax": 33.622777448228966
}
},
"F:/project/xian/xian_algorithm_new/app/data/template/rainfall/暴雨附近医院分布图.qgz": {
"file": "F:\\project\\xian\\xian_algorithm_new\\app\\data\\cache\\qgis_templates\\ef423854adc709b4.qgz",
"layout": "A3",
"texts": {
"mapTitle": "四川长宁6.0级地震震中附近乡镇距离分布图",
"mapTime": "制图时间:2020年11月",
"mapUint": "制图单位:四川省地震应急分服务中心",
"info": "时间:2019年06月17日22点55分\n震级:6.0级\n位置:四川省宜宾市长宁县双河镇"
},
"extent": {
"xmin": 107.95707951802657,
"ymin": 32.9127480757085,
"xmax": 109.03945634967837,
"ymax": 33.622777448228966
}
},
"F:/project/xian/xian_algorithm_new/app/data/template/rainfall/暴雨附近水库分布图.qgz": {
"file": "F:\\project\\xian\\xian_algorithm_new\\app\\data\\cache\\qgis_templates\\dc2534d5788a805f.qgz",
"layout": "A3",
"texts": {
"mapTitle": "四川长宁6.0级地震震中附近乡镇距离分布图",
"mapTime": "制图时间:2020年11月",
"mapUint": "制图单位:四川省地震应急分服务中心",
"info": "时间:2019年06月17日22点55分\n震级:6.0级\n位置:四川省宜宾市长宁县双河镇"
},
"extent": {
"xmin": 107.95707951802657,
"ymin": 32.9127480757085,
"xmax": 109.03945634967837,
"ymax": 33.622777448228966
}
}
}
}
+42
View File
@@ -0,0 +1,42 @@
"""
在所有 qgis 动态表中添加 tid 自增主键列
QGIS 的 datasource key='tid' 需要此列
"""
import psycopg2
TABLES = [
'hazard_hydrops',
'lifeline_outfall',
'lifeline_pipe',
'risk_census_population',
'sx_xa_towns',
]
c = psycopg2.connect(
host='47.92.216.173', port=7654,
user='postgres', password='zhangsan',
database='xian_new'
)
c.autocommit = True
cur = c.cursor()
for t in TABLES:
try:
# 检查 tid 列是否存在
cur.execute(f"""
SELECT EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema='qgis' AND table_name='{t}' AND column_name='tid'
)
""")
exists = cur.fetchone()[0]
if exists:
print(f"qgis.{t}: tid 列已存在, 跳过")
else:
cur.execute(f'ALTER TABLE qgis."{t}" ADD COLUMN tid SERIAL')
print(f"qgis.{t}: tid 列添加成功")
except Exception as e:
print(f"qgis.{t}: 失败 - {e}")
c.close()
+72
View File
@@ -0,0 +1,72 @@
"""
批量检查所有 rainfall 模板的图层,看哪些动态图层需要显示、表是否存在
"""
import zipfile, re, os, psycopg2
TEMPLATE_DIR = r"F:\project\xian\xian_algorithm_new\app\data\template\rainfall"
# 收集所有非GPKG动态图层的表名
all_dynamic = set()
for fname in sorted(os.listdir(TEMPLATE_DIR)):
if not fname.endswith('.qgz') or fname.startswith('tmp'):
continue
path = os.path.join(TEMPLATE_DIR, fname)
z = zipfile.ZipFile(path)
qgs_name = [n for n in z.namelist() if n.endswith('.qgs')][0]
content = z.read(qgs_name).decode('utf-8')
maplayer_re = re.compile(r'<maplayer[^>]*>(.*?)</maplayer>', re.DOTALL)
for m in maplayer_re.finditer(content):
block = m.group(1)
name_m = re.search(r'<layername>([^<]+)</layername>', block)
provider_m = re.search(r'<provider[^>]*>(\w+)</provider>', block)
ds_m = re.search(r'<datasource>(.*?)</datasource>', block, re.DOTALL)
provider = provider_m.group(1) if provider_m else '?'
if provider != 'postgres':
continue
ds = ds_m.group(1).strip() if ds_m else ''
table_m = re.search(r'table="(\w+)"\."(\w+)"', ds)
layer_name = name_m.group(1) if name_m else '?'
if table_m:
schema = table_m.group(1)
table = table_m.group(2)
key = f"{schema}.{table}"
all_dynamic.add((layer_name, key))
# 已知的GPKG静态层(会被替换为ogr)
static_tables = {
'base.rivers', 'base.river', 'base.sx', 'base.sx_capital',
'base.sx_street', 'base.sx_xa_county', 'base.sx_xa_county_boundary',
'base.sx_zb_county_boundary', 'base.sx_zb_city', 'base.sx_zb_county',
'base.active_fault', 'base.traffic_expressway', 'base.traffic_provincial',
'base.traffic_railway', 'base.traffic_township', 'base.traffic_trunk_line',
}
# DB检查
c = psycopg2.connect(host='47.92.216.173', port=7654, user='postgres', password='zhangsan', database='xian_new')
c.autocommit = True
cur = c.cursor()
print(f"{'图层名':20s} {'原表':35s} {'qgis中存在':12s} {'行数':>8s}")
print("-" * 80)
for layer_name, table_key in sorted(all_dynamic):
if table_key in static_tables:
continue # 会被GPKG替换,跳过
schema, table = table_key.split('.', 1)
try:
cur.execute(f'SELECT count(*) FROM qgis.{table}')
count = cur.fetchone()[0]
exists = "YES"
except:
exists = "NO"
count = 0
marker = " !!!" if exists == "NO" else ""
print(f"{layer_name:20s} {table_key:35s} {exists:12s} {count:>8,d}{marker}")
c.close()
+54
View File
@@ -0,0 +1,54 @@
"""检查模板中所有图层的渲染顺序(z-order)"""
import zipfile, re, os
TEMPLATE_DIR = r"F:\project\xian\xian_algorithm_new\app\data\template\rainfall"
fname = "暴雨内涝潜在隐患点及人口分布图.qgz"
path = os.path.join(TEMPLATE_DIR, fname)
z = zipfile.ZipFile(path)
qgs_name = [n for n in z.namelist() if n.endswith('.qgs')][0]
content = z.read(qgs_name).decode('utf-8')
# 提取所有 maplayer 块(保持顺序 — 这就是渲染顺序)
maplayer_re = re.compile(r'<maplayer[^>]*>(.*?)</maplayer>', re.DOTALL)
print(f"模板: {fname}")
print(f"{'#':>3s} {'图层名':20s} {'类型':10s} {'可见':4s} {'表名'}")
print("-" * 80)
for i, m in enumerate(maplayer_re.finditer(content)):
block = m.group(1)
# 图层名
name_m = re.search(r'<layername>([^<]+)</layername>', block)
name = name_m.group(1) if name_m else '?'
# provider
provider_m = re.search(r'<provider[^>]*>(\w+)</provider>', block)
provider = provider_m.group(1) if provider_m else '?'
# datasource table
ds_m = re.search(r'<datasource>(.*?)</datasource>', block, re.DOTALL)
ds = ds_m.group(1).strip() if ds_m else ''
table_m = re.search(r'table="(\w+)"\."(\w+)"', ds)
table = f"{table_m.group(1)}.{table_m.group(2)}" if table_m else '?'
# 可见性
visible = 'Y' if 'visible="1"' in m.group(0) or 'visible="1"' in block else 'N'
# 判断图层类型
is_static = table in [
'base.rivers', 'base.river', 'base.sx', 'base.sx_capital',
'base.sx_street', 'base.sx_xa_county', 'base.sx_xa_county_boundary',
'base.sx_zb_county_boundary', 'base.sx_zb_city', 'base.sx_zb_county',
'base.active_fault', 'base.traffic_expressway', 'base.traffic_provincial',
'base.traffic_railway', 'base.traffic_township', 'base.traffic_trunk_line',
]
layer_type = "底图" if is_static else "动态"
marker = " <<<" if not is_static and provider == 'postgres' else ""
print(f"{i+1:>3d} {name:20s} {provider:10s} {visible:4s} {table:35s} [{layer_type}]{marker}")
print()
print("提示: 图层按从上到下的顺序渲染(序号小的在底层,序号大的在顶层)")
print("动态图层如果在底图下方,会被底图完全遮盖")
+65
View File
@@ -0,0 +1,65 @@
"""检查布局 Map 项的图层配置"""
import zipfile, re, os
TEMPLATE_DIR = r"F:\project\xian\xian_algorithm_new\app\data\template\rainfall"
fname = "暴雨内涝潜在隐患点及人口分布图.qgz"
path = os.path.join(TEMPLATE_DIR, fname)
z = zipfile.ZipFile(path)
qgs_name = [n for n in z.namelist() if n.endswith('.qgs')][0]
content = z.read(qgs_name).decode('utf-8')
# 提取 Layout 元素(找 Map 项)
# QGIS 有两种布局格式:<Composer> (QGIS 2.x style) 和 <Layout> (QGIS 3.x style)
layout_re = re.compile(r'<(?:Composer|Layout)[^>]*name="([^"]*)"[^>]*>(.*?)</(?:Composer|Layout)>', re.DOTALL)
for m in layout_re.finditer(content):
layout_name = m.group(1)
layout_content = m.group(2)
print(f"=== 布局: {layout_name} ===")
# 检查 Map 项
map_item_re = re.compile(r'<ComposerMap[^>]*>(.*?)</ComposerMap>', re.DOTALL)
for mm in map_item_re.finditer(layout_content):
map_content = mm.group(1)
# 检查是否有 lockedLayers
locked = re.findall(r'<lockedLayers>(.*?)</lockedLayers>', map_content, re.DOTALL)
if locked:
print(f" lockedLayers: {locked[0][:500]}")
# 检查 keepLayerSet
keep_set = re.findall(r'keepLayerSet="([^"]*)"', mm.group(0))
if keep_set:
print(f" keepLayerSet: {keep_set[0]}")
# 检查 followPreset
preset = re.findall(r'followPreset="([^"]*)"', mm.group(0))
if preset:
print(f" followPreset: {preset[0]}")
# 检查 followPresetName
preset_name = re.findall(r'followPresetName="([^"]*)"', mm.group(0))
if preset_name:
print(f" followPresetName: {preset_name[0]}")
# 检查 <layerSet>
layer_set = re.findall(r'<layerSet>(.*?)</layerSet>', map_content, re.DOTALL)
if layer_set:
print(f" layerSet: {layer_set[0][:500]}")
# 检查 <ComposerMapGrid>
grids = re.findall(r'<ComposerMapGrid[^>]*/>', map_content)
print(f" grids: {len(grids)}")
# 也检查 <Layout> 格式 (QGIS 3.x)
map_item2_re = re.compile(r'<LayoutItem[^>]*type="[^"]*map[^"]*"[^>]*>(.*?)</LayoutItem>', re.DOTALL | re.IGNORECASE)
for mm in map_item2_re.finditer(layout_content):
map_content = mm.group(1)
print(f" [LayoutItem Map] attributes: {mm.group(0)[:300]}")
# 打印模板中所有的 map theme / visibility preset
presets = re.findall(r'<(?:visibility-presets|map-theme-collection).*?</(?:visibility-presets|map-theme-collection)>', content, re.DOTALL)
if presets:
print("\n=== 可见性预设 ===")
print(presets[0][:500])
+71
View File
@@ -0,0 +1,71 @@
"""检查暴雨避难场所分布图的所有动态图层是否有数据"""
import zipfile, re, os, psycopg2
TEMPLATE_PATH = r"F:\project\xian\xian_algorithm_new\app\data\template\rainfall\暴雨避难场所分布图.qgz"
z = zipfile.ZipFile(TEMPLATE_PATH)
qgs_name = [n for n in z.namelist() if n.endswith('.qgs')][0]
content = z.read(qgs_name).decode('utf-8')
maplayer_re = re.compile(r'<maplayer[^>]*>(.*?)</maplayer>', re.DOTALL)
# 收集所有 PostgreSQL 动态图层
layers = []
for m in maplayer_re.finditer(content):
block = m.group(1)
name_m = re.search(r'<layername>([^<]+)</layername>', block)
provider_m = re.search(r'<provider[^>]*>(\w+)</provider>', block)
ds_m = re.search(r'<datasource>(.*?)</datasource>', block, re.DOTALL)
provider = provider_m.group(1) if provider_m else '?'
if provider != 'postgres':
continue
name = name_m.group(1) if name_m else '?'
ds = ds_m.group(1).strip() if ds_m else ''
table_m = re.search(r'table="(\w+)"\."(\w+)"', ds)
table_key = f"{table_m.group(1)}.{table_m.group(2)}" if table_m else '?'
layers.append((name, table_key))
# 已知 GPKG 静态层
static_tables = {
'base.rivers', 'base.river', 'base.sx', 'base.sx_capital',
'base.sx_street', 'base.sx_xa_county', 'base.sx_xa_county_boundary',
'base.sx_zb_county_boundary', 'base.sx_zb_city', 'base.sx_zb_county',
'base.active_fault', 'base.traffic_expressway', 'base.traffic_provincial',
'base.traffic_railway', 'base.traffic_township', 'base.traffic_trunk_line',
}
c = psycopg2.connect(host='47.92.216.173', port=7654, user='postgres', password='zhangsan', database='xian_new')
c.autocommit = True
cur = c.cursor()
print(f"模板: 暴雨避难场所分布图")
print(f"{'图层名':20s} {'原表':40s} {'qgis有数据':10s} {'行数':>6s}")
print("-" * 85)
for name, table_key in layers:
if table_key in static_tables:
continue
schema, table = table_key.split('.', 1)
mapped_table = 'hazard_hydrops' if table == 'hazard_waterlogging' else table
try:
cur.execute(f'SELECT count(*) FROM qgis.{mapped_table}')
count = cur.fetchone()[0]
exists = "YES" if count > 0 else "EMPTY"
except:
try:
cur.execute(f'SELECT count(*) FROM {schema}.{table}')
count = cur.fetchone()[0]
exists = f"{schema}"
except:
exists = "NO"
count = 0
marker = " <-- 无数据!" if count == 0 else ""
print(f"{name:20s} {table_key:40s} {exists:10s} {count:>6,d}{marker}")
c.close()
+38
View File
@@ -0,0 +1,38 @@
import psycopg2
c = psycopg2.connect(host='47.92.216.173', port=7654, user='postgres', password='zhangsan', database='xian_new')
cur = c.cursor()
tables = ['lifeline_outfall', 'lifeline_pipe', 'risk_census_population', 'hazard_waterlogging', 'sx_street', 'sx_xa_county']
print("=== qgis schema ===")
for t in tables:
try:
cur.execute(f'SELECT COUNT(*) FROM qgis."{t}"')
count = cur.fetchone()[0]
cur.execute(f"SELECT GeometryType(geom) FROM qgis.\"{t}\" LIMIT 1")
g = cur.fetchone()
print(f" qgis.{t}: {count} 行, geom={g[0] if g else 'N/A'}")
except Exception as e:
print(f" qgis.{t}: 不存在")
print("\n=== base schema (检查积水点原始表) ===")
for t in ['hazard_waterlogging']:
try:
cur.execute(f'SELECT COUNT(*) FROM base."{t}"')
print(f" base.{t}: {cur.fetchone()[0]}")
except:
print(f" base.{t}: 不存在")
# 查找所有含 water 或 hazard 的表
print("\n=== 搜索含 water/hazard 的表 ===")
cur.execute("""
SELECT table_schema, table_name
FROM information_schema.tables
WHERE (table_name LIKE '%water%' OR table_name LIKE '%hazard%')
AND table_schema IN ('qgis', 'base', 'public')
ORDER BY table_schema, table_name
""")
for r in cur.fetchall():
print(f" {r[0]}.{r[1]}")
c.close()
+42
View File
@@ -0,0 +1,42 @@
"""提取模板 .qgs 中所有图层名称、类型、数据源"""
import zipfile, re, os, sys
template_dir = r"F:\project\xian\xian_algorithm_new\app\data\template\rainfall"
for fname in sorted(os.listdir(template_dir)):
if not fname.endswith('.qgz') or fname.startswith('tmp'):
continue
path = os.path.join(template_dir, fname)
z = zipfile.ZipFile(path)
# 找到 .qgs 文件
qgs_name = [n for n in z.namelist() if n.endswith('.qgs')][0]
content = z.read(qgs_name).decode('utf-8')
# 提取 maplayer 块
maplayer_re = re.compile(r'<maplayer[^>]*>(.*?)</maplayer>', re.DOTALL)
layers = []
for m in maplayer_re.finditer(content):
block = m.group(1)
# 图层名
name_m = re.search(r'<layername>([^<]+)</layername>', block)
# provider
provider_m = re.search(r'<provider[^>]*>(\w+)</provider>', block)
# datasource (截取前120字符)
ds_m = re.search(r'<datasource>(.*?)</datasource>', block, re.DOTALL)
name = name_m.group(1) if name_m else '?'
provider = provider_m.group(1) if provider_m else '?'
ds = ds_m.group(1).strip()[:120] if ds_m else '?'
layers.append((name, provider, ds))
print(f"\n{'='*60}")
print(f"模板: {fname}")
print(f"{'='*60}")
for name, provider, ds in layers:
print(f" [{provider:10s}] {name}")
if ds:
print(f" ds: {ds}")
# 只分析第一个模板(所有 rainfall 模板结构相同)
if fname == '暴雨内涝潜在隐患点及人口分布图.qgz':
break
+16
View File
@@ -0,0 +1,16 @@
import zipfile, re
z = zipfile.ZipFile(r"F:\project\xian\xian_algorithm_new\app\data\template\rainfall\暴雨避难场所分布图.qgz")
c = z.read([n for n in z.namelist() if n.endswith('.qgs')][0]).decode()
layers = re.findall(r'<maplayer[^>]*>(.*?)</maplayer>', c, re.DOTALL)
for i, l in enumerate(layers):
nm = re.search(r'<layername>([^<]+)</layername>', l)
pv = re.search(r'<provider[^>]*>(\w+)</provider>', l)
ds = re.search(r'<datasource>(.*?)</datasource>', l, re.DOTALL)
name = nm.group(1) if nm else '?'
prov = pv.group(1) if pv else '?'
ds_text = ds.group(1).strip()[:120] if ds else ''
print(f"{i:2d}. [{prov:10s}] {name}")
if ds_text:
print(f" ds: {ds_text}")
+79 -58
View File
@@ -14,7 +14,6 @@ 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")
@@ -29,43 +28,58 @@ class MapService:
def generate(self, model: dict) -> str:
"""
执行完整的地图生成流程。
Args:
model: 包含地图参数的字典
Returns:
地图名称
"""
t_start = time.time()
_timing = {} # {step: seconds}
t_total = time.time()
template_path = model["path"]
template_name = os.path.basename(template_path)
project = QgsProject.instance()
is_cache_hit = template_cache.is_loaded(template_path)
# 加载/恢复模板
# ── 步骤 1加载/恢复模板 ──
t0 = time.time()
if is_cache_hit:
logger.info(f"[缓存命中] {os.path.basename(template_path)}")
logger.info(f"[缓存命中] {template_name}")
project, texts, extent = template_cache.restore_template(template_path)
template_cache.reset_project_state(project, texts, extent)
# FlagDontResolveLayers 跳过了数据库连接,手动触发 PostgreSQL 图层解析
self._resolve_postgres_layers(project)
else:
logger.info(f"[首次加载] {os.path.basename(template_path)}")
# 直接读原始模板,不做文件级修改(避免 ZIP 兼容性问题)
project.read(template_path)
logger.info(f"[首次加载] {template_name}")
modifier = TemplateModifier(self.config)
actual_path = modifier.modify(template_path)
project.read(actual_path)
if actual_path != template_path:
try:
os.remove(actual_path)
except OSError:
pass
_timing["1.load"] = time.time() - t0
# 更新图层连接 + GPKG/SRID/表名修正(仅首次加载)
# ── 步骤 2:图层连接修正 ──
t0 = time.time()
if not is_cache_hit:
self._update_db_connections(project)
self._fix_invalid_layers(project)
_timing["2.connections"] = time.time() - t0
# 图层过滤
# ★ 首次加载完成后再保存缓存(确保图层已修正)
if not is_cache_hit:
template_cache.save_current(template_path, model.get("mapLayout", "A3"))
# ── 步骤 3:图层过滤 ──
t0 = time.time()
LayerFilter().apply(project, model)
_timing["3.filter"] = time.time() - t0
# 地图缩放
# ── 步骤 4地图缩放 ──
t0 = time.time()
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"], {
@@ -73,61 +87,69 @@ class MapService:
"Y": model["centerY"],
"value": model["zoomValue"],
})
_timing["4.zoom"] = time.time() - t0
# 文本更新 + 比例尺 + 导出
# ── 步骤 5文本 + 比例尺 + 导出 ──
t0 = time.time()
exporter = MapExporter(self.config, layout)
exporter.update_texts(model)
exporter.update_scale_bar()
exporter.export(model["outFile"])
_timing["5.export"] = time.time() - t0
elapsed = time.time() - t_start
total = time.time() - t_total
steps = ", ".join(f"{k}={v:.1f}s" for k, v in _timing.items())
logger.info(
f"{'[缓存命中]' if is_cache_hit else '[首次加载]'} "
f"导出完成: {model['name']},耗时 {elapsed:.1f}s"
f"{template_name} {total:.1f}s ({steps})"
)
return model["name"]
def _update_db_connections(self, project: QgsProject) -> None:
"""更新图层连接 + SRID修正 + GPKG静态层替换"""
def _fix_invalid_layers(self, project: QgsProject) -> None:
"""只修复 TemplateModifier 处理后仍无效的图层(TemplateModifier 已修正大部分连接)"""
t0 = time.time()
db_config = self.config["db"]
override = self.config.get("template_override", {})
actual_schema = override.get("actual", {}).get("schema", "qgis")
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", {})
static_count = 0
fixed = 0
failed = 0
total = 0
for layer in project.mapLayers().values():
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)
if layer.providerType() != "postgres":
continue
if provider == "ogr":
static_count += 1
continue
if provider == "postgres":
total += 1
if layer.isValid():
continue # TemplateModifier 已修正,跳过
try:
self._fix_postgres_layer(layer, db_config, actual_schema)
if layer.isValid():
fixed += 1
else:
failed += 1
except Exception as e:
logger.error(f" 修复图层 {layer.name()} 失败: {e}")
failed += 1
if static_count:
logger.info(f"静态底图已本地化: {static_count} 个图层")
elapsed = time.time() - t0
if total:
logger.info(
f" 图层修正: 总计{total}个PG层, 跳过{total - fixed - failed}(已有效), "
f"修复{fixed}, 仍失败{failed}, 耗时{elapsed:.1f}s"
)
@staticmethod
def _resolve_postgres_layers(project: QgsProject) -> None:
"""FlagDontResolveLayers 跳过了数据库连接,重新 setDataSource 触发"""
for layer in project.mapLayers().values():
if layer.providerType() != "postgres":
continue
try:
source = layer.source()
if source:
layer.setDataSource(source, layer.name(), "postgres")
except Exception:
pass
@staticmethod
def _fix_postgres_layer(layer, db_config, actual_schema):
@@ -168,9 +190,8 @@ class MapService:
layer.setDataSource(uri.uri(), layer.name(), "postgres")
if layer.isValid():
fc = layer.featureCount()
logger.info(f"图层 {layer.name()} 连接更新成功 ({fc} features)")
logger.debug(f" 图层 {layer.name()} 连接更新成功")
else:
logger.error(f"图层 {layer.name()} 更新后仍无效")
logger.error(f" 图层 {layer.name()} 更新后仍无效")
except Exception as e:
logger.error(f"更新图层 {layer.name()} 连接失败: {e}")
logger.error(f" 更新图层 {layer.name()} 连接失败: {e}")
+6
View File
@@ -138,6 +138,12 @@ def main():
# 初始化 QgsApplication(只做一次)
qgs_app = _init_qgis()
# 从磁盘恢复已缓存的模板(跨进程加速)
from app.services.qgis.map_service import template_cache
cached_count = template_cache.load_persistent_cache()
if cached_count > 0:
print(f"[qgis_runner] 磁盘缓存命中: {cached_count} 个模板", file=sys.stderr)
try:
from app.services.qgis.map_service import MapService
+115 -41
View File
@@ -1,13 +1,15 @@
"""
模板缓存引擎。解决 QgsProject 单例问题
模板缓存引擎(支持持久化)
流程:
1. 首次请求:project.read() 加载模板(慢,仅一次)
2. 加载后 project.write() 保存到临时文件
3. 后续同模板请求:从临时文件恢复(快,连接复用
4. 手动恢复文本/过滤/缩放(毫秒级)
1. 首次加载:TemplateModifier 修改模板 → project.read() → 保存到磁盘缓存
2. 同进程内缓存命中:从内存恢复(~1s
3. 跨进程/重启:从磁盘缓存恢复(首次 ~5s,后续 ~1s
"""
import hashlib
import json
import os
import shutil
import time
import tempfile
@@ -17,36 +19,37 @@ from app.config.paths import get_logger
logger = get_logger("qgis.cache")
# 持久化缓存目录(项目根目录下)
_CACHE_DIR = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))),
"app", "data", "cache", "qgis_templates",
)
_INDEX_FILE = os.path.join(_CACHE_DIR, "index.json")
class TemplateCache:
def __init__(self):
self._cache: dict[str, dict] = {}
# ── 公开接口 ──
def is_loaded(self, template_path: str) -> bool:
return template_path in self._cache
def load_template(self, template_path: str, layout_name: str = "A4") -> None:
"""首次加载模板"""
start = time.time()
def save_current(self, template_path: str, layout_name: str = "A4") -> None:
"""将当前已加载的项目保存到内存缓存 + 磁盘缓存"""
project = QgsProject.instance()
logger.info(f"首次加载: {os.path.basename(template_path)}")
project.read(template_path)
logger.info(f"project.read() 耗时: {time.time() - start:.1f}s")
# 保存到临时文件
tmp_file = tempfile.NamedTemporaryFile(
suffix=".qgz", delete=False,
dir=tempfile.gettempdir(),
)
tmp_path = tmp_file.name
tmp_file.close()
# ── 保存项目到持久化缓存目录 ──
os.makedirs(_CACHE_DIR, exist_ok=True)
key = self._path_key(template_path)
cache_file = os.path.join(_CACHE_DIR, f"{key}.qgz")
t_save = time.time()
project.write(tmp_path)
logger.info(f"项目保存耗时: {time.time() - t_save:.1f}s")
project.write(cache_file)
logger.info(f" 缓存写入磁盘: {os.path.basename(cache_file)} ({time.time() - t_save:.1f}s)")
# 记录初始状态
# ── 记录状态 ──
texts = {}
extent = None
layout = project.layoutManager().layoutByName(layout_name)
@@ -59,38 +62,41 @@ class TemplateCache:
if map_item:
extent = QgsRectangle(map_item.extent())
self._cache[template_path] = {
"file": tmp_path,
entry = {
"file": cache_file,
"texts": texts,
"extent": extent,
"layout": layout_name,
}
logger.info(f"模板加载完成,总耗时: {time.time() - start:.1f}s")
self._cache[template_path] = entry
# ── 更新索引文件 ──
self._update_index(template_path, entry)
logger.info(f" 模板已缓存: {os.path.basename(template_path)} ({len(self._cache)} 个)")
def restore_template(self, template_path: str) -> tuple:
"""从缓存恢复模板"""
"""从缓存恢复模板(跳过图层解析以加速)"""
cached = self._cache.get(template_path)
if not cached:
raise RuntimeError(f"模板未缓存: {template_path}")
start = time.time()
project = QgsProject.instance()
t0 = time.time()
logger.info(f"恢复模板: {os.path.basename(template_path)}")
project.read(cached["file"])
logger.info(f"project.read() 耗时: {time.time() - start:.1f}s")
# FlagDontResolveLayers: 跳过数据库连接验证,缓存文件已含正确连接
flags = QgsProject.ReadFlags()
flags |= QgsProject.FlagDontResolveLayers
project.read(cached["file"], flags)
logger.debug(f" 恢复耗时: {time.time() - t0:.1f}s")
return project, cached["texts"], cached["extent"]
def reset_project_state(self, project: QgsProject, texts: dict, extent) -> None:
"""重置项目到干净状态"""
start = time.time()
for layer in project.mapLayers().values():
if layer.subsetString():
layer.setSubsetString("")
# 获取 layout 名称(从缓存中)
layout_name = "A4"
for cached in self._cache.values():
layout_name = cached.get("layout", "A4")
@@ -107,14 +113,82 @@ class TemplateCache:
if map_item:
map_item.zoomToExtent(extent)
logger.info(f"状态重置耗时: {time.time() - start:.3f}s")
def load_persistent_cache(self) -> int:
"""从磁盘恢复所有缓存到内存(qgis_runner 启动时调用)"""
if not os.path.isfile(_INDEX_FILE):
logger.info(" 磁盘缓存为空,首次启动需要加载模板")
return 0
try:
with open(_INDEX_FILE, "r", encoding="utf-8") as f:
index = json.load(f)
except (json.JSONDecodeError, OSError) as e:
logger.warning(f" 缓存索引损坏: {e}")
return 0
count = 0
for template_path, entry in index.get("templates", {}).items():
cache_file = entry.get("file", "")
if not cache_file or not os.path.isfile(cache_file):
logger.debug(f" 跳过无效缓存: {os.path.basename(template_path)}")
continue
texts = entry.get("texts", {})
extent = entry.get("extent")
# 反序列化 extent
if extent and isinstance(extent, dict):
extent = QgsRectangle(
extent.get("xmin", 0), extent.get("ymin", 0),
extent.get("xmax", 0), extent.get("ymax", 0),
)
else:
extent = None
self._cache[template_path] = {
"file": cache_file,
"texts": texts,
"extent": extent,
"layout": entry.get("layout", "A4"),
}
count += 1
logger.info(f" 磁盘缓存加载: {count} 个模板")
return count
def cleanup(self) -> None:
"""清理所有临时文件"""
for cached in self._cache.values():
try:
os.remove(cached["file"])
except OSError:
pass
"""清理内存缓存(磁盘缓存保留)"""
self._cache.clear()
logger.info("已清理所有缓存")
logger.info("已清理内存缓存")
# ── 内部方法 ──
@staticmethod
def _path_key(template_path: str) -> str:
"""模板路径 → 缓存文件名 key"""
return hashlib.md5(template_path.encode()).hexdigest()[:16]
def _update_index(self, template_path: str, entry: dict) -> None:
"""更新磁盘索引文件"""
index = {}
if os.path.isfile(_INDEX_FILE):
try:
with open(_INDEX_FILE, "r", encoding="utf-8") as f:
index = json.load(f)
except (json.JSONDecodeError, OSError):
pass
index.setdefault("templates", {})[template_path] = {
"file": entry["file"],
"layout": entry.get("layout", "A4"),
"texts": entry.get("texts", {}),
"extent": (
{
"xmin": entry["extent"].xMinimum(),
"ymin": entry["extent"].yMinimum(),
"xmax": entry["extent"].xMaximum(),
"ymax": entry["extent"].yMaximum(),
}
if entry.get("extent") else None
),
}
with open(_INDEX_FILE, "w", encoding="utf-8") as f:
json.dump(index, f, ensure_ascii=False, indent=2)