Return location from searchManager.geocode callback - bing-maps

I'm trying to make a call to the searchManager.geocode function and return the location object to be stored in an array.
I'm a javascript novice, so I expect this is probably a question more fundamental to javacript than to bing maps, but...
I'm experimenting with the Bing Maps search using this demo as my template:
https://www.bing.com/api/maps/sdk/mapcontrol/isdk/searchbyaddress
Here is the example:
var map = new Microsoft.Maps.Map(document.getElementById('myMap'), {
/* No need to set credentials if already passed in URL */
center: new Microsoft.Maps.Location(47.624527, -122.355255),
zoom: 8 });
Microsoft.Maps.loadModule('Microsoft.Maps.Search', function () {
var searchManager = new Microsoft.Maps.Search.SearchManager(map);
var requestOptions = {
bounds: map.getBounds(),
where: 'Seattle',
callback: function (answer, userData) {
map.setView({ bounds: answer.results[0].bestView });
map.entities.push(new Microsoft.Maps.Pushpin(answer.results[0].location));
}
};
searchManager.geocode(requestOptions);
});
What I would like to do is not actually do any work in the callback function, but simply return the location (`answer.results[0]') so that I can make a series of calls like this:
locations.push(geocodeAddress("address 1, city, state"));
locations.push(geocodeAddress("address 2, city, state"));
That locations array will be used for map bounds, adding pushpins, etc later.

Since the geocode function is an async one, you can't ask it to immediately return something for later use. However, what you could do is pushing the location to array directly in the callback function:
var map = new Microsoft.Maps.Map(document.getElementById('myMap'), {
/* No need to set credentials if already passed in URL */
center: new Microsoft.Maps.Location(47.624527, -122.355255),
zoom: 8 });
var locations = [];
Microsoft.Maps.loadModule('Microsoft.Maps.Search', function () {
var searchManager = new Microsoft.Maps.Search.SearchManager(map);
var requestOptions = {
bounds: map.getBounds(),
where: 'Seattle',
callback: function (answer, userData) {
locations.push(answer.results[0].location);
}
};
searchManager.geocode(requestOptions);
});
And your later calls would just be below since pushing to array is already taken care of:
geocodeAddress("address 1, city, state");
geocodeAddress("address 2, city, state");
Of course, you would still need some mechanism to keep track that all geocode calls are finished before using the locations array (perhaps through a counter in each callback). Also you would only need to load the Microsoft.Maps.Search module once despite repetitive usage.

Related

Esri-Leaflet - Search within a distance

I need to design an application using a feature layer stored in ArcGIS online. Using a geocoder/search, I need to be able to enter an address and select a distance (1 block, 2 blocks, etc). The result will show the new point, a distance radius, and all points within the radius. I would also like to have a table of the results.
What I need is exactly like this app created by Derek Eder from DataMade: https://carto-template.netlify.app/, except mine needs the data stored in a secured ArcGIS layer. Can anyone point me to an example, tutorial, etc with an esri-leaflet implementation similar to this application? I have spent the past five days trying to convert the code, and I feel like I am getting no where.
Here is a link to guthub: https://github.com/datamade/searchable-map-template-carto
-------UPDATE-------
Seth - I can get the layer to display; however, the query to join the searched point with the layer does not work. I imagine I’m leaving something out, because the console error reads “token required”. See below:
const radius = 1610;
/**************************************************************************************************/
// ArcGIS Authoization
/**************************************************************************************************/
$("#loginModal").modal({ backdrop: 'static', keyboard: false });
// submit element of form
var submitBtn = document.getElementById('btnArcGISOnline');
// add event listener to form
submitBtn.addEventListener('click', addServicesFromServer);
// create map and set zoom level and center coordinates
var map = L.map('mapCanvas', {
}).setView([30.46258, -91.13171], 12);
// set basemap to Esri Streets
L.esri.basemapLayer('Streets').addTo(map);
var layerurl = 'secure/layer/URL';
var tokenUrl = 'https://www.arcgis.com/sharing/generateToken';
// function to make request to server
function serverAuth(server, username, password, callback) {
L.esri.post(server, {
username: username,
password: password,
f: 'json',
expiration: 86400,
client: 'referer',
referer: window.location.origin
}, callback);
}
// function to run when form submitted
function addServicesFromServer(e) {
// prevent page from refreshing
e.preventDefault();
// get values from form
var username = document.getElementById('username').value;
var password = document.getElementById('password').value;
// generate token from server and add service from callback function
serverAuth(tokenUrl, username, password, function (error, response) {
if (error) {
return;
}
// add layer to map
var featureLayer = L.esri.featureLayer({
url: layerurl,
opacity: 1,
token: response.token
});
featureLayer.addTo(map);
$("#loginModal").modal("hide");
}); // end serverAuth call
} // end addServicesFromServer call
// HARNESS GEOCODER RESULTS
let circle;
// GeoSearch
const search = L.esri.Geocoding.geosearch({
useMapBounds: false,
expanded: true,
collapseAfterResult: false
});
search.addTo(map);
search.on("results", (results) => {
if (results && results.latlng) {
if (circle) {
circle.remove();
}
circle = L.circle(results.latlng, { radius });
circle.addTo(map);
queryLayer(results.latlng);
}
});
// SET UP QUERY FUNCTION
function queryLayer(point) {
const query = L.esri.query({ url: layerurl }).nearby(point, radius);
query.run(function (error, featureCollection, response) {
if (error) {
console.log(error);
return;
}
console.log(featureCollection.features);
populateList(featureCollection.features);
});
}
// WRITE RESULTS INTO A LIST
function populateList(features) {
const list = document.getElementById("results-list");
let listItems = "";
features.forEach((feature) => {
listItems =
listItems +
`
<li>
Place: ${feature.properties?.Location} <br>
Lat: ${feature.properties?.Latitude} <br>
Lng: ${feature.properties?.Longitude} <br>
</li>
`;
list.innerHTML = listItems;
});
}
I attempted to pass the token to the query as pasted below, but then I get an invalid token error.
var layerUrl_token = layerurl + "?token=" + response.token;
I also tried using turf.js, but I haven’t been successful. I know turf.js uses long/lat, but I haven’t even been able to get the correct syntax to pull the lat and long from the feature layer.
What you're trying to do is not too hard. While there are a handful of tutorials on different parts of what you want to do, let's piece things together. I'm going to use esri-leaflet-geocoder for my search functionality, as its consistent with esri-leaflet, and IMO its one of the best geocoders available for leaflet.
Setting up the geocoder
After setting up a basic leaflet map, let's import esri-leaflet and esri-leaflet-geocoder, and create a geocoder:
import L from "leaflet";
import * as EL from "esri-leaflet";
import * as ELG from "esri-leaflet-geocoder";
const search = ELG.geosearch({
useMapBounds: false,
expanded: true,
collapseAfterResult: false
});
search.addTo(map);
Don't forget to add the geocoder css to your html, as shown in the documentation example.
Add your layer:
const layerurl = "YOUR_LAYER_URL";
const featureLayer = EL.featureLayer({ url: layerurl });
featureLayer.addTo(map);
If you are using an authenication-required layer, you will need to get a token and use it as one of the options in featurelayer, (featureLayer({ url: layerurl, token: token })). If you're not sure how to get a token, make a comment and I can add some code for that, but there are some nice tutorials already available for that.
Harness the results of the search
The ELG.geosearch comes with a results event that you can harness. It is called when the user selects one of the results in the autocomplete dropdown of the geosearch. In that event, we can get the location data of location the user selected. We center the map there (which is a default of the geosearch actually), draw a circle with a given radius, and perform a query (more on that layer):
let circle;
search.on("results", (results) => {
if (results && results.latlng) {
if (circle) {
circle.remove();
}
circle = L.circle(results.latlng, { radius });
circle.addTo(map);
queryLayer(results.latlng);
}
});
Query the layer
Now we know the latlng of the location the user selected from the search. We can create an esri-leaflet query, which can query your feature layer in various ways. We'll see up a nearby query, which will query the layer for any features within a given radius of a point:
function queryLayer(point) {
const query = EL.query({ url: layerurl }).nearby(point, radius);
query.run(function (error, featureCollection, response) {
if (error) {
console.log(error);
return;
}
populateList(featureCollection.features);
});
}
If you are querying an authenticated layer, you'll need to add a token to the request. I'm fairly certain the way to do this is like so:
function queryLayer(point) {
const query = EL.query({ url: layerurl })
.token(<your_token_here>)
.nearby(point, radius);
// ... same as above
}
You may also be able to run a query directly off of your layer:
featureLayer.query().nearby(point, radius)
I'm not as familiar with this second way, but you can read more about it here: Query a feature layer.
Render to the page
Once we .run the query, we will have access to the results in the form of a featureCollection. We can loop through the features of that featureCollection and render some HTML:
function populateList(features) {
const list = document.getElementById("results-list");
let listItems = "";
features.forEach((feature) => {
listItems =
listItems +
`
<li>
Place: ${feature.properties?.Location} <br>
Lat: ${feature.properties?.Latitude} <br>
Lng: ${feature.properties?.Longitude} <br>
</li>
`;
list.innerHTML = listItems;
});
}
In this particular example, I am using a point layer I made that is being served through arcgis online. This point layer does not have address data, so feature.properties doesn't contain any address info. For your featurelayer, the attributes of your layer will be available in a feature.properties. So depending on what's there, you might want to use feature.properties?.address or whatever. This last bit is just an example, you will probably customize that a lot differently for your own purposes.
Working Codesandbox
Try searching heavily populated areas in this example. Note that in this featurelayer there are many overlapping locations, so there are more results in the list than it looks like there are markers on the map.
Also note, this example I'm posting using esri-leaflet and esri-leaflet-geocoder versions 2^. These were just updated to versions 3 about 1-2 months ago, and the new versions require use of an API key in the geocoder and in the layer declaration, so if you want to use the latest versions (recommended), you will need to add those in. I used version 2 so as not to expose an API key in a sandbox (and I sort of hate the new API key requirement . The new arcgis developers documentation for esri-leaflet has some examples of that, but the official documentation has not yet been updated to match those examples.

How to update geojson markers periodically

What I am trying to do is to use Leaflet with OSM map,
and load data from PHP in GeoJSON format + update periodically.
I can manage to display a map, load data, but do not know how to update points instead of still adding a new ones.
function update_position() {
$.getJSON('link_to_php', function(data) {
//get data into object
var geojsonFeature = JSON.parse(data);
// how to remove here old markers???
//add new layer
var myLayer = L.geoJSON().addTo(mymap);
//add markers to layet
myLayer.addData(geojsonFeature);
setTimeout(update_position, 1000);
});
}
update_position();
have tried mymap.removeLayer("myLayer"); but this seems to now work inside of function. Please help
L.geoJSON extends from LayerGroup which provide a function named clearLayers(docs), so you call that to clear markers from the layer.
Also, it is recommended that you put the layer variable outside the function:
var geoJSONLayer = L.geoJSON().addTo(mymap);
function update_position() {
$.getJSON('link_to_php', function(data) {
//get data into object
var geojsonFeature = JSON.parse(data);
geoJSONLayer.clearLayers();
//add markers to layet
geoJSONLayer.addData(geojsonFeature);
setTimeout(update_position, 1000);
});
}
update_position();

Here Maps not showing in Ionic tabs

I have a quite basic Here Map in an Ionic tap that is loaded in a Ionic tab with this JavaScript.
var platform = new H.service.Platform({
useCIT: true,
'app_id': $(component).data('appid'),
'app_code': $(component).data('appcode'),
useHTTPS: true
});
// Obtain the default map types from the platform object
var maptypes = platform.createDefaultLayers();
// Instantiate (and display) a map object:
var map = new H.Map(
document.getElementById('mapContainer'),
maptypes.normal.map,
{
zoom: 10,
center: { lng: $(component).data('long'),
lat: $(component).data('lat')
}
});
// Enable the event system on the map instance:
var mapEvents = new H.mapevents.MapEvents(map);
// Add event listeners:
map.addEventListener('tap', function(evt) {
// Log 'tap' and 'mouse' events:
console.log(evt.type, evt.currentPointer.type);
When adding this not in tab 1 the map is not showing. I tried and searched for several things but they only are for Google Maps. How can i get this working in the Ionic tabs?
Please try with following code snippet :
function moveMapToBerlin(map){
map.setCenter({lat:52.5159, lng:13.3777});
map.setZoom(14);
}
/**
* Boilerplate map initialization code starts below:
*/
//Step 1: initialize communication with the platform
// In your own code, replace variable window.apikey with your own apikey
var platform = new H.service.Platform({
apikey: window.apikey
});
var defaultLayers = platform.createDefaultLayers();
//Step 2: initialize a map - this map is centered over Europe
var map = new H.Map(document.getElementById('map'),
defaultLayers.vector.normal.map,{
center: {lat:50, lng:5},
zoom: 4,
pixelRatio: window.devicePixelRatio || 1
});
// add a resize listener to make sure that the map occupies the whole container
window.addEventListener('resize', () => map.getViewPort().resize());
//Step 3: make the map interactive
// MapEvents enables the event system
// Behavior implements default interactions for pan/zoom (also on mobile touch environments)
var behavior = new H.mapevents.Behavior(new H.mapevents.MapEvents(map));
// Create the default UI components
var ui = H.ui.UI.createDefault(map, defaultLayers);
// Now use the map as required...
window.onload = function () {
moveMapToBerlin(map);
}

MapboxGL: querying rendered features after multiple geocodes

Situation: I have a working site where upon entering an address, MapboxGL marks a point on the map and queries a polygon layer (queryRenderedFeatures) and displays the polygon feature containing the point.
This works; however, if I then want to geocode a second address that changes the map view, it fails the second time because map.queryRenderedFeatures returns an empty array.
var userDistrictsGeoJson;
map.on('load', function() {
//add layers from Mapbox account
addLayers(); //details left out of example, but this works.
// Listen for geocoding result
// This works the first time through, but fails if the user searchs for a second address because queryRenderedFeatures is working with a smaller set of features
geocoder.on('result', function(e) {
//need to clear geojson layer and
userDistrictsGeoJson = {
"type": "FeatureCollection",
"features": []
};
map.getSource('single-point').setData(e.result.geometry);
//project to use (pixel xy coordinates instead of lat/lon for WebGL)
var point = map.project([e.result.center[0], e.result.center[1]]);
var features = map.queryRenderedFeatures(point, { layers: ['congress-old'] });
var filter = featuresOld.reduce(function(memo, feature){
// console.log(feature.properties);
memo.push(feature.properties.GEOID);
return memo;
}, ['in', 'GEOID']);
map.setFilter('user-congress-old', filter);
var userCongressOldGeoJson = map.querySourceFeatures('congressional-districts', {
sourceLayer: 'congress_old',
filter: map.getFilter('user-congress-old')
});
userDistrictsGeoJson.features.push(userCongressOldGeoJson[0]);
var bbox = turf.bbox(userDistrictsGeoJson);
var bounds = [[bbox[0], bbox[1]], [bbox[2], bbox[3]]];
map.fitBounds(bounds, {
padding: 40
});
}); //geocoder result
}); //map load
So like I said, everything that runs on the geocodes 'result' event works the first time through, but it seems that on the second time through (user searches new address, but doesn't reload map) queryRenderedFeatures returns a smaller subset of features that doesn't include the tiles where the geocoder lands.
Any suggestions are much appreciated.
I ended up solving this by triggering the querying code once on 'moveend' event.
So now the syntax is:
geocoder.on('result', function(e){
map.once('moveend', function(){
.... rest of code
}
}
I thought I had tried this before posting the question, but seems to be working for me now.

Cannot set style on leaflet L.geoJSON layer

I am having a bit of trouble setting the style on a L.geoJSON based layer,my code looks like this:
var addProperties = function addProperties(prop,map)
{
//the API does not seem to support adding properties to an existing feature,
//the idea here is simple:
//(1) currentFeature.toGeoJson() needs to be called to obtain a json representation
//(2) set the properties on the geojson
//(3) create a new feature based on the geojson
//(4) remove res and add the new feature as res
var style = function style(feature){
var markerStyle = {
draggable: 'true',
icon: L.AwesomeMarkers.icon({
icon: 'link',
prefix: 'glyphicon',
markerColor: 'red',
spin: true
})
};
if(feature.geometry.type==='Point')
return markerStyle;
};
var onEachFeature = function onEachFeature(feature,layer){
console.log("Inside on each feature,checking to see if feature was passed to it ",feature);
layer.on('click',function(e){
//open display sidebar
console.log("Checking to see if setupTabs exists ",setupTabs);
setupTabs('#display-feature-tabs');
console.log("Checking to see if featureInfo exists ",featureInfo);
var featureInfoAPI =featureInfo('feature-properties');
featureInfoAPI.swap(feature.properties);
setTimeout(function() {
sidebar.show();
}, 100);
});
};
console.log("Inside add properties");
var geoJSON,feature;
if(res != null)
{
geoJSON = res.toGeoJSON();
geoJSON.properties = prop;
console.log(geoJSON);
feature = L.geoJson(geoJSON,{style:style,onEachFeature:onEachFeature});
console.log("The new feature that has been created ",feature);
removeFeature(map);
addFeature(feature);
feature.addTo(map);
}
};
I have also tried the style method as well,I am looking to add styles to the active layer for points(styles will also be added for lines and polylines by type).
To style point features, you should use pointToLayer option instead.