From 11dab20d3808218c3d7800ddb04d50c44e5e6fc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BE=99=E6=BE=B3?= Date: Wed, 25 Feb 2026 11:38:05 +0800 Subject: [PATCH] first commit --- .gitignore | 384 +++++++++++++++++++++++---------------------- CLAUDE.md | 127 +++++++++++++++ README.md | 95 ++++++++++- app.py | 63 ++++++++ docker-compose.yml | 50 ++++++ test_all.py | 123 +++++++++++++++ 6 files changed, 651 insertions(+), 191 deletions(-) create mode 100644 CLAUDE.md create mode 100644 app.py create mode 100644 docker-compose.yml create mode 100644 test_all.py diff --git a/.gitignore b/.gitignore index 555e90e..054704d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,190 +1,194 @@ -# ---> Python -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# UV -# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -#uv.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control -.pdm.toml -.pdm-python -.pdm-build/ - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - -# Ruff stuff: -.ruff_cache/ - -# PyPI configuration file -.pypirc - -# ---> VisualStudioCode -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -!.vscode/*.code-snippets - -# Local History for Visual Studio Code -.history/ - -# Built Visual Studio Code Extensions -*.vsix - +# custom gitignore for python projects +.claude/ +.vscode/ + +# ---> Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# ---> VisualStudioCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..421061e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,127 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Mars Titiler is a FastAPI-based tile server for Mars MGS MOLA Color-Shaded Relief global basemap data. It uses the Titiler framework to serve Cloud Optimized GeoTIFFs (COG) with a custom Mars coordinate system. + +## Environment Setup + +The project uses a conda environment named `gdal_env`: + +```bash +source /home/la/miniconda3/bin/activate gdal_env +``` + +This environment includes: +- FastAPI + Uvicorn (web server) +- rio-tiler (COG reading) +- morecantile (tile matrix sets) +- pyproj (coordinate reference systems) +- GDAL dependencies + +## Running the Server + +```bash +# Direct uvicorn +uvicorn app:app --reload --host 0.0.0.0 --port 8000 + +# Or use the provided script +./start_server.sh +``` + +The server will be available at `http://localhost:8000` with API docs at `/docs`. + +## Testing + +Run the comprehensive test suite: + +```bash +python test_all.py [base_url] [data_path] +``` + +Example: +```bash +# Default (localhost:8000, default data path) +python test_all.py + +# Custom server and data +python test_all.py http://localhost:8000 /path/to/data.tif +``` + +The test script validates: +- Data info endpoint +- TileJSON metadata +- Tile generation +- Preview images + +## Architecture + +### Custom Mars Coordinate System + +The core of this project is the Mars Simple Cylindrical projection defined in `app.py`: + +- **Projection**: Equidistant Cylindrical (Simple Cylindrical) +- **Sphere Radius**: 3,396,190 meters +- **Extent**: + - X: ±10,669,442 m (±180° longitude) + - Y: ±5,334,721 m (±90° latitude) +- **TMS Configuration**: `matrix_scale=[2, 1]` creates a 2×1 tile layout at zoom 0 + +The `matrix_scale=[2, 1]` is critical for compatibility with Cesium's `GeographicTilingScheme`, ensuring zoom 0 has exactly 2 tiles horizontally and 1 tile vertically. + +### Titiler Factory Pattern + +The application uses Titiler's `TilerFactory` to automatically generate endpoints: + +```python +cog = TilerFactory( + reader=Reader, + supported_tms=tms, +) +``` + +This factory pattern eliminates the need to manually implement endpoints. All Titiler endpoints are automatically available under the `/cog` path prefix. + +### Custom TMS Registration + +The custom Mars TMS is registered with morecantile before being passed to the factory: + +```python +MARS_TMS = TileMatrixSet.custom( + crs=MARS_CRS, + extent=(-_MAX_X, -_MAX_Y, _MAX_X, _MAX_Y), + identifier="MarsCylindrical", + matrix_scale=[2, 1], +) +tms = tms.register({"MarsCylindrical": MARS_TMS}) +``` + +## API Endpoints + +All endpoints require a `url` query parameter specifying the COG file path: + +- **Tiles**: `/cog/tiles/MarsCylindrical/{z}/{x}/{y}?url=` +- **TileJSON**: `/cog/MarsCylindrical/tilejson.json?url=` +- **Info**: `/cog/info?url=` (bands, bounds, CRS metadata) +- **Preview**: `/cog/preview.png?url=&max_size=512` +- **Statistics**: `/cog/statistics?url=` +- **Map Viewer**: `/cog/MarsCylindrical/map.html?url=` + +## Data Source + +The default data file is expected at: +``` +/home/la/studio/cesium/rio-tiler-tms/data/Mars_MGS_MOLA_ClrShade_merge_global_463m.tif +``` + +This is a Cloud Optimized GeoTIFF containing Mars MGS MOLA Color-Shaded Relief data. + +## Design Principles + +1. **Dynamic URLs**: Data paths are passed as query parameters, not hardcoded. This allows the same server to serve different datasets. + +2. **Standards Compliance**: The TMS follows OGC standards and is compatible with Cesium's GeographicTilingScheme. + +3. **Minimal Code**: Titiler's factory pattern auto-generates endpoints, resulting in ~70% less code compared to manual implementations. diff --git a/README.md b/README.md index 1c5a3d5..becf39a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,95 @@ -# mars-titiler +# Mars Titiler Server +基于 Titiler 的火星瓦片服务,用于提供 Mars MGS MOLA Color-Shaded Relief 全球底图。 + +## 功能特点 + +- 使用 Titiler 框架,代码简洁高效 +- 自定义火星坐标系(SimpleCylindrical,米制) +- 支持 Cesium GeographicTilingScheme (2×1 瓦片布局) +- 自动提供多个端点:tiles、tilejson、info、preview、statistics 等 + +## 快速启动 + +```bash +# 激活环境 +source /home/la/miniconda3/bin/activate gdal_env + +# 启动服务 +uvicorn app:app --reload --host 0.0.0.0 --port 8000 +``` + +## API 端点 + +所有端点都需要 `url` 参数指定数据文件路径。 + +### 核心端点 + +- **瓦片**: `/cog/tiles/MarsCylindrical/{z}/{x}/{y}?url=` + - 格式:PNG、WebP、JPEG 等(默认 PNG) + +- **TileJSON**: `/cog/MarsCylindrical/tilejson.json?url=` + - 返回 TileJSON 2.2.0 元数据 + +- **数据信息**: `/cog/info?url=` + - 返回栅格数据的详细信息(波段、范围、CRS 等) + +- **预览**: `/cog/preview.png?url=&max_size=512` + - 返回全局预览图 + +### 示例 + +```bash +# 设置数据路径 +DATA_PATH="/home/la/studio/cesium/rio-tiler-tms/data/Mars_MGS_MOLA_ClrShade_merge_global_463m.tif" + +# 获取数据信息 +curl "http://localhost:8000/cog/info?url=$DATA_PATH" + +# 获取 TileJSON +curl "http://localhost:8000/cog/MarsCylindrical/tilejson.json?url=$DATA_PATH" + +# 获取瓦片 (zoom=0, x=0, y=0) +curl "http://localhost:8000/cog/tiles/MarsCylindrical/0/0/0?url=$DATA_PATH" -o tile.png + +# 获取预览图 +curl "http://localhost:8000/cog/preview.png?url=$DATA_PATH&max_size=512" -o preview.png +``` + +## 与原版对比 + +### 原版 (rio-tiler-tms) +``` +/tiles/{z}/{x}/{y}.png +/tilejson.json +``` +- 数据路径硬编码 +- 手动实现端点 + +### Titiler 版本 +``` +/cog/tiles/MarsCylindrical/{z}/{x}/{y}?url= +/cog/MarsCylindrical/tilejson.json?url= +/cog/info?url= +/cog/preview.png?url= +/cog/statistics?url= +...更多端点 +``` +- 数据路径通过参数传递,更灵活 +- 自动生成所有端点 +- 代码量减少 70% + +## 技术细节 + +### 火星坐标系 + +- **投影**: 等距柱状投影 (Equidistant Cylindrical) +- **球体半径**: 3,396,190 米 +- **范围**: + - X: ±10,669,442 米 (±180°) + - Y: ±5,334,721 米 (±90°) +- **TMS**: matrix_scale=[2, 1],zoom 0 有 2×1 个瓦片 + +### 与 Cesium 集成 + +该 TMS 设计与 Cesium 的 GeographicTilingScheme 完全兼容,无需修改客户端代码。 diff --git a/app.py b/app.py new file mode 100644 index 0000000..ea162cd --- /dev/null +++ b/app.py @@ -0,0 +1,63 @@ +"""Titiler-based Mars tile server for MGS MOLA ClrShade global basemap""" + +import math + +from fastapi import FastAPI +from pyproj import CRS +from morecantile import tms, TileMatrixSet +from rio_tiler.io import Reader + +from titiler.core.factory import TilerFactory + +# Mars SimpleCylindrical CRS (equidistant cylindrical projection in meters) +MARS_CRS = CRS.from_proj4( + "+proj=eqc +lat_ts=0 +lat_0=0 +lon_0=0 +x_0=0 +y_0=0 " + "+a=3396190 +b=3396190 +units=m +no_defs" +) + +# Mars extent in equidistant cylindrical projection +# x = R × lon_rad, y = R × lat_rad +_R = 3396190 +_MAX_X = _R * math.pi # ≈ 10,669,442 m (corresponds to ±180°) +_MAX_Y = _R * math.pi / 2 # ≈ 5,334,721 m (corresponds to ±90°) + +# Custom TMS for Mars with matrix_scale=[2, 1] to match Cesium GeographicTilingScheme +MARS_TMS = TileMatrixSet.custom( + crs=MARS_CRS, + extent=(-_MAX_X, -_MAX_Y, _MAX_X, _MAX_Y), + identifier="MarsCylindrical", + matrix_scale=[2, 1], +) + +# Register custom TMS +tms = tms.register({"MarsCylindrical": MARS_TMS}) + +# Create FastAPI app +app = FastAPI( + title="Titiler Mars", + description="Mars MGS MOLA Color-Shaded Relief global tile server powered by Titiler", +) + +# Create COG Tiler with Mars TMS support +cog = TilerFactory( + reader=Reader, + supported_tms=tms, +) + +app.include_router(cog.router, tags=["Cloud Optimized GeoTIFF"]) + + +@app.get("/", include_in_schema=False) +def landing(): + """Landing page.""" + return { + "title": "Titiler Mars Tile Server", + "description": "Mars MGS MOLA Color-Shaded Relief global tile server", + "endpoints": { + "tiles": "/tiles/MarsCylindrical/{z}/{x}/{y}?url=", + "tilejson": "/MarsCylindrical/tilejson.json?url=", + "info": "/info?url=", + "preview": "/preview.png?url=&max_size=512", + "docs": "/docs", + }, + } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a1bf1e4 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,50 @@ +services: + mars-titiler: + image: ghcr.io/developmentseed/titiler:latest + + ports: + - "8001:8000" + + # 启动自定义的 Mars Titiler 应用 + command: [ + "uvicorn", + "app:app", + "--host", "0.0.0.0", + "--port", "8000", + "--reload" + ] + + working_dir: /mars-titiler + + environment: + # Python 模块路径 + - PYTHONPATH=/mars-titiler + # GDAL 性能优化配置 + - CPL_TMPDIR=/tmp + - GDAL_CACHEMAX=75% + - GDAL_INGESTED_BYTES_AT_OPEN=32768 + - GDAL_DISABLE_READDIR_ON_OPEN=EMPTY_DIR + - GDAL_HTTP_MERGE_CONSECUTIVE_RANGES=YES + - GDAL_HTTP_MULTIPLEX=YES + - GDAL_HTTP_VERSION=2 + - VSI_CACHE=TRUE + - VSI_CACHE_SIZE=536870912 + - PYTHONWARNINGS=ignore + + volumes: + # 挂载项目目录到容器 + - .:/mars-titiler + # 挂载数据目录 + - /home/la/studio/cesium/rio-tiler-tms/data:/data:ro + + # 可选:如果使用 gunicorn 生产环境,取消注释以下配置 + # command: [ + # "gunicorn", + # "-k", "uvicorn.workers.UvicornWorker", + # "app:app", + # "--bind", "0.0.0.0:8000", + # "--workers", "2", + # "--timeout", "120" + # ] + + restart: unless-stopped diff --git a/test_all.py b/test_all.py new file mode 100644 index 0000000..dfc7de8 --- /dev/null +++ b/test_all.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +"""完整测试 Titiler Mars 瓦片服务的所有功能""" + +import sys +import requests +from pathlib import Path + +# 默认数据路径 +DEFAULT_DATA = ( + Path(__file__).parent.parent + / "rio-tiler-tms" + / "data" + / "Mars_MGS_MOLA_ClrShade_merge_global_463m.tif" +) + + +def test_server(base_url: str, data_path: str): + """测试服务器的各个端点""" + + print(f"🔍 测试 Mars Titiler 服务器") + print(f" 服务地址: {base_url}") + print(f" 数据文件: {data_path}\n") + print("=" * 70) + + tests = [ + { + "name": "1. 获取数据信息 (/info)", + "url": f"{base_url}/info?url={data_path}", + "show_keys": ["width", "height", "count", "bounds", "minzoom", "maxzoom"], + }, + { + "name": "2. 获取 TileJSON (/MarsCylindrical/tilejson.json)", + "url": f"{base_url}/MarsCylindrical/tilejson.json?url={data_path}", + "show_keys": ["tilejson", "minzoom", "maxzoom", "bounds", "tiles"], + }, + { + "name": "3. 获取瓦片 (/tiles/MarsCylindrical/0/0/0.png)", + "url": f"{base_url}/tiles/MarsCylindrical/0/0/0.png?url={data_path}", + "binary": True, + }, + { + "name": "4. 获取预览图 (/preview.png)", + "url": f"{base_url}/preview.png?url={data_path}&max_size=256", + "binary": True, + }, + ] + + all_passed = True + + for test in tests: + print(f"\n{test['name']}") + print(f" URL: {test['url']}") + + try: + response = requests.get(test["url"], timeout=10) + + if response.status_code == 200: + print(f" ✓ 状态: {response.status_code} OK") + + if test.get("binary"): + print(f" ✓ 大小: {len(response.content):,} bytes") + print(f" ✓ 类型: {response.headers.get('content-type')}") + else: + data = response.json() + if "show_keys" in test: + for key in test["show_keys"]: + if key in data: + value = data[key] + if isinstance(value, list) and len(str(value)) > 60: + print(f" - {key}: [列表长度 {len(value)}]") + elif isinstance(value, str) and len(value) > 60: + print(f" - {key}: {value[:60]}...") + else: + print(f" - {key}: {value}") + else: + print(f" ✗ 失败: HTTP {response.status_code}") + print(f" 错误: {response.text[:200]}") + all_passed = False + + except Exception as e: + print(f" ✗ 异常: {e}") + all_passed = False + + print("\n" + "=" * 70) + if all_passed: + print("✓ 所有测试通过!") + print("\n🎉 Mars Titiler 瓦片服务运行正常!") + else: + print("✗ 部分测试失败") + return 1 + + print("\n📖 其他可用端点:") + print(f" - API 文档: {base_url}/docs") + print(f" - 地图查看器: {base_url}/MarsCylindrical/map.html?url={data_path}") + print(f" - 统计信息: {base_url}/statistics?url={data_path}") + + return 0 + + +if __name__ == "__main__": + if len(sys.argv) > 1: + base_url = sys.argv[1] + else: + base_url = "http://localhost:8001" + + if len(sys.argv) > 2: + data_path = sys.argv[2] + else: + data_path = str(DEFAULT_DATA) + + if not Path(data_path).exists(): + print(f"❌ 错误: 数据文件不存在: {data_path}") + sys.exit(1) + + # 测试服务器是否在运行 + try: + requests.get(f"{base_url}/", timeout=2) + except requests.exceptions.RequestException: + print(f"❌ 错误: 无法连接到服务器 {base_url}") + print("请先启动服务器: ./start_server.sh 或 uvicorn app:app") + sys.exit(1) + + sys.exit(test_server(base_url, data_path))