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.
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 : 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>
Solved! Go to Solution.
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 : 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>
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 : 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>
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!