From b4c2679ad86e55f0c1ac4d3b03b79ea5d858d3e7 Mon Sep 17 00:00:00 2001 From: along Date: Mon, 23 Feb 2026 11:57:35 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=81=AB=E6=98=9FTMS?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app_mars.py | 98 +++++++++++++++++++++++++++++++++++++++++++++++++ lunaserv-python | 1 + 2 files changed, 99 insertions(+) create mode 100644 app_mars.py create mode 160000 lunaserv-python diff --git a/app_mars.py b/app_mars.py new file mode 100644 index 0000000..aae2ef9 --- /dev/null +++ b/app_mars.py @@ -0,0 +1,98 @@ +"""rio-tiler dynamic tile server for Mars MGS MOLA ClrShade global basemap""" + +import math +from pathlib import Path + +from fastapi import FastAPI, HTTPException +from starlette.requests import Request +from starlette.responses import Response + +from pyproj import CRS +from morecantile import TileMatrixSet + +from rio_tiler.errors import TileOutsideBounds +from rio_tiler.io import Reader +from rio_tiler.profiles import img_profiles + +DATA_PATH = str( + Path(__file__).parent / "data" / "Mars_MGS_MOLA_ClrShade_merge_global_463m.tif" +) + +# TIF 的原生 CRS 是 SimpleCylindrical Mars(等距柱状投影,单位:米) +# 用 proj4 字符串定义,直接匹配 TIF —— 参考 lunaserv-python 的 proj4 字符串方式 +# +# 关键设计决策:TMS 与 TIF 使用同一个 CRS(eqc 米制),rio-tiler 读取时无需任何跨 CRS 转换。 +# 若改用 longlat(度),PROJ 会尝试通过 WGS84 做 datum shift:火星球体(R=3396km) vs +# 地球椭球(R=6378km) 差异极大,导致中间计算出现 "PROJ: eqc: Invalid latitude" 错误。 +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" +) + +# 火星球体在等距柱状投影下的全球范围(米) +# 等距柱状: x = R × lon_rad, y = R × lat_rad +# → max_x = R × π(对应 180°),max_y = R × π/2(对应 90°) +_R = 3396190 +_MAX_X = _R * math.pi # ≈ 10,669,442 m +_MAX_Y = _R * math.pi / 2 # ≈ 5,334,721 m + +# WorldCRS84Quad 等效 TMS(米制版本): +# - 范围覆盖全球等距柱状投影 +# - matrix_scale=[2, 1] → zoom 0 有 2×1 个瓦片,与 Cesium GeographicTilingScheme(2×1) 对齐 +# - 瓦片编号 (z, x, y) 与地理位置的对应关系和经纬度版完全一致,Cesium 无需任何修改 +MARS_TMS = TileMatrixSet.custom( + crs=MARS_CRS, + extent=(-_MAX_X, -_MAX_Y, _MAX_X, _MAX_Y), + matrix_scale=[2, 1], +) + +app = FastAPI( + title="rio-tiler Mars", + description="Mars MGS MOLA Color-Shaded Relief global tile server", +) + + +@app.get( + "/tiles/{z}/{x}/{y}.png", + responses={ + 200: {"content": {"image/png": {}}, "description": "Return a PNG tile."}, + 404: {"description": "Tile outside bounds."}, + }, + response_class=Response, + description="Read Mars tile and return PNG (uint8 RGB, no rescaling needed)", +) +def tile(z: int, x: int, y: int): + """Return a map tile for the given z/x/y coordinates.""" + try: + with Reader(DATA_PATH, tms=MARS_TMS) as cog: + img = cog.tile(x, y, z) + except TileOutsideBounds: + raise HTTPException(status_code=404, detail="Tile outside bounds") + + content = img.render( + img_format="PNG", + **img_profiles.get("png", {}), + ) + return Response(content, media_type="image/png") + + +@app.get("/tilejson.json", responses={200: {"description": "Return a TileJSON document"}}) +def tilejson(request: Request): + """Return a TileJSON 2.2.0 document.""" + with Reader(DATA_PATH, tms=MARS_TMS) as cog: + bounds = cog.get_geographic_bounds(cog.tms.rasterio_geographic_crs) + minzoom = cog.minzoom + maxzoom = cog.maxzoom + + base_url = str(request.base_url).rstrip("/") + tile_url = f"{base_url}/tiles/{{z}}/{{x}}/{{y}}.png" + + return { + "tilejson": "2.2.0", + "name": "Mars MGS MOLA ClrShade", + "description": "Mars MGS MOLA Color-Shaded Relief Global Mosaic 463m/px", + "tiles": [tile_url], + "bounds": list(bounds), + "minzoom": minzoom, + "maxzoom": maxzoom, + "center": [0, 0, minzoom], + } diff --git a/lunaserv-python b/lunaserv-python new file mode 160000 index 0000000..b60eac1 --- /dev/null +++ b/lunaserv-python @@ -0,0 +1 @@ +Subproject commit b60eac11a594387cfb1df7794da0bea6d4c3cd9a