How to limit search results to an extent with Esri Javascript map? - esri

I believe by default the 4.13 Esri Javascript map will search addresses by the current map view extent. The issue is that if the user zooms in or out too far, the search results return addresses VERY far away. Here is my code:
function initESRIMap() {
require(["esri/Map", "esri/views/MapView", "esri/widgets/Search", "esri/layers/FeatureLayer", "esri/widgets/Popup", "esri/geometry/Extent", "esri/geometry/Geometry"], function (
Map,
MapView,
Search,
FeatureLayer,
Popup,
Extent,
Geometry
) {
var esriMap = new Map({
basemap: "streets",
});
var esriView = new MapView({
container: "map-div",
map: esriMap,
center: LatLong,
zoom: 11,
});
var search = new Search({
view: esriView,
});}
I want to be able to get the same search results REGARDLESS of my map view location. BUT limit results to a specific area. Therefore if I'm viewing another country I'll still see search results from my extent.

A search widget can be customized to use custom sources, and a custom source can be customized to use a filter, which can include a map extent.
For example, let's define a custom SearchSource - we'll just use the same search endpoint as the standard ArcGIS World Geocoder service, but we'll add a filter:
const source = {
locator: new Locator({
url: "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer"
}),
filter: {
geometry: new Extent({
xmax: -12921954.804910611,
xmin: -13126806.04071496,
ymax: 3909898.0736186495,
ymin: 3801204.619397087,
spatialReference: {
wkid: 102100
}
})
},
maxSuggestions: 10
};
Now when we're defining our search widget, we'll include this in the source property array, and we'll turn off includeDefaultSources so that the original, unbounded source, is not used:
const search = new Search({
view,
includeDefaultSources: false,
sources: [source]
});
Working Codesandbox
In this example, I set the extent in the filter to be around the area of san diego / tijuana. Wherever you pan or zoom on the map, you'll still only get results from that area. For example, move the map over to seattle, or china, or whatever, and search for something generic ("park", "road", whatever), and you'll see that no matter what the map's extent is, the search results will be limited to what you put in the search source's filter.
Note this answer was written with 4.18.

Related

Google map ignoring marker size for retina icons

I have a Google map driven by Advanced Custom Fields. I have created an iconType to dictate which icon is used as the marker depending on the custom field.
My marker images are 80px square but I want to force them to 40px square for retina reasons. I have tried lots of variations of code I've found via google etc but nothing works and the size gets ignored showing my markers at the larger 80px. My guess is because I have the 2 icons depending on iconType?
Can anyone help me force the size of these markers to 40px square please...
function add_marker( $marker, map ) {
// var
var latlng = new google.maps.LatLng( $marker.attr('data-lat'), $marker.attr('data-lng'));
var iconType = $marker.attr('data-icon');
var iconBase = '<?php echo get_template_directory_uri(); ?>/assets/';
var size = new google.maps.Size(40, 40);
if (iconType === 'Costa') {
iconName = 'costa#2x.png';
} else {
iconName = 'starbucks#2x.png';
}
// create marker
var marker = new google.maps.Marker({
position : latlng,
map : map,
clickable: false,
animation: google.maps.Animation.DROP,
icon: iconBase + iconName,
});
// add to array
map.markers.push( marker );
// if marker contains HTML, add it to an infoWindow
if( $marker.html() )
{
// create info window
var infowindow = new google.maps.InfoWindow({
content : $marker.html()
});
}
}
I did something like this recently, and I believe you'll need to use the new Icon object to specify your custom Icon properties, and use the size and scaledSize properties. I fixed my issues following this guide:
How to add retina ready pin marker to your Google maps?

Use Dojo boxConstrainedMoveable to constrain movable div to window

I have a div, to which I applied Dojo dojo/dnd/Moveable. But, I'd like to prevent the user from dragging the div offscreen. So, I think I need to implement dojo/dnd/move/boxConstrainedMoveable.
I'm starting with this:
var dnd = new Moveable(this.domNode, {
'handle': this.titleNode
});
There's a similar SO question here:
Constrain a moveable object in Dojo. Applying that answer, I get something like this:
var dnd = new move.boxConstrainedMoveable(
'handle': this.titleNode
constraints: {
l: 0,
t: 20,
w: 500,
h: 500
},
within: true
);
But, I just can't understand how the bounding box works. I simply want the div to stay inside the window. I've tried implementing a few things with the window box, the div's margin box. Nothing's worked, and all I've made is a big mess.
I read the docs here:
http://dojotoolkit.org/api/?qs=1.9/dojo/dnd/move.boxConstrainedMoveable
Has anyone done this with Dojo? I'd be very appreciate of an example.
I looked up some old code I have and I did implement this type of movable once. This was written against Dojo 1.7, so things may have changed in 1.9. Fiddle demonstration: https://jsfiddle.net/4ev1daqr/26/
The main difference between your attempted solution and this is that the constraints property in the moveable needs to be a function rather than a static bounding box. When using the boxConstrainedMoveable module, the static bounding box should be assigned to a box property, rather than the constraints property.
This is actually a nice design, IMHO, because it allows the constraints to react to changes in application state, e.g. hiding a sidebar or moving a splitter, but it does make the simple case a bit more difficult to get working.
define(["dojo/_base/declare",
"dojo/dnd/move",
"dojo/dom",
"dojo/_base/window",
"dojo/dom-style",
"dojo/dom-geometry",
],
function (declare, move, dom, win, domStyle, domGeom) {
return declare( "my/dnd/move/BodyConstrainedMoveable", [move.constrainedMoveable], {
markupFactory: function(params, node){
return new this(node, params);
},
constructor: function(node, params) {
// Constrain the node to be within the body
this.constraints = function() {
var n = win.body(),
s = domStyle.getComputedStyle(n),
mb = domGeom.getMarginBox(n, s);
if ( this.node ) {
var menubox = domGeom.getMarginBox(this.node);
mb.w -= menubox.w;
mb.h -= menubox.h;
}
return mb;
};
}
})});

Panning the map to certain extent javascript API

I want to limit map extent to the initial extent of the map and limit user from panning more than certain extent.
I tried following but nothing has changed:
map = new Map( "map" , {
basemap: "gray",
center: [-85.416, 49.000],
zoom : 6,
logo: false,
sliderStyle: "small"
});
dojo.connect(map, "onExtentChange", function (){
var initExtent = map.extent;
var extent = map.extent.getCenter();
if(initExtent.contains(extent)){}
else{map.setExtent(initExtent)}
});
Just to flesh out Simon's answer somewhat, and give an example. Ideally you need two variables at the same scope as map:
initExtent to store the boundary of your valid extent, and
validExtent to store the last valid extent found while panning, so that you can bounce back to it.
I've used the newer dojo.on event syntax as well for this example, it's probably a good idea to move to this as per the documentation's recommendation - I assume ESRI will discontinue the older style at some point.
var map;
var validExtent;
var initExtent;
[...]
require(['dojo/on'], function(on) {
on(map, 'pan', function(evt) {
if ( !initExtent.contains(evt.extent) ) {
console.log('Outside bounds!');
} else {
console.log('Updated extent');
validExtent = evt.extent;
}
});
on(map, 'pan-end', function(evt) {
if ( !initExtent.contains(evt.extent) ) {
map.setExtent(validExtent);
}
});
});
You can do the same with the zoom events, or use extent-change if you want to trap everything. Up to you.
It looks like your extent changed function is setting the initial extent variable to the maps current extent and then checking if that extent contains the current extents centre point - which of course it always will.
Instead, declare initExtent at the same scope of the map variable. Then, change the on load event to set this global scope variable rather than a local variable. In the extent changed function, don't update the value of initExtent, simply check the initExtent contains the entire of the current extent.
Alternatively you could compare each bound of the current extent to each bound of the initExtent, e.g. is initExtent.xmin < map.extent.xmin and if any are, create a new extent setting any exceeded bounds to the initExtent values.
The only problem is these techniques will allow the initExtent to be exceeded briefly, but will then snap the extent back once the extent changed function fires and catches up.
I originally posted this solution on gis.stackexchange in answer to this question: https://gis.stackexchange.com/a/199366
Here's a code sample from that post:
//This function limits the extent of the map to prevent users from scrolling
//far away from the initial extent.
function limitMapExtent(map) {
var initialExtent = map.extent;
map.on('extent-change', function(event) {
//If the map has moved to the point where it's center is
//outside the initial boundaries, then move it back to the
//edge where it moved out
var currentCenter = map.extent.getCenter();
if (!initialExtent.contains(currentCenter) &&
event.delta.x !== 0 && event.delta.y !== 0) {
var newCenter = map.extent.getCenter();
//check each side of the initial extent and if the
//current center is outside that extent,
//set the new center to be on the edge that it went out on
if (currentCenter.x < initialExtent.xmin) {
newCenter.x = initialExtent.xmin;
}
if (currentCenter.x > initialExtent.xmax) {
newCenter.x = initialExtent.xmax;
}
if (currentCenter.y < initialExtent.ymin) {
newCenter.y = initialExtent.ymin;
}
if (currentCenter.y > initialExtent.ymax) {
newCenter.y = initialExtent.ymax;
}
map.centerAt(newCenter);
}
});
}
And here's a working jsFiddle example: http://jsfiddle.net/sirhcybe/aL1p24xy/

Dojo/Dijit TitlePane

How do you make a titlePane's height dynamic so that if content is added to the pane after the page has loaded the TitlePane will expand?
It looks like the rich content editor being an iframe that is loaded asynchronously confuses the initial layout.
As #missingno mentioned, the resize function is what you want to look at.
If you execute the following function on your page, you can see that it does correctly resize everything:
//iterate through all widgets
dijit.registry.forEach(function(widget){
//if widget has a resize function, call it
if(widget.resize){
widget.resize()
}
});
The above function iterates through all widgets and resizes all of them. This is probably unneccessary. I think you would only need to call it on each of your layout-related widgets, after the dijit.Editor is initialized.
The easiest way to do this on the actual page would probably to add it to your addOnLoad function. For exampe:
dojo.addOnLoad(function() {
dijit.byId("ContentLetterTemplate").set("href","index2.html");
//perform resize on widgets after they are created and parsed.
dijit.registry.forEach(function(widget){
//if widget has a resize function, call it
if(widget.resize){
widget.resize()
}
});
});
EDIT: Another possible fix to the problem is setting the doLayout property on your Content Panes to false. By default all ContentPane's (including subclasses such as TitlePane and dojox.layout.ContentPane) have this property set to true. This means that the size of the ContentPane is predetermined and static. By setting the doLayout property to false, the size of the ContentPanes will grow organically as the content becomes larger or smaller.
Layout widgets have a .resize() method that you can call to trigger a recalculation. Most of the time you don't need to call it yourself (as shown in the examples in the comments) but in some situations you have no choice.
I've made an example how to load data after the pane is open and build content of pane.
What bothers me is after creating grid, I have to first put it into DOM, and after that into title pane, otherwise title pane won't get proper height. There should be cleaner way to do this.
Check it out: http://jsfiddle.net/keemor/T46tt/2/
dojo.require("dijit.TitlePane");
dojo.require("dojo.store.Memory");
dojo.require("dojo.data.ObjectStore");
dojo.require("dojox.grid.DataGrid");
dojo.ready(function() {
var pane = new dijit.TitlePane({
title: 'Dynamic title pane',
open: false,
toggle: function() {
var self = this;
self.inherited('toggle', arguments);
self._setContent(self.onDownloadStart(), true);
if (!self.open) {
return;
}
var xhr = dojo.xhrGet({
url: '/echo/json/',
load: function(r) {
var someData = [{
id: 1,
name: "One"},
{
id: 2,
name: "Two"}];
var store = dojo.data.ObjectStore({
objectStore: new dojo.store.Memory({
data: someData
})
});
var grid = new dojox.grid.DataGrid({
store: store,
structure: [{
name: "Name",
field: "name",
width: "200px"}],
autoHeight: true
});
//After inserting grid anywhere on page it gets height
//Without this line title pane doesn't resize after inserting grid
dojo.place(grid.domNode, dojo.body());
grid.startup();
self.set('content', grid.domNode);
}
});
}
});
dojo.place(pane.domNode, dojo.body());
pane.toggle();
});​
My solution is to move innerWidget.startup() into the after advice to "toggle".
titlePane.aspect = aspect.after(titlePane, 'toggle', function () {
if (titlePane.open) {
titlePane.grid.startup();
titlePane.aspect.remove();
}
});
See the dojo/aspect reference documentation for more information.

Using Pusher to add markers on a map?

I am working to create a map to show based upon IP's of the people on my site. I was thinking a cool map showing all the places around the globe that were hitting the map. But I have come into a little issue. I have the ip's being tracked, and then from their they are processed in a Geo query, and finally I'm using pusherapp to post the data to the map. However, now I can make a unordered list, by appending the datato my site, but I can't figure out how to add it to my google map, nor have that map redraw with the data. Can someone help me here?
So basically on the server side:
var bounds = new google.maps.LatLngBounds();
var latlng = new google.maps.LatLng( #{#a.lat}, #{#a.lng});
var marker = new google.maps.Marker({
animation: google.maps.Animation.DROP,
icon: 'images/marker.png',
map: map,
position: latlng,
title: 'Here'
});
bounds.extend(latlng);
map.fitBounds(bounds);
followed by on the client side:
channel.bind('latitude', function(data){
$('#list').append(data);
});
But even if I do get it to plop into the script section is that google map going to know how to update?
I would really like to keep this all in Rails, and for the service I'm right now using pusher
Thanks!
thanks for using our service.
By the sounds of it you should be using our presence functionality so that you can keep track of users leaving and joining your site.
Part of the presence functionality is that when a user subscribes to a channel an AJAX call is made to your server. Within that call you can do your IP lookup and then return the details (I would suggest just the lat and long values) of that lookup as the user_info to the presence request. See Authenticating Users for more information on this. These docs also provide a Rails example of authenticating a user and providing user_info for that user.
When any user subscribes the the presence channel in the client they will get an initial list of connected users. You can loop through that user list and get the info for each user and add a marker to the Google map. When new users join you will get member_added events on the channel and you can add a new marker. When users leave you will get a member_removed event and you can remove a marker.
An example of the client code might be:
var channel = pusher.subscribe('presence-site-map-channel');
// Initial list of users on the site
channel.bind('pusher:subscription_succeeded', function(members) {
members.each(function(member) {
addMember(member);
});
});
channel.bind('pusher:member_added, addMember);
channel.bind('pusher:member_removed, function() { /* TODO: implement */});
function addMember(member) {
var bounds = new google.maps.LatLngBounds();
var latlng = new google.maps.LatLng( member.info.lat, member.info.long);
var marker = new google.maps.Marker({
animation: google.maps.Animation.DROP,
icon: 'images/marker.png',
map: map,
position: latlng,
title: member.info.id // each user must have a unique ID and you could use it here
});
bounds.extend(latlng);
map.fitBounds(bounds);
};