@@ -33,7 +33,7 @@ class VerticalTransform:
3333
3434 def __init__ (self , region , nx , ny , epsg_in , epsg_out ,
3535 geoid_in = None , geoid_out = None , epoch_in = 2010.0 , epoch_out = 2010.0 ,
36- cache_dir = None , verbose = True ):
36+ decay_pixels = 100 , cache_dir = None , verbose = True ):
3737
3838 self .region = region
3939 self .nx = nx
@@ -55,6 +55,8 @@ def __init__(self, region, nx, ny, epsg_in, epsg_out,
5555 self .ref_in = Datums .get_frame_type (self .epsg_in )
5656 self .ref_out = Datums .get_frame_type (self .epsg_out )
5757
58+ self .decay_pixels = decay_pixels
59+
5860 # --- HUB SELECTION ---
5961 # Determine the Native Ellipsoid of Input and Output
6062 native_in = self ._get_native_ellipsoid (self .epsg_in , self .ref_in )
@@ -178,9 +180,9 @@ def _get_grid(self, provider, name):
178180 if provider == 'seanoe' or provider == 'fes' :
179181 var_name = "lat_elevation" if "lat" in name .lower () else "msl_elevation"
180182 nc_path = f"netcdf:{ files [0 ]} :{ var_name } "
181- return GridEngine .load_and_interpolate ([nc_path ], self .region , self .nx , self .ny )
183+ return GridEngine .load_and_interpolate ([nc_path ], self .region , self .nx , self .ny , decay_pixels = self . decay_pixels )
182184
183- return GridEngine .load_and_interpolate (files , self .region , self .nx , self .ny )
185+ return GridEngine .load_and_interpolate (files , self .region , self .nx , self .ny , decay_pixels = self . decay_pixels )
184186
185187 def _get_htdp_shift (self , epsg_from , epsg_to , epoch_from , epoch_to ):
186188 """Calculate Frame Shift via HTDP."""
@@ -232,33 +234,65 @@ def _fetch_geoid_with_fallback(self, target_geoid):
232234 # =========================================================================
233235 def _get_vdatum_chain (self , datum_name , geoid_name ):
234236 """Builds shift: Tidal -> [NAD83 Native]."""
235- total_shift = np .zeros ((self .ny , self .nx ))
237+ hydro_shift = np .zeros ((self .ny , self .nx ))
236238 desc = []
237239
238240 # Tidal -> LMSL
239241 if datum_name not in ['msl' , '5714' , 'lmsl' ]:
240242 grid = self ._get_grid ('vdatum' , datum_name )
241- if not np .any (grid ):
243+ if np .isnan (grid ). all () or ( grid == 0 ). all ( ):
242244 return None , f"Missing Tidal Grid: { datum_name } "
243- total_shift += grid
245+ hydro_shift += grid
244246 desc .append (f"({ datum_name } ->LMSL)" )
245247
246248 # LMSL -> Ortho (TSS)
247249 tss = self ._get_grid ('vdatum' , 'tss' )
248-
249- if not np .any (tss ):
250+ if np .isnan (tss ).all () or (tss == 0 ).all ():
250251 return None , "Outside VDatum coverage (Missing TSS)"
251252
252- total_shift += tss
253+ hydro_shift += tss
253254 desc .append ("TSS(LMSL->NAVD88)" )
254255
255- # Ortho -> NAD83 (Smart Geoid Fallback)
256+ # Ortho -> NAD83 (Geoid)
257+ # We fetch the geoid, but DO NOT add it to the shift yet!
256258 actual_geoid = geoid_name if geoid_name else 'g2018'
257259 geoid_grid , used_geoid = self ._fetch_geoid_with_fallback (actual_geoid )
258-
259- total_shift += geoid_grid
260260 desc .append (f"Geoid({ used_geoid } ->NAD83)" )
261261
262+ # =======================================================
263+ # Coastal Blend
264+ # =======================================================
265+ total_shift = np .zeros ((self .ny , self .nx ))
266+
267+ if np .isnan (hydro_shift ).any ():
268+ proxy_name = Datums .get_global_proxy (datum_name )
269+ if proxy_name :
270+ logger .info (f"Partial VDatum coverage detected. Fetching { proxy_name .upper ()} (FES) for offshore blending..." )
271+ global_shift , d_global = self ._get_global_chain (proxy_name , model = 'fes2014' )
272+
273+ if global_shift is not None and np .any (global_shift ):
274+ # We have valid FES data. We must align it to NAD83.
275+ htdp_wgs_to_nad = self ._get_htdp_shift (WGS84_EPSG , NAD83_EPSG , self .epoch_in , 2010.0 )
276+ fes_full = global_shift + htdp_wgs_to_nad
277+
278+ hydro_shift = GridEngine .coastal_aware_composite (
279+ vdatum_grid = hydro_shift ,
280+ global_grid = fes_full ,
281+ decay_pixels = self .decay_pixels ,
282+ buffer_pixels = 10 ,
283+ max_discontinuity = 0.5
284+ )
285+ desc .append (f"Blended w/ Global({ proxy_name .upper ()} )" )
286+ else :
287+ hydro_shift = GridEngine .fill_nans (hydro_shift , decay_pixels = self .decay_pixels , buffer_pixels = 10 )
288+ desc .append ("Inland Hydro Decay" )
289+ else :
290+ hydro_shift = GridEngine .fill_nans (hydro_shift , decay_pixels = self .decay_pixels , buffer_pixels = 10 )
291+ desc .append ("Inland Hydro Decay" )
292+
293+ total_shift = hydro_shift + geoid_grid
294+ total_shift [np .isnan (total_shift )] = 0.0
295+
262296 return total_shift , " + " .join (desc )
263297
264298 def _get_global_chain (self , datum_name , model = "fes2014" ):
0 commit comments