diff --git a/hvplot/converter.py b/hvplot/converter.py index 6eb4a7249..cd1fa1ae1 100644 --- a/hvplot/converter.py +++ b/hvplot/converter.py @@ -1383,7 +1383,10 @@ def _apply_layers(self, obj): scale) else: feature_obj = feature_obj.opts(scale=scale) - obj = feature_obj * obj + if feature_obj.group in ["Land", "Ocean"]: + obj = feature_obj * obj # Underlay land/ocean + else: + obj = obj * feature_obj # overlay everything else if self.tiles: tile_source = 'EsriImagery' if self.tiles == 'ESRI' else self.tiles diff --git a/hvplot/tests/testgeo.py b/hvplot/tests/testgeo.py index 6a3435421..6b2980021 100644 --- a/hvplot/tests/testgeo.py +++ b/hvplot/tests/testgeo.py @@ -138,6 +138,12 @@ def test_plot_with_coastline_sets_geo_by_default(self): coastline = plot.get(1) self.assertIsInstance(coastline, gv.Feature) + def test_plot_with_features_properly_overlaid_underlaid(self): + # land should be under, borders should be over + plot = self.df.hvplot.points('x', 'y', features=["land", "borders"]) + assert plot.get(0).group == "Land" + assert plot.get(2).group == "Borders" + def test_plot_with_coastline_scale(self): plot = self.df.hvplot.points('x', 'y', geo=True, coastline='10m') opts = plot.get(1).opts.get('plot') diff --git a/hvplot/tests/testutil.py b/hvplot/tests/testutil.py index 704b6e7f3..314b7dd23 100644 --- a/hvplot/tests/testutil.py +++ b/hvplot/tests/testutil.py @@ -217,6 +217,14 @@ def test_proj_to_cartopy(self): assert isinstance(crs, self.ccrs.CRS) + def test_proj_to_cartopy_wkt_string(self): + from ..util import proj_to_cartopy + crs = proj_to_cartopy('GEOGCRS["unnamed",BASEGEOGCRS["unknown",DATUM["unknown",ELLIPSOID["WGS 84",6378137,298.257223563,LENGTHUNIT["metre",1,ID["EPSG",9001]]]],PRIMEM["Greenwich",0,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8901]]],DERIVINGCONVERSION["unknown",METHOD["PROJ ob_tran o_proj=latlon"],PARAMETER["o_lon_p",0,ANGLEUNIT["degree",0.0174532925199433,ID["EPSG",9122]]],PARAMETER["o_lat_p",37.5,ANGLEUNIT["degree",0.0174532925199433,ID["EPSG",9122]]],PARAMETER["lon_0",357.5,ANGLEUNIT["degree",0.0174532925199433,ID["EPSG",9122]]]],CS[ellipsoidal,2],AXIS["longitude",east,ORDER[1],ANGLEUNIT["degree",0.0174532925199433,ID["EPSG",9122]]],AXIS["latitude",north,ORDER[2],ANGLEUNIT["degree",0.0174532925199433,ID["EPSG",9122]]]]') # noqa: E501 + + assert isinstance(crs, self.ccrs.RotatedPole) + assert crs.proj4_params["lon_0"] == 357.5 + assert crs.proj4_params["o_lat_p"] == 37.5 + class TestDynamicArgs(TestCase): diff --git a/hvplot/util.py b/hvplot/util.py index 83043c8f2..ea9cb1ea1 100644 --- a/hvplot/util.py +++ b/hvplot/util.py @@ -79,8 +79,11 @@ class Dummy: out = pyproj.Proj(crs.to_wkt(), preserve_units=True) elif isinstance(crs, dict) or isinstance(crs, str): if isinstance(crs, str): - # quick fix for https://github.com/pyproj4/pyproj/issues/345 - crs = crs.replace(' ', '').replace('+', ' +') + try: + crs = pyproj.CRS.from_wkt(crs) + except RuntimeError: + # quick fix for https://github.com/pyproj4/pyproj/issues/345 + crs = crs.replace(' ', '').replace('+', ' +') try: out = pyproj.Proj(crs, preserve_units=True) except RuntimeError: @@ -117,7 +120,6 @@ def proj_to_cartopy(proj): a cartopy.crs.Projection object """ - import cartopy import cartopy.crs as ccrs try: from osgeo import osr @@ -126,9 +128,8 @@ def proj_to_cartopy(proj): has_gdal = False proj = check_crs(proj) - - if proj_is_latlong(proj): - return ccrs.PlateCarree() + if proj is None: + raise ValueError(f"proj must be a valid pyproj.Proj object {proj}") srs = proj.srs if has_gdal: @@ -168,19 +169,23 @@ def proj_to_cartopy(proj): except: pass if k == 'proj': - if v == 'tmerc': + if v == "longlat": + cl = ccrs.PlateCarree + elif v == 'tmerc': cl = ccrs.TransverseMercator kw_proj['approx'] = True - if v == 'lcc': + elif v == 'lcc': cl = ccrs.LambertConformal - if v == 'merc': + elif v == 'merc': cl = ccrs.Mercator - if v == 'utm': + elif v == 'utm': cl = ccrs.UTM - if v == 'stere': + elif v == 'stere': cl = ccrs.Stereographic - if v == 'ob_tran': + elif v == 'ob_tran': cl = ccrs.RotatedPole + else: + raise NotImplementedError('Unknown projection {}'.format(v)) if k in km_proj: if k == 'zone': v = int(v) @@ -200,7 +205,7 @@ def proj_to_cartopy(proj): if cl.__name__ == 'Mercator': kw_proj.pop('false_easting', None) kw_proj.pop('false_northing', None) - if Version(cartopy.__version__) < Version('0.15'): + if "scale_factor" in kw_proj: kw_proj.pop('latitude_true_scale', None) elif cl.__name__ == 'Stereographic': kw_proj.pop('scale_factor', None) @@ -214,6 +219,8 @@ def proj_to_cartopy(proj): else: kw_proj.pop('latitude_true_scale', None) + print(kw_proj) + try: return cl(globe=globe, **kw_proj) except TypeError: @@ -229,7 +236,8 @@ def process_crs(crs): 1. EPSG codes: Defined as string of the form "EPSG: {code}" or an integer 2. proj.4 string: Defined as string of the form "{proj.4 string}" 3. cartopy.crs.CRS instance - 4. None defaults to crs.PlateCaree + 4. WKT string: Defined as string of the form "{WKT string}" + 5. None defaults to crs.PlateCaree """ missing = [] try: @@ -253,25 +261,24 @@ def process_crs(crs): errors = [] if isinstance(crs, str) and crs.lower().startswith('epsg'): try: - crs = crs[5:].lstrip().rstrip() - return ccrs.epsg(crs) + crs = pyproj.CRS.from_epsg(crs).to_wkt() except Exception as e: errors.append(e) if isinstance(crs, int): try: - return ccrs.epsg(crs) + crs = pyproj.CRS.from_epsg(crs).to_wkt() except Exception as e: crs = str(crs) errors.append(e) if isinstance(crs, (str, pyproj.Proj)): - try: - return proj_to_cartopy(crs) - except Exception as e: - errors.append(e) + return proj_to_cartopy(crs) if isinstance(crs, ccrs.CRS): return crs - raise ValueError("Projection must be defined as a EPSG code, proj4 string, cartopy CRS or pyproj.Proj.") from Exception(*errors) + raise ValueError( + "Projection must be defined as a EPSG code, proj4 string, " + "WKT string, cartopy CRS, or pyproj.Proj." + ) from Exception(*errors) def is_list_like(obj):