@@ -157,6 +157,210 @@ plot_prism(
157157plot_prism(variable = " ppt" , date = " 2024-03" , freq = " monthly" , resolution = " 4km" )
158158```
159159
160+ ### Example: Stream PRISM to a polygon
161+
162+ Building on the point example above, the script below uses a GeoJSON polygon to
163+ clip PRISM data on the fly, warp it to EPSG:4326, and optionally return stats
164+ for the clipped grid.
165+
166+ ``` python
167+ # !/usr/bin/env python3
168+ """
169+ stream_prism_to_polygon()
170+ - Streams PRISM via /vsicurl inside /vsizip (no manual download)
171+ - Clips/crops to a user-provided polygon (GeoJSON/GPKG/SHP path, GeoJSON dict/str, or WKT)
172+ - Warps to EPSG:4326 for easy plotting
173+ - Optionally returns data array, extent, and simple stats
174+ """
175+
176+ from __future__ import annotations
177+ from osgeo import gdal, ogr, osr
178+ import numpy as np
179+ import matplotlib.pyplot as plt
180+ import datetime as dt
181+ from typing import Union, Tuple, Optional, Dict, Any
182+ import json
183+ import uuid
184+
185+ # Enable GDAL exceptions (GDAL 4.0 will default to this)
186+ gdal.UseExceptions()
187+
188+ # ---------------- PRISM helpers ---------------- #
189+ _RES_CODE = {" 4km" : " 25m" , " 800m" : " 30s" , " 400m" : " 15s" }
190+
191+ def _as_datecode (date : Union[str , dt.date, dt.datetime], freq : str ) -> Tuple[str , str ]:
192+ if isinstance (date, (dt.datetime, dt.date)):
193+ d = date if isinstance (date, dt.date) and not isinstance (date, dt.datetime) else date.date()
194+ elif isinstance (date, str ):
195+ s = date.strip()
196+ if freq == " daily" :
197+ try : d = dt.datetime.strptime(s, " %Y-%m-%d " ).date()
198+ except ValueError : d = dt.datetime.strptime(s, " %Y%m%d " ).date()
199+ elif freq == " monthly" :
200+ try : d = dt.datetime.strptime(s, " %Y-%m" ).date().replace(day = 1 )
201+ except ValueError : d = dt.datetime.strptime(s, " %Y%m" ).date().replace(day = 1 )
202+ elif freq == " annual" :
203+ d = dt.datetime.strptime(s, " %Y" ).date().replace(month = 1 , day = 1 )
204+ else :
205+ raise ValueError (" freq must be one of: 'daily','monthly','annual'" )
206+ else :
207+ raise TypeError (" date must be str, datetime, or date" )
208+
209+ if freq == " daily" : return d.strftime(" %Y%m%d " ), d.strftime(" %Y" )
210+ if freq == " monthly" : return d.strftime(" %Y%m" ), d.strftime(" %Y" )
211+ return d.strftime(" %Y" ), d.strftime(" %Y" )
212+
213+ def _build_prism_vsi (variable : str , date , resolution : str , region : str , freq : str , network : str ) -> str :
214+ if resolution not in _RES_CODE :
215+ raise ValueError (" resolution must be one of {'800m','4km','400m'}" )
216+ datecode, yyyy = _as_datecode(date, freq)
217+ res_code = _RES_CODE [resolution]
218+ base_dir = f " https://data.prism.oregonstate.edu/time_series/ { region} / { network} / { resolution} / { variable} / { freq} / { yyyy} / "
219+ zip_name = f " prism_ { variable} _ { region} _ { res_code} _ { datecode} .zip "
220+ tif_name = f " prism_ { variable} _ { region} _ { res_code} _ { datecode} .tif "
221+ return f " /vsizip//vsicurl/ { base_dir}{ zip_name} / { tif_name} "
222+
223+ def _gdal_open_prism (** kwargs ) -> gdal.Dataset:
224+ vsi = _build_prism_vsi(** kwargs)
225+ gdal.SetConfigOption(" GDAL_DISABLE_READDIR_ON_OPEN" , " YES" )
226+ gdal.SetConfigOption(" CPL_VSIL_CURL_ALLOWED_EXTENSIONS" , " .zip,.tif,.xml,.stx,.prj,.aux.xml" )
227+ ds = gdal.Open(vsi, gdal.GA_ReadOnly )
228+ if ds is None :
229+ raise RuntimeError (f " GDAL failed to open PRISM via VSI: \n { vsi} " )
230+ return ds
231+
232+ def _extent_from_gt (ds ) -> Tuple[float , float , float , float ]:
233+ gt = ds.GetGeoTransform()
234+ w, h = ds.RasterXSize, ds.RasterYSize
235+ xmin, ymax = gt[0 ], gt[3 ]
236+ xmax, ymin = xmin + w * gt[1 ], ymax + h * gt[5 ]
237+ return (xmin, xmax, ymin, ymax)
238+
239+ # --------------- Polygon input helpers --------------- #
240+ def _write_geojson_vsimmem (geojson_obj ) -> str :
241+ geojson_str = json.dumps(geojson_obj) if isinstance (geojson_obj, dict ) else str (geojson_obj)
242+ path = f " /vsimem/cutline_ { uuid.uuid4().hex} .geojson "
243+ gdal.FileFromMemBuffer(path, geojson_str.encode(" utf-8" ))
244+ return path
245+
246+ def _wkt_to_geojson_vsimmem (wkt : str , srs_epsg : str = " EPSG:4326" ) -> str :
247+ sr = osr.SpatialReference(); sr.SetFromUserInput(srs_epsg)
248+ geom = ogr.CreateGeometryFromWkt(wkt)
249+ if geom is None :
250+ raise ValueError (" Could not parse WKT geometry." )
251+ feat = ogr.Feature(ogr.FeatureDefn()); feat.SetGeometry(geom)
252+ ds = ogr.GetDriverByName(" GeoJSON" ).CreateDataSource(" /vsimem/tmp_geojson.json" )
253+ try :
254+ layer = ds.CreateLayer(" cutline" , srs = sr, geom_type = ogr.wkbUnknown); layer.CreateFeature(feat); ds.SyncToDisk()
255+ vsipath = f " /vsimem/cutline_ { uuid.uuid4().hex} .geojson "
256+ buf = gdal.VSIGetMemFileBuffer(" /vsimem/tmp_geojson.json" , 0 ); gdal.FileFromMemBuffer(vsipath, buf)
257+ return vsipath
258+ finally :
259+ gdal.Unlink(" /vsimem/tmp_geojson.json" )
260+
261+ def _normalize_cutline_input (cutline , srs : str = " EPSG:4326" ) -> Tuple[Optional[str ], Optional[str ]]:
262+ if cutline is None : return None , None
263+ if isinstance (cutline, str ) and cutline.lower().endswith((" .geojson" , " .json" , " .gpkg" , " .shp" , " .zip" )):
264+ return cutline, None
265+ if isinstance (cutline, str ) and cutline.strip().upper().startswith((" POLYGON" , " MULTIPOLYGON" )):
266+ return _wkt_to_geojson_vsimmem(cutline, srs), None
267+ return _write_geojson_vsimmem(cutline), None
268+
269+ # ----------------- MAIN ONE-CALL FUNCTION ----------------- #
270+ def stream_prism_to_polygon (
271+ polygon : Union[str , dict ],
272+ polygon_srs : str = " EPSG:4326" ,
273+ * ,
274+ variable : str = " tmax" ,
275+ date : Union[str , dt.date, dt.datetime] = " 2025-07-15" ,
276+ resolution : str = " 800m" ,
277+ freq : str = " daily" ,
278+ region : str = " us" ,
279+ network : str = " an" ,
280+ title : Optional[str ] = None ,
281+ vmin : Optional[float ] = None ,
282+ vmax : Optional[float ] = None ,
283+ mask_outside : bool = True ,
284+ return_array : bool = False ,
285+ compute_stats : bool = False
286+ ) -> Optional[Dict[str , Any]]:
287+ ds = _gdal_open_prism(variable = variable, date = date, resolution = resolution,
288+ region = region, freq = freq, network = network)
289+
290+ cutline_path, cutline_layer = _normalize_cutline_input(polygon, polygon_srs)
291+
292+ warp_opts = gdal.WarpOptions(
293+ format = " VRT" ,
294+ dstSRS = " EPSG:4326" ,
295+ resampleAlg = " nearest" ,
296+ cutlineDSName = cutline_path,
297+ cutlineLayer = cutline_layer,
298+ cropToCutline = True ,
299+ dstAlpha = False ,
300+ warpOptions = [f " CUTLINE_SRS= { polygon_srs} " , f " INIT_DEST= { ' NO_DATA' if mask_outside else ' 0' } " ],
301+ )
302+ vrt = gdal.Warp(" " , ds, options = warp_opts)
303+ if vrt is None :
304+ raise RuntimeError (" GDAL.Warp failed to build VRT with the provided polygon." )
305+
306+ band = vrt.GetRasterBand(1 )
307+ nodata = band.GetNoDataValue()
308+ arr = band.ReadAsArray()
309+ if nodata is not None :
310+ arr = np.where(arr == nodata, np.nan, arr)
311+
312+ extent = _extent_from_gt(vrt)
313+ plt.figure(figsize = (8 , 6 ))
314+ im = plt.imshow(arr, extent = extent, origin = " upper" , vmin = vmin, vmax = vmax)
315+ plt.xlabel(" Longitude" ); plt.ylabel(" Latitude" )
316+ if title is None :
317+ title = f " PRISM { variable.upper()} ( { freq} ) { date} "
318+ plt.title(title)
319+ cb = plt.colorbar(im, shrink = 0.85 ); cb.set_label(f " { variable} (native units) " )
320+ plt.tight_layout(); plt.show()
321+
322+ if not return_array and not compute_stats:
323+ return None
324+
325+ out: Dict[str , Any] = {" array" : arr, " extent" : extent, " nodata" : nodata}
326+ if compute_stats:
327+ finite = np.isfinite(arr)
328+ if finite.any():
329+ out[" stats" ] = {
330+ " count" : int (finite.sum()),
331+ " mean" : float (np.nanmean(arr)),
332+ " min" : float (np.nanmin(arr)),
333+ " max" : float (np.nanmax(arr)),
334+ " std" : float (np.nanstd(arr)),
335+ }
336+ else :
337+ out[" stats" ] = {" count" : 0 , " mean" : np.nan, " min" : np.nan, " max" : np.nan, " std" : np.nan}
338+ return out
339+
340+ # ---------------- Example usage ---------------- #
341+ if __name__ == " __main__" :
342+ boulder_poly = {
343+ " type" : " FeatureCollection" ,
344+ " features" : [{
345+ " type" : " Feature" ,
346+ " properties" : {},
347+ " geometry" : {" type" : " Polygon" , " coordinates" : [[
348+ [- 105.38 , 40.15 ], [- 105.10 , 40.25 ], [- 104.95 , 40.15 ],
349+ [- 104.95 , 39.95 ], [- 105.15 , 39.90 ], [- 105.38 , 40.00 ],
350+ [- 105.38 , 40.15 ]
351+ ]]}
352+ }]
353+ }
354+
355+ out = stream_prism_to_polygon(
356+ polygon = boulder_poly, polygon_srs = " EPSG:4326" ,
357+ variable = " tmax" , date = " 2025-07-15" , resolution = " 800m" , freq = " daily" ,
358+ title = " PRISM TMAX daily (°C×10) — Boulder polygon" ,
359+ return_array = True , compute_stats = True
360+ )
361+ print (" Stats:" , out[" stats" ])
362+ ```
363+
160364## R
161365Paste into your R console:
162366``` r
0 commit comments