Source code for pycsamt.gis.config

# Author: Kouadio Laurent alias Daniel <etanoyau@gmail.com>
# License: LGPL-3.0
r"""
pycsamt.gis.config

Configuration loader for the GIS subpackage.

- Verifies availability of GDAL or a PROJ-based fallback
  (``pyproj``).
- Loads EPSG definitions into :data:`EPSG_DICT` for use by
  lightweight transformers.

On import, the module attempts to locate GDAL resources.
If unavailable, it verifies that ``pyproj`` can be imported.
EPSG definitions are then loaded from ``pyproj`` data where
possible; otherwise a local ``epsg.npy`` mapping is used.

Exports
-------
HAS_GDAL : bool
    ``True`` when GDAL/OSR is available.
EPSG_DICT : dict[int, str]
    Mapping of EPSG codes to PROJ strings.
GDALMissingError : Exception
    Raised when a GDAL-dependent routine is used without GDAL.
GisError : Exception
    Base exception for GIS utilities.

Notes
-----
- Modern ``pyproj`` (>=3) ships a SQLite ``proj.db`` rather
  than a flat ``epsg`` file. This loader uses a best-effort
  approach to find legacy definitions; if none are found, the
  local ``epsg.npy`` is used as a fallback.
"""

from __future__ import annotations

import os
import re

import numpy as np

from ..decorators import GdalDataCheck
from ..log.logger import get_logger

logger = get_logger(__name__)


# Availability checks

_HAS_GDAL = GdalDataCheck._locate_gdal_data()
if not _HAS_GDAL:
    try:
        import pyproj  # noqa: F401
    except ImportError as exc:  # pragma: no cover
        raise RuntimeError(
            "Either GDAL or pyproj must be installed for GIS features."
        ) from exc


# Build EPSG dictionary
EPSG_DICT: dict[int, str] = {}

try:
    import pyproj  # noqa: F401

    # pyproj 3+ uses proj.db; older releases may ship 'epsg'
    data_dir = getattr(pyproj, "pyproj_datadir", None)
    epsg_path = os.path.join(data_dir, "epsg") if data_dir else None

    if epsg_path and os.path.isfile(epsg_path):
        with open(epsg_path, encoding="utf-8") as f:
            for line in f:
                if line.strip().startswith("#"):
                    continue
                codes = re.findall(r"<(\d+)>", line)
                if not codes:
                    continue
                code = int(codes[0])
                m = re.search(r">([^<]+)<", line)
                if m:
                    EPSG_DICT[code] = m.group(1).strip()
    else:
        raise FileNotFoundError

except Exception:
    # Fallback: load local numpy file of EPSG mappings
    here = os.path.dirname(os.path.abspath(__file__))
    epsg_file = os.path.join(here, "epsg.npy")
    try:
        EPSG_DICT = np.load(
            epsg_file,
            allow_pickle=True,
        ).item()
    except Exception as e:  # pragma: no cover
        logger.error(
            "Failed to load EPSG definitions: %s",
            str(e),
        )
        EPSG_DICT = {}

# Expose availability
HAS_GDAL = _HAS_GDAL


[docs] class GDALMissingError(RuntimeError): r""" Raised when a GDAL/OSR-dependent function is called while GDAL is not available. Notes ----- Install GDAL (and the ``osgeo`` Python bindings) to use GDAL-backed transforms. Prefer high-level helpers that can fall back to PROJ, such as :func:`project_point_ll2utm` and :func:`project_point_utm2ll`, when GDAL is absent. """
[docs] class GisError(Exception): r""" Base exception for GIS utilities. """
# Create a configuration class for elevation APIs class ElevationAPIConfig: """Configuration for elevation API services.""" # Available elevation API endpoints APIS = { "open_meteo": { "url": "https://api.open-meteo.com/v1/elevation", "params_format": "comma_separated", "response_key": "elevation", }, "open_topo_data": { "url": "https://api.opentopodata.org/v1/elevation", "params_format": "pipe_separated", "response_key": "results.elevation", }, "usgs_ned": { "url": "https://nationalmap.gov/epqs/pqs.php", "params_format": "individual", "response_key": ( "USGS_Elevation_Point_Query_Service.Elevation_Query" ), }, } # Default API to use DEFAULT_API = "open_meteo" @classmethod def get_api_config(cls, api_name=None): """Get configuration for a specific API.""" api_name = api_name or cls.DEFAULT_API return cls.APIS.get(api_name, cls.APIS[cls.DEFAULT_API]) __all__ = [ "HAS_GDAL", "EPSG_DICT", "GDALMissingError", "GisError", ]