Pingul commited on
Commit
0e9faee
·
verified ·
1 Parent(s): 6eaf92d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +46 -57
app.py CHANGED
@@ -1,9 +1,8 @@
1
- # app.py (versión CONSERVADORA - solo cache, nada más)
2
  import io
3
  import os
4
  import logging
5
  import numpy as np
6
- from functools import lru_cache
7
  from fastapi import FastAPI, Response
8
  from fastapi.middleware.cors import CORSMiddleware
9
  from rio_tiler.io import COGReader
@@ -18,7 +17,7 @@ TILE_SIZE = 256
18
  logging.basicConfig(level=logging.INFO)
19
  logger = logging.getLogger("dem-tiler")
20
 
21
- app = FastAPI(title="Mexico DEM TerrainRGB API (con cache)")
22
 
23
  app.add_middleware(
24
  CORSMiddleware,
@@ -50,82 +49,38 @@ def elev_to_grayscale(arr: np.ndarray) -> np.ndarray:
50
  """
51
  a = np.array(arr, dtype=np.float32)
52
  a = np.where(np.isfinite(a), a, np.nan)
 
53
  vmin = np.nanpercentile(a, 2) if np.isfinite(np.nanpercentile(a, 2)) else np.nanmin(a)
54
  vmax = np.nanpercentile(a, 98) if np.isfinite(np.nanpercentile(a, 98)) else np.nanmax(a)
55
  if np.isnan(vmin) or np.isnan(vmax) or vmin == vmax:
56
  vmin, vmax = np.nanmin(a), np.nanmax(a)
 
57
  if np.isnan(vmin): vmin = 0.0
58
  if np.isnan(vmax): vmax = vmin + 1.0
59
 
60
  norm = (a - vmin) / (vmax - vmin)
61
  norm = np.clip(norm, 0.0, 1.0)
62
  img = (norm * 255).astype(np.uint8)
 
63
  rgb = np.stack([img, img, img], axis=-1)
64
  return rgb
65
 
66
- # ============================================================================
67
- # ÚNICA OPTIMIZACIÓN: Cache de tiles
68
- # ============================================================================
69
- @lru_cache(maxsize=512)
70
- def get_tile_data(z: int, x: int, y: int) -> bytes:
71
- """
72
- Genera y cachea tiles. Esta es la única optimización añadida.
73
- """
74
- try:
75
- with COGReader(COG_URL) as cog:
76
- data, mask = cog.tile(x, y, z, tilesize=TILE_SIZE)
77
- elev = data[0].astype(np.float32)
78
-
79
- if mask is not None:
80
- invalid = (mask == 0)
81
- elev[invalid] = np.nan
82
-
83
- if np.all(np.isnan(elev)):
84
- raise TileOutsideBounds(f"All NaN for tile {z}/{x}/{y}")
85
-
86
- rgb = encode_terrainrgb(np.nan_to_num(elev, nan=-10000.0))
87
- img = Image.fromarray(rgb, "RGB")
88
- buf = io.BytesIO()
89
- # compress_level=1 hace PNG más rápido (vs default 6)
90
- img.save(buf, format="PNG", compress_level=1)
91
- return buf.getvalue()
92
-
93
- except TileOutsideBounds:
94
- blank = Image.new("RGBA", (TILE_SIZE, TILE_SIZE), (0, 0, 0, 0))
95
- buf = io.BytesIO()
96
- blank.save(buf, format="PNG", compress_level=1)
97
- return buf.getvalue()
98
-
99
- # ============================================================================
100
- # ENDPOINTS (tu código original, sin cambios)
101
- # ============================================================================
102
  @app.get("/")
103
  def root():
104
- return {"message": "Mexico DEM TerrainRGB API running (with cache)", "cog_url": COG_URL}
105
 
106
  @app.get("/health")
107
  def health():
108
  return {"status": "ok"}
109
 
110
- @app.get("/cache/stats")
111
- def cache_stats():
112
- """Nuevo: Ver estadísticas del cache"""
113
- info = get_tile_data.cache_info()
114
- return {
115
- "hits": info.hits,
116
- "misses": info.misses,
117
- "size": info.currsize,
118
- "maxsize": info.maxsize,
119
- "hit_rate": f"{info.hits / (info.hits + info.misses) * 100:.1f}%" if (info.hits + info.misses) > 0 else "0%"
120
- }
121
-
122
  @app.get("/info")
123
  def get_info():
124
  try:
125
  with COGReader(COG_URL) as cog:
126
  meta = cog.info()
 
127
  out = {
128
- "bounds": meta.bounds,
129
  "minzoom": meta.minzoom,
130
  "maxzoom": meta.maxzoom,
131
  "band_descriptions": meta.band_descriptions,
@@ -143,31 +98,65 @@ def get_info():
143
  @app.get("/tiles/{z}/{x}/{y}.png")
144
  def get_tile(z: int, x: int, y: int):
145
  """
146
- Tile endpoint con cache - LA ÚNICA DIFERENCIA con tu código original
 
147
  """
148
  try:
149
- tile_bytes = get_tile_data(z, x, y)
150
- return Response(tile_bytes, media_type="image/png")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  except Exception as e:
152
  logger.exception(f"Unexpected error generating tile {z}/{x}/{y}")
 
153
  return Response(status_code=500, content=f"Server error generating tile: {str(e)}")
154
 
155
  @app.get("/debug/{z}/{x}/{y}.png")
156
  def debug_tile(z: int, x: int, y: int):
157
  """
158
- Endpoint de debug (sin cambios de tu código original)
 
159
  """
160
  try:
161
  with COGReader(COG_URL) as cog:
162
  data, mask = cog.tile(x, y, z, tilesize=TILE_SIZE)
163
  elev = data[0].astype(np.float32)
 
164
  if mask is not None:
165
  elev[mask == 0] = np.nan
166
 
167
  if np.all(np.isnan(elev)):
168
  raise TileOutsideBounds()
169
 
 
170
  rgb = elev_to_grayscale(elev)
 
171
  alpha = np.where(np.isfinite(elev), 255, 0).astype(np.uint8)
172
  rgba = np.dstack([rgb, alpha])
173
  img = Image.fromarray(rgba, "RGBA")
 
1
+ # app.py (versión con logging, manejo de TileOutsideBounds y endpoint debug hillshade)
2
  import io
3
  import os
4
  import logging
5
  import numpy as np
 
6
  from fastapi import FastAPI, Response
7
  from fastapi.middleware.cors import CORSMiddleware
8
  from rio_tiler.io import COGReader
 
17
  logging.basicConfig(level=logging.INFO)
18
  logger = logging.getLogger("dem-tiler")
19
 
20
+ app = FastAPI(title="Mexico DEM TerrainRGB API (debuggable)")
21
 
22
  app.add_middleware(
23
  CORSMiddleware,
 
49
  """
50
  a = np.array(arr, dtype=np.float32)
51
  a = np.where(np.isfinite(a), a, np.nan)
52
+ # clamp to sensible range (optional)
53
  vmin = np.nanpercentile(a, 2) if np.isfinite(np.nanpercentile(a, 2)) else np.nanmin(a)
54
  vmax = np.nanpercentile(a, 98) if np.isfinite(np.nanpercentile(a, 98)) else np.nanmax(a)
55
  if np.isnan(vmin) or np.isnan(vmax) or vmin == vmax:
56
  vmin, vmax = np.nanmin(a), np.nanmax(a)
57
+ # handle edge cases
58
  if np.isnan(vmin): vmin = 0.0
59
  if np.isnan(vmax): vmax = vmin + 1.0
60
 
61
  norm = (a - vmin) / (vmax - vmin)
62
  norm = np.clip(norm, 0.0, 1.0)
63
  img = (norm * 255).astype(np.uint8)
64
+ # convert to RGB
65
  rgb = np.stack([img, img, img], axis=-1)
66
  return rgb
67
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  @app.get("/")
69
  def root():
70
+ return {"message": "Mexico DEM TerrainRGB API running", "cog_url": COG_URL}
71
 
72
  @app.get("/health")
73
  def health():
74
  return {"status": "ok"}
75
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  @app.get("/info")
77
  def get_info():
78
  try:
79
  with COGReader(COG_URL) as cog:
80
  meta = cog.info()
81
+ # return a small, JSON-serializable subset
82
  out = {
83
+ "bounds": meta.bounds, # [left, bottom, right, top] in CRS of dataset (EPSG:3857)
84
  "minzoom": meta.minzoom,
85
  "maxzoom": meta.maxzoom,
86
  "band_descriptions": meta.band_descriptions,
 
98
  @app.get("/tiles/{z}/{x}/{y}.png")
99
  def get_tile(z: int, x: int, y: int):
100
  """
101
+ Tile endpoint: devuelve Terrain-RGB PNG (Mapbox spec).
102
+ Si el tile está fuera de bounds, devuelve PNG transparente.
103
  """
104
  try:
105
+ with COGReader(COG_URL) as cog:
106
+ data, mask = cog.tile(x, y, z, tilesize=TILE_SIZE)
107
+ elev = data[0].astype(np.float32)
108
+
109
+ # Replace nodata values with NaN if mask indicates no-data
110
+ if mask is not None:
111
+ # rio-tiler mask: 255 valid, 0 invalid (varies), handle robustly
112
+ invalid = (mask == 0)
113
+ elev = elev.astype(np.float32)
114
+ elev[invalid] = np.nan
115
+
116
+ # Handle all-nodata -> return transparent
117
+ if np.all(np.isnan(elev)):
118
+ raise TileOutsideBounds(f"All NaN for tile {z}/{x}/{y}")
119
+
120
+ rgb = encode_terrainrgb(np.nan_to_num(elev, nan=-10000.0))
121
+ img = Image.fromarray(rgb, "RGB")
122
+ buf = io.BytesIO()
123
+ img.save(buf, format="PNG")
124
+ buf.seek(0)
125
+ return Response(buf.getvalue(), media_type="image/png")
126
+
127
+ except TileOutsideBounds:
128
+ # tile fuera de bounds: PNG transparente
129
+ blank = Image.new("RGBA", (TILE_SIZE, TILE_SIZE), (0, 0, 0, 0))
130
+ buf = io.BytesIO()
131
+ blank.save(buf, format="PNG")
132
+ buf.seek(0)
133
+ return Response(buf.getvalue(), media_type="image/png")
134
+
135
  except Exception as e:
136
  logger.exception(f"Unexpected error generating tile {z}/{x}/{y}")
137
+ # Devuelve 500 con mensaje corto (no stacktrace)
138
  return Response(status_code=500, content=f"Server error generating tile: {str(e)}")
139
 
140
  @app.get("/debug/{z}/{x}/{y}.png")
141
  def debug_tile(z: int, x: int, y: int):
142
  """
143
+ Endpoint de debug: devuelve una imagen en escala de grises (hillshade-like) para ver visualmente
144
+ si la tile solicitada coincide con tu DEM.
145
  """
146
  try:
147
  with COGReader(COG_URL) as cog:
148
  data, mask = cog.tile(x, y, z, tilesize=TILE_SIZE)
149
  elev = data[0].astype(np.float32)
150
+ # mask nodata
151
  if mask is not None:
152
  elev[mask == 0] = np.nan
153
 
154
  if np.all(np.isnan(elev)):
155
  raise TileOutsideBounds()
156
 
157
+ # convert elevation to grayscale RGB for debug
158
  rgb = elev_to_grayscale(elev)
159
+ # if any nodata, set those pixels transparent by returning RGBA
160
  alpha = np.where(np.isfinite(elev), 255, 0).astype(np.uint8)
161
  rgba = np.dstack([rgb, alpha])
162
  img = Image.fromarray(rgba, "RGBA")