55from matplotlib .collections import PatchCollection
66from matplotlib .patches import PathPatch
77from matplotlib .path import Path
8+ from numpy .lib import recfunctions as nprecfns
89from pandas import DataFrame
910
10- from .geometry import transform
1111from .geospatial_utils import GeoSpatialUtil
1212from .utl_import import import_optional_dependency
1313
@@ -156,6 +156,7 @@ def intersect(
156156 return_all_intersections = False ,
157157 contains_centroid = False ,
158158 min_area_fraction = None ,
159+ handle_z = "ignore" ,
159160 geo_dataframe = False ,
160161 ):
161162 """Method to intersect a shape with a model grid.
@@ -185,6 +186,13 @@ def intersect(
185186 float defining minimum intersection area threshold, if intersection
186187 area is smaller than min_frac_area * cell_area, do not store
187188 intersection result, only used if shape type is "polygon"
189+ handle_z : str, optional
190+ Method for handling z dimension in intersection results for point
191+ intersections. Default is "ignore" which ignores z-dimension. Other
192+ options are "drop" which only returns results for points within grid
193+ top and bottom, or "return" which returns the computed layer position
194+ for each point. Points above the grid are returned as +np.inf and below
195+ the grid as -np.inf.
188196 geo_dataframe : bool, optional
189197 if True, return a geopandas GeoDataFrame, default is False
190198
@@ -214,6 +222,31 @@ def intersect(
214222 sort_by_cellid = sort_by_cellid ,
215223 return_all_intersections = return_all_intersections ,
216224 )
225+
226+ # handle elevation data for points
227+ # if handle_z is "drop" or "return"
228+ # if shp has z information
229+ # if there are intersection results
230+ if (
231+ (handle_z != "ignore" )
232+ and shapely .has_z (shp ).any ()
233+ and len (rec .cellids ) > 0
234+ ):
235+ laypos = self .get_layer_from_z (shp , rec .cellids )
236+ if handle_z == "drop" :
237+ mask_z = np .isfinite (laypos )
238+ rec = rec [mask_z ]
239+ elif handle_z == "return" :
240+ # copy data to new array to include layer position
241+ rec = nprecfns .append_fields (
242+ rec ,
243+ names = "layer" ,
244+ data = laypos ,
245+ dtypes = "f8" ,
246+ usemask = False ,
247+ asrecarray = True ,
248+ )
249+
217250 elif shapetype in {
218251 "LineString" ,
219252 "MultiLineString" ,
@@ -430,10 +463,8 @@ def _intersect_point(
430463 sort_by_cellid = True ,
431464 return_all_intersections = False ,
432465 ):
433- if self .rtree :
434- qcellids = self .strtree .query (shp , predicate = "intersects" )
435- else :
436- qcellids = self .filter_query_result (self .cellids , shp )
466+ r = self .intersects (shp , return_nodenumbers = True )
467+ qcellids = r .cellids [np .isfinite (r .cellids )].astype (int )
437468
438469 if sort_by_cellid :
439470 qcellids = np .sort (qcellids )
@@ -442,7 +473,10 @@ def _intersect_point(
442473 # discard empty intersection results
443474 mask_empty = shapely .is_empty (ixresult )
444475 # keep only Point and MultiPoint
445- mask_type = np .isin (shapely .get_type_id (ixresult ), [0 , 4 ])
476+ mask_type = np .isin (
477+ shapely .get_type_id (ixresult ),
478+ [shapely .GeometryType .POINT , shapely .GeometryType .MULTIPOINT ],
479+ )
446480 ixresult = ixresult [~ mask_empty & mask_type ]
447481 qcellids = qcellids [~ mask_empty & mask_type ]
448482
@@ -801,6 +835,7 @@ def _nodenumbers_to_rowcol(self, nodes):
801835 def points_to_cellids (
802836 self ,
803837 pts ,
838+ handle_z = "ignore" ,
804839 dataframe = False ,
805840 return_nodenumbers = False ,
806841 ):
@@ -813,6 +848,13 @@ def points_to_cellids(
813848 points shape to intersect with the grid
814849 dataframe : bool, optional
815850 if True, return a pandas.DataFrame, default is False
851+ handle_z : str, optional
852+ Method for handling z dimension in intersection results for point
853+ intersections. Default is "ignore" which ignores z-dimension. Other
854+ options are "drop" which only returns results for points within grid
855+ top and bottom, or "return" which returns the computed layer position
856+ for each point. Points above the grid are returned as +np.inf and below
857+ the grid as -np.inf.
816858 return_nodenumbers : bool, optional
817859 if False (default), return cellids of intersected grid cells.
818860 If True, return grid node numbers, i.e. index of entry in
@@ -873,6 +915,22 @@ def points_to_cellids(
873915 if self .mfgrid .grid_type == "structured" and not return_nodenumbers :
874916 rec .cellids = self ._nodenumbers_to_rowcol (rec .cellids )
875917
918+ if handle_z != "ignore" :
919+ laypos = self .get_layer_from_z (pts , rec .cellids )
920+ if handle_z == "drop" :
921+ mask_z = np .isfinite (laypos )
922+ rec = rec [mask_z ]
923+ elif handle_z == "return" :
924+ # copy data to new array to include layer position
925+ rec = nprecfns .append_fields (
926+ rec ,
927+ names = "layer" ,
928+ data = laypos ,
929+ dtypes = "f8" ,
930+ usemask = False ,
931+ asrecarray = True ,
932+ )
933+
876934 if dataframe :
877935 return DataFrame (rec ).set_index ("shp_ids" )
878936 return rec
@@ -1080,11 +1138,53 @@ def plot_intersection_result(self, result, plot_grid=True, ax=None, **kwargs):
10801138
10811139 return ax
10821140
1141+ def get_layer_from_z (self , pts , cellids ):
1142+ """Method to handle z values for points.
10831143
1084- def _polygon_patch (polygon , ** kwargs ):
1085- from matplotlib .patches import PathPatch
1086- from matplotlib .path import Path
1144+ Parameters
1145+ ----------
1146+ pts : shapely.geometry
1147+ points geometry
1148+ cellids : array_like
1149+ array of cellids
10871150
1151+ Returns
1152+ -------
1153+ cellids : array_like
1154+ array of cellids with z values handled
1155+ """
1156+
1157+ def valid_mask (v ):
1158+ if isinstance (v , tuple ):
1159+ return True
1160+ else :
1161+ return not np .isnan (v )
1162+
1163+ z_arr = np .atleast_1d (shapely .get_z (pts ))
1164+ if self .mfgrid .grid_type == "structured" :
1165+ mask_valid = list (map (valid_mask , cellids ))
1166+ row , col = list (zip (* cellids [mask_valid ]))
1167+ surface_elevations = self .mfgrid .top_botm [:, row , col ]
1168+ elif self .mfgrid .grid_type == "vertex" :
1169+ mask_valid = ~ np .isnan (cellids )
1170+ surface_elevations = self .mfgrid .top_botm [:, cellids [mask_valid ]]
1171+ else :
1172+ raise NotImplementedError (
1173+ "get_layer_from_z() is only implemented for "
1174+ "structured and vertex grids."
1175+ )
1176+ zb = surface_elevations < z_arr [mask_valid ]
1177+ mask_above = zb .all (axis = 0 )
1178+ mask_below = (~ zb ).all (axis = 0 )
1179+ laypos = (np .nanargmax (zb , axis = 0 ) - 1 ).astype (float )
1180+ laypos [mask_above ] = np .inf
1181+ laypos [mask_below ] = - np .inf
1182+ laypos_full = np .full_like (z_arr , np .nan , dtype = float )
1183+ laypos_full [mask_valid ] = laypos
1184+ return laypos_full
1185+
1186+
1187+ def _polygon_patch (polygon , ** kwargs ):
10881188 patch = PathPatch (
10891189 Path .make_compound_path (
10901190 Path (np .asarray (polygon .exterior .coords )[:, :2 ]),
0 commit comments