266 lines
10 KiB
Python
266 lines
10 KiB
Python
"""
|
||
QGIS 环境初始化模块(跨平台:Windows / Linux)。
|
||
在 server.py lifespan 启动阶段调用 init_qgis_env(),
|
||
完成共享库注入、环境变量设置、QgsApplication 初始化。
|
||
"""
|
||
import os
|
||
import sys
|
||
import platform
|
||
from pathlib import Path
|
||
|
||
from app.config.paths import get_logger
|
||
|
||
logger = get_logger("qgis.env")
|
||
|
||
_qgs_app = None
|
||
_initialized = False
|
||
_IS_WINDOWS = platform.system() == "Windows"
|
||
|
||
|
||
def init_qgis_env(qgis_root: str) -> None:
|
||
"""
|
||
初始化 QGIS 运行环境。整个应用生命周期只需调用一次。
|
||
|
||
Args:
|
||
qgis_root: QGIS 安装根目录
|
||
Windows: "D:/QGIS"
|
||
Linux: "/usr" 或 "/opt/QGIS"
|
||
"""
|
||
global _qgs_app, _initialized
|
||
if _initialized:
|
||
return
|
||
|
||
qgis_app_dir = _find_qgis_app_dir(qgis_root)
|
||
|
||
# 共享库搜索路径(平台相关)
|
||
# Windows: os.add_dll_directory() 显式注册 DLL 目录
|
||
# Linux: LD_LIBRARY_PATH 追加 .so 搜索目录
|
||
_add_library_paths(qgis_root, qgis_app_dir)
|
||
|
||
# 环境变量
|
||
_set_environment(qgis_root, qgis_app_dir)
|
||
|
||
# Python 模块路径
|
||
_add_python_paths(qgis_root, qgis_app_dir)
|
||
|
||
# 初始化 QgsApplication
|
||
_init_qgs_application(qgis_app_dir)
|
||
|
||
_initialized = True
|
||
logger.info(f"QGIS 环境初始化完成 ({platform.system()})")
|
||
|
||
|
||
def cleanup_qgis_env() -> None:
|
||
"""清理 QGIS 资源(应用退出时调用)"""
|
||
global _qgs_app, _initialized
|
||
if _qgs_app is not None:
|
||
_qgs_app.exitQgis()
|
||
_qgs_app = None
|
||
_initialized = False
|
||
logger.info("QGIS 资源已清理")
|
||
|
||
|
||
def is_qgis_ready() -> bool:
|
||
"""检查 QGIS 是否已初始化"""
|
||
return _initialized
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────────
|
||
# 平台检测
|
||
# ─────────────────────────────────────────────────────────────
|
||
|
||
def _find_qgis_app_dir(root: str) -> str:
|
||
"""
|
||
自动检测 QGIS 应用目录。
|
||
|
||
Windows(OSGeo4W 安装): {root}/apps/qgis-ltr/
|
||
Linux(包管理器安装): {root}/share/qgis/(Python 在 {root}/share/qgis/python)
|
||
"""
|
||
if _IS_WINDOWS:
|
||
return _find_qgis_app_dir_windows(root)
|
||
else:
|
||
return _find_qgis_app_dir_linux(root)
|
||
|
||
|
||
def _find_qgis_app_dir_windows(root: str) -> str:
|
||
"""Windows: 在 {root}/apps/ 下查找 qgis* 目录"""
|
||
apps_dir = Path(root) / "apps"
|
||
if apps_dir.is_dir():
|
||
for name in sorted(apps_dir.iterdir()):
|
||
if name.name.startswith("qgis") and name.is_dir():
|
||
logger.info(f"检测到 QGIS 应用目录: {name}")
|
||
return str(name)
|
||
fallback = str(apps_dir / "qgis")
|
||
logger.warning(f"未检测到 qgis* 目录,使用默认路径: {fallback}")
|
||
return fallback
|
||
|
||
|
||
def _find_qgis_app_dir_linux(root: str) -> str:
|
||
"""
|
||
Linux: 返回 QGIS_PREFIX_PATH。
|
||
包管理器安装的标准路径:
|
||
Debian/Ubuntu: /usr (qgis-core 在 /usr/lib/qgis/)
|
||
RHEL/CentOS: /usr
|
||
QGIS.org 官方: /usr 或 /opt/QGIS
|
||
"""
|
||
candidates = [
|
||
Path(root) / "share" / "qgis", # /usr/share/qgis
|
||
Path(root) / "lib" / "qgis", # /usr/lib/qgis
|
||
Path(root) / "share" / "qgis-ltr", # qgis-ltr 变体
|
||
]
|
||
for c in candidates:
|
||
if c.is_dir():
|
||
logger.info(f"检测到 QGIS 应用目录: {c}")
|
||
return str(c)
|
||
|
||
# 回退:用 root 本身(/usr),由 QgsApplication 自行探测
|
||
logger.warning(f"未找到 QGIS 标准目录,使用 root: {root}")
|
||
return str(root)
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────────
|
||
# 共享库注入
|
||
# ─────────────────────────────────────────────────────────────
|
||
|
||
def _add_library_paths(root: str, qgis_app: str) -> None:
|
||
"""
|
||
注入共享库搜索路径。
|
||
|
||
Windows: 调用 os.add_dll_directory() 显式注册每个 DLL 目录,
|
||
Python 3.8+ 不再自动搜索 PATH 中的 DLL。
|
||
Linux: 追加 LD_LIBRARY_PATH,让动态链接器 (ld-linux) 找到 .so。
|
||
"""
|
||
if _IS_WINDOWS:
|
||
_add_dll_directories_windows(root, qgis_app)
|
||
else:
|
||
_add_ld_library_path_linux(root, qgis_app)
|
||
|
||
|
||
def _add_dll_directories_windows(root: str, qgis_app: str) -> None:
|
||
"""Windows: 显式注册 DLL 搜索目录"""
|
||
if not hasattr(os, "add_dll_directory"):
|
||
return # Python < 3.8
|
||
|
||
dll_dirs = [
|
||
os.path.join(root, "apps", "Qt5", "bin"),
|
||
os.path.join(qgis_app, "bin"),
|
||
os.path.join(root, "bin"),
|
||
os.path.join(root, "apps", "gdal", "bin"),
|
||
os.path.join(root, "apps", "gdal", "lib"),
|
||
]
|
||
for d in dll_dirs:
|
||
if os.path.isdir(d):
|
||
os.add_dll_directory(d)
|
||
logger.debug(f"注册 DLL 目录: {d}")
|
||
|
||
|
||
def _add_ld_library_path_linux(root: str, qgis_app: str) -> None:
|
||
"""
|
||
Linux: 追加 LD_LIBRARY_PATH,使动态链接器找到 QGIS/GDAL 的 .so。
|
||
|
||
QGIS 包管理器安装时的标准 .so 路径:
|
||
/usr/lib/ — libqgis_core.so, libqgis_analysis.so
|
||
/usr/lib/qgis/ — 插件 .so
|
||
/usr/lib/qgis/plugins/ — provider .so
|
||
/usr/share/qgis/python/ — Python 模块
|
||
"""
|
||
ld_dirs = [
|
||
os.path.join(root, "lib"), # /usr/lib
|
||
os.path.join(root, "lib", "qgis"), # /usr/lib/qgis
|
||
os.path.join(root, "lib", "qgis", "plugins"), # /usr/lib/qgis/plugins
|
||
os.path.join(root, "share", "qgis", "lib"), # 某些安装方式
|
||
os.path.join(root, "apps", "gdal", "lib"), # OSGeo4W 风格
|
||
]
|
||
existing = os.environ.get("LD_LIBRARY_PATH", "")
|
||
new_dirs = [d for d in ld_dirs if os.path.isdir(d) and d not in existing]
|
||
if new_dirs:
|
||
joined = ":".join(new_dirs)
|
||
os.environ["LD_LIBRARY_PATH"] = f"{joined}:{existing}" if existing else joined
|
||
logger.info(f"LD_LIBRARY_PATH 追加: {new_dirs}")
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────────
|
||
# 环境变量
|
||
# ─────────────────────────────────────────────────────────────
|
||
|
||
def _set_environment(root: str, qgis_app: str) -> None:
|
||
"""设置 QGIS 和 GDAL 相关环境变量(平台自适应)"""
|
||
env_vars = {
|
||
"QGIS_PREFIX_PATH": qgis_app,
|
||
"PYTHONUTF8": "1",
|
||
"GDAL_FILENAME_IS_UTF8": "YES",
|
||
"VSI_CACHE": "TRUE",
|
||
"VSI_CACHE_SIZE": "1000000",
|
||
}
|
||
|
||
if _IS_WINDOWS:
|
||
env_vars["QT_PLUGIN_PATH"] = (
|
||
f"{os.path.join(qgis_app, 'qtplugins')};"
|
||
f"{os.path.join(root, 'apps', 'Qt5', 'plugins')}"
|
||
)
|
||
env_vars["GDAL_DATA"] = os.path.join(root, "apps", "gdal", "share", "gdal")
|
||
else:
|
||
# Linux: GDAL 数据通常在系统路径 /usr/share/gdal/ 下
|
||
gdal_candidates = [
|
||
os.path.join(root, "share", "gdal"),
|
||
os.path.join(root, "share", "qgis", "resources"),
|
||
]
|
||
for p in gdal_candidates:
|
||
if os.path.isdir(p):
|
||
env_vars["GDAL_DATA"] = p
|
||
break
|
||
|
||
# QGIS 插件路径(Linux 标准)
|
||
qt_plugin = os.path.join(root, "lib", "qt", "plugins")
|
||
if os.path.isdir(qt_plugin):
|
||
env_vars["QT_PLUGIN_PATH"] = qt_plugin
|
||
|
||
for key, value in env_vars.items():
|
||
os.environ[key] = value
|
||
logger.debug(f"环境变量: {key}={value}")
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────────
|
||
# Python 模块路径
|
||
# ─────────────────────────────────────────────────────────────
|
||
|
||
def _add_python_paths(root: str, qgis_app: str) -> None:
|
||
"""将 QGIS Python 模块路径加入 sys.path(平台自适应)"""
|
||
if _IS_WINDOWS:
|
||
python_paths = [
|
||
os.path.join(qgis_app, "python"),
|
||
os.path.join(root, "apps", "Python312", "Lib", "site-packages"),
|
||
]
|
||
else:
|
||
python_paths = [
|
||
os.path.join(qgis_app, "python"), # /usr/share/qgis/python
|
||
os.path.join(root, "lib", "python3", "dist-packages"), # Debian/Ubuntu
|
||
os.path.join(root, "lib", "python3.10", "site-packages"), # 通用
|
||
]
|
||
|
||
for p in python_paths:
|
||
if os.path.isdir(p) and p not in sys.path:
|
||
sys.path.insert(0, p)
|
||
logger.info(f"添加 Python 路径: {p}")
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────────
|
||
# QgsApplication 初始化
|
||
# ─────────────────────────────────────────────────────────────
|
||
|
||
def _init_qgs_application(qgis_app: str) -> None:
|
||
"""创建并初始化 QgsApplication 实例"""
|
||
global _qgs_app
|
||
|
||
from qgis.core import QgsApplication, QgsSettings
|
||
|
||
QgsApplication.setPrefixPath(qgis_app, True)
|
||
_qgs_app = QgsApplication([], False) # False = 不启动 GUI
|
||
|
||
settings = QgsSettings()
|
||
settings.setValue("/qgis/render_decorations", False)
|
||
settings.setValue("/qgis/parallel_rendering", True)
|
||
settings.setValue("/qgis/use_spatial_index", True)
|
||
|
||
_qgs_app.initQgis()
|
||
logger.info("QgsApplication 初始化完成") |