From da773b139f9fe3df63b1c7c93ee1b20bd397c748 Mon Sep 17 00:00:00 2001 From: duxin Date: Tue, 7 Jul 2026 16:23:38 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=BD=BB=E5=BA=95=E8=A7=A3=E5=86=B3=20P?= =?UTF-8?q?ROJ=20=E7=89=88=E6=9C=AC=E5=86=B2=E7=AA=81=20=E2=80=94=20?= =?UTF-8?q?=E6=B7=B1=E5=BA=A6=E6=90=9C=E7=B4=A2=20conda=20env=20+=20pip=20?= =?UTF-8?q?wheel=20=E4=B8=AD=E7=9A=84=20proj.db?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题: step11 所有内容报错: 'C:\ITRES\ATK\app\GDAL\projlib\proj.db contains DATABASE.LAYOUT.VERSION.MINOR = 2 whereas a number >= 6 is expected' 原因: 系统有旧版 GDAL (C:\ITRES\ATK\app\GDAL), 其 proj.db 为 v2, GDAL 内部搜索先找到旧版 → PROJ 坐标转换全部失败 增强的搜索策略 (由简到深): 1. Python 可执行文件位置回推 prefix (最可靠) 2. sys.path 中 site-packages 回推所有 conda/env 前缀 3. CONDA_PREFIX + sys.prefix + CONDA_ROOT 4. PATH 中推断 conda 安装路径 5. 硬编码已知路径 6. 扫描 conda 根下的 envs/*/ 所有环境 7. pip wheel 路径: {site-packages}/osgeo/data/proj/ 新增 proj.db 版本检测: - _check_proj_db_version(): 读取 SQLite 头, 通过文件大小判定 v6+ (>3MB) 或 v2 (<3MB) - 仅选择 v6+ 的 proj.db 设置 PROJ_LIB 新增递归搜索: - _find_proj_in_prefix(): 在 prefix 下深度搜索 Library/share/proj/ 限制深度 6 层避免全盘扫描 找不到时打印明确的修复建议: '请手动设置: set PROJ_LIB=\Library\share\proj' --- src/gui/water_quality_gui_v2.py | 204 +++++++++++++++++++++++--------- 1 file changed, 151 insertions(+), 53 deletions(-) diff --git a/src/gui/water_quality_gui_v2.py b/src/gui/water_quality_gui_v2.py index 637e7b1..2b43f28 100644 --- a/src/gui/water_quality_gui_v2.py +++ b/src/gui/water_quality_gui_v2.py @@ -25,105 +25,203 @@ import sys # ═══════════════════════════════════════════════════════════════ def _find_and_set_gdal_env(): - """动态推断并强设 PROJ_LIB 和 GDAL_DATA 环境变量 + """动态推断并强设 PROJ_LIB 和 GDAL_DATA 环境变量。 - 按优先级搜索 proj.db 和 GDAL data 目录: - 1. CONDA_PREFIX 环境变量(当前激活的 Conda 环境) - 2. sys.prefix(Python 安装前缀) - 3. CONDA_ROOT / CONDA_BASE(base 环境) - 4. 常见 Conda 安装目录 - 5. 硬编码回退路径(WQ_GUI 环境) + 解决"C:\\ITRES\\ATK\\app\\GDAL\\projlib"等旧系统 PROJ 安装的干扰: + 强制指向与当前 GDAL 版本匹配的 proj.db(通常在同 Conda 环境的 + Library/share/proj 下),而非系统路径中的过时版本。 + + 搜索策略(按优先级): + 1. CONDA_PREFIX + sys.prefix 的 Library/share/ 子目录 + 2. 搜索 sys.path 找到 osgeo 模块所在位置,回推 Library 路径 + 3. CONDA_ROOT / base 环境的 Library 路径 + 4. PATH 中推断的 conda 安装路径 + 5. 已知的硬编码回退路径 """ - def _check_proj(path): - """检查路径下是否含有效的 proj.db""" - proj_db = os.path.join(path, "proj.db") - if os.path.isfile(proj_db): - return path + # ── 辅助函数 ── + def _check_proj_db_version(db_path): + """读取 proj.db 的 SQLite 头,返回主版本号。失败返回 -1。""" + try: + with open(db_path, "rb") as f: + header = f.read(100) + # SQLite 3 文件头: "SQLite format 3\0",之后是页大小等 + # DATABASE.LAYOUT.VERSION 在 offset 60 附近的 schema 中, + # 但最简单的是读整个头中 "major" / "minor" 文本 + if b"SQLite format 3" not in header: + return -2 + # PROJ 在 proj.db 的 metadata 表存储版本,我们直接看文件大小 + # 粗略判定:< 2MB → v2-v4, 2~6MB → v5-v6, > 6MB → v7+ + size_mb = os.path.getsize(db_path) / (1024 * 1024) + return 6 if size_mb > 3.0 else 2 # v6+ 的 proj.db 通常 > 3MB + except Exception: + return -1 + + def _find_proj_in_prefix(prefix): + """在指定前缀下递归搜索 Library/share/proj/proj.db""" + candidates = [] + for root, dirs, files in os.walk(prefix): + # 限制深度避免全盘扫描 + depth = root.replace(prefix, "").count(os.sep) + if depth > 6: + dirs.clear() + continue + if "proj.db" in files: + db_path = os.path.join(root, "proj.db") + ver = _check_proj_db_version(db_path) + if ver >= 6: + candidates.append((ver, root)) + # 跳过明显的非目标目录 + dirs[:] = [d for d in dirs if d not in (".git", "__pycache__", "node_modules")] + if candidates: + candidates.sort(key=lambda x: -x[0]) # 最新版本优先 + return candidates[0][1] return None - def _check_gdal(path): - """检查路径下是否含 GDAL 数据文件(以 gdalvrt.xsd 或 pcs.csv 为准)""" - for marker in ("gdalvrt.xsd", "pcs.csv", "gcs.csv"): - if os.path.isfile(os.path.join(path, marker)): - return path + def _find_gdal_in_prefix(prefix): + """在指定前缀下查找 Library/share/gdal""" + for sub in ("Library/share/gdal", "share/gdal", + "Library\\share\\gdal", "share\\gdal"): + path = os.path.join(prefix, sub) + if os.path.isdir(path): + for marker in ("gdalvrt.xsd", "pcs.csv", "gcs.csv"): + if os.path.isfile(os.path.join(path, marker)): + return path return None - # --- 构建搜索前缀列表 --- + # ── 构建搜索前缀 ── prefixes = [] - # 1) CONDA_PREFIX(当前激活环境,最优先) - conda_prefix = os.environ.get("CONDA_PREFIX", "") - if conda_prefix: - prefixes.append(conda_prefix) + # 1) CONDA_PREFIX + sys.prefix(最优先) + for src in (os.environ.get("CONDA_PREFIX", ""), sys.prefix): + if src and src not in prefixes: + prefixes.append(src) - # 2) sys.prefix(Python 运行环境) - if sys.prefix and sys.prefix not in prefixes: - prefixes.append(sys.prefix) + # 2) 从 Python 可执行文件路径回推(最可靠:python.exe 就在 {prefix}/ 下) + try: + py_exe = sys.executable + py_prefix = os.path.dirname(py_exe) # python.exe 所在目录 + if os.path.basename(py_prefix).lower() == "scripts": + # Windows venv: {prefix}/Scripts/python.exe → 前缀是父目录 + exe_prefix = os.path.dirname(py_prefix) + else: + exe_prefix = py_prefix + if exe_prefix and exe_prefix not in prefixes: + prefixes.insert(0, exe_prefix) # 插到最前面,最高优先级 + except Exception: + pass - # 3) CONDA_ROOT / 其他 conda 线索 + # 3) 从 sys.path 中 site-packages 位置回推所有可能的 conda/env 前缀 + try: + for sp in sys.path: + if not sp or sp in prefixes: + continue + norm = os.path.normpath(sp) + parts = norm.split(os.sep) + # 检查 sp 是否在 site-packages 下 → 回推到 {prefix} + for marker in ("site-packages", "Lib"): + if marker in parts: + idx = parts.index(marker) + possible = os.sep.join(parts[:idx]) + if os.path.isdir(possible) and possible not in prefixes: + prefixes.append(possible) + break + except Exception: + pass + + # 3) CONDA_ROOT / base 环境 for key in ("CONDA_ROOT", "MAMBA_ROOT_PREFIX"): val = os.environ.get(key, "") if val and val not in prefixes: prefixes.append(val) - # 4) 从 PATH 推断 conda 安装根目录 - for path_dir in os.environ.get("PATH", "").split(os.pathsep): - lower = path_dir.lower() - if "anaconda" in lower or "miniconda" in lower or "anconda" in lower: - # 提取 conda 根目录: .../anconda/ 或 .../anconda/envs/xxx - if os.path.basename(path_dir).lower() == "condabin": - root = os.path.dirname(path_dir) - elif lower.endswith("scripts") or lower.endswith("bin"): - root = os.path.dirname(path_dir) + # 4) 从 PATH 推断 + for d in os.environ.get("PATH", "").split(os.pathsep): + if not d or d in prefixes: + continue + lower = d.lower() + if any(kw in lower for kw in ("anaconda", "miniconda", "anconda")): + # 找到 conda 根: 去掉 /Scripts 或 /condabin + if os.path.basename(d).lower() in ("scripts", "condabin", "bin"): + root = os.path.dirname(d) else: - root = path_dir + root = d if root and root not in prefixes: prefixes.append(root) - # 5) 硬编码回退(用户已知的 WQ_GUI 路径 + 常见目录) - hardcoded = [ + # 5) 已知硬编码路径 + for p in [ r"D:\111\changyongruanjian\anconda\envs\WQ_GUI", r"D:\111\changyongruanjian\anconda", r"C:\ProgramData\anaconda3", r"C:\ProgramData\miniconda3", - ] - for p in hardcoded: + ]: if os.path.isdir(p) and p not in prefixes: prefixes.append(p) - # --- 对每个前缀尝试 Windows + Linux 子目录 --- + # ── 额外:扫描 conda 根目录下的 envs/*/Library/share/proj ── + _extra_prefixes = [] + for prefix in list(prefixes): + envs_dir = os.path.join(prefix, "envs") + if os.path.isdir(envs_dir): + try: + for entry in os.listdir(envs_dir): + env_path = os.path.join(envs_dir, entry) + if os.path.isdir(env_path) and env_path not in prefixes: + _extra_prefixes.append(env_path) + except Exception: + pass + prefixes.extend(_extra_prefixes) + + # 6) pip wheel GDAL: {site-packages}/osgeo/data/proj/proj.db + # (conda-forge GDAL wheel on Windows 有时把 proj.db 放这里) + try: + import site as _site + for _sp in _site.getsitepackages(): + pip_proj = os.path.join(_sp, "osgeo", "data", "proj") + if os.path.isfile(os.path.join(pip_proj, "proj.db")): + pip_gdal = os.path.join(_sp, "osgeo", "data", "gdal") + if _sp not in prefixes: + prefixes.append(_sp) + except Exception: + pass proj_lib = None gdal_data = None for prefix in prefixes: if not proj_lib: - for sub in ("Library/share/proj", "share/proj", "Library\\share\\proj", "share\\proj"): - path = os.path.join(prefix, sub) - proj_lib = _check_proj(path) - if proj_lib: + # 先尝试标准子目录(快速路径) + for sub in ("Library/share/proj", "share/proj", + "Library\\share\\proj", "share\\proj"): + db_path = os.path.join(prefix, sub, "proj.db") + if os.path.isfile(db_path) and _check_proj_db_version(db_path) >= 6: + proj_lib = os.path.join(prefix, sub) break + # 标准路径未找到 → 递归搜索(仅对 conda 环境前缀) + if not proj_lib and ( + os.path.basename(prefix) not in ("", "share", "bin", "Scripts") + ): + proj_lib = _find_proj_in_prefix(prefix) if not gdal_data: - for sub in ("Library/share/gdal", "share/gdal", "Library\\share\\gdal", "share\\gdal"): - path = os.path.join(prefix, sub) - gdal_data = _check_gdal(path) - if gdal_data: - break + gdal_data = _find_gdal_in_prefix(prefix) if proj_lib and gdal_data: break + # ── 应用环境变量 ── if proj_lib: os.environ["PROJ_LIB"] = proj_lib print(f"[ENV] PROJ_LIB → {proj_lib}") else: - print("[ENV] ⚠ 未找到 proj.db,PROJ_LIB 保持系统默认,可能影响坐标转换") - print(f"[ENV] (已搜索 {len(prefixes)} 个前缀: {prefixes[:3]}...)") + print("[ENV] ⚠ 未找到兼容的 proj.db (需要 PROJ v6+),PROJ_LIB 保持系统默认") + print(f"[ENV] (已搜索 {len(prefixes)} 个前缀: {[os.path.basename(p) for p in prefixes[:5]]}...)") + print("[ENV] 如果 step11 地图渲染报 PROJ 版本错误,") + print("[ENV] 请手动设置: set PROJ_LIB=\\Library\\share\\proj") if gdal_data: os.environ["GDAL_DATA"] = gdal_data print(f"[ENV] GDAL_DATA → {gdal_data}") else: print("[ENV] ⚠ 未找到 GDAL data 目录,GDAL_DATA 保持系统默认") - print(f"[ENV] (已搜索 {len(prefixes)} 个前缀: {prefixes[:3]}...)") + print(f"[ENV] (已搜索 {len(prefixes)} 个前缀)") _find_and_set_gdal_env()