From ae41b970a5524a1a0889bcc328fead7699d7331a Mon Sep 17 00:00:00 2001 From: tsutterley Date: Mon, 18 Apr 2022 11:18:15 -0700 Subject: [PATCH] convert times to seconds since epoch within js add attributions within imageservicelayer notebook use widget controls for raster functions use widget control to show different years --- docs/source/layers/image_service_layer.rst | 33 ++++++ docs/source/layers/index.rst | 1 + examples/ImageServiceLayer.ipynb | 128 +++++++++++++++++++-- ipyleaflet/leaflet.py | 17 ++- js/src/layers/ImageServiceLayer.js | 8 +- js/src/leaflet-imageservice.js | 20 +++- 6 files changed, 184 insertions(+), 23 deletions(-) create mode 100644 docs/source/layers/image_service_layer.rst diff --git a/docs/source/layers/image_service_layer.rst b/docs/source/layers/image_service_layer.rst new file mode 100644 index 000000000..7f84b74d4 --- /dev/null +++ b/docs/source/layers/image_service_layer.rst @@ -0,0 +1,33 @@ +Image Service Layer +=================== + +Example +------- + +.. jupyter-execute:: + + from ipyleaflet import Map, ImageServiceLayer, basemaps + + im = ImageServiceLayer( + 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_layer(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.ImageServiceLayer + :members: diff --git a/docs/source/layers/index.rst b/docs/source/layers/index.rst index 2ab5e2085..a4858a640 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_layer antpath polyline polygon diff --git a/examples/ImageServiceLayer.ipynb b/examples/ImageServiceLayer.ipynb index 9c202b0f1..8280bb650 100644 --- a/examples/ImageServiceLayer.ipynb +++ b/examples/ImageServiceLayer.ipynb @@ -6,7 +6,7 @@ "source": [ "## Image Service Layer\n", "\n", - "This notebook shows how you can overlay an images from an ESRI Image Server on a Leaflet map" + "This notebook shows how you can overlay images from an ESRI Image Server on a Leaflet map" ] }, { @@ -15,21 +15,64 @@ "metadata": {}, "outputs": [], "source": [ + "import time\n", + "from ipywidgets import Dropdown\n", "from ipyleaflet import (\n", " Map,\n", " basemaps,\n", " ImageServiceLayer,\n", " projections,\n", + " WidgetControl\n", ")\n", "\n", + "# Columbia Glacier with Landsat\n", "m1 = Map(\n", - " center=[47.655548, -122.303200],\n", - " zoom=12,\n", + " center=[61.1, -146.9],\n", + " zoom=10,\n", " basemap=basemaps.Esri.WorldTopoMap,\n", ")\n", - "# add image service layer with Landsat pan-sharpened imagery\n", - "im1 = ImageServiceLayer(url='https://landsat.arcgis.com/arcgis/rest/services/Landsat/PS/ImageServer')\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 = ImageServiceLayer(url=url,\n", + " band_ids=['5','4','2'],\n", + " time=years[range_dropdown1.index],\n", + " attribution=attribution)\n", "m1.add_layer(im1)\n", + "\n", + "# add control for year range\n", + "widget_control1 = WidgetControl(widget=range_dropdown1, position=\"topright\")\n", + "m1.add_control(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_layer(im1)\n", + " m1.add_layer(im1)\n", + "\n", + "# watch year range function widget for changes\n", + "range_dropdown1.observe(set_year_range)\n", "m1" ] }, @@ -39,6 +82,7 @@ "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", @@ -47,13 +91,44 @@ " crs=projections.EPSG5936.Basemap,\n", ")\n", "m2.add_layer(basemaps.Esri.ArcticOceanReference)\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\": \"Height Orthometric\"}\n", + "rendering_rule = {\"rasterFunction\": raster_dropdown2.value}\n", "im2 = ImageServiceLayer(url=url,\n", - " format='png', rendering_rule=rendering_rule,\n", - " crs=projections.EPSG3031.Basemap)\n", - "m2.add_layer(im2)\n", + " format='jpgpng', rendering_rule=rendering_rule,\n", + " attribution='PGC, UMN, Esri',\n", + " crs=projections.EPSG5936.Basemap)\n", + "m2.add_layer(im2) \n", + "\n", + "# add control for raster function\n", + "widget_control2 = WidgetControl(widget=raster_dropdown2, position=\"topright\")\n", + "m2.add_control(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_layer(im2)\n", + " m2.add_layer(im2)\n", + "\n", + "# watch raster function widget for changes\n", + "raster_dropdown2.observe(set_raster_function2)\n", "m2" ] }, @@ -63,6 +138,7 @@ "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", @@ -70,13 +146,43 @@ " 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\": \"Hillshade Gray\"}\n", + "rendering_rule = {\"rasterFunction\": raster_dropdown3.value}\n", "im3 = ImageServiceLayer(url=url,\n", - " format='png', rendering_rule=rendering_rule,\n", + " format='jpgpng', rendering_rule=rendering_rule,\n", + " attribution='PGC, UMN, Esri',\n", " crs=projections.EPSG3031.Basemap)\n", "m3.add_layer(im3)\n", + "\n", + "# add control for raster function\n", + "widget_control3 = WidgetControl(widget=raster_dropdown3, position=\"topright\")\n", + "m3.add_control(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_layer(im3)\n", + " m3.add_layer(im3)\n", + "\n", + "# watch raster function widget for changes\n", + "raster_dropdown3.observe(set_raster_function3)\n", "m3" ] } diff --git a/ipyleaflet/leaflet.py b/ipyleaflet/leaflet.py index 56eb4e09a..3f6a88ea4 100644 --- a/ipyleaflet/leaflet.py +++ b/ipyleaflet/leaflet.py @@ -770,7 +770,7 @@ class VideoOverlay(RasterLayer): attribution = Unicode().tag(sync=True, o=True) -class ImageServiceLayer(RasterLayer): +class ImageServiceLayer(Layer): """ImageServiceLayer class Image Service layer for raster data served through a web service @@ -781,9 +781,9 @@ class ImageServiceLayer(RasterLayer): Url to the image service f: string, default "image" response format (use `'image'` to stream as bytes) - format: string, default "png" - format of exported image (use `'png'` for layers with transparency) - pixel_type: string, default "F32" + 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 @@ -803,8 +803,14 @@ class ImageServiceLayer(RasterLayer): rules for mosaicking transparent: boolean, default False If true, the image service will return images with transparency + 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('LeafletImageServiceLayerView').tag(sync=True) @@ -835,7 +841,10 @@ class ImageServiceLayer(RasterLayer): rendering_rule = Dict({}).tag(sync=True, o=True) mosaic_rule = Dict({}).tag(sync=True, o=True) transparent = Bool(False).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): diff --git a/js/src/layers/ImageServiceLayer.js b/js/src/layers/ImageServiceLayer.js index 812747692..4ca860bd6 100644 --- a/js/src/layers/ImageServiceLayer.js +++ b/js/src/layers/ImageServiceLayer.js @@ -2,10 +2,10 @@ // Distributed under the terms of the Modified BSD License. const L = require('../leaflet.js'); -const rasterlayer = require('./RasterLayer.js'); +const layer = require('./Layer.js'); const proj = require('../projections.js'); -export class LeafletImageServiceLayerModel extends rasterlayer.LeafletRasterLayerModel { +export class LeafletImageServiceLayerModel extends layer.LeafletLayerModel { defaults() { return { ...super.defaults(), @@ -37,6 +37,8 @@ export class LeafletImageServiceLayerModel extends rasterlayer.LeafletRasterLaye mosaicRule: {}, // image transparency transparent: false, + // image service attribution + attribution: '', // coordinate reference system crs: null, // emit when clicked or hovered @@ -47,7 +49,7 @@ export class LeafletImageServiceLayerModel extends rasterlayer.LeafletRasterLaye } } -export class LeafletImageServiceLayerView extends rasterlayer.LeafletRasterLayerView { +export class LeafletImageServiceLayerView extends layer.LeafletLayerView { create_obj() { this.obj = L.imageServiceLayer({ url: this.model.get('url'), diff --git a/js/src/leaflet-imageservice.js b/js/src/leaflet-imageservice.js index 2e404be3c..b7165a82a 100644 --- a/js/src/leaflet-imageservice.js +++ b/js/src/leaflet-imageservice.js @@ -29,6 +29,8 @@ L.ImageServiceLayer = L.Layer.extend({ mosaicRule: {}, // image transparency transparent: false, + // image service attribution + attribution: '', // coordinate reference system crs: null, // emit when clicked or hovered @@ -53,7 +55,7 @@ L.ImageServiceLayer = L.Layer.extend({ this._map = map; this.updateUrl(); if (!this._image) { - this._initImage(); + this._initImage(); } this._map.on('moveend', () => { L.Util.throttle(this._update(),this.options.updateInterval,this); @@ -125,7 +127,8 @@ L.ImageServiceLayer = L.Layer.extend({ }, getCenter: function () { - return this._bounds.getCenter(); + // get map center + return this._bounds.getCenter(); }, _getBBox: function() { @@ -160,11 +163,18 @@ L.ImageServiceLayer = L.Layer.extend({ 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(), - size: this._getSize(), + bbox: this._getBBox().join(','), + size: this._getSize().join(','), bboxSR: this._getEPSG(), imageSR: this._getEPSG(), f: this.options.f, @@ -196,7 +206,7 @@ L.ImageServiceLayer = L.Layer.extend({ params['bandIds'] = this.options.bandIds.join(','); } if (this.options.time.length) { - params['time'] = this.options.time.join(','); + params['time'] = this._getTime().join(','); } // convert dictionary parameters to JSON if (Object.keys(this.options.renderingRule).length) {