Select to view content in your preferred language

Location of Popup after panning 'Around the World'

348
2
Jump to solution
05-23-2023 11:44 AM
GregKnight_Eptura
New Contributor

Hi all,

We have a situation where we want to override the default popup location and position the popup precisely on the feature. This works fine if the user hasn't panned 'around the world'.  If the user has panned around the world, using the original feature location, the popup is positioned off the map.

For example, in this screenshot, the user clicked on the marker off the coast of Africa.

greg_knight_eptura_0-1684867074534.png

It seems the web mercator coordinates of the click differ between the markers, but the geometries themselves are the same.  

Looking for possible solutions here.  

Attaching sample code that demonstrates issue for good measure.

Thanks in advance.

Greg

 

<html>

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no">
  <title>ArcGIS Maps SDK for Javascript &colon; Marker Playground</title>
  <style>
    html,
    body,
    #viewDiv {
      padding: 0;
      margin: 0;
      height: 100%;
      width: 100%;
    }
  </style>
  <link rel="stylesheet" href="https://js.arcgis.com/4.25/esri/themes/light/main.css">
  <script src="https://js.arcgis.com/4.25/"></script>
  <script>
    require([
      "esri/config",
      "esri/Map",
      "esri/views/MapView",
      "esri/Basemap",
      "esri/Graphic",
      "esri/geometry/Point",
      "esri/PopupTemplate",
      "esri/core/reactiveUtils"
    ], function(esriConfig, Map, MapView, Basemap, Graphic, Point, PopupTemplate, reactiveUtils) {
      // api key
      esriConfig.apiKey = "";
      
      // init the map
      const map = new Map({
        basemap: "topo-vector" 
      });
      const view = new MapView({
        map: map,
        center: [0,0], 
        zoom: 1,
        container: "viewDiv",
        constraints: {
          //minZoom: 2
        }
      });
  const template = {
    title: "{name}"
  };    
  const graphics = new Graphic({
    geometry: {
      type: "point",
      x: 0,
      y: 0, 
      spatialReference: {
        wkid: 102100
      }
    },
    attributes: {
      name: "Coordinates: 0,0"
    },
    popupTemplate: template,
    symbol: {
      type: "text", // autocasts as new TextSymbol()
      color: "blue",
      text: "\ue61d", // esri-icon-map-pin
      font: {
        // autocasts as new Font()
        size: 20,
        family: "CalciteWebCoreIcons" // Esri Icon Font
      }
    }
  });

  view.graphics.add(graphics);
            
  // listen for marker click
  reactiveUtils.watch(
        () => view.popup.selectedFeature,
        (feature) => {
          if (feature) {
            if (feature.isAggregate) return;
            const { geometry } = feature;
            const point = new Point(geometry);
            view.popup.location = point;
          }
        }
      );
      
    // Set up a click event handler 
      view.on("click", function(event) {
        console.log("Click event emitted: ", event);
        console.log("Current view extent: ", view.extent);
      });  

    });
  </script>
</head>

<body>
  <div id="viewDiv"></div>
</body>

</html>

 

 

 

 

0 Kudos
1 Solution

Accepted Solutions
JoelBennett
MVP Regular Contributor

The main thing here is that you need to normalize the point to the view's extent.  That is, you need to ensure the point's 'x' value is within the view's xmin and xmax values, which can be different for the same location if the map has been panned all the way around the world.

See modified code below.  This adds a "normalizeToView" function to the Point class, which generates a copy of the instance with the 'x' value modified according to the view's extent if the view's been panned around the world.

This function is used in two places: (1) when initially assigning the popup's location, and (2) whenever the view's extent is changed, in which case the popup's location is checked, and if necessary, its 'x' value is modified.

The only caveat is that this uses the undocumented esri/geometry/support/spatialReferenceUtils module, which could change in future releases without notice.

 

<html>

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no">
  <title>ArcGIS Maps SDK for Javascript &colon; Marker Playground</title>
  <style>
    html,
    body,
    #viewDiv {
      padding: 0;
      margin: 0;
      height: 100%;
      width: 100%;
    }
  </style>
  <link rel="stylesheet" href="https://js.arcgis.com/4.25/esri/themes/light/main.css">
  <script src="https://js.arcgis.com/4.25/"></script>
  <script>
    require([
      "esri/config",
      "esri/Map",
      "esri/views/MapView",
      "esri/Basemap",
      "esri/Graphic",
      "esri/geometry/Point",
      "esri/PopupTemplate",
      "esri/core/reactiveUtils",
      "esri/geometry/support/spatialReferenceUtils"
    ], function(esriConfig, Map, MapView, Basemap, Graphic, Point, PopupTemplate, reactiveUtils, spatialReferenceUtils) {
      Point.prototype.normalizeToView = function(view) {
        var point = this.clone();

        if ((view) && (view.extent) && (!view.extent.intersects(this))) {
          var info = spatialReferenceUtils.getInfo(view.spatialReference);

          if (info) {
            var extents = view.extent.clone().normalize();
            var nPoint = this.clone().normalize();
            var intersects = false;

            for (var x = 0; x < extents.length; x++) {
              if (extents[x].intersects(nPoint)) {
                intersects = true;
                break;
              }
            }

            if (intersects) {
              var worldWidth = info.valid[1] * 2;

              while (point.x < view.extent.xmin)
                point.x += worldWidth;

              while (point.x > view.extent.xmax)
                point.x -= worldWidth;
            }
          }
        }

        return point;
      };

      // api key
      esriConfig.apiKey = "";

      // init the map
      const map = new Map({
        basemap: "topo-vector" 
      });
      const view = new MapView({
        map: map,
        center: [0,0], 
        zoom: 1,
        container: "viewDiv",
        constraints: {
          //minZoom: 2
        }
      });
      const template = {
        title: "{name}"
      };
      const graphics = new Graphic({
        geometry: {
          type: "point",
          x: 0,
          y: 0, 
          spatialReference: {
            wkid: 102100
          }
        },
        attributes: {
          name: "Coordinates: 0,0"
        },
        popupTemplate: template,
        symbol: {
          type: "text", // autocasts as new TextSymbol()
          color: "blue",
          text: "\ue61d", // esri-icon-map-pin
          font: {
            // autocasts as new Font()
            size: 20,
            family: "CalciteWebCoreIcons" // Esri Icon Font
          }
        }
      });

      view.graphics.add(graphics);

      // listen for marker click
      reactiveUtils.watch(
        () => view.popup.selectedFeature,
        (feature) => {
          if (feature) {
            if (feature.isAggregate) return;
            const { geometry } = feature;
            const point = new Point(geometry);
            view.popup.location = point.normalizeToView(view);
          }
        }
      );

      // Set up a click event handler 
      view.on("click", function(event) {
        console.log("Click event emitted: ", event);
        console.log("Current view extent: ", view.extent);
      });  

      view.watch("stationary", function(newValue, oldValue, propertyName, target) {
        if ((newValue) && (target.popup.visible)) {
          var normalizedPoint = target.popup.location.normalizeToView(target);

          if (normalizedPoint.x != target.popup.location.x)
            target.popup.location = normalizedPoint;
        }
      });
    });
  </script>
</head>

<body>
  <div id="viewDiv"></div>
</body>

</html>

 

View solution in original post

0 Kudos
2 Replies
JoelBennett
MVP Regular Contributor

The main thing here is that you need to normalize the point to the view's extent.  That is, you need to ensure the point's 'x' value is within the view's xmin and xmax values, which can be different for the same location if the map has been panned all the way around the world.

See modified code below.  This adds a "normalizeToView" function to the Point class, which generates a copy of the instance with the 'x' value modified according to the view's extent if the view's been panned around the world.

This function is used in two places: (1) when initially assigning the popup's location, and (2) whenever the view's extent is changed, in which case the popup's location is checked, and if necessary, its 'x' value is modified.

The only caveat is that this uses the undocumented esri/geometry/support/spatialReferenceUtils module, which could change in future releases without notice.

 

<html>

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no">
  <title>ArcGIS Maps SDK for Javascript &colon; Marker Playground</title>
  <style>
    html,
    body,
    #viewDiv {
      padding: 0;
      margin: 0;
      height: 100%;
      width: 100%;
    }
  </style>
  <link rel="stylesheet" href="https://js.arcgis.com/4.25/esri/themes/light/main.css">
  <script src="https://js.arcgis.com/4.25/"></script>
  <script>
    require([
      "esri/config",
      "esri/Map",
      "esri/views/MapView",
      "esri/Basemap",
      "esri/Graphic",
      "esri/geometry/Point",
      "esri/PopupTemplate",
      "esri/core/reactiveUtils",
      "esri/geometry/support/spatialReferenceUtils"
    ], function(esriConfig, Map, MapView, Basemap, Graphic, Point, PopupTemplate, reactiveUtils, spatialReferenceUtils) {
      Point.prototype.normalizeToView = function(view) {
        var point = this.clone();

        if ((view) && (view.extent) && (!view.extent.intersects(this))) {
          var info = spatialReferenceUtils.getInfo(view.spatialReference);

          if (info) {
            var extents = view.extent.clone().normalize();
            var nPoint = this.clone().normalize();
            var intersects = false;

            for (var x = 0; x < extents.length; x++) {
              if (extents[x].intersects(nPoint)) {
                intersects = true;
                break;
              }
            }

            if (intersects) {
              var worldWidth = info.valid[1] * 2;

              while (point.x < view.extent.xmin)
                point.x += worldWidth;

              while (point.x > view.extent.xmax)
                point.x -= worldWidth;
            }
          }
        }

        return point;
      };

      // api key
      esriConfig.apiKey = "";

      // init the map
      const map = new Map({
        basemap: "topo-vector" 
      });
      const view = new MapView({
        map: map,
        center: [0,0], 
        zoom: 1,
        container: "viewDiv",
        constraints: {
          //minZoom: 2
        }
      });
      const template = {
        title: "{name}"
      };
      const graphics = new Graphic({
        geometry: {
          type: "point",
          x: 0,
          y: 0, 
          spatialReference: {
            wkid: 102100
          }
        },
        attributes: {
          name: "Coordinates: 0,0"
        },
        popupTemplate: template,
        symbol: {
          type: "text", // autocasts as new TextSymbol()
          color: "blue",
          text: "\ue61d", // esri-icon-map-pin
          font: {
            // autocasts as new Font()
            size: 20,
            family: "CalciteWebCoreIcons" // Esri Icon Font
          }
        }
      });

      view.graphics.add(graphics);

      // listen for marker click
      reactiveUtils.watch(
        () => view.popup.selectedFeature,
        (feature) => {
          if (feature) {
            if (feature.isAggregate) return;
            const { geometry } = feature;
            const point = new Point(geometry);
            view.popup.location = point.normalizeToView(view);
          }
        }
      );

      // Set up a click event handler 
      view.on("click", function(event) {
        console.log("Click event emitted: ", event);
        console.log("Current view extent: ", view.extent);
      });  

      view.watch("stationary", function(newValue, oldValue, propertyName, target) {
        if ((newValue) && (target.popup.visible)) {
          var normalizedPoint = target.popup.location.normalizeToView(target);

          if (normalizedPoint.x != target.popup.location.x)
            target.popup.location = normalizedPoint;
        }
      });
    });
  </script>
</head>

<body>
  <div id="viewDiv"></div>
</body>

</html>

 

0 Kudos
GregKnight_Eptura
New Contributor

This is very interesting. It works perfectly.  I guess we'll have to gauge the risk of using an undocumented API.  Thank you very much!

0 Kudos