Select to view content in your preferred language

Auto-pan popup to fit Map

530
3
01-25-2024 08:33 PM
Paco
by
Occasional Contributor

Hello all!  I am looking for a way to deal with popups that open off-screen.  If a Feature is near the edge of the Map sometimes the popup gets cutoff.  I would like it to just move the popup to fit, not 'center' the popup.

I have seen MapBox applications that automatically quick pan the popup into full view on-click.   I have also found a fiddle that has this same action that would work perfectly, altho I just cannot decipher how the code works to accomplish this?  it seems to be using "view.watch" and "view.popup.features"?

https://jsfiddle.net/gavinr/1jLmfLLL/

(you will need to add your own Feature Service URL to make this fiddle work)

https://www.nps.gov/subjects/geology/geologic-resources-inventory-products.htm

(this is the MapBox app that has the action im looking for)

thx!

**another ESRI example-  https://totalapis.github.io/sample-code/watch-for-changes/live/index.html

 

0 Kudos
3 Replies
JoelBennett
MVP Regular Contributor

If your goal is simply to keep the popup from getting cut off, you might consider the solution here.  It doesn't pan the map, but instead forces to the popup to open towards the direction of the map where there's the most space.

0 Kudos
Paco
by
Occasional Contributor

Thanks.  but as the original poster had mentioned about the ESRI code,  that first click is still off the screen? a strange first click bug? Also thx for your reactiveUtils code,  altho I tried it(in 4.26x) and it didn't seem to work, then tried with 4.28x and it was closer but still gave me some random cutoffs for certain popup clicks.

So,  that's why the "pan" concept is so great, it opens off screen then quickly pans to fit the entire popup within the "view".   Im still not sure why view.popup "alignment" and "dockEnabled" seem to work fine, but I can't seem to get "visible" to work?   the example code I included seems to be..fairly simple, but I cant get that "pan" to work.  thx

0 Kudos
JoelBennett
MVP Regular Contributor

I adjusted the "Intro to popups" sample to the following, which works although there's some explanation further below:

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
    <title>Intro to popups | Sample | ArcGIS Maps SDK for JavaScript 4.28</title>

    <style>
      html,
      body,
      #viewDiv {
        padding: 0;
        margin: 0;
        height: 100%;
        width: 100%;
      }

      #instruction {
        z-index: 99;
        position: absolute;
        top: 15px;
        left: 50%;
        padding: 5px;
        margin-left: -175px;
        height: 30px;
        width: 355px;
        background: rgba(25, 25, 25, 0.8);
        color: white;
      }
    </style>

    <link rel="stylesheet" href="https://js.arcgis.com/4.28/esri/themes/light/main.css" />
    <script src="https://js.arcgis.com/4.28/"></script>

    <script>
      function adjustViewForPopup(view, popup) {
        if (popup.visible) {
          var contentFeatureElement = popup.container.querySelector(".esri-features__content-feature");
          var headerElement = contentFeatureElement?.shadowRoot.querySelector("calcite-panel")?.shadowRoot?.querySelector("div.header-container");

          if (headerElement) {
            var popupRect = popup.container.getBoundingClientRect();
            var viewRect = view.container.getBoundingClientRect();
            var xDiff = 0, yDiff = 0;

            if (popupRect.right > viewRect.right)
              xDiff = (viewRect.right - popupRect.right) - 5;
            else if (popupRect.left < viewRect.left)
              xDiff = (viewRect.left - popupRect.left) + 5;

            if (popupRect.bottom > viewRect.bottom)
              yDiff = (viewRect.bottom - popupRect.bottom) - 5;
            else if (popupRect.top < viewRect.top)
              yDiff = (viewRect.top - popupRect.top) + 5;

            if ((xDiff !== 0) || (yDiff !== 0)) {
              var screenPoint = view.toScreen(view.extent.center);

              view.goTo(view.toMap({x:screenPoint.x - xDiff, y:screenPoint.y - yDiff}));
            }
          } else
            window.setTimeout(adjustViewForPopup.bind(window, view, popup), 100);
        }
      }

      require(["esri/rest/locator", "esri/Map", "esri/views/MapView", "esri/widgets/Popup"], (locator, Map, MapView, Popup) => {
        // Set up a locator url using the world geocoding service
        const locatorUrl = "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer";

        // Create the Map
        const map = new Map({
          basemap: "streets-navigation-vector"
        });

        var popup = new Popup();
        popup.alignment = "top-center";

        // Create the MapView
        const view = new MapView({
          container: "viewDiv",
          map: map,
          center: [-71.6899, 43.7598],
          zoom: 12,
          popup: popup
        });

        new MutationObserver(adjustViewForPopup.bind(window, view, popup)).observe(popup.container, {childList:true,subtree:true});

        popup.watch(["content","selectedFeatureWidget"], adjustViewForPopup.bind(window, view, popup));

        /*******************************************************************
         * This click event sets generic content on the popup not tied to
         * a layer, graphic, or popupTemplate. The location of the point is
         * used as input to a reverse geocode method and the resulting
         * address is printed to the popup content.
         *******************************************************************/
        view.popupEnabled = false;
        view.on("click", (event) => {
          // Get the coordinates of the click on the view
          const lat = Math.round(event.mapPoint.latitude * 1000) / 1000;
          const lon = Math.round(event.mapPoint.longitude * 1000) / 1000;

          view.openPopup({
            // Set the popup's title to the coordinates of the location
            title: "Reverse geocode: [" + lon + ", " + lat + "]",
            location: event.mapPoint // Set the location of the popup to the clicked location
          });

          const params = {
            location: event.mapPoint
          };

          // Display the popup
          // Execute a reverse geocode using the clicked location
          locator
            .locationToAddress(locatorUrl, params)
            .then((response) => {
              // If an address is successfully found, show it in the popup's content
              view.popup.content = response.address;
            })
            .catch(() => {
              // If the promise fails and no result is found, show a generic message
              view.popup.content = "No address was found for this location";
            });
        });
      });
    </script>
  </head>

  <body>
    <div id="viewDiv"></div>
    <div id="instruction" class="esri-widget">Click any location on the map to see its street address</div>
  </body>
</html>

 

The main part is the function "adjustViewForPopup" added on lines 35-63.  I also had to explicitly require the Popup widget on line 65, instantiate one on line 74, and assign it to the view on line 83 in order to get around an issue where it didn't fully pan on the first click.  I chalk those first-click issues up to the modules being lazy loaded, coupled with the use of a shadow DOM starting in 4.28.

The MutationObserver of line 86 doesn't appear to be notified when elements within a shadow root are altered, thus the additional use of "watch" two lines below it for good measure.

I used a fixed alignment on line 75, because without it, the alignment will change anchors when the map is panned, thus making it seem like the panning went too far.

It seems the popup's contents are fully present when the headerElement of line 40 is present, which is when the domRect has its expected values.

0 Kudos