0

I'm aware of binding a pop-up to ESRI's L.esri.DynamicMapLayer here. The following code below is successful.

$.ajax({
      type: 'GET',
      url: url + '?f=json',
      data: { layer: fooType },
      dataType: 'json',
      success: function(json) {

          var foo_layer = fooLayers[fooType].layers;
          
          foo = L.esri.dynamicMapLayer({
            url: url,
            layers: [foo_layer],
            transparent: true
          }).addTo(map).bringToFront();

          foo.bindPopup(function(error, featureCollection) {

            if (error || featureCollection.features.length === 0) {
              return false;
            } else {
              var obj = featureCollection.features[0].properties;
              var val = obj['Pixel Value'];
              var lat = featureCollection.features[0].geometry.coordinates[1];
              var lon = featureCollection.features[0].geometry.coordinates[0];
            
              new L.responsivePopup({
                autoPanPadding: [10, 10],
                closeButton: true,
                autoPan: false
              }).setContent(parseFloat(val).toFixed(2)).setLatLng([lat, lon]).openOn(map);

            }
          });
      }        
});

But rather than a click response I am wondering as to whether you can mouseover using bindTooltip instead on a dynamic map. I've looked at the documentation for L.esri.DynamicMapLayer which says it is an extension of L.ImageOverlay. But perhaps there is an issue outlined here that I'm not fully understanding. Maybe it is not even related.

Aside, I've been testing multiple variations of even the simplest code to get things to work below but have been unsuccessful. Perhaps because this is asynchronous behavior it isn't possible. Looking for any guidance and/or explanation(s). Very novice programmer and much obliged for expertise.

$.ajax({
      type: 'GET',
      url: url + '?f=json',
      data: { layer: fooType },
      dataType: 'json',
      success: function(json) {

          var foo_layer = fooLayers[fooType].layers;
          
          foo = L.esri.dynamicMapLayer({
            url: url,
            layers: [foo_layer],
            transparent: true
          }).addTo(map).bringToFront();

          foo.bindTooltip(function(error, featureCollection) {

            if (error || featureCollection.features.length === 0) {
              return false;
            } else {
              new L.tooltip({ 
                sticky: true
              }).setContent('blah').setLatLng([lat,lng]).openOn(map);

            }
          });
      }        
});

  • I'm a little confused as to what you're asking / trying to do. You want to attach a tooltip to your L.esri.DynamicMapLayer, and have it open when you mouseover that layer? – Seth Lutske Jan 21 '21 at 06:24
  • Yes, attaching a tooltip to the mouse pointer using `sticky: true`. The layer being dynamic, there's a value at each pixel set within the featureCollection. What I'm trying to figure out is whether it is possible to hover seemlessly over the dyanmic map to get the pixel value rather than having to click. – redchair218 Jan 22 '21 at 13:56
  • Interesting. I understand the effect you're trying to create. Perhaps you can explain your logic flow - there's an ajax call to the layer's json, and when succeeds, you create the layer and add it to the map? (Is there a reason for that flow? Why not do an `var foo = L.esri.dynamicMapLayer()` and use `foo.on('load', callback)` pattern, and add your tooltip in the callback?). I'm also a little confused as to where you're making the call to get the feature details at a given `latlng`. I have some clues as to how you can do what you want, but I need more details. Can you make a codesandbox? – Seth Lutske Jan 22 '21 at 19:10
  • Alright, sorry for the delay. Here is what I'm accomplishing with clicking on the map at any point to get the data value ... [https://jsfiddle.net/redchair218/5oksd2fr/](https://jsfiddle.net/redchair218/5oksd2fr/). To answer your question, yes, that is the flow, and certainly what you suggest is possible and I'm going to look into that. What I'm trying to accomplish in the end, just as an example of the behavior, is this ... [https://jsfiddle.net/redchair218/5oksd2fr/1/](https://jsfiddle.net/redchair218/5oksd2fr/1/) – redchair218 Jan 24 '21 at 20:00
  • Ok I think I have enough information now, I will write an answer soon – Seth Lutske Jan 24 '21 at 22:19

1 Answers1

0

Serendipitously, I have been working on a different problem, and one of the byproducts of that problem may come in handy for you.

Your primary issue is the asynchronous nature of the click event. If you open up your map (the first jsfiddle in your comment), open your dev tools network tab, and start clicking around, you will see a new network request made for every click. That's how a lot of esri query functions work - they need to query the server and check the database for the value you want at the given latlng. If you tried to attach that same behavior to a mousemove event, you'll trigger a huge number of network requests and you'll overload the browser - bad news.

One solution of what you can do, and its a lot more work, is to read the pixel data under the cursor of the image returned from the esri image service. If you know the exact rgb value of the pixel under the cursor, and you know what value that rgb value corresponds to in the map legend, you can achieve your result.

Here is a working example

And Here is the codesandbox source code. Don't be afraid to hit refresh, CSB is little wonky in the way it transpiles the modules.

What is happening here? Let's look step by step:

  1. On map events like load, zoomend, moveend, a specialized function is fetching the same image that L.esri.dynamicMapLayer does, using something called EsriImageRequest, which is a class I wrote that reuses a lot of esri-leaflet's internal logic:
map.on("load moveend zoomend resize", applyImage);

const flashFloodImageRequest = new EsriImageRequest({
  url: layer_url,
  f: "image",
  sublayer: "3",
});


function applyImage() {
  flashFloodImageRequest
    .fetchImage([map.getBounds()], map.getZoom())
    .then((image) => {
      //do something with the image
    });
}

An instance of EsriImageRequest has the fetchImage method, which takes an array of L.LatLngBounds and a map zoom level, and returns an image - the same image that your dynamicMapLayer displays on the map.

EsriImageRequest is probably extra code that you don't need, but I happen to have just run into this issue. I wrote this because my app runs on a nodejs server, and I don't have a map instance with an L.esri.dynamicMapLayer. As a simpler alternative, you can target the leaflet DOM <img> element that shows your dynamicMapLayer, use that as your image source that we'll need in step 2. You will have to set up a listener on the src attribute of that element, and run the applyImage in that listener. If you're not familiar with how leaflet manages the DOM, look into your elements tab in the inspector, and you can find the <img> element here:

enter image description here

I'd recommend doing it that way, and not the way my example shows. Like I said, I happened to have just been working on a sort-of related issue.

  1. Earlier in the code, I had set up a canvas, and using the css position, pointer-events, and opacity properties, it lays exactly over the map, but is set to take no interaction (I gave it a small amount of opacity in the example, but you'd probably want to set opacity to 0). In the applyImage function, the image we got is written to that canvas:
// earlier...
const mapContainer = document.getElementById("leafletMapid");
const canvas = document.getElementById("mycanvas");
const height = mapContainer.getBoundingClientRect().height;
const width = mapContainer.getBoundingClientRect().width;
canvas.height = height;
canvas.width = width;
const ctx = canvas.getContext("2d");

// inside applyImage .then:
.then((image) => {
  image.crossOrigin = "*";
  ctx.drawImage(image, 0, 0, width, height);
});

Now we have an invisible canvas who's pixel content is exactly the same as the dynamicMapLayer's.

  1. Now we can listen to the map's mousemove event, and get the mouse's rgba pixel value from the canvas we created. If you read into my other question, you can see how I got the array of legend values, and how I'm using that array to map the pixel's rgba value back to the legend's value for that color. We can use the legend's value for that pixel, and set the popup content to that value.
map.on("mousemove", (e) => {
  // get xy position on cavnas of the latlng
  const { x, y } = map.latLngToContainerPoint(e.latlng);
  // get the pixeldata for that xy position
  const pixelData = ctx.getImageData(x, y, 1, 1);
  const [R, G, B, A] = pixelData.data;
  const rgbvalue = { R, G, B, A };
  // get the value of that pixel according to the layer's legend
  const value = legend.find((symbol) =>
    compareObjectWithTolerance(symbol.rgbvalue, rgbvalue, 5)
  );
  // open the popup if its not already open
  if (!popup.isOpen()) {
    popup.setLatLng(e.latlng);
    popup.openOn(map);
  }
  // set the position of the popup to the mouse cursor
  popup.setLatLng(e.latlng);
  // set the value of the popup content to the value you got from the legend
  popup.setContent(`Value: ${value?.label || "unknown"}`);
});

As you can see, I'm also setting the latlng of the popup to wherever the mouse is. With closeButton: false in the popup options, it behaves much like a tooltip. I tried getting it to work with a proper L.tooltip, but I was having some trouble myself. This seems to create the same effect.

Sorry if this was a long answer. There are many ways to adapt / improve my code sample, but this should get you started.

Seth Lutske
  • 9,154
  • 5
  • 29
  • 78
  • Your second paragraph, _Your primary issue is the asynchronous nature of the click event_, is what I assumed and just need reassurance. It being a dynamic map, the underlying json has an actual value to the pixel to which you can get upon `click`. But this is a great readout / listen approach for non-dynamic layers and gradient-layers to which some of my layers are within my leaflet mapping application. This should definitely get me started. All that being said I'm going to mark the answer correct with many thanks. – redchair218 Jan 25 '21 at 21:08
  • If you don't mind teaching me something here, what do you mean "the underlying json has an actual value to the pixel to which you can get upon click"? Do you mean that the `latlng` of the click is compared against the JSON's polygons to determine if it falls in a given shape, and then returns that shape's value? – Seth Lutske Jan 25 '21 at 21:54
  • Hopefully I haven't misspoken and hopefully I have this right. Looking at [https://jsfiddle.net/redchair218/5oksd2fr/](https://jsfiddle.net/redchair218/5oksd2fr/) when calling the dynamic map it has underlying geoJson data per `featureCollection`. Within the `featureCollection` are the various features with respect to the `latlng` point clicked. I added a `console.log` within [https://jsfiddle.net/redchair218/5oksd2fr/2/](https://jsfiddle.net/redchair218/5oksd2fr/2/) to show that. The `return` in this case is the `pixel value`. – redchair218 Jan 25 '21 at 23:14