From bfaa1c82a6e1b6645365173f83169d24ff95366e Mon Sep 17 00:00:00 2001 From: tsutterley Date: Wed, 20 Jul 2022 12:32:33 -0700 Subject: [PATCH 1/8] Adding image service layers to address #932 --- docs/source/layers/image_service.rst | 33 ++++ docs/source/layers/index.rst | 1 + examples/CustomProjections.ipynb | 66 ++++++- examples/ImageService.ipynb | 221 +++++++++++++++++++++ ipyleaflet/leaflet.py | 73 +++++++ ipyleaflet/projections.py | 178 +++++++++++++---- js/src/jupyter-leaflet.js | 1 + js/src/layers/ImageService.js | 74 +++++++ js/src/leaflet-imageservice.js | 283 +++++++++++++++++++++++++++ js/src/leaflet.js | 1 + 10 files changed, 883 insertions(+), 48 deletions(-) create mode 100644 docs/source/layers/image_service.rst create mode 100644 examples/ImageService.ipynb create mode 100644 js/src/layers/ImageService.js create mode 100644 js/src/leaflet-imageservice.js diff --git a/docs/source/layers/image_service.rst b/docs/source/layers/image_service.rst new file mode 100644 index 000000000..39890a438 --- /dev/null +++ b/docs/source/layers/image_service.rst @@ -0,0 +1,33 @@ +Image Service +============= + +Example +------- + +.. jupyter-execute:: + + from ipyleaflet import Map, ImageService, basemaps + + im = ImageService( + url='https://landsat.arcgis.com/arcgis/rest/services/Landsat/PS/ImageServer', + rendering_rule={"rasterFunction":"Pansharpened Enhanced with DRA"}, + format='jpgpng', + attribution='United States Geological Survey (USGS), National Aeronautics and Space Administration (NASA)' + ) + + m = Map(basemap=basemaps.Esri.WorldTopoMap, center=(47.655548, -122.303200), zoom=12) + + m.add(im) + + m + +Usage +----- + +By default, options like ``format``, ``band_ids``, ``time``, ``rendering_rule`` are appended to the request URL when making the image service layer request. + +Attributes +---------- + +.. autoclass:: ipyleaflet.leaflet.ImageService + :members: diff --git a/docs/source/layers/index.rst b/docs/source/layers/index.rst index 2ab5e2085..b7c943bb2 100644 --- a/docs/source/layers/index.rst +++ b/docs/source/layers/index.rst @@ -14,6 +14,7 @@ Layers popup wms_layer image_video_overlay + image_service antpath polyline polygon diff --git a/examples/CustomProjections.ipynb b/examples/CustomProjections.ipynb index 319837a28..a9a93be37 100644 --- a/examples/CustomProjections.ipynb +++ b/examples/CustomProjections.ipynb @@ -6,8 +6,9 @@ "source": [ "## New Built-In Projections\n", "\n", - "ipyleaflet now supports custom map projections and includes 2 base layers for polar projections:\n", - "NASA's Next GenerationBlue Marble 500m for the Arctic and Antarctic regions." + "ipyleaflet now supports custom map projections and includes base layers for polar projections:\n", + "- NASA's Next Generation Blue Marble 500m for the Arctic and Antarctic regions\n", + "- Esri Arctic Ocean Basemap and Antarctic Basemap" ] }, { @@ -56,12 +57,57 @@ " center=(90, 0),\n", " zoom=0,\n", " basemap=basemaps.NASAGIBS.BlueMarble3413,\n", - " crs=projections.EPSG3413,\n", + " crs=projections.EPSG3413.NASAGIBS,\n", ")\n", "m1.add(dc)\n", "m1" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# note that we need to use the same projection for the our layer and the map.\n", + "m2 = Map(\n", + " center=(-90, 0),\n", + " zoom=0,\n", + " basemap=basemaps.Esri.AntarcticBasemap,\n", + " crs=projections.EPSG3031.Basemap,\n", + ")\n", + "\n", + "# add draw control on Antarctic map\n", + "dc2 = DrawControl(marker={\"shapeOptions\": {\"color\": \"#0000FF\"}})\n", + "dc2.on_draw(handle_draw)\n", + "m2.add(dc2)\n", + " \n", + "# MODIS Mosaic of Antarctica (MOA)\n", + "MOA3031 = dict(\n", + " name='EPSG:3031',\n", + " custom=True,\n", + " proj4def=\"\"\"+proj=stere +lat_0=-90 +lat_ts=-71 +lon_0=0 +k=1\n", + " +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs\"\"\",\n", + " bounds=[[-3174450,-2816050],[2867175,2406325]]\n", + ")\n", + "\n", + "MOA = WMSLayer(\n", + " attribution=\"\"\"\n", + " U.S. Geological Survey (USGS), British Antarctic Survey (BAS),\n", + " National Aeronautics and Space Administration (NASA)\n", + " \"\"\",\n", + " layers=\"MOA_125_HP1_090_230\",\n", + " format='image/png',\n", + " transparent=False,\n", + " opacity=0.5,\n", + " url='https://nimbus.cr.usgs.gov/arcgis/services/Antarctica/USGS_EROS_Antarctica_Reference/MapServer/WmsServer',\n", + " crs=MOA3031\n", + ")\n", + "m2.add(MOA)\n", + "\n", + "m2" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -100,21 +146,21 @@ " crs=my_projection, # I'm asking this WMS service to reproject the tile layer using EPSG:2163\n", ")\n", "\n", - "m2 = Map(center=(40, -104), zoom=0, layers=(wms,), crs=my_projection)\n", + "m3 = Map(center=(40, -104), zoom=0, layers=(wms,), crs=my_projection)\n", "\n", "\n", - "dc2 = DrawControl(marker={\"shapeOptions\": {\"color\": \"#0000FF\"}})\n", - "dc2.on_draw(handle_draw)\n", + "dc3 = DrawControl(marker={\"shapeOptions\": {\"color\": \"#0000FF\"}})\n", + "dc3.on_draw(handle_draw)\n", "\n", - "m2.add(dc2)\n", + "m3.add(dc3)\n", "\n", - "m2" + "m3" ] } ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "Python 3", "language": "python", "name": "python3" }, @@ -128,7 +174,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.5" + "version": "3.8.10" } }, "nbformat": 4, diff --git a/examples/ImageService.ipynb b/examples/ImageService.ipynb new file mode 100644 index 000000000..1af0bdc6e --- /dev/null +++ b/examples/ImageService.ipynb @@ -0,0 +1,221 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Using an Image Service\n", + "\n", + "This notebook shows how you can overlay images from an ESRI Image Server on a Leaflet map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import time\n", + "from ipywidgets import Dropdown\n", + "from ipyleaflet import (\n", + " Map,\n", + " basemaps,\n", + " basemap_to_tiles,\n", + " ImageService,\n", + " projections,\n", + " WidgetControl\n", + ")\n", + "\n", + "# Columbia Glacier with Landsat\n", + "m1 = Map(\n", + " center=[61.1, -146.9],\n", + " zoom=10,\n", + " basemap=basemaps.Esri.WorldTopoMap,\n", + ")\n", + "\n", + "# create a widget control for years of observations\n", + "years = [\n", + " ['1980-01-01','1989-12-31'],\n", + " ['2000-01-01','2009-12-31'],\n", + " ['2010-01-01','2019-12-31']\n", + "]\n", + "def range_formatter(d):\n", + " st = time.strptime(d[0],'%Y-%m-%d')\n", + " et = time.strptime(d[1],'%Y-%m-%d')\n", + " return u'{0:d}\\u2013{1:d}'.format(st.tm_year,et.tm_year)\n", + "\n", + "range_dropdown1 = Dropdown(\n", + " value=u'{0:d}\\u2013{1:d}'.format(1980,1989),\n", + " options=[range_formatter(d) for d in years],\n", + " description=\"Years:\",\n", + ")\n", + "\n", + "# add image service layer with Landsat multi-spectral imagery\n", + "# create a false color image to highlight ice and snow\n", + "url = 'https://landsat.arcgis.com/arcgis/rest/services/LandsatGLS/MS/ImageServer'\n", + "attribution = \"\"\"United States Geological Survey (USGS),\n", + " National Aeronautics and Space Administration (NASA)\"\"\"\n", + "im1 = ImageService(url=url,\n", + " band_ids=['5','4','2'],\n", + " time=years[range_dropdown1.index],\n", + " attribution=attribution)\n", + "m1.add(im1)\n", + "\n", + "# add control for year range\n", + "widget_control1 = WidgetControl(widget=range_dropdown1, position=\"topright\")\n", + "m1.add(widget_control1)\n", + "\n", + "# set the year range\n", + "def set_year_range(sender):\n", + " im1.time = years[range_dropdown1.index]\n", + " # force redrawing of map by removing and adding layer\n", + " m1.remove(im1)\n", + " m1.add(im1)\n", + "\n", + "# watch year range function widget for changes\n", + "range_dropdown1.observe(set_year_range)\n", + "m1" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# ArcticDEM\n", + "# note that we need to use the same projection for the our image service layer and the map.\n", + "m2 = Map(\n", + " center=(90, 0),\n", + " zoom=4,\n", + " basemap=basemaps.Esri.ArcticOceanBase,\n", + " crs=projections.EPSG5936.Basemap,\n", + ")\n", + "# add arctic ocean reference basemap\n", + "tl2 = basemap_to_tiles(basemaps.Esri.ArcticOceanReference)\n", + "m2.add(tl2)\n", + "\n", + "# create a widget control for the raster function\n", + "raster_functions = [\n", + " \"Aspect Map\",\n", + " \"Contour 25\",\n", + " \"Hillshade Elevation Tinted\",\n", + " \"Hillshade Gray\",\n", + " \"Height Ellipsoidal\",\n", + " \"Height Orthometric\",\n", + " \"Slope Map\"]\n", + "raster_dropdown2 = Dropdown(\n", + " value=\"Hillshade Gray\",\n", + " options=raster_functions,\n", + " description=\"Raster:\",\n", + ")\n", + "\n", + "# add image service layer with ArcticDEM\n", + "url = 'https://elevation2.arcgis.com/arcgis/rest/services/Polar/ArcticDEM/ImageServer'\n", + "rendering_rule = {\"rasterFunction\": raster_dropdown2.value}\n", + "im2 = ImageService(url=url,\n", + " format='jpgpng', rendering_rule=rendering_rule,\n", + " attribution='Esri, PGC, UMN, NSF, NGA, DigitalGlobe',\n", + " crs=projections.EPSG5936.Basemap)\n", + "m2.add(im2) \n", + "\n", + "# add control for raster function\n", + "widget_control2 = WidgetControl(widget=raster_dropdown2, position=\"topright\")\n", + "m2.add(widget_control2)\n", + "\n", + "# set the rendering rule\n", + "def set_raster_function2(sender):\n", + " im2.rendering_rule = {\"rasterFunction\": raster_dropdown2.value}\n", + " # force redrawing of map by removing and adding layer\n", + " m2.remove(im2)\n", + " m2.add(im2)\n", + "\n", + "# watch raster function widget for changes\n", + "raster_dropdown2.observe(set_raster_function2)\n", + "m2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Reference Elevation Model of Antarctica (REMA)\n", + "# note that we need to use the same projection for the our image service layer and the map.\n", + "m3 = Map(\n", + " center=(-90, 0),\n", + " zoom=3,\n", + " basemap=basemaps.Esri.AntarcticBasemap,\n", + " crs=projections.EPSG3031.Basemap,\n", + ")\n", + "\n", + "# create a widget control for the raster function\n", + "raster_functions = [\n", + " \"Aspect Map\",\n", + " \"Contour 25\",\n", + " \"Hillshade Elevation Tinted\",\n", + " \"Hillshade Gray\",\n", + " \"Height Orthometric\",\n", + " \"Slope Degrees Map\"]\n", + "raster_dropdown3 = Dropdown(\n", + " value=\"Hillshade Gray\",\n", + " options=raster_functions,\n", + " description=\"Raster:\",\n", + ")\n", + "\n", + "# add image service layer with REMA imagery\n", + "url = 'https://elevation2.arcgis.com/arcgis/rest/services/Polar/AntarcticDEM/ImageServer'\n", + "rendering_rule = {\"rasterFunction\": raster_dropdown3.value}\n", + "im3 = ImageService(url=url,\n", + " format='jpgpng', rendering_rule=rendering_rule,\n", + " attribution='Esri, PGC, UMN, NSF, NGA, DigitalGlobe',\n", + " crs=projections.EPSG3031.Basemap)\n", + "m3.add(im3)\n", + "\n", + "# add control for raster function\n", + "widget_control3 = WidgetControl(widget=raster_dropdown3, position=\"topright\")\n", + "m3.add(widget_control3)\n", + "\n", + "# set the rendering rule\n", + "def set_raster_function3(sender):\n", + " im3.rendering_rule = {\"rasterFunction\": raster_dropdown3.value}\n", + " # force redrawing of map by removing and adding layer\n", + " m3.remove(im3)\n", + " m3.add(im3)\n", + "\n", + "# watch raster function widget for changes\n", + "raster_dropdown3.observe(set_raster_function3)\n", + "m3" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/ipyleaflet/leaflet.py b/ipyleaflet/leaflet.py index 7ed22c154..0ac43feed 100644 --- a/ipyleaflet/leaflet.py +++ b/ipyleaflet/leaflet.py @@ -782,6 +782,79 @@ class VideoOverlay(RasterLayer): attribution = Unicode().tag(sync=True, o=True) +class ImageService(Layer): + """ImageService class + + Image Service layer for raster data served through a web service + + Attributes + ---------- + url: string, default "" + Url to the image service + f: string, default "image" + response format (use `'image'` to stream as bytes) + format: string, default "jpgpng" + format of exported image + pixel_type: string, default "UNKNOWN" + data type of the raster image + no_data: list, default [] + pixel value or comma-delimited list of pixel values representing no data + no_data_interpretation: string, default "" + how to interpret no data values + interpolation: string, default "" + resampling process for interpolating the pixel values + compression_quality: int, default 100 + lossy quality for image compression + band_ids: List, default [] + Order of bands to export for multiple band images + time: List, default [] + time instance or extent for image + rendering_rule: dict, default {} + rules for rendering + mosaic_rule: dict, default {} + rules for mosaicking + transparent: boolean, default False + If true, the image service will return images with transparency + endpoint: str, default 'Esri' + Endpoint format for building the export image url + attribution: string, default "" + Image service attribution. + crs: dict, default ipyleaflet.projections.EPSG3857 + Projection used for this image service. + interactive: bool, default False + Emit when clicked or hovered + update_interval: int, default 200 + Update interval for panning + """ + + _view_name = Unicode('LeafletImageServiceView').tag(sync=True) + _model_name = Unicode('LeafletImageServiceModel').tag(sync=True) + + _formats = ['jpgpng', 'png', 'png8', 'png24', 'jpg', 'bmp', 'gif', 'tiff', 'png32', 'bip', 'bsq', 'lerc'] + _pixel_types = ['C128', 'C64', 'F32', 'F64', 'S16', 'S32', 'S8', 'U1', 'U16', 'U2', 'U32', 'U4', 'U8', 'UNKNOWN'] + _no_data_interpretations = ['esriNoDataMatchAny', 'esriNoDataMatchAll'] + _interpolations = ['RSP_BilinearInterpolation', 'RSP_CubicConvolution', 'RSP_Majority', 'RSP_NearestNeighbor'] + + url = Unicode().tag(sync=True) + f = Unicode('image').tag(sync=True, o=True) + format = Enum(values=_formats, default_value='jpgpng').tag(sync=True, o=True) + pixel_type = Enum(values=_pixel_types, default_value='UNKNOWN').tag(sync=True, o=True) + no_data = List(allow_none=True).tag(sync=True, o=True) + no_data_interpretation = Enum(values=_no_data_interpretations, allow_none=True).tag(sync=True, o=True) + interpolation = Enum(values=_interpolations, allow_none=True).tag(sync=True, o=True) + compression_quality = Unicode().tag(sync=True, o=True) + band_ids = List(allow_none=True).tag(sync=True, o=True) + time = List(allow_none=True).tag(sync=True, o=True) + rendering_rule = Dict({}).tag(sync=True, o=True) + mosaic_rule = Dict({}).tag(sync=True, o=True) + transparent = Bool(False).tag(sync=True, o=True) + endpoint = Unicode('Esri').tag(sync=True, o=True) + attribution = Unicode('').tag(sync=True, o=True) + crs = Dict(default_value=projections.EPSG3857).tag(sync=True) + interactive = Bool(False).tag(sync=True, o=True) + update_interval = Int(200).tag(sync=True, o=True) + + class Heatmap(RasterLayer): """Heatmap class, with RasterLayer as parent class. diff --git a/ipyleaflet/projections.py b/ipyleaflet/projections.py index c820e1416..12795009f 100644 --- a/ipyleaflet/projections.py +++ b/ipyleaflet/projections.py @@ -25,44 +25,146 @@ name='Simple', custom=False ), - EPSG3413=dict( - name='EPSG3413', - custom=True, - proj4def="""+proj=stere +lat_0=90 +lat_ts=70 +lon_0=-45 +k=1 +x_0=0 +y_0=0 - +ellps=WGS84 +datum=WGS84 +units=m +no_defs""", - origin=[-4194304, 4194304], - resolutions=[ - 16384.0, - 8192.0, - 4096.0, - 2048.0, - 1024.0, - 512.0, - 256.0 - ], - bounds=[ - [-4194304, -4194304], - [4194304, 4194304] - ] + EPSG3413=Bunch( + NASAGIBS=dict( + name='EPSG:3413', + custom=True, + proj4def="""+proj=stere +lat_0=90 +lat_ts=70 +lon_0=-45 +k=1 +x_0=0 +y_0=0 + +ellps=WGS84 +datum=WGS84 +units=m +no_defs""", + origin=[-4194304, 4194304], + resolutions=[ + 16384.0, + 8192.0, + 4096.0, + 2048.0, + 1024.0, + 512.0, + 256.0 + ], + bounds=[ + [-4194304, -4194304], + [4194304, 4194304] + ] + ) ), - EPSG3031=dict( - name='EPSG3031', - custom=True, - proj4def="""+proj=stere +lat_0=-90 +lat_ts=-71 +lon_0=0 +k=1 +x_0=0 +y_0=0 - +ellps=WGS84 +datum=WGS84 +units=m +no_defs""", - origin=[-4194304, 4194304], - resolutions=[ - 16384.0, - 8192.0, - 4096.0, - 2048.0, - 1024.0, - 512.0, - 256.0 - ], - bounds=[ - [-4194304, -4194304], - [4194304, 4194304] - ] + EPSG5936=Bunch( + Basemap=dict( + name='EPSG:5936', + custom=True, + proj4def="""+proj=stere +lat_0=90 +lat_ts=90 +lon_0=-150 +k=0.994 + +x_0=2000000 +y_0=2000000 +datum=WGS84 +units=m +no_defs""", + origin=[-2.8567784109255e+07, 3.2567784109255e+07], + resolutions=[ + 238810.813354, + 119405.406677, + 59702.7033384999, + 29851.3516692501, + 14925.675834625, + 7462.83791731252, + 3731.41895865639, + 1865.70947932806, + 932.854739664032, + 466.427369832148, + 233.213684916074, + 116.60684245803701, + 58.30342122888621, + 29.151710614575396, + 14.5758553072877, + 7.28792765351156, + 3.64396382688807, + 1.82198191331174, + 0.910990956788164, + 0.45549547826179, + 0.227747739130895, + 0.113873869697739, + 0.05693693484887, + 0.028468467424435 + ], + bounds=[ + [-2623285.8808999992907047, -2623285.8808999992907047], + [6623285.8803000003099442, 6623285.8803000003099442] + ] + ) + ), + EPSG3031=Bunch( + NASAGIBS=dict( + name='EPSG:3031', + custom=True, + proj4def="""+proj=stere +lat_0=-90 +lat_ts=-71 +lon_0=0 +k=1 + +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs""", + origin=[-4194304, 4194304], + resolutions=[ + 16384.0, + 8192.0, + 4096.0, + 2048.0, + 1024.0, + 512.0, + 256.0 + ], + bounds=[ + [-4194304, -4194304], + [4194304, 4194304] + ] + ), + Basemap=dict( + name='EPSG:3031', + custom=True, + proj4def="""+proj=stere +lat_0=-90 +lat_ts=-71 +lon_0=0 +k=1 + +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs""", + origin=[-3.06361E7, 3.0636099999999993E7], + resolutions=[ + 67733.46880027094, + 33866.73440013547, + 16933.367200067736, + 8466.683600033868, + 4233.341800016934, + 2116.670900008467, + 1058.3354500042335, + 529.1677250021168, + 264.5838625010584, + ], + bounds=[ + [-4524583.19363305, -4524449.487765655], + [4524449.4877656475, 4524583.193633042] + ] + ), + Imagery=dict( + name='EPSG:3031', + custom=True, + proj4def="""+proj=stere +lat_0=-90 +lat_ts=-71 +lon_0=0 +k=1 + +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs""", + origin=[-3.369955099203E7, 3.369955101703E7], + resolutions=[ + 238810.81335399998, + 119405.40667699999, + 59702.70333849987, + 29851.351669250063, + 14925.675834625032, + 7462.837917312516, + 3731.4189586563907, + 1865.709479328063, + 932.8547396640315, + 466.42736983214803, + 233.21368491607402, + 116.60684245803701, + 58.30342122888621, + 29.151710614575396, + 14.5758553072877, + 7.28792765351156, + 3.64396382688807, + 1.82198191331174, + 0.910990956788164, + 0.45549547826179, + 0.227747739130895, + 0.113873869697739, + 0.05693693484887, + 0.028468467424435 + ], + bounds=[ + [-9913957.327914657, -5730886.461772691], + [9913957.327914657, 5730886.461773157] + ] + ) ) ) diff --git a/js/src/jupyter-leaflet.js b/js/src/jupyter-leaflet.js index badc6b042..508cfed25 100644 --- a/js/src/jupyter-leaflet.js +++ b/js/src/jupyter-leaflet.js @@ -15,6 +15,7 @@ export * from './layers/WMSLayer.js'; export * from './layers/MagnifyingGlass.js'; export * from './layers/ImageOverlay.js'; export * from './layers/VideoOverlay.js'; +export * from './layers/ImageService.js'; export * from './layers/Velocity.js'; export * from './layers/Heatmap.js'; export * from './layers/VectorLayer.js'; diff --git a/js/src/layers/ImageService.js b/js/src/layers/ImageService.js new file mode 100644 index 000000000..8f2f31d6d --- /dev/null +++ b/js/src/layers/ImageService.js @@ -0,0 +1,74 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +const L = require('../leaflet.js'); +const layer = require('./Layer.js'); +const proj = require('../projections.js'); + +export class LeafletImageServiceModel extends layer.LeafletLayerModel { + defaults() { + return { + ...super.defaults(), + _view_name: 'LeafletImageServiceView', + _model_name: 'LeafletImageServiceModel', + // image server url + url: '', + // response format + f: 'image', + // output image format + format: 'jpgpng', + // data type of the raster image + pixelType: 'UNKNOWN', + // pixel value or list of pixel values representing no data + noData: [], + // how to interpret no data values + noDataInterpretation: '', + // resampling process for interpolating the pixel values + interpolation: '', + // lossy quality for image compression + compressionQuality: '', + // order of bands to export for multiple band images + bandIds: [], + // time instance or extent for image + time: [], + // rules for rendering + renderingRule: {}, + // rules for mosaicking + mosaicRule: {}, + // image transparency + transparent: false, + // endpoint format for building the export image url + endpoint: '', + // image service attribution + attribution: '', + // coordinate reference system + crs: null, + // emit when clicked or hovered + interactive: false, + // update interval for panning + updateInterval: 200 + }; + } +} + +export class LeafletImageServiceView extends layer.LeafletLayerView { + create_obj() { + this.obj = L.imageService({ + url: this.model.get('url'), + ...this.get_options(), + crs: proj.getProjection(this.model.get('crs')), + }); + } + + model_events() { + super.model_events(); + this.model.on('change:url', () => { + this.obj._update(); + }); + for (var option in this.get_options()) { + this.model.on('change:' + option, () => { + this.obj._update(); + }); + }; + } +} diff --git a/js/src/leaflet-imageservice.js b/js/src/leaflet-imageservice.js new file mode 100644 index 000000000..6256c21d2 --- /dev/null +++ b/js/src/leaflet-imageservice.js @@ -0,0 +1,283 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +L.ImageService = L.Layer.extend({ + options: { + // image server url + url: '', + // response format + f: 'image', + // output image format + format: 'jpgpng', + // data type of the raster image + pixelType: 'UNKNOWN', + // pixel value or list of pixel values representing no data + noData: [], + // how to interpret no data values + noDataInterpretation: '', + // resampling process for interpolating the pixel values + interpolation: '', + // lossy quality for image compression + compressionQuality: '', + // order of bands to export for multiple band images + bandIds: [], + // time instance or extent for image + time: [], + // rules for rendering + renderingRule: {}, + // rules for mosaicking + mosaicRule: {}, + // image transparency + transparent: false, + // endpoint format for building the export image url + endpoint: '', + // image service attribution + attribution: '', + // coordinate reference system + crs: null, + // emit when clicked or hovered + interactive: false, + // update interval for panning + updateInterval: 200 + }, + + initialize: function (options) { + L.Util.setOptions(this, options); + }, + + updateUrl: function() { + // update the url for the current bounds + if (this.options.endpoint === 'Esri') { + this._url = this.options.url + '/exportImage' + this._buildParams(); + } else { + this._url = this.options.url; + } + this._bounds = this.toLatLngBounds(this._getBounds()); + this._topLeft = this._map.getPixelBounds().min; + return this; + }, + + onAdd: function (map) { + this._map = map; + this.updateUrl(); + if (!this._image) { + this._initImage(); + } + this._map.on('moveend', () => { + L.Util.throttle(this._update(),this.options.updateInterval,this); + }); + if (this.options.interactive) { + L.DomUtil.addClass(this._image, 'leaflet-interactive'); + this.addInteractiveTarget(this._image); + } + this.getPane().appendChild(this._image); + this._reset(); + }, + + onRemove: function (map) { + L.DomUtil.remove(this._image); + if (this.options.interactive) { + this.removeInteractiveTarget(this._image); + } + }, + + bringToFront: function () { + // bring layer to the top of all overlays + if (this._map) { + L.DomUtil.toFront(this._image); + } + return this; + }, + + bringToBack: function () { + // bring layer to the bottom of all overlays + if (this._map) { + L.DomUtil.toBack(this._image); + } + return this; + }, + + setUrl: function (url) { + // change the URL of the image + if (this.options.endpoint === 'Esri') { + this._url = url + '/exportImage' + this._buildParams(); + } else { + this._url = url; + } + if (this._image) { + this._image.src = url; + } + return this; + }, + + getEvents: function () { + var events = { + zoom: this._reset, + viewreset: this._reset + }; + return events; + }, + + getBounds: function () { + // get bounds + return this._bounds; + }, + + toLatLngBounds: function(a, b) { + // convert bounds to LatLngBounds object + if (a instanceof L.LatLngBounds) { + return a; + } + return new L.LatLngBounds(a, b); + }, + + getElement: function () { + // get image element + return this._image; + }, + + getCenter: function () { + // get map center + return this._bounds.getCenter(); + }, + + _getBBox: function() { + // get the bounding box of the current map formatted for exportImage + var pixelbounds = this._map.getPixelBounds(); + var sw = this._map.unproject(pixelbounds.getBottomLeft()); + var ne = this._map.unproject(pixelbounds.getTopRight()); + return [ + this._map.options.crs.project(ne).x, + this._map.options.crs.project(ne).y, + this._map.options.crs.project(sw).x, + this._map.options.crs.project(sw).y + ]; + }, + + _getBounds: function() { + // get the bounds of the current map + return [[this._map.getBounds().getSouth(),this._map.getBounds().getWest()], + [this._map.getBounds().getNorth(),this._map.getBounds().getEast()]]; + }, + + _getSize: function() { + // get the size of the current map + var size = this._map.getSize(); + return [size.x, size.y]; + }, + + _getEPSG: function() { + // get the EPSG code (numeric) of the current map + var epsg = this.options.crs.code; + var spatial_reference = parseInt(epsg.split(':')[1], 10); + return spatial_reference; + }, + + _getTime: function() { + // get start and end times and convert to seconds since epoch + var st = new Date(this.options.time[0]).getTime().valueOf(); + var et = new Date(this.options.time[1]).getTime().valueOf(); + return [st, et]; + }, + + _buildParams: function() { + // parameters for image server query + var params = { + bbox: this._getBBox().join(','), + size: this._getSize().join(','), + bboxSR: this._getEPSG(), + imageSR: this._getEPSG(), + f: this.options.f, + }; + // add string parameters + if (this.options.format) { + params['format'] = this.options.format; + } + if (this.options.pixelType) { + params['pixelType'] = this.options.pixelType; + } + if (this.options.noDataInterpretation) { + params['noDataInterpretation'] = this.options.noDataInterpretation; + } + if (this.options.interpolation) { + params['interpolation'] = this.options.interpolation; + } + if (this.options.compressionQuality) { + params['compressionQuality'] = this.options.compressionQuality; + } + if (this.options.transparent) { + params['transparent'] = this.options.transparent; + } + // merge list parameters + if (this.options.noData.length) { + params['noData'] = this.options.noData.join(','); + } + if (this.options.bandIds.length) { + params['bandIds'] = this.options.bandIds.join(','); + } + if (this.options.time.length) { + params['time'] = this._getTime().join(','); + } + // convert dictionary parameters to JSON + if (Object.keys(this.options.renderingRule).length) { + params['renderingRule'] = JSON.stringify(this.options.renderingRule); + } + if (Object.keys(this.options.mosaicRule).length) { + params['mosaicRule'] = JSON.stringify(this.options.mosaicRule); + } + // return the formatted query string + return L.Util.getParamString(params); + }, + + _initImage: function () { + var wasElementSupplied = this._url.tagName === 'IMG'; + var img = this._image = L.DomUtil.create('img'); + L.DomUtil.addClass(img, 'leaflet-image-layer'); + img.onselectstart = L.Util.falseFn; + img.onmousemove = L.Util.falseFn; + img.onload = L.Util.bind(this.fire, this, 'load'); + if (wasElementSupplied) { + this._url = img.src; + return; + } + img.src = this._url; + }, + + _reset: function () { + var img = this._image; + var size = this._getSize(); + img.style.width = size[0] + 'px'; + img.style.height = size[1] + 'px'; + if (this._getEPSG() === 3857) { + var bounds = new L.Bounds( + this._map.latLngToLayerPoint(this._bounds.getNorthWest()), + this._map.latLngToLayerPoint(this._bounds.getSouthEast())); + L.DomUtil.setPosition(img, bounds.min); + } else { + var pixelorigin = this._topLeft.subtract(this._map.getPixelOrigin()); + L.DomUtil.setPosition(img, pixelorigin); + } + }, + + _update: function () { + if (!this._map) { + return; + } + // don't update if currently panning + if (this._map._panTransition && this._map._panTransition._inProgress) { + return; + } + // update the url for the current bounds + this.updateUrl() + // update image source + if (this._image && this._map) { + this._image.src = this._url; + this._reset(); + } + }, + +}); + +L.imageService = function (options) { + return new L.ImageService(options); +}; diff --git a/js/src/leaflet.js b/js/src/leaflet.js index 65b0027d9..946ed4d9b 100644 --- a/js/src/leaflet.js +++ b/js/src/leaflet.js @@ -9,6 +9,7 @@ require('leaflet.markercluster'); require('leaflet-velocity'); require('leaflet-measure'); require('./leaflet-heat.js'); +require('./leaflet-imageservice.js'); require('./leaflet-magnifyingglass.js'); require('leaflet-rotatedmarker'); require('leaflet-fullscreen'); From 93394bf33857d3572ecacdb7d6180807569984f1 Mon Sep 17 00:00:00 2001 From: tsutterley Date: Mon, 29 Aug 2022 09:34:47 -0700 Subject: [PATCH 2/8] drop comments on imageservice options --- js/src/layers/ImageService.js | 18 ------------------ js/src/leaflet-imageservice.js | 18 ------------------ 2 files changed, 36 deletions(-) diff --git a/js/src/layers/ImageService.js b/js/src/layers/ImageService.js index 8f2f31d6d..733619b80 100644 --- a/js/src/layers/ImageService.js +++ b/js/src/layers/ImageService.js @@ -11,41 +11,23 @@ export class LeafletImageServiceModel extends layer.LeafletLayerModel { ...super.defaults(), _view_name: 'LeafletImageServiceView', _model_name: 'LeafletImageServiceModel', - // image server url url: '', - // response format f: 'image', - // output image format format: 'jpgpng', - // data type of the raster image pixelType: 'UNKNOWN', - // pixel value or list of pixel values representing no data noData: [], - // how to interpret no data values noDataInterpretation: '', - // resampling process for interpolating the pixel values interpolation: '', - // lossy quality for image compression compressionQuality: '', - // order of bands to export for multiple band images bandIds: [], - // time instance or extent for image time: [], - // rules for rendering renderingRule: {}, - // rules for mosaicking mosaicRule: {}, - // image transparency transparent: false, - // endpoint format for building the export image url endpoint: '', - // image service attribution attribution: '', - // coordinate reference system crs: null, - // emit when clicked or hovered interactive: false, - // update interval for panning updateInterval: 200 }; } diff --git a/js/src/leaflet-imageservice.js b/js/src/leaflet-imageservice.js index 6256c21d2..e8d26680c 100644 --- a/js/src/leaflet-imageservice.js +++ b/js/src/leaflet-imageservice.js @@ -3,41 +3,23 @@ L.ImageService = L.Layer.extend({ options: { - // image server url url: '', - // response format f: 'image', - // output image format format: 'jpgpng', - // data type of the raster image pixelType: 'UNKNOWN', - // pixel value or list of pixel values representing no data noData: [], - // how to interpret no data values noDataInterpretation: '', - // resampling process for interpolating the pixel values interpolation: '', - // lossy quality for image compression compressionQuality: '', - // order of bands to export for multiple band images bandIds: [], - // time instance or extent for image time: [], - // rules for rendering renderingRule: {}, - // rules for mosaicking mosaicRule: {}, - // image transparency transparent: false, - // endpoint format for building the export image url endpoint: '', - // image service attribution attribution: '', - // coordinate reference system crs: null, - // emit when clicked or hovered interactive: false, - // update interval for panning updateInterval: 200 }, From a15c3bb3df6e2c8497d23c4027ee00a2f4ea52be Mon Sep 17 00:00:00 2001 From: tsutterley Date: Sun, 4 Sep 2022 17:42:49 -0700 Subject: [PATCH 3/8] review updates docs: add options to `format`, `pixel_type`, `no_data_interpretation` and `endpoint` feat: add function for handling mouse clicks refactor: remove columbia glacier example remove transparency option (defunct) --- examples/CustomProjections.ipynb | 8 +- examples/ImageService.ipynb | 121 +++++++++---------------------- ipyleaflet/leaflet.py | 71 ++++++++++++++---- js/src/layers/ImageService.js | 1 - js/src/leaflet-imageservice.js | 4 - 5 files changed, 97 insertions(+), 108 deletions(-) diff --git a/examples/CustomProjections.ipynb b/examples/CustomProjections.ipynb index a9a93be37..f26669ec3 100644 --- a/examples/CustomProjections.ipynb +++ b/examples/CustomProjections.ipynb @@ -52,7 +52,7 @@ "dc = DrawControl(marker={\"shapeOptions\": {\"color\": \"#0000FF\"}})\n", "dc.on_draw(handle_draw)\n", "\n", - "# note that we need to use the same projection for the our layer and the map.\n", + "# note that we need to use the same projection for the layer and the map.\n", "m1 = Map(\n", " center=(90, 0),\n", " zoom=0,\n", @@ -69,7 +69,7 @@ "metadata": {}, "outputs": [], "source": [ - "# note that we need to use the same projection for the our layer and the map.\n", + "# note that we need to use the same projection for the layer and the map.\n", "m2 = Map(\n", " center=(-90, 0),\n", " zoom=0,\n", @@ -160,7 +160,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -174,7 +174,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.10" + "version": "3.10.4" } }, "nbformat": 4, diff --git a/examples/ImageService.ipynb b/examples/ImageService.ipynb index 1af0bdc6e..2c9726735 100644 --- a/examples/ImageService.ipynb +++ b/examples/ImageService.ipynb @@ -15,7 +15,6 @@ "metadata": {}, "outputs": [], "source": [ - "import time\n", "from ipywidgets import Dropdown\n", "from ipyleaflet import (\n", " Map,\n", @@ -24,57 +23,7 @@ " ImageService,\n", " projections,\n", " WidgetControl\n", - ")\n", - "\n", - "# Columbia Glacier with Landsat\n", - "m1 = Map(\n", - " center=[61.1, -146.9],\n", - " zoom=10,\n", - " basemap=basemaps.Esri.WorldTopoMap,\n", - ")\n", - "\n", - "# create a widget control for years of observations\n", - "years = [\n", - " ['1980-01-01','1989-12-31'],\n", - " ['2000-01-01','2009-12-31'],\n", - " ['2010-01-01','2019-12-31']\n", - "]\n", - "def range_formatter(d):\n", - " st = time.strptime(d[0],'%Y-%m-%d')\n", - " et = time.strptime(d[1],'%Y-%m-%d')\n", - " return u'{0:d}\\u2013{1:d}'.format(st.tm_year,et.tm_year)\n", - "\n", - "range_dropdown1 = Dropdown(\n", - " value=u'{0:d}\\u2013{1:d}'.format(1980,1989),\n", - " options=[range_formatter(d) for d in years],\n", - " description=\"Years:\",\n", - ")\n", - "\n", - "# add image service layer with Landsat multi-spectral imagery\n", - "# create a false color image to highlight ice and snow\n", - "url = 'https://landsat.arcgis.com/arcgis/rest/services/LandsatGLS/MS/ImageServer'\n", - "attribution = \"\"\"United States Geological Survey (USGS),\n", - " National Aeronautics and Space Administration (NASA)\"\"\"\n", - "im1 = ImageService(url=url,\n", - " band_ids=['5','4','2'],\n", - " time=years[range_dropdown1.index],\n", - " attribution=attribution)\n", - "m1.add(im1)\n", - "\n", - "# add control for year range\n", - "widget_control1 = WidgetControl(widget=range_dropdown1, position=\"topright\")\n", - "m1.add(widget_control1)\n", - "\n", - "# set the year range\n", - "def set_year_range(sender):\n", - " im1.time = years[range_dropdown1.index]\n", - " # force redrawing of map by removing and adding layer\n", - " m1.remove(im1)\n", - " m1.add(im1)\n", - "\n", - "# watch year range function widget for changes\n", - "range_dropdown1.observe(set_year_range)\n", - "m1" + ")" ] }, { @@ -84,16 +33,16 @@ "outputs": [], "source": [ "# ArcticDEM\n", - "# note that we need to use the same projection for the our image service layer and the map.\n", - "m2 = Map(\n", + "# note that we need to use the same projection for the image service layer and the map.\n", + "m1 = Map(\n", " center=(90, 0),\n", " zoom=4,\n", " basemap=basemaps.Esri.ArcticOceanBase,\n", " crs=projections.EPSG5936.Basemap,\n", ")\n", "# add arctic ocean reference basemap\n", - "tl2 = basemap_to_tiles(basemaps.Esri.ArcticOceanReference)\n", - "m2.add(tl2)\n", + "tl1 = basemap_to_tiles(basemaps.Esri.ArcticOceanReference)\n", + "m1.add(tl1)\n", "\n", "# create a widget control for the raster function\n", "raster_functions = [\n", @@ -104,35 +53,35 @@ " \"Height Ellipsoidal\",\n", " \"Height Orthometric\",\n", " \"Slope Map\"]\n", - "raster_dropdown2 = Dropdown(\n", - " value=\"Hillshade Gray\",\n", + "raster_dropdown1 = Dropdown(\n", + " value=raster_functions[3],\n", " options=raster_functions,\n", " description=\"Raster:\",\n", ")\n", "\n", "# add image service layer with ArcticDEM\n", "url = 'https://elevation2.arcgis.com/arcgis/rest/services/Polar/ArcticDEM/ImageServer'\n", - "rendering_rule = {\"rasterFunction\": raster_dropdown2.value}\n", - "im2 = ImageService(url=url,\n", + "rendering_rule = {\"rasterFunction\": raster_dropdown1.value}\n", + "im1 = ImageService(url=url,\n", " format='jpgpng', rendering_rule=rendering_rule,\n", " attribution='Esri, PGC, UMN, NSF, NGA, DigitalGlobe',\n", " crs=projections.EPSG5936.Basemap)\n", - "m2.add(im2) \n", + "m1.add(im1) \n", "\n", "# add control for raster function\n", - "widget_control2 = WidgetControl(widget=raster_dropdown2, position=\"topright\")\n", - "m2.add(widget_control2)\n", + "widget_control1 = WidgetControl(widget=raster_dropdown1, position=\"topright\")\n", + "m1.add(widget_control1)\n", "\n", "# set the rendering rule\n", - "def set_raster_function2(sender):\n", - " im2.rendering_rule = {\"rasterFunction\": raster_dropdown2.value}\n", + "def set_raster_function1(sender):\n", + " im1.rendering_rule = {\"rasterFunction\": raster_dropdown1.value}\n", " # force redrawing of map by removing and adding layer\n", - " m2.remove(im2)\n", - " m2.add(im2)\n", + " m1.remove(im1)\n", + " m1.add(im1)\n", "\n", "# watch raster function widget for changes\n", - "raster_dropdown2.observe(set_raster_function2)\n", - "m2" + "raster_dropdown1.observe(set_raster_function1)\n", + "m1" ] }, { @@ -142,8 +91,8 @@ "outputs": [], "source": [ "# Reference Elevation Model of Antarctica (REMA)\n", - "# note that we need to use the same projection for the our image service layer and the map.\n", - "m3 = Map(\n", + "# note that we need to use the same projection for the image service layer and the map.\n", + "m2 = Map(\n", " center=(-90, 0),\n", " zoom=3,\n", " basemap=basemaps.Esri.AntarcticBasemap,\n", @@ -158,35 +107,35 @@ " \"Hillshade Gray\",\n", " \"Height Orthometric\",\n", " \"Slope Degrees Map\"]\n", - "raster_dropdown3 = Dropdown(\n", - " value=\"Hillshade Gray\",\n", + "raster_dropdown2 = Dropdown(\n", + " value=raster_functions[3],\n", " options=raster_functions,\n", " description=\"Raster:\",\n", ")\n", "\n", "# add image service layer with REMA imagery\n", "url = 'https://elevation2.arcgis.com/arcgis/rest/services/Polar/AntarcticDEM/ImageServer'\n", - "rendering_rule = {\"rasterFunction\": raster_dropdown3.value}\n", - "im3 = ImageService(url=url,\n", + "rendering_rule = {\"rasterFunction\": raster_dropdown2.value}\n", + "im2 = ImageService(url=url,\n", " format='jpgpng', rendering_rule=rendering_rule,\n", " attribution='Esri, PGC, UMN, NSF, NGA, DigitalGlobe',\n", " crs=projections.EPSG3031.Basemap)\n", - "m3.add(im3)\n", + "m2.add(im2)\n", "\n", "# add control for raster function\n", - "widget_control3 = WidgetControl(widget=raster_dropdown3, position=\"topright\")\n", - "m3.add(widget_control3)\n", + "widget_control2 = WidgetControl(widget=raster_dropdown2, position=\"topright\")\n", + "m2.add(widget_control2)\n", "\n", "# set the rendering rule\n", - "def set_raster_function3(sender):\n", - " im3.rendering_rule = {\"rasterFunction\": raster_dropdown3.value}\n", + "def set_raster_function2(sender):\n", + " im2.rendering_rule = {\"rasterFunction\": raster_dropdown2.value}\n", " # force redrawing of map by removing and adding layer\n", - " m3.remove(im3)\n", - " m3.add(im3)\n", + " m2.remove(im2)\n", + " m2.add(im2)\n", "\n", "# watch raster function widget for changes\n", - "raster_dropdown3.observe(set_raster_function3)\n", - "m3" + "raster_dropdown2.observe(set_raster_function2)\n", + "m2" ] }, { @@ -199,7 +148,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -213,7 +162,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.10" + "version": "3.10.4" } }, "nbformat": 4, diff --git a/ipyleaflet/leaflet.py b/ipyleaflet/leaflet.py index 0ac43feed..1c6d56da7 100644 --- a/ipyleaflet/leaflet.py +++ b/ipyleaflet/leaflet.py @@ -790,41 +790,77 @@ class ImageService(Layer): Attributes ---------- url: string, default "" - Url to the image service + URL to the image service f: string, default "image" response format (use `'image'` to stream as bytes) format: string, default "jpgpng" format of exported image + + - ``jpgpng`` + - ``png`` + - ``png8`` + - ``png24`` + - ``jpg`` + - ``bmp`` + - ``gif`` + - ``tiff`` + - ``png32`` + - ``bip`` + - ``bsq`` + - ``lerc`` pixel_type: string, default "UNKNOWN" data type of the raster image + + - ``C128`` + - ``C64`` + - ``F32`` + - ``F64`` + - ``S16`` + - ``S32`` + - ``S8`` + - ``U1`` + - ``U16`` + - ``U2`` + - ``U32`` + - ``U4`` + - ``U8`` + - ``UNKNOWN`` no_data: list, default [] - pixel value or comma-delimited list of pixel values representing no data + pixel values representing no data no_data_interpretation: string, default "" how to interpret no data values + + - ``esriNoDataMatchAny`` + - ``esriNoDataMatchAll`` interpolation: string, default "" resampling process for interpolating the pixel values + + - ``RSP_BilinearInterpolation`` + - ``RSP_CubicConvolution`` + - ``RSP_Majority`` + - ``RSP_NearestNeighbor`` compression_quality: int, default 100 lossy quality for image compression band_ids: List, default [] - Order of bands to export for multiple band images + order of bands to export for multiple band images time: List, default [] - time instance or extent for image + time range for image rendering_rule: dict, default {} rules for rendering mosaic_rule: dict, default {} rules for mosaicking - transparent: boolean, default False - If true, the image service will return images with transparency - endpoint: str, default 'Esri' - Endpoint format for building the export image url + endpoint: str, default "Esri" + endpoint format for building the export image URL + + - ``Esri`` attribution: string, default "" - Image service attribution. + include image service attribution crs: dict, default ipyleaflet.projections.EPSG3857 - Projection used for this image service. + projection used for this image service. interactive: bool, default False - Emit when clicked or hovered + emit when clicked for registered callback update_interval: int, default 200 - Update interval for panning + minimum time interval to query for updates when panning (ms) """ _view_name = Unicode('LeafletImageServiceView').tag(sync=True) @@ -847,13 +883,22 @@ class ImageService(Layer): time = List(allow_none=True).tag(sync=True, o=True) rendering_rule = Dict({}).tag(sync=True, o=True) mosaic_rule = Dict({}).tag(sync=True, o=True) - transparent = Bool(False).tag(sync=True, o=True) endpoint = Unicode('Esri').tag(sync=True, o=True) attribution = Unicode('').tag(sync=True, o=True) crs = Dict(default_value=projections.EPSG3857).tag(sync=True) interactive = Bool(False).tag(sync=True, o=True) update_interval = Int(200).tag(sync=True, o=True) + _click_callbacks = Instance(CallbackDispatcher, ()) + + def __init__(self, **kwargs): + super(ImageService, self).__init__(**kwargs) + self.on_msg(self._handle_mouse_events) + + def _handle_mouse_events(self, _, content, buffers): + event_type = content.get('type', '') + if event_type == 'click': + self._click_callbacks(**content) class Heatmap(RasterLayer): """Heatmap class, with RasterLayer as parent class. diff --git a/js/src/layers/ImageService.js b/js/src/layers/ImageService.js index 733619b80..4d650887c 100644 --- a/js/src/layers/ImageService.js +++ b/js/src/layers/ImageService.js @@ -23,7 +23,6 @@ export class LeafletImageServiceModel extends layer.LeafletLayerModel { time: [], renderingRule: {}, mosaicRule: {}, - transparent: false, endpoint: '', attribution: '', crs: null, diff --git a/js/src/leaflet-imageservice.js b/js/src/leaflet-imageservice.js index e8d26680c..cc405b61a 100644 --- a/js/src/leaflet-imageservice.js +++ b/js/src/leaflet-imageservice.js @@ -15,7 +15,6 @@ L.ImageService = L.Layer.extend({ time: [], renderingRule: {}, mosaicRule: {}, - transparent: false, endpoint: '', attribution: '', crs: null, @@ -187,9 +186,6 @@ L.ImageService = L.Layer.extend({ if (this.options.compressionQuality) { params['compressionQuality'] = this.options.compressionQuality; } - if (this.options.transparent) { - params['transparent'] = this.options.transparent; - } // merge list parameters if (this.options.noData.length) { params['noData'] = this.options.noData.join(','); From 3bf86c593c765cdd27c8ecebbf3e85d586203f0f Mon Sep 17 00:00:00 2001 From: tsutterley Date: Wed, 12 Oct 2022 11:10:04 -0700 Subject: [PATCH 4/8] add whitespace to fix linting error --- ipyleaflet/leaflet.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ipyleaflet/leaflet.py b/ipyleaflet/leaflet.py index 1c6d56da7..d493c748e 100644 --- a/ipyleaflet/leaflet.py +++ b/ipyleaflet/leaflet.py @@ -900,6 +900,7 @@ def _handle_mouse_events(self, _, content, buffers): if event_type == 'click': self._click_callbacks(**content) + class Heatmap(RasterLayer): """Heatmap class, with RasterLayer as parent class. From 0dbaaf3b331d5f21718b84ff08fe8ff6d845ce05 Mon Sep 17 00:00:00 2001 From: tsutterley Date: Fri, 14 Oct 2022 09:19:20 -0700 Subject: [PATCH 5/8] update projection names following Beto's review proj changes in notebook --- examples/CustomProjections.ipynb | 2 +- examples/ImageService.ipynb | 4 ++-- ipyleaflet/projections.py | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/CustomProjections.ipynb b/examples/CustomProjections.ipynb index f26669ec3..c41c13cef 100644 --- a/examples/CustomProjections.ipynb +++ b/examples/CustomProjections.ipynb @@ -74,7 +74,7 @@ " center=(-90, 0),\n", " zoom=0,\n", " basemap=basemaps.Esri.AntarcticBasemap,\n", - " crs=projections.EPSG3031.Basemap,\n", + " crs=projections.EPSG3031.ESRIBasemap,\n", ")\n", "\n", "# add draw control on Antarctic map\n", diff --git a/examples/ImageService.ipynb b/examples/ImageService.ipynb index 2c9726735..f2889c010 100644 --- a/examples/ImageService.ipynb +++ b/examples/ImageService.ipynb @@ -38,7 +38,7 @@ " center=(90, 0),\n", " zoom=4,\n", " basemap=basemaps.Esri.ArcticOceanBase,\n", - " crs=projections.EPSG5936.Basemap,\n", + " crs=projections.EPSG5936.ESRIBasemap,\n", ")\n", "# add arctic ocean reference basemap\n", "tl1 = basemap_to_tiles(basemaps.Esri.ArcticOceanReference)\n", @@ -96,7 +96,7 @@ " center=(-90, 0),\n", " zoom=3,\n", " basemap=basemaps.Esri.AntarcticBasemap,\n", - " crs=projections.EPSG3031.Basemap,\n", + " crs=projections.EPSG3031.ESRIBasemap,\n", ")\n", "\n", "# create a widget control for the raster function\n", diff --git a/ipyleaflet/projections.py b/ipyleaflet/projections.py index 12795009f..1c60b4545 100644 --- a/ipyleaflet/projections.py +++ b/ipyleaflet/projections.py @@ -48,7 +48,7 @@ ) ), EPSG5936=Bunch( - Basemap=dict( + ESRIBasemap=dict( name='EPSG:5936', custom=True, proj4def="""+proj=stere +lat_0=90 +lat_ts=90 +lon_0=-150 +k=0.994 @@ -107,7 +107,7 @@ [4194304, 4194304] ] ), - Basemap=dict( + ESRIBasemap=dict( name='EPSG:3031', custom=True, proj4def="""+proj=stere +lat_0=-90 +lat_ts=-71 +lon_0=0 +k=1 @@ -129,7 +129,7 @@ [4524449.4877656475, 4524583.193633042] ] ), - Imagery=dict( + ESRIImagery=dict( name='EPSG:3031', custom=True, proj4def="""+proj=stere +lat_0=-90 +lat_ts=-71 +lon_0=0 +k=1 From 16629b0c945c7033045e6fa8ee337a7b9078b867 Mon Sep 17 00:00:00 2001 From: Tyler Sutterley Date: Wed, 19 Oct 2022 04:10:02 -0700 Subject: [PATCH 6/8] Fix proj for image service example --- examples/ImageService.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/ImageService.ipynb b/examples/ImageService.ipynb index f2889c010..9e1aea5a8 100644 --- a/examples/ImageService.ipynb +++ b/examples/ImageService.ipynb @@ -65,7 +65,7 @@ "im1 = ImageService(url=url,\n", " format='jpgpng', rendering_rule=rendering_rule,\n", " attribution='Esri, PGC, UMN, NSF, NGA, DigitalGlobe',\n", - " crs=projections.EPSG5936.Basemap)\n", + " crs=projections.EPSG5936.ESRIBasemap)\n", "m1.add(im1) \n", "\n", "# add control for raster function\n", @@ -119,7 +119,7 @@ "im2 = ImageService(url=url,\n", " format='jpgpng', rendering_rule=rendering_rule,\n", " attribution='Esri, PGC, UMN, NSF, NGA, DigitalGlobe',\n", - " crs=projections.EPSG3031.Basemap)\n", + " crs=projections.EPSG3031.ESRIBasemap)\n", "m2.add(im2)\n", "\n", "# add control for raster function\n", From 89c18522ffeae2a4ea65d949730ede4d07ef7bf1 Mon Sep 17 00:00:00 2001 From: tsutterley Date: Wed, 26 Oct 2022 06:55:38 -0700 Subject: [PATCH 7/8] JS lint fixes --- js/src/layers/ImageService.js | 2 +- js/src/leaflet-imageservice.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/js/src/layers/ImageService.js b/js/src/layers/ImageService.js index 4d650887c..982d0dc57 100644 --- a/js/src/layers/ImageService.js +++ b/js/src/layers/ImageService.js @@ -50,6 +50,6 @@ export class LeafletImageServiceView extends layer.LeafletLayerView { this.model.on('change:' + option, () => { this.obj._update(); }); - }; + } } } diff --git a/js/src/leaflet-imageservice.js b/js/src/leaflet-imageservice.js index cc405b61a..67ccfab13 100644 --- a/js/src/leaflet-imageservice.js +++ b/js/src/leaflet-imageservice.js @@ -55,7 +55,7 @@ L.ImageService = L.Layer.extend({ this._reset(); }, - onRemove: function (map) { + onRemove: function () { L.DomUtil.remove(this._image); if (this.options.interactive) { this.removeInteractiveTarget(this._image); From c1550671137dcda5f7ccb5b51034d11c5812c86b Mon Sep 17 00:00:00 2001 From: tsutterley Date: Thu, 27 Oct 2022 06:57:09 -0700 Subject: [PATCH 8/8] docstring edits for review comments --- ipyleaflet/leaflet.py | 76 +++++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/ipyleaflet/leaflet.py b/ipyleaflet/leaflet.py index 7a2abe009..280f44658 100644 --- a/ipyleaflet/leaflet.py +++ b/ipyleaflet/leaflet.py @@ -808,67 +808,67 @@ class ImageService(Layer): url: string, default "" URL to the image service f: string, default "image" - response format (use `'image'` to stream as bytes) + response format (use ``"image"`` to stream as bytes) format: string, default "jpgpng" format of exported image - - ``jpgpng`` - - ``png`` - - ``png8`` - - ``png24`` - - ``jpg`` - - ``bmp`` - - ``gif`` - - ``tiff`` - - ``png32`` - - ``bip`` - - ``bsq`` - - ``lerc`` + - ``"jpgpng"`` + - ``"png"`` + - ``"png8"`` + - ``"png24"`` + - ``"jpg"`` + - ``"bmp"`` + - ``"gif"`` + - ``"tiff"`` + - ``"png32"`` + - ``"bip"`` + - ``"bsq"`` + - ``"lerc"`` pixel_type: string, default "UNKNOWN" data type of the raster image - - ``C128`` - - ``C64`` - - ``F32`` - - ``F64`` - - ``S16`` - - ``S32`` - - ``S8`` - - ``U1`` - - ``U16`` - - ``U2`` - - ``U32`` - - ``U4`` - - ``U8`` - - ``UNKNOWN`` - no_data: list, default [] + - ``"C128"`` + - ``"C64"`` + - ``"F32"`` + - ``"F64"`` + - ``"S16"`` + - ``"S32"`` + - ``"S8"`` + - ``"U1"`` + - ``"U16"`` + - ``"U2"`` + - ``"U32"`` + - ``"U4"`` + - ``"U8"`` + - ``"UNKNOWN"`` + no_data: List[int], default [] pixel values representing no data no_data_interpretation: string, default "" how to interpret no data values - - ``esriNoDataMatchAny`` - - ``esriNoDataMatchAll`` + - ``"esriNoDataMatchAny"`` + - ``"esriNoDataMatchAll"`` interpolation: string, default "" resampling process for interpolating the pixel values - - ``RSP_BilinearInterpolation`` - - ``RSP_CubicConvolution`` - - ``RSP_Majority`` - - ``RSP_NearestNeighbor`` + - ``"RSP_BilinearInterpolation"`` + - ``"RSP_CubicConvolution"`` + - ``"RSP_Majority"`` + - ``"RSP_NearestNeighbor"`` compression_quality: int, default 100 lossy quality for image compression - band_ids: List, default [] + band_ids: List[int], default [] order of bands to export for multiple band images - time: List, default [] + time: List[string], default [] time range for image rendering_rule: dict, default {} rules for rendering mosaic_rule: dict, default {} rules for mosaicking - endpoint: str, default "Esri" + endpoint: string, default "Esri" endpoint format for building the export image URL - - ``Esri`` + - ``"Esri"`` attribution: string, default "" include image service attribution crs: dict, default ipyleaflet.projections.EPSG3857