Source code for mpas_tools.mesh.creation.signed_distance

from __future__ import absolute_import, division, print_function, \
    unicode_literals

import numpy as np
import pyflann
from scipy import spatial
import timeit
from rasterio.features import rasterize
from affine import Affine
import shapely.geometry
from geometric_features.plot import subdivide_geom
from mpas_tools.mesh.creation.util import lonlat2xyz


[docs]def signed_distance_from_geojson(fc, lon_grd, lat_grd, earth_radius, max_length=None): """ Get the distance for each point on a lon/lat grid from the closest point on the boundary of the geojson regions. Parameters ---------- fc : geometrics_features.FeatureCollection The regions to be rasterized lon_grd : numpy.ndarray A 1D array of evenly spaced longitude values lat_grd : numpy.ndarray A 1D array of evenly spaced latitude values earth_radius : float Earth radius in meters max_length : float, optional The maximum distance (in degrees) between points on the boundary of the geojson region. If the boundary is too coarse, it will be subdivided. Returns ------- signed_distance : numpy.ndarray A 2D field of distances (negative inside the region, positive outside) to the shape boundary """ distance = distance_from_geojson(fc, lon_grd, lat_grd, earth_radius, nn_search='flann', max_length=max_length) mask = mask_from_geojson(fc, lon_grd, lat_grd) signed_distance = (-2.0 * mask + 1.0) * distance return signed_distance
[docs]def mask_from_geojson(fc, lon_grd, lat_grd): """ Make a rasterized mask on a lon/lat grid from shapes (geojson multipolygon data). Parameters ---------- fc : geometrics_features.FeatureCollection The regions to be rasterized lon_grd : numpy.ndarray A 1D array of evenly spaced longitude values lat_grd : numpy.ndarray A 1D array of evenly spaced latitude values Returns ------- mask : numpy.ndarray A 2D mask with the shapes rasterized (0.0 outside, 1.0 inside) """ print("Mask from geojson") print("-----------------") nlon = len(lon_grd) nlat = len(lat_grd) dlon = (lon_grd[-1] - lon_grd[0])/(nlon-1) dlat = (lat_grd[-1] - lat_grd[0])/(nlat-1) transform = Affine(dlon, 0.0, lon_grd[0], 0.0, dlat, lat_grd[0]) shapes = [] for feature in fc.features: # a list of feature geometries and mask values (always 1.0) shape = shapely.geometry.shape(feature['geometry']) # expand a bit to make sure we hit the edges of the domain shape = shape.buffer(dlat) shapes.append((shapely.geometry.mapping(shape), 1.0)) mask = rasterize(shapes, out_shape=(nlat, nlon), transform=transform) if np.abs(lon_grd[0] - (-180.)) < 1e-3 and \ np.abs(lon_grd[-1] - (180.)) < 1e-3: # the extra column at the periodic boundary needs to be copied print(' fixing periodic boundary') mask[:, -1] = mask[:, 0] return mask
[docs]def distance_from_geojson(fc, lon_grd, lat_grd, earth_radius, nn_search, max_length=None): # {{{ """ Get the distance for each point on a lon/lat grid from the closest point on the boundary of the geojson regions. Parameters ---------- fc : geometrics_features.FeatureCollection The regions to be rasterized lon_grd : numpy.ndarray A 1D array of evenly spaced longitude values lat_grd : numpy.ndarray A 1D array of evenly spaced latitude values earth_radius : float Earth radius in meters nn_search: {'kdtree', 'flann'} The method used to find the nearest point on the shape boundary max_length : float, optional The maximum distance (in degrees) between points on the boundary of the geojson region. If the boundary is too coarse, it will be subdivided. Returns ------- distance : numpy.ndarray A 2D field of distances to the shape boundary """ print("Distance from geojson") print("---------------------") print(" Finding region boundaries") boundary_lon = [] boundary_lat = [] for feature in fc.features: # get the boundary of each shape shape = shapely.geometry.shape(feature['geometry']).boundary if max_length is not None: # subdivide the shape if it's too coarse geom_type = shape.geom_type shape = subdivide_geom(shape, geom_type, max_length) x, y = shape.coords.xy boundary_lon.extend(x) boundary_lat.extend(y) boundary_lon = np.array(boundary_lon) boundary_lat = np.array(boundary_lat) # Remove point at +/- 180 lon and +/- 90 lat because these are "fake". # Need a little buffer (0.01 degrees) to avoid misses due to rounding. mask = np.logical_not(np.logical_or( np.logical_or(boundary_lon <= -179.99, boundary_lon >= 179.99), np.logical_or(boundary_lat <= -89.99, boundary_lat >= 89.99))) boundary_lon = boundary_lon[mask] boundary_lat = boundary_lat[mask] print(" Mean boundary latitude: {0:.2f}".format(np.mean(boundary_lat))) # Convert coastline points to x,y,z and create kd-tree npoints = len(boundary_lon) boundary_xyz = np.zeros((npoints, 3)) boundary_xyz[:, 0], boundary_xyz[:, 1], boundary_xyz[:, 2] = \ lonlat2xyz(boundary_lon, boundary_lat, earth_radius) flann = None tree = None if nn_search == "kdtree": tree = spatial.KDTree(boundary_xyz) elif nn_search == "flann": flann = pyflann.FLANN() flann.build_index( boundary_xyz, algorithm='kdtree', target_precision=1.0, random_seed=0) else: raise ValueError('Bad nn_search: expected kdtree or flann, got ' '{}'.format(nn_search)) # Convert backgound grid coordinates to x,y,z and put in a nx_grd x 3 array # for kd-tree query Lon_grd, Lat_grd = np.meshgrid(lon_grd, lat_grd) X_grd, Y_grd, Z_grd = lonlat2xyz(Lon_grd, Lat_grd, earth_radius) pts = np.vstack([X_grd.ravel(), Y_grd.ravel(), Z_grd.ravel()]).T # Find distances of background grid coordinates to the coast print(" Finding distance") start = timeit.default_timer() distance = None if nn_search == "kdtree": distance, _ = tree.query(pts) elif nn_search == "flann": _, distance = flann.nn_index(pts, checks=2000, random_seed=0) distance = np.sqrt(distance) end = timeit.default_timer() print(" Done") print(" {0:.0f} seconds".format(end-start)) # Make distance array that corresponds with cell_width array distance = np.reshape(distance, Lon_grd.shape) return distance