4646import shapely .vectorized
4747import shapely .wkt
4848from cartopy .io import shapereader
49+ from pyproj .crs import CRS as PCRS
4950from shapely .geometry import MultiPolygon , Point , box
5051from sklearn .neighbors import BallTree
5152
@@ -127,14 +128,14 @@ def check_if_geo_coords(lat, lon, raise_error=True):
127128 return like_geo
128129
129130
130- def get_crs_unit (coords ):
131+ def get_crs_unit (crs ):
131132 """
132133 Retrieve the unit of measurement for the coordinate reference system (CRS).
133134
134135 Parameters
135136 ----------
136- coords : GeoDataFrame
137- An object with a coordinate reference system (CRS) attribute.
137+ crs : PyProj CRS
138+ Coordinate reference system (CRS)
138139
139140 Returns
140141 -------
@@ -143,7 +144,7 @@ def get_crs_unit(coords):
143144 CRS axis information.
144145 """
145146 try :
146- unit = coords . crs .axis_info [0 ].unit_name # assume both axes have the same unit
147+ unit = crs .axis_info [0 ].unit_name # assume both axes have the same unit
147148 except :
148149 LOGGER .warning ("The units of the input crs are undefined." )
149150 unit = "undefined"
@@ -1057,12 +1058,20 @@ def estimate_matching_threshold(coords_to_assign):
10571058 return 2 * max (abs (r ) for r in get_resolution (coords_to_assign .T ))
10581059
10591060
1061+ def degree_to_km (deg ):
1062+ return np .deg2rad (deg ) * EARTH_RADIUS_KM
1063+
1064+
1065+ def km_to_degree (km ):
1066+ return np .rad2deg (km / EARTH_RADIUS_KM )
1067+
1068+
10601069def match_coordinates (
10611070 coords ,
10621071 coords_to_assign ,
10631072 distance = "euclidean" ,
1064- unit = "degree" ,
10651073 threshold = None ,
1074+ crs = DEF_CRS ,
10661075 ** kwargs ,
10671076):
10681077 """To each coordinate in `coords`, assign a matching coordinate in `coords_to_assign`
@@ -1122,21 +1131,23 @@ def match_coordinates(
11221131 across the antimeridian. However, when exact matches are enforced with `threshold=0`, lat/lon
11231132 coordinates need to be given in the same longitudinal range (such as (-180, 180)).
11241133 """
1134+ crs = PCRS .from_user_input (DEF_CRS )
1135+
11251136 if coords .shape [0 ] == 0 :
11261137 return np .array ([])
11271138
11281139 if coords_to_assign .shape [0 ] == 0 :
11291140 return - np .ones (coords .shape [0 ]).astype (int )
11301141
1131- if distance not in nearest_neighbor_funcs :
1132- raise ValueError (
1133- f'Coordinate assignment with "{ distance } " distance is not supported.'
1134- )
11351142 nearest_neighbor_funcs = {
11361143 "euclidean" : _nearest_neighbor_euclidean ,
11371144 "haversine" : _nearest_neighbor_haversine ,
11381145 "approx" : _nearest_neighbor_approx ,
11391146 }
1147+ if distance not in nearest_neighbor_funcs :
1148+ raise ValueError (
1149+ f'Coordinate assignment with "{ distance } " distance is not supported.'
1150+ )
11401151
11411152 coords = coords .astype ("float64" )
11421153 coords_to_assign = coords_to_assign .astype ("float64" )
@@ -1175,14 +1186,14 @@ def match_coordinates(
11751186 "No exact centroid match found. Reprojecting coordinates "
11761187 "to nearest neighbor closer than the threshold = %s %s" ,
11771188 threshold ,
1178- unit ,
1189+ get_crs_unit ( crs ) ,
11791190 )
11801191
11811192 not_assigned_idx_mask = assigned_idx == - 1
11821193 assigned_idx [not_assigned_idx_mask ] = nearest_neighbor_funcs [distance ](
11831194 coords_to_assign ,
11841195 coords [not_assigned_idx_mask ],
1185- unit ,
1196+ crs ,
11861197 threshold ,
11871198 ** kwargs ,
11881199 )
@@ -1265,14 +1276,11 @@ def match_centroids(
12651276 "Please provide coord_gdf and centroids with valid crs attributes."
12661277 ) from exc
12671278
1268- # get unit of coordinate systems from axis of crs
1269- unit = get_crs_unit (coord_gdf )
1270-
12711279 assigned = match_coordinates (
12721280 np .stack ([coord_gdf .geometry .y .values , coord_gdf .geometry .x .values ], axis = 1 ),
12731281 centroids .coord ,
1282+ crs = centroids .crs ,
12741283 distance = distance ,
1275- unit = unit ,
12761284 threshold = threshold ,
12771285 )
12781286 return assigned
@@ -1288,7 +1296,7 @@ def _dist_sqr_approx(lats1, lons1, cos_lats1, lats2, lons2):
12881296
12891297
12901298def _nearest_neighbor_approx (
1291- centroids , coordinates , unit , threshold , check_antimeridian = True
1299+ centroids , coordinates , crs , threshold , check_antimeridian = True
12921300):
12931301 """Compute the nearest centroid for each coordinate using the
12941302 squared equirectangular approximation distance d = ((dlon)cos(lat))^2+(dlat)^2.
@@ -1318,15 +1326,16 @@ def _nearest_neighbor_approx(
13181326 np.array
13191327 with as many rows as coordinates containing the centroids indexes
13201328 """
1321- # first check that unit is in degree
1322- if unit != "degree" :
1329+ if not crs .is_geographic :
13231330 raise ValueError (
1324- "Only degree unit is supported for nearest neighbor matching using"
1331+ "Only geographic crs are supported for nearest neighbor matching using"
13251332 "'approx' distance."
13261333 )
1327- # check that coords are indeed geographic degrees
1334+ # check that coords are indeed geographic degrees (in case we do no trust the crs)
13281335 check_if_geo_coords (centroids [:, 0 ], centroids [:, 1 ])
13291336 check_if_geo_coords (coordinates [:, 0 ], coordinates [:, 1 ])
1337+ # convert the degree threshold in to km at equator threshold
1338+ threshold_km = degree_to_km (threshold )
13301339 # Compute only for the unique coordinates. Copy the results for the
13311340 # not unique coordinates
13321341 _ , idx , inv = np .unique (coordinates , axis = 0 , return_index = True , return_inverse = True )
@@ -1345,7 +1354,7 @@ def _nearest_neighbor_approx(
13451354 min_idx = dist .argmin ()
13461355 # Raise a warning if the minimum distance is greater than the
13471356 # threshold and set an unvalid index -1
1348- if np .sqrt (dist .min ()) > threshold :
1357+ if np .sqrt (dist .min ()) * ONE_LAT_KM > threshold_km :
13491358 num_warn += 1
13501359 min_idx = - 1
13511360
@@ -1361,13 +1370,13 @@ def _nearest_neighbor_approx(
13611370
13621371 if check_antimeridian :
13631372 assigned = _nearest_neighbor_antimeridian (
1364- centroids , coordinates , threshold , assigned , unit = unit
1373+ centroids , coordinates , threshold , assigned , crs = crs
13651374 )
13661375
13671376 return assigned
13681377
13691378
1370- def _nearest_neighbor_haversine (centroids , coordinates , unit , threshold ):
1379+ def _nearest_neighbor_haversine (centroids , coordinates , crs , threshold ):
13711380 """Compute the neareast centroid for each coordinate using a Ball tree with haversine distance.
13721381
13731382 Parameters
@@ -1389,13 +1398,12 @@ def _nearest_neighbor_haversine(centroids, coordinates, unit, threshold):
13891398 np.array
13901399 with as many rows as coordinates containing the centroids indexes
13911400 """
1392- # first check that unit is in degree
1393- if unit != "degree" :
1401+ if not crs .is_geographic :
13941402 raise ValueError (
1395- "Only coordinates in degree units are supported for nearest neighbor matching using"
1403+ "Only geographic crs are supported for nearest neighbor matching using"
13961404 "'haversine' distance."
13971405 )
1398- # check that coords are indeed geographic degrees
1406+ # check that coords are indeed geographic degrees (in case we do no trust the crs)
13991407 check_if_geo_coords (centroids [:, 0 ], centroids [:, 1 ])
14001408 check_if_geo_coords (coordinates [:, 0 ], coordinates [:, 1 ])
14011409 # Construct tree from centroids
@@ -1433,7 +1441,7 @@ def _nearest_neighbor_haversine(centroids, coordinates, unit, threshold):
14331441
14341442
14351443def _nearest_neighbor_euclidean (
1436- centroids , coordinates , unit , threshold , check_antimeridian = True
1444+ centroids , coordinates , crs , threshold , check_antimeridian = True
14371445):
14381446 """Compute the neareast centroid for each coordinate using a k-d tree.
14391447
@@ -1462,6 +1470,7 @@ def _nearest_neighbor_euclidean(
14621470 np.array
14631471 with as many rows as coordinates containing the centroids indexes
14641472 """
1473+ unit = get_crs_unit (crs )
14651474 if (
14661475 unit == "degree"
14671476 ): # if unit is in degree convert to radians for dist calculations
@@ -1504,14 +1513,14 @@ def _nearest_neighbor_euclidean(
15041513
15051514 if check_antimeridian :
15061515 assigned = _nearest_neighbor_antimeridian (
1507- centroids , coordinates [idx ], threshold , assigned , unit = unit
1516+ centroids , coordinates [idx ], threshold , assigned , crs = crs
15081517 )
15091518
15101519 # Copy result to all exposures and return value
15111520 return assigned [inv ]
15121521
15131522
1514- def _nearest_neighbor_antimeridian (centroids , coordinates , threshold , assigned , unit ):
1523+ def _nearest_neighbor_antimeridian (centroids , coordinates , threshold , assigned , crs ):
15151524 """Recompute nearest neighbors close to the anti-meridian with the Haversine distance
15161525
15171526 Parameters
@@ -1533,11 +1542,16 @@ def _nearest_neighbor_antimeridian(centroids, coordinates, threshold, assigned,
15331542 np.array
15341543 with as many rows as coordinates containing the centroids indexes
15351544 """
1545+ if not crs .is_geographic :
1546+ raise ValueError (
1547+ "Only geographic crs are supported for nearest neighbor matching using"
1548+ "'haversine' distance at the antimeridian."
1549+ )
15361550 # first check that unit is in degree
1537- if unit != "degree" :
1551+ if get_crs_unit ( crs ) != "degree" :
15381552 raise ValueError (
15391553 "Only coordinates in degree units are supported for nearest neighbor matching using"
1540- "'haversine' distance a the antimeridian."
1554+ "'haversine' distance at the antimeridian."
15411555 )
15421556 lon_min = min (centroids [:, 1 ].min (), coordinates [:, 1 ].min ())
15431557 lon_max = max (centroids [:, 1 ].max (), coordinates [:, 1 ].max ())
@@ -1559,7 +1573,7 @@ def _nearest_neighbor_antimeridian(centroids, coordinates, threshold, assigned,
15591573 if np .any (cent_strip_bool ):
15601574 cent_strip = centroids [cent_strip_bool ]
15611575 strip_assigned = _nearest_neighbor_haversine (
1562- cent_strip , coord_strip , "degree" , threshold
1576+ cent_strip , coord_strip , crs , threshold
15631577 )
15641578 new_coords = cent_strip_bool .nonzero ()[0 ][strip_assigned ]
15651579 new_coords [strip_assigned == - 1 ] = - 1
0 commit comments