From e5265e9f3e06aab5111081fc71a1399893415dcf Mon Sep 17 00:00:00 2001 From: David Brochart Date: Sun, 1 Aug 2021 21:22:33 +0200 Subject: [PATCH 1/9] Fix tile URL --- xarray_leaflet/xarray_leaflet.py | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/xarray_leaflet/xarray_leaflet.py b/xarray_leaflet/xarray_leaflet.py index 87b213b..a271fe1 100644 --- a/xarray_leaflet/xarray_leaflet.py +++ b/xarray_leaflet/xarray_leaflet.py @@ -271,29 +271,12 @@ def _start(self): self.m.add_control(self.spinner_control) self._da, self.transform0_args = get_transform(self.transform0(self._da, debug_output=self.debug_output)) - self.url = self.m.window_url - if self.get_base_url is not None: - self.base_url = self.get_base_url(self.url) - else: - if self.url.endswith('/lab'): - # we are in JupyterLab - self.base_url = self.url[:-4] - else: - i_notebooks = self.url.find('/notebooks/') - i_voila = self.url.find('/voila/') - if i_notebooks != -1 and (i_voila == -1 or i_voila > i_notebooks): - # we are in classical Notebooks - self.base_url = self.url[:i_notebooks] - elif i_voila != -1 and (i_notebooks == -1 or i_notebooks > i_voila): - # we are in Voila - self.base_url = self.url[:i_voila] - if self.tile_dir is None: self.tile_temp_dir = tempfile.TemporaryDirectory(prefix='xarray_leaflet_') self.tile_path = self.tile_temp_dir.name else: self.tile_path = self.tile_dir - self.url = self.base_url + '/xarray_leaflet' + self.tile_path + '/{z}/{x}/{y}.png' + self.url = '/xarray_leaflet' + self.tile_path + '/{z}/{x}/{y}.png' self.l.path = self.url self.m.remove_control(self.spinner_control) @@ -333,7 +316,7 @@ def _get_tiles(self, change=None): self.tile_temp_dir.cleanup() self.tile_temp_dir = tempfile.TemporaryDirectory(prefix='xarray_leaflet_') new_tile_path = self.tile_temp_dir.name - new_url = self.base_url + '/xarray_leaflet' + new_tile_path + '/{z}/{x}/{y}.png' + new_url = '/xarray_leaflet' + new_tile_path + '/{z}/{x}/{y}.png' if self.l in self.m.layers: self.m.remove_layer(self.l) From dd3a2dab63cc307e077df79087a822fd3d43a5d8 Mon Sep 17 00:00:00 2001 From: David Brochart Date: Sun, 1 Aug 2021 21:41:04 +0200 Subject: [PATCH 2/9] Binder install from GitHub --- binder/environment.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/binder/environment.yml b/binder/environment.yml index fa99b94..4cfda23 100644 --- a/binder/environment.yml +++ b/binder/environment.yml @@ -6,4 +6,6 @@ dependencies: - tqdm - scipy - dask - - xarray_leaflet + - pip + - pip: + - git+https://github.com/davidbrochart/xarray_leaflet.git@fix_tile_url#egg=xarray_leaflet From ad9dea5c32e3471e070ab207f6fde278c7335e53 Mon Sep 17 00:00:00 2001 From: David Brochart Date: Sun, 1 Aug 2021 21:54:46 +0200 Subject: [PATCH 3/9] Use JupyterLab in Binder --- README.md | 2 +- binder/environment.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a438d74..9fad25d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/davidbrochart/xarray_leaflet/master?filepath=examples%2Fintroduction.ipynb) +[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/davidbrochart/xarray_leaflet/master?urlpath=lab%2Ftree%2Fexamples%2Fintroduction.ipynb) # xarray-leaflet: an xarray extension for tiled map plotting diff --git a/binder/environment.yml b/binder/environment.yml index 4cfda23..ee7c6c8 100644 --- a/binder/environment.yml +++ b/binder/environment.yml @@ -8,4 +8,4 @@ dependencies: - dask - pip - pip: - - git+https://github.com/davidbrochart/xarray_leaflet.git@fix_tile_url#egg=xarray_leaflet + - git+https://github.com/davidbrochart/xarray_leaflet.git@fix_tile_url From 171813ad0aae12f9f802e29cf0f927e31e3e9936 Mon Sep 17 00:00:00 2001 From: David Brochart Date: Sun, 1 Aug 2021 22:00:59 +0200 Subject: [PATCH 4/9] Add jupyterlab=3 to environment.yml --- binder/environment.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/binder/environment.yml b/binder/environment.yml index ee7c6c8..1d29690 100644 --- a/binder/environment.yml +++ b/binder/environment.yml @@ -6,6 +6,7 @@ dependencies: - tqdm - scipy - dask + - jupyterlab=3 - pip - pip: - git+https://github.com/davidbrochart/xarray_leaflet.git@fix_tile_url From 8d35e37ce11b7abaf1d4a33bf98e51955c035e80 Mon Sep 17 00:00:00 2001 From: David Brochart Date: Mon, 2 Aug 2021 23:23:52 +0200 Subject: [PATCH 5/9] Use ipyurl --- binder/environment.yml | 4 +--- examples/introduction.ipynb | 34 +++++++++++++++++++++++++++++--- xarray_leaflet/xarray_leaflet.py | 18 +++++++++++++---- 3 files changed, 46 insertions(+), 10 deletions(-) diff --git a/binder/environment.yml b/binder/environment.yml index 1d29690..7c9a077 100644 --- a/binder/environment.yml +++ b/binder/environment.yml @@ -7,6 +7,4 @@ dependencies: - scipy - dask - jupyterlab=3 - - pip - - pip: - - git+https://github.com/davidbrochart/xarray_leaflet.git@fix_tile_url + - xarray_leaflet diff --git a/examples/introduction.ipynb b/examples/introduction.ipynb index fd814fa..4a13370 100644 --- a/examples/introduction.ipynb +++ b/examples/introduction.ipynb @@ -32,9 +32,37 @@ "import xarray_leaflet\n", "from ipyleaflet import Map, basemaps, LayersControl, WidgetControl\n", "from ipywidgets import FloatSlider\n", + "from ipyurl import Url\n", "import matplotlib.pyplot as plt" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "These two cells are necessary to retrieve the server's URL:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "w = Url()\n", + "w" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def get_base_url(_):\n", + " return w.url.rstrip('/')" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -148,7 +176,7 @@ }, "outputs": [], "source": [ - "l = da.leaflet.plot(m, colormap=plt.cm.terrain)\n", + "l = da.leaflet.plot(m, colormap=plt.cm.terrain, get_base_url=get_base_url)\n", "# l.interact(opacity=(0., 1.))" ] }, @@ -232,7 +260,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -246,7 +274,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.6" + "version": "3.9.6" } }, "nbformat": 4, diff --git a/xarray_leaflet/xarray_leaflet.py b/xarray_leaflet/xarray_leaflet.py index a271fe1..ed40793 100644 --- a/xarray_leaflet/xarray_leaflet.py +++ b/xarray_leaflet/xarray_leaflet.py @@ -9,6 +9,7 @@ import mercantile from ipyleaflet import LocalTileLayer, WidgetControl, DrawControl from ipyspin import Spinner +from ipyurl import Url from ipywidgets import Output from IPython.display import display, Image from traitlets import HasTraits, Bool, observe @@ -116,8 +117,6 @@ def plot(self, self.debug_output = debug_output with self.debug_output: - self.get_base_url = get_base_url - if 'proj4def' in m.crs: # it's a custom projection if dynamic: @@ -204,6 +203,13 @@ def plot(self, self.dx = float((self.x_right - self.x_left) / (x.size - 1)) self.dy = float((self.y_upper - self.y_lower) / (y.size - 1)) + if get_base_url is not None: + self.base_url = get_base_url(self.m.window_url) + else: + self.url_widget = Url() + display(self.url_widget) + self.base_url = self.url_widget.url.rstrip('/') or None + if fit_bounds: asyncio.ensure_future(self.async_fit_bounds()) else: @@ -276,7 +282,7 @@ def _start(self): self.tile_path = self.tile_temp_dir.name else: self.tile_path = self.tile_dir - self.url = '/xarray_leaflet' + self.tile_path + '/{z}/{x}/{y}.png' + self.url = self.base_url + '/xarray_leaflet' + self.tile_path + '/{z}/{x}/{y}.png' self.l.path = self.url self.m.remove_control(self.spinner_control) @@ -316,7 +322,7 @@ def _get_tiles(self, change=None): self.tile_temp_dir.cleanup() self.tile_temp_dir = tempfile.TemporaryDirectory(prefix='xarray_leaflet_') new_tile_path = self.tile_temp_dir.name - new_url = '/xarray_leaflet' + new_tile_path + '/{z}/{x}/{y}.png' + new_url = self.base_url + '/xarray_leaflet' + new_tile_path + '/{z}/{x}/{y}.png' if self.l in self.m.layers: self.m.remove_layer(self.l) @@ -415,6 +421,8 @@ async def async_wait_for_bounds(self): with self.debug_output: if len(self.m.bounds) == 0: await wait_for_change(self.m, 'bounds') + if self.base_url is None: + self.base_url = await self.url_widget.get_url().rstrip('/') self.map_ready = True @@ -447,4 +455,6 @@ async def async_fit_bounds(self): self.m.zoom = self.m.zoom - 1 await wait_for_change(self.m, 'bounds') break + if self.base_url is None: + self.base_url = await self.url_widget.get_url().rstrip('/') self.map_ready = True From 97fbf4735e8f878c35315aa8f1bf3118a4f38035 Mon Sep 17 00:00:00 2001 From: David Brochart Date: Mon, 2 Aug 2021 23:36:48 +0200 Subject: [PATCH 6/9] Pin rasterio=1.2.6 in Binder, add ipyurl dependency --- binder/environment.yml | 1 + setup.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/binder/environment.yml b/binder/environment.yml index 7c9a077..cfc98c2 100644 --- a/binder/environment.yml +++ b/binder/environment.yml @@ -6,5 +6,6 @@ dependencies: - tqdm - scipy - dask + - rasterio=1.2.6 - jupyterlab=3 - xarray_leaflet diff --git a/setup.py b/setup.py index e1f995f..d907c13 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,8 @@ def get_data_files(): 'matplotlib>=3', 'affine>=2', 'mercantile>=1', - 'ipyspin>=0.1.1' + 'ipyspin>=0.1.1', + 'ipyurl', ] setup_requirements = [ ] From 617a41fa6571d6401b828369bc2dfa55a114f842 Mon Sep 17 00:00:00 2001 From: David Brochart Date: Mon, 2 Aug 2021 23:43:39 +0200 Subject: [PATCH 7/9] Add ipyurl to binder env --- binder/environment.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/binder/environment.yml b/binder/environment.yml index cfc98c2..6816777 100644 --- a/binder/environment.yml +++ b/binder/environment.yml @@ -9,3 +9,6 @@ dependencies: - rasterio=1.2.6 - jupyterlab=3 - xarray_leaflet + - pip + - pip: + - ipyurl From 6c06331225ef90f0d43e0b43fe88fedf7a41971b Mon Sep 17 00:00:00 2001 From: David Brochart Date: Tue, 3 Aug 2021 15:18:40 +0200 Subject: [PATCH 8/9] Update with ipyurl v0.1.2 --- examples/introduction.ipynb | 29 +---------------------------- setup.py | 2 +- xarray_leaflet/xarray_leaflet.py | 16 +++++++++------- 3 files changed, 11 insertions(+), 36 deletions(-) diff --git a/examples/introduction.ipynb b/examples/introduction.ipynb index 4a13370..c59965b 100644 --- a/examples/introduction.ipynb +++ b/examples/introduction.ipynb @@ -36,33 +36,6 @@ "import matplotlib.pyplot as plt" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "These two cells are necessary to retrieve the server's URL:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "w = Url()\n", - "w" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def get_base_url(_):\n", - " return w.url.rstrip('/')" - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -176,7 +149,7 @@ }, "outputs": [], "source": [ - "l = da.leaflet.plot(m, colormap=plt.cm.terrain, get_base_url=get_base_url)\n", + "l = da.leaflet.plot(m, colormap=plt.cm.terrain)\n", "# l.interact(opacity=(0., 1.))" ] }, diff --git a/setup.py b/setup.py index d907c13..735d55d 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def get_data_files(): 'affine>=2', 'mercantile>=1', 'ipyspin>=0.1.1', - 'ipyurl', + 'ipyurl>=0.1.2', ] setup_requirements = [ ] diff --git a/xarray_leaflet/xarray_leaflet.py b/xarray_leaflet/xarray_leaflet.py index ed40793..7a3d4ac 100644 --- a/xarray_leaflet/xarray_leaflet.py +++ b/xarray_leaflet/xarray_leaflet.py @@ -203,12 +203,10 @@ def plot(self, self.dx = float((self.x_right - self.x_left) / (x.size - 1)) self.dy = float((self.y_upper - self.y_lower) / (y.size - 1)) - if get_base_url is not None: - self.base_url = get_base_url(self.m.window_url) + if get_base_url is None: + self.base_url = None else: - self.url_widget = Url() - display(self.url_widget) - self.base_url = self.url_widget.url.rstrip('/') or None + self.base_url = get_base_url(self.m.window_url) if fit_bounds: asyncio.ensure_future(self.async_fit_bounds()) @@ -422,7 +420,9 @@ async def async_wait_for_bounds(self): if len(self.m.bounds) == 0: await wait_for_change(self.m, 'bounds') if self.base_url is None: - self.base_url = await self.url_widget.get_url().rstrip('/') + self.url_widget = Url() + display(self.url_widget) + self.base_url = (await self.url_widget.get_url()).rstrip('/') self.map_ready = True @@ -456,5 +456,7 @@ async def async_fit_bounds(self): await wait_for_change(self.m, 'bounds') break if self.base_url is None: - self.base_url = await self.url_widget.get_url().rstrip('/') + self.url_widget = Url() + display(self.url_widget) + self.base_url = (await self.url_widget.get_url()).rstrip('/') self.map_ready = True From 687f777757dce2009a4229c9e3b81c7dfb270d03 Mon Sep 17 00:00:00 2001 From: David Brochart Date: Tue, 3 Aug 2021 19:23:44 +0200 Subject: [PATCH 9/9] Remove debug_output --- binder/environment.yml | 2 +- examples/introduction.ipynb | 1 - xarray_leaflet/xarray_leaflet.py | 547 +++++++++++++++---------------- 3 files changed, 267 insertions(+), 283 deletions(-) diff --git a/binder/environment.yml b/binder/environment.yml index 6816777..759c431 100644 --- a/binder/environment.yml +++ b/binder/environment.yml @@ -11,4 +11,4 @@ dependencies: - xarray_leaflet - pip - pip: - - ipyurl + - ipyurl=0.1.2 diff --git a/examples/introduction.ipynb b/examples/introduction.ipynb index c59965b..7a2da43 100644 --- a/examples/introduction.ipynb +++ b/examples/introduction.ipynb @@ -32,7 +32,6 @@ "import xarray_leaflet\n", "from ipyleaflet import Map, basemaps, LayersControl, WidgetControl\n", "from ipywidgets import FloatSlider\n", - "from ipyurl import Url\n", "import matplotlib.pyplot as plt" ] }, diff --git a/xarray_leaflet/xarray_leaflet.py b/xarray_leaflet/xarray_leaflet.py index 7a3d4ac..b26f034 100644 --- a/xarray_leaflet/xarray_leaflet.py +++ b/xarray_leaflet/xarray_leaflet.py @@ -9,8 +9,8 @@ import mercantile from ipyleaflet import LocalTileLayer, WidgetControl, DrawControl from ipyspin import Spinner -from ipyurl import Url from ipywidgets import Output +from ipyurl import Url from IPython.display import display, Image from traitlets import HasTraits, Bool, observe from rasterio.warp import Resampling @@ -53,8 +53,7 @@ def plot(self, tile_height=256, tile_width=256, resampling=Resampling.nearest, - get_base_url=None, - debug_output=None): + get_base_url=None): """Display an array as an interactive map. Assumes that the pixels are given on a regular grid @@ -111,148 +110,143 @@ def plot(self, A handler to the layer that is added to the map. """ - if debug_output is None: - self.debug_output = Output() + + if 'proj4def' in m.crs: + # it's a custom projection + if dynamic: + raise RuntimeError('Dynamic maps are only supported for Web Mercator (EPSG:3857), not {}'.format(m.crs)) + self.dst_crs = m.crs['proj4def'] + self.web_mercator = False + self.custom_proj = True + elif m.crs['name'].startswith('EPSG'): + epsg = m.crs['name'][4:] + if dynamic and epsg != '3857': + raise RuntimeError('Dynamic maps are only supported for Web Mercator (EPSG:3857), not {}'.format(m.crs)) + self.dst_crs = 'EPSG:' + epsg + self.web_mercator = epsg == '3857' + self.custom_proj = False else: - self.debug_output = debug_output - - with self.debug_output: - if 'proj4def' in m.crs: - # it's a custom projection - if dynamic: - raise RuntimeError('Dynamic maps are only supported for Web Mercator (EPSG:3857), not {}'.format(m.crs)) - self.dst_crs = m.crs['proj4def'] - self.web_mercator = False - self.custom_proj = True - elif m.crs['name'].startswith('EPSG'): - epsg = m.crs['name'][4:] - if dynamic and epsg != '3857': - raise RuntimeError('Dynamic maps are only supported for Web Mercator (EPSG:3857), not {}'.format(m.crs)) - self.dst_crs = 'EPSG:' + epsg - self.web_mercator = epsg == '3857' - self.custom_proj = False - else: - raise RuntimeError('Unsupported map projection: {}'.format(m.crs)) - - self.nodata = self._da.rio.nodata - var_dims = self._da.dims - expected_dims = [y_dim, x_dim] - if rgb_dim is not None: - expected_dims.append(rgb_dim) - if set(var_dims) != set(expected_dims): - raise ValueError( - "Invalid dimensions in DataArray: " - "should include only {}, found {}." - .format(tuple(expected_dims), var_dims) - ) - - if rgb_dim is not None and colormap is not None: - raise ValueError( - "Cannot have a RGB dimension and a " - "colormap at the same time." - ) - elif rgb_dim is None: - if colormap is None: - colormap = plt.cm.viridis - if transform0 is None: - transform0 = normalize - else: - # there is a RGB dimension - if transform0 is None: - transform0 = passthrough - - self.resampling = resampling - self.tile_dir = tile_dir - self.persist = persist - self.attrs = self._da.attrs - self.m = m - self.dynamic = dynamic - self.tile_width = tile_width - self.tile_height = tile_height - self.transform0 = transform0 - self.transform1 = transform1 - self.transform2 = transform2 - self.transform3 = transform3 - self.colormap = colormap - self.colorbar = None - self.colorbar_position = colorbar_position - if self.dynamic: - self.persist = False - self.tile_dir = None - - self._da = self._da.rename({y_dim: 'y', x_dim: 'x'}) - if rgb_dim is None: - self.is_rgb = False - else: - self.is_rgb = True - self._da = self._da.rename({rgb_dim: 'rgb'}) + raise RuntimeError('Unsupported map projection: {}'.format(m.crs)) + + self.nodata = self._da.rio.nodata + var_dims = self._da.dims + expected_dims = [y_dim, x_dim] + if rgb_dim is not None: + expected_dims.append(rgb_dim) + if set(var_dims) != set(expected_dims): + raise ValueError( + "Invalid dimensions in DataArray: " + "should include only {}, found {}." + .format(tuple(expected_dims), var_dims) + ) + + if rgb_dim is not None and colormap is not None: + raise ValueError( + "Cannot have a RGB dimension and a " + "colormap at the same time." + ) + elif rgb_dim is None: + if colormap is None: + colormap = plt.cm.viridis + if transform0 is None: + transform0 = normalize + else: + # there is a RGB dimension + if transform0 is None: + transform0 = passthrough + + self.resampling = resampling + self.tile_dir = tile_dir + self.persist = persist + self.attrs = self._da.attrs + self.m = m + self.dynamic = dynamic + self.tile_width = tile_width + self.tile_height = tile_height + self.transform0 = transform0 + self.transform1 = transform1 + self.transform2 = transform2 + self.transform3 = transform3 + self.colormap = colormap + self.colorbar = None + self.colorbar_position = colorbar_position + if self.dynamic: + self.persist = False + self.tile_dir = None + + self._da = self._da.rename({y_dim: 'y', x_dim: 'x'}) + if rgb_dim is None: + self.is_rgb = False + else: + self.is_rgb = True + self._da = self._da.rename({rgb_dim: 'rgb'}) - # ensure latitudes are descending - if np.any(np.diff(self._da.y.values) >= 0): - self._da = self._da.sel(y=slice(None, None, -1)) + # ensure latitudes are descending + if np.any(np.diff(self._da.y.values) >= 0): + self._da = self._da.sel(y=slice(None, None, -1)) - # infer grid specifications (assume a rectangular grid) - y = self._da.y.values - x = self._da.x.values + # infer grid specifications (assume a rectangular grid) + y = self._da.y.values + x = self._da.x.values - self.x_left = float(x.min()) - self.x_right = float(x.max()) - self.y_lower = float(y.min()) - self.y_upper = float(y.max()) + self.x_left = float(x.min()) + self.x_right = float(x.max()) + self.y_lower = float(y.min()) + self.y_upper = float(y.max()) - self.dx = float((self.x_right - self.x_left) / (x.size - 1)) - self.dy = float((self.y_upper - self.y_lower) / (y.size - 1)) + self.dx = float((self.x_right - self.x_left) / (x.size - 1)) + self.dy = float((self.y_upper - self.y_lower) / (y.size - 1)) - if get_base_url is None: - self.base_url = None - else: - self.base_url = get_base_url(self.m.window_url) + if get_base_url is None: + self.base_url = None + self.url_widget = Url() + display(self.url_widget) + else: + self.base_url = get_base_url(self.m.window_url) - if fit_bounds: - asyncio.ensure_future(self.async_fit_bounds()) - else: - asyncio.ensure_future(self.async_wait_for_bounds()) + if fit_bounds: + asyncio.ensure_future(self.async_fit_bounds()) + else: + asyncio.ensure_future(self.async_wait_for_bounds()) - self.l = LocalTileLayer() - if self._da.name is not None: - self.l.name = self._da.name + self.l = LocalTileLayer() + if self._da.name is not None: + self.l.name = self._da.name - self._da_notransform = self._da + self._da_notransform = self._da - self.spinner = Spinner() - self.spinner.radius = 5 - self.spinner.length = 3 - self.spinner.width = 5 - self.spinner.lines = 8 - self.spinner.color = '#000000' - self.spinner.layout.height = '30px' - self.spinner.layout.width = '30px' - self.spinner_control = WidgetControl(widget=self.spinner, position='bottomright') + self.spinner = Spinner() + self.spinner.radius = 5 + self.spinner.length = 3 + self.spinner.width = 5 + self.spinner.lines = 8 + self.spinner.color = '#000000' + self.spinner.layout.height = '30px' + self.spinner.layout.width = '30px' + self.spinner_control = WidgetControl(widget=self.spinner, position='bottomright') - return self.l + return self.l def select(self, draw_control=None): - with self.debug_output: - if draw_control is None: - self._draw_control = DrawControl() - self._draw_control.polygon = {} - self._draw_control.polyline = {} - self._draw_control.circlemarker = {} - self._draw_control.rectangle = { - 'shapeOptions': { - 'fillOpacity': 0.5 - } + if draw_control is None: + self._draw_control = DrawControl() + self._draw_control.polygon = {} + self._draw_control.polyline = {} + self._draw_control.circlemarker = {} + self._draw_control.rectangle = { + 'shapeOptions': { + 'fillOpacity': 0.5 } - else: - self._draw_control = draw_control - self._draw_control.on_draw(self._get_selection) - self.m.add_control(self._draw_control) + } + else: + self._draw_control = draw_control + self._draw_control.on_draw(self._get_selection) + self.m.add_control(self._draw_control) def unselect(self): - with self.debug_output: - self.m.remove_control(self._draw_control) + self.m.remove_control(self._draw_control) def get_selection(self): @@ -260,35 +254,33 @@ def get_selection(self): def _get_selection(self, *args, **kwargs): - with self.debug_output: - if self._draw_control.last_draw['geometry'] is not None: - lonlat = self._draw_control.last_draw['geometry']['coordinates'][0] - lats = [ll[1] for ll in lonlat] - lons = [ll[0] for ll in lonlat] - lt0, lt1 = min(lats), max(lats) - ln0, ln1 = min(lons), max(lons) - self._da_selected = self._da_notransform.sel(y=slice(lt1, lt0), x=slice(ln0, ln1)) + if self._draw_control.last_draw['geometry'] is not None: + lonlat = self._draw_control.last_draw['geometry']['coordinates'][0] + lats = [ll[1] for ll in lonlat] + lons = [ll[0] for ll in lonlat] + lt0, lt1 = min(lats), max(lats) + ln0, ln1 = min(lons), max(lons) + self._da_selected = self._da_notransform.sel(y=slice(lt1, lt0), x=slice(ln0, ln1)) def _start(self): - with self.debug_output: - self.m.add_control(self.spinner_control) - self._da, self.transform0_args = get_transform(self.transform0(self._da, debug_output=self.debug_output)) + self.m.add_control(self.spinner_control) + self._da, self.transform0_args = get_transform(self.transform0(self._da)) - if self.tile_dir is None: - self.tile_temp_dir = tempfile.TemporaryDirectory(prefix='xarray_leaflet_') - self.tile_path = self.tile_temp_dir.name - else: - self.tile_path = self.tile_dir - self.url = self.base_url + '/xarray_leaflet' + self.tile_path + '/{z}/{x}/{y}.png' - self.l.path = self.url + if self.tile_dir is None: + self.tile_temp_dir = tempfile.TemporaryDirectory(prefix='xarray_leaflet_') + self.tile_path = self.tile_temp_dir.name + else: + self.tile_path = self.tile_dir + self.url = self.base_url + '/xarray_leaflet' + self.tile_path + '/{z}/{x}/{y}.png' + self.l.path = self.url - self.m.remove_control(self.spinner_control) - self._get_tiles() - self.m.observe(self._get_tiles, names='pixel_bounds') - if not self.dynamic: - self._show_colorbar(self._da_notransform) - self.m.add_layer(self.l) + self.m.remove_control(self.spinner_control) + self._get_tiles() + self.m.observe(self._get_tiles, names='pixel_bounds') + if not self.dynamic: + self._show_colorbar(self._da_notransform) + self.m.add_layer(self.l) def _show_colorbar(self, da): @@ -314,149 +306,142 @@ def _show_colorbar(self, da): def _get_tiles(self, change=None): - with self.debug_output: - self.m.add_control(self.spinner_control) - if self.dynamic: - self.tile_temp_dir.cleanup() - self.tile_temp_dir = tempfile.TemporaryDirectory(prefix='xarray_leaflet_') - new_tile_path = self.tile_temp_dir.name - new_url = self.base_url + '/xarray_leaflet' + new_tile_path + '/{z}/{x}/{y}.png' - if self.l in self.m.layers: - self.m.remove_layer(self.l) - - (left, top), (right, bottom) = self.m.pixel_bounds - (south, west), (north, east) = self.m.bounds - z = int(self.m.zoom) # TODO: support non-integer zoom levels? - if self.custom_proj: - resolution = self.m.crs['resolutions'][z] - - if self.web_mercator: - tiles = list(mercantile.tiles(west, south, east, north, z)) - else: - x0, x1 = int(left) // self.tile_width, int(right) // self.tile_width + 1 - y0, y1 = int(top) // self.tile_height, int(bottom) // self.tile_height + 1 - tiles = [mercantile.Tile(x, y, z) for x in range(x0, x1) for y in range(y0, y1)] - - if self.dynamic: - # dynamic maps are redrawn at each interaction with the map - # so we can take exactly the corresponding slice in the original data - da_visible = self._da.sel(y=slice(north, south), x=slice(west, east)) - elif self.web_mercator: - # for static web mercator maps we can't redraw a tile once it has been (partly) displayed, - # so we must slice the original data on tile boundaries - bbox = get_bbox_tiles(tiles) - # take one more source data point to avoid glitches - da_visible = self._da.sel(y=slice(bbox.north + self.dy, bbox.south - self.dy), x=slice(bbox.west - self.dx, bbox.east + self.dx)) - else: - # it's a custom projection or not web mercator, the visible tiles don't translate easily - # to a slice of the original data, so we keep everything - # TODO: slice the data for EPSG3395, EPSG4326, Earth, Base and Simple - da_visible = self._da - - # check if we have some data to show - if 0 not in da_visible.shape: - da_visible, transform1_args = get_transform(self.transform1(da_visible, *self.transform0_args, debug_output=self.debug_output)) - - if self.dynamic: - self.tile_path = new_tile_path - self.url = new_url - - for tile in tiles: - x, y, z = tile - path = f'{self.tile_path}/{z}/{x}/{y}.png' - # if static map, check if we already have the tile - # if dynamic map, new tiles are always created - if self.dynamic or not os.path.exists(path): - if self.web_mercator: - bbox = mercantile.bounds(tile) - xy_bbox = mercantile.xy_bounds(tile) - x_pix = (xy_bbox.right - xy_bbox.left) / self.tile_width - y_pix = (xy_bbox.top - xy_bbox.bottom) / self.tile_height - # take one more source data point to avoid glitches - da_tile = da_visible.sel(y=slice(bbox.north + self.dy, bbox.south - self.dy), x=slice(bbox.west - self.dx, bbox.east + self.dx)) - else: - da_tile = da_visible - # check if we have data for this tile - if 0 in da_tile.shape: - write_image(path, None, self.persist) + self.m.add_control(self.spinner_control) + if self.dynamic: + self.tile_temp_dir.cleanup() + self.tile_temp_dir = tempfile.TemporaryDirectory(prefix='xarray_leaflet_') + new_tile_path = self.tile_temp_dir.name + new_url = self.base_url + '/xarray_leaflet' + new_tile_path + '/{z}/{x}/{y}.png' + if self.l in self.m.layers: + self.m.remove_layer(self.l) + + (left, top), (right, bottom) = self.m.pixel_bounds + (south, west), (north, east) = self.m.bounds + z = int(self.m.zoom) # TODO: support non-integer zoom levels? + if self.custom_proj: + resolution = self.m.crs['resolutions'][z] + + if self.web_mercator: + tiles = list(mercantile.tiles(west, south, east, north, z)) + else: + x0, x1 = int(left) // self.tile_width, int(right) // self.tile_width + 1 + y0, y1 = int(top) // self.tile_height, int(bottom) // self.tile_height + 1 + tiles = [mercantile.Tile(x, y, z) for x in range(x0, x1) for y in range(y0, y1)] + + if self.dynamic: + # dynamic maps are redrawn at each interaction with the map + # so we can take exactly the corresponding slice in the original data + da_visible = self._da.sel(y=slice(north, south), x=slice(west, east)) + elif self.web_mercator: + # for static web mercator maps we can't redraw a tile once it has been (partly) displayed, + # so we must slice the original data on tile boundaries + bbox = get_bbox_tiles(tiles) + # take one more source data point to avoid glitches + da_visible = self._da.sel(y=slice(bbox.north + self.dy, bbox.south - self.dy), x=slice(bbox.west - self.dx, bbox.east + self.dx)) + else: + # it's a custom projection or not web mercator, the visible tiles don't translate easily + # to a slice of the original data, so we keep everything + # TODO: slice the data for EPSG3395, EPSG4326, Earth, Base and Simple + da_visible = self._da + + # check if we have some data to show + if 0 not in da_visible.shape: + da_visible, transform1_args = get_transform(self.transform1(da_visible, *self.transform0_args)) + + if self.dynamic: + self.tile_path = new_tile_path + self.url = new_url + + for tile in tiles: + x, y, z = tile + path = f'{self.tile_path}/{z}/{x}/{y}.png' + # if static map, check if we already have the tile + # if dynamic map, new tiles are always created + if self.dynamic or not os.path.exists(path): + if self.web_mercator: + bbox = mercantile.bounds(tile) + xy_bbox = mercantile.xy_bounds(tile) + x_pix = (xy_bbox.right - xy_bbox.left) / self.tile_width + y_pix = (xy_bbox.top - xy_bbox.bottom) / self.tile_height + # take one more source data point to avoid glitches + da_tile = da_visible.sel(y=slice(bbox.north + self.dy, bbox.south - self.dy), x=slice(bbox.west - self.dx, bbox.east + self.dx)) + else: + da_tile = da_visible + # check if we have data for this tile + if 0 in da_tile.shape: + write_image(path, None, self.persist) + else: + da_tile.attrs = self.attrs + da_tile, transform2_args = get_transform(self.transform2(da_tile, tile_width=self.tile_width, tile_height=self.tile_height), *transform1_args) + # reproject each RGB component if needed + # TODO: must be doable with xarray.apply_ufunc + if self.is_rgb: + das = [da_tile.isel(rgb=i) for i in range(3)] else: - da_tile.attrs = self.attrs - da_tile, transform2_args = get_transform(self.transform2(da_tile, tile_width=self.tile_width, tile_height=self.tile_height, debug_output=self.debug_output), *transform1_args) - # reproject each RGB component if needed - # TODO: must be doable with xarray.apply_ufunc - if self.is_rgb: - das = [da_tile.isel(rgb=i) for i in range(3)] + das = [da_tile] + for i in range(len(das)): + das[i] = das[i].rio.write_nodata(self.nodata) + if self.custom_proj: + das[i] = reproject_custom(das[i], self.dst_crs, x, y, z, resolution, resolution, self.tile_width, self.tile_height, self.resampling) else: - das = [da_tile] - for i in range(len(das)): - das[i] = das[i].rio.write_nodata(self.nodata) - if self.custom_proj: - das[i] = reproject_custom(das[i], self.dst_crs, x, y, z, resolution, resolution, self.tile_width, self.tile_height, self.resampling) - else: - das[i] = reproject_not_custom(das[i], self.dst_crs, xy_bbox.left, xy_bbox.top, x_pix, y_pix, self.tile_width, self.tile_height, self.resampling) - das[i], transform3_args = get_transform(self.transform3(das[i], *transform2_args, debug_output=self.debug_output)) - if self.is_rgb: - alpha = np.where(das[0]==self._da.rio.nodata, 0, 255) - das.append(alpha) - da_tile = np.stack(das, axis=2) - write_image(path, da_tile, self.persist) - else: - da_tile = self.colormap(das[0]) - write_image(path, da_tile*255, self.persist) + das[i] = reproject_not_custom(das[i], self.dst_crs, xy_bbox.left, xy_bbox.top, x_pix, y_pix, self.tile_width, self.tile_height, self.resampling) + das[i], transform3_args = get_transform(self.transform3(das[i], *transform2_args)) + if self.is_rgb: + alpha = np.where(das[0]==self._da.rio.nodata, 0, 255) + das.append(alpha) + da_tile = np.stack(das, axis=2) + write_image(path, da_tile, self.persist) + else: + da_tile = self.colormap(das[0]) + write_image(path, da_tile*255, self.persist) - if self.dynamic: - if self.colorbar in self.m.controls: - self.m.remove_control(self.colorbar) - self._show_colorbar(self._da_notransform.sel(y=slice(north, south), x=slice(west, east))) - self.l.path = self.url - self.m.add_layer(self.l) - self.l.redraw() + if self.dynamic: + if self.colorbar in self.m.controls: + self.m.remove_control(self.colorbar) + self._show_colorbar(self._da_notransform.sel(y=slice(north, south), x=slice(west, east))) + self.l.path = self.url + self.m.add_layer(self.l) + self.l.redraw() - self.m.remove_control(self.spinner_control) + self.m.remove_control(self.spinner_control) async def async_wait_for_bounds(self): - with self.debug_output: - if len(self.m.bounds) == 0: - await wait_for_change(self.m, 'bounds') - if self.base_url is None: - self.url_widget = Url() - display(self.url_widget) - self.base_url = (await self.url_widget.get_url()).rstrip('/') - self.map_ready = True + if len(self.m.bounds) == 0: + await wait_for_change(self.m, 'bounds') + if self.base_url is None: + self.base_url = (await self.url_widget.get_url()).rstrip('/') + self.map_ready = True async def async_fit_bounds(self): - with self.debug_output: - center = self.y_lower + (self.y_upper - self.y_lower) / 2, self.x_left + (self.x_right - self.x_left) / 2 - if center != self.m.center: - self.m.center = center + center = self.y_lower + (self.y_upper - self.y_lower) / 2, self.x_left + (self.x_right - self.x_left) / 2 + if center != self.m.center: + self.m.center = center + await wait_for_change(self.m, 'bounds') + zoomed_out = False + # zoom out + while True: + if self.m.zoom <= 1: + break + (south, west), (north, east) = self.m.bounds + if south > self.y_lower or north < self.y_upper or west > self.x_left or east < self.x_right: + self.m.zoom = self.m.zoom - 1 await wait_for_change(self.m, 'bounds') - zoomed_out = False - # zoom out + zoomed_out = True + else: + break + if not zoomed_out: + # zoom in while True: - if self.m.zoom <= 1: - break (south, west), (north, east) = self.m.bounds - if south > self.y_lower or north < self.y_upper or west > self.x_left or east < self.x_right: - self.m.zoom = self.m.zoom - 1 + if south < self.y_lower and north > self.y_upper and west < self.x_left and east > self.x_right: + self.m.zoom = self.m.zoom + 1 await wait_for_change(self.m, 'bounds') - zoomed_out = True else: + self.m.zoom = self.m.zoom - 1 + await wait_for_change(self.m, 'bounds') break - if not zoomed_out: - # zoom in - while True: - (south, west), (north, east) = self.m.bounds - if south < self.y_lower and north > self.y_upper and west < self.x_left and east > self.x_right: - self.m.zoom = self.m.zoom + 1 - await wait_for_change(self.m, 'bounds') - else: - self.m.zoom = self.m.zoom - 1 - await wait_for_change(self.m, 'bounds') - break - if self.base_url is None: - self.url_widget = Url() - display(self.url_widget) - self.base_url = (await self.url_widget.get_url()).rstrip('/') - self.map_ready = True + if self.base_url is None: + self.base_url = (await self.url_widget.get_url()).rstrip('/') + self.map_ready = True