def distance_to_point(self, lon, lat): """Find the shortest geodesic distance from (lon, lat) to a nonzero cell of the probability matrix.""" aeqd_pt = pyproj.Proj(proj='aeqd', ellps='WGS84', datum='WGS84', lon_0=lon, lat_0=lat) wgs_to_aeqd = functools.partial(pyproj.transform, wgs_proj, aeqd_pt) # mathematically, wgs_to_aeqd(Point(lon, lat)) == Point(0, 0); # the latter is faster and more precise pt = Point(0, 0) # It is unacceptably costly to construct a shapely MultiPoint # out of some locations with large regions (requires more than # 32GB of scratch memory). Instead, iterate over the points # one at a time. min_distance = math.inf for x, y, v in iter_csr_nonzero(self.probability): cell = sh_transform(wgs_to_aeqd, Point(self.longitudes[x], self.latitudes[y])) if pt.distance(cell) - self.resolution * 2 < min_distance: cell = cell.buffer(self.resolution * 3 / 2) min_distance = min(min_distance, pt.distance(cell)) if min_distance < self.resolution * 3 / 2: return 0 return min_distance
def area(self): """Weighted area of the nonzero region of the probability matrix.""" if self._area is None: # Notionally, each grid point should be treated as a # rectangle of parallels and meridians _centered_ on the # point. The area of such a rectangle, however, only # depends on its latitude and its breadth; the actual # longitude values don't matter. Since the grid is # equally spaced, we can use [0],[1] always, and then we # do not have to worry about crossing the discontinuity at ±180. west = self.longitudes[0] east = self.longitudes[1] # For latitude, the actual values do matter, but the map # never goes all the way to the poles, and the grid is # equally spaced, so we can precompute the north-south # delta from any pair of latitudes and not have to worry # about running off the ends of the array. d_lat = (self.latitudes[1] - self.latitudes[0]) / 2 # We don't need X, so throw it away immediately. (We # don't use iter_csr_nonzero here because we need to # modify V.) X, Y, V = sparse.find(self.probability) X = None # The value vector is supposed to be normalized, but make # sure it is, and then adjust from 1-overall to 1-per-cell # normalization. assert len(V.shape) == 1 S = V.sum() if S == 0: return 0 if S != 1: V /= S V *= V.shape[0] area = 0 for y, v in zip(Y, V): north = self.latitudes[y] + d_lat south = self.latitudes[y] - d_lat if not (-90 <= south < north <= 90): raise AssertionError( "expected -90 <= {} < {} <= 90".format(south, north)) tile = sh_transform(wgs_to_cea, Box(west, south, east, north)) area += v * tile.area self._area = area return self._area
def coord_transform_geometry(geo: (BaseGeometry, BaseMultipartGeometry), from_srs: SRS, to_srs: SRS): """对Geomery内的所有点进行坐标转换,返回转换后的Geometry 该方法可以支持所有的Shapely Geometry形状,包括Point, Line, Polygon, MultiPloygon等,返回的Geometry和输入的形状保持一致 Args: geo: 输入的shapely Geometry from_srs: 输入的坐标格式 to_srs: 输出的坐标格式 Returns: 转换后的shapely Geometry """ return sh_transform( lambda x, y, z=None: coord_transform(x, y, from_srs, to_srs), geo)
def DiskOnGlobe(x, y, radius): """Return a shapely polygon which is a circle centered at longitude X, latitude Y, with radius RADIUS (meters), projected onto the surface of the Earth. """ # Make sure the radius is at least 1km to prevent underflow. radius = max(radius, 1000) # If the radius is too close to half the circumference of the # Earth, the projection operation below will produce an invalid # polygon. We don't get much use out of a region that includes # the whole planet but for a tiny disk (which will probably be # somewhere in the ocean anyway) so just give up and say that the # region is the entire planet. if radius > 19975000: return Box(-180, -90, 180, 90) # To find all points on the Earth within a certain distance of # a reference latitude and longitude, back-project onto the # Earth from an azimuthal-equidistant map with its zero point # at the reference latitude and longitude. aeqd = pyproj.Proj(proj='aeqd', ellps='WGS84', datum='WGS84', lat_0=y, lon_0=x) disk = sh_transform( functools.partial(pyproj.transform, aeqd, wgs_proj), Point(0, 0).buffer(radius, resolution=64)) # Two special cases must be manually dealt with. First, if any # side of the "disk" (really a many-sided polygon) crosses the # coordinate singularity at longitude ±180, we must replace that # side with a diversion to either the north or south pole # (whichever is closer) to ensure the diskstill encloses all of # the area it should. boundary = np.array(disk.boundary) i = 0 while i < boundary.shape[0] - 1: if abs(boundary[i+1,0] - boundary[i,0]) > 180: pole = -90 if boundary[i,1] < 0 else 90 west = -180 if boundary[i,0] < 0 else 180 east = 180 if boundary[i,0] < 0 else -180 boundary = np.insert(boundary, i+1, [ [west, boundary[i,1]], [west, pole], [east, pole], [east, boundary[i+1,1]] ], axis=0) i += 5 else: i += 1 # If there were two sides that crossed the singularity and they # were both on the same side of the equator, the excursions will # coincide and shapely will be unhappy. buffer(0) corrects this. disk = Polygon(boundary).buffer(0) # Second, if the radius is very large, the projected disk might # enclose the complement of the region that it ought to enclose. # If it doesn't contain the reference point, we must subtract it # from the entire map. origin = Point(x, y) if not disk.contains(origin): disk = Box(-180, -90, 180, 90).difference(disk) assert disk.is_valid assert disk.contains(origin) return disk
def compute_bounding_region_now(self): if self._bounds is not None: return distance_bound = self.range_fn.distance_bound() # If the distance bound is too close to half the circumference # of the Earth, the projection operation below will produce an # invalid polygon. We don't get much use out of a bounding # region that includes the whole planet but for a tiny disk # (which will probably be somewhere in the ocean anyway) so # just give up and say that the bound is the entire planet. # Similarly, if the distance bound is zero, give up. if distance_bound > 19975000 or distance_bound == 0: self._bounds = Box(self.west, self.south, self.east, self.north) return # To find all points on the Earth within a certain distance of # a reference latitude and longitude, back-project onto the # Earth from an azimuthal-equidistant map with its zero point # at the reference latitude and longitude. aeqd = pyproj.Proj(proj='aeqd', ellps='WGS84', datum='WGS84', lat_0=self.ref_lat, lon_0=self.ref_lon) try: disk = sh_transform( functools.partial(pyproj.transform, aeqd, wgs_proj), Disk(0, 0, distance_bound)) # Two special cases must be manually dealt with. First, if # any side of the "circle" (really a many-sided polygon) # crosses the coordinate singularity at longitude ±180, we # must replace it with a diversion to either the north or # south pole (whichever is closer) to ensure that it still # encloses all of the area it should. boundary = np.array(disk.boundary) i = 0 while i < boundary.shape[0] - 1: if abs(boundary[i + 1, 0] - boundary[i, 0]) > 180: pole = self.south if boundary[i, 1] < 0 else self.north west = self.west if boundary[i, 0] < 0 else self.east east = self.east if boundary[i, 0] < 0 else self.west boundary = np.insert( boundary, i + 1, [[west, boundary[i, 1]], [west, pole], [east, pole], [east, boundary[i + 1, 1]]], axis=0) i += 5 else: i += 1 # If there were two edges that crossed the singularity and they # were both on the same side of the equator, the excursions will # coincide and shapely will be unhappy. buffer(0) corrects this. disk = Polygon(boundary).buffer(0) # Second, if the disk is very large, the projected disk might # enclose the complement of the region that it ought to enclose. # If it doesn't contain the reference point, we must subtract it # from the entire map. origin = Point(self.ref_lon, self.ref_lat) if not disk.contains(origin): disk = (Box(self.west, self.south, self.east, self.north).difference(disk)) assert disk.is_valid assert disk.contains(origin) self._bounds = disk except Exception as e: setattr(e, 'offending_disk', disk) setattr(e, 'offending_obs', self) raise