I need to iterate over an AJAX response and break out of an event handler when a condition is met. I'm having trouble with this code:
$.each(response, function(i, v) {
// create mapbox object
var map = L.mapbox.map('map', v.map_embed_id, {
zoomAnimation: false
});
var polygonLayer = L.mapbox.featureLayer().loadURL('https://a.tiles.mapbox.com/v4/' + v.map_embed_id + '/features.json?access_token=abcde').addTo(map);
polygonLayer.on('ready', function() {
var layer = leafletPip.pointInLayer(latlng, polygonLayer, true);
if (layer.length) {
// this is where I need to break out of $.on
// and the current $.each iteration
}
});
});
I know return false would break out of the $.each iteration but this is more difficult since I need to break out of the $.on event handler. What can I do? Could I use a trigger maybe?
Thanks to #Kevin B's advice to use recursion, this is how I fixed my code to make it work.
getMapsList().done(function(maps) {
getMapboxMap(maps, geocode);
});
function getMapboxMap(maps, geocode) {
var map_params = maps[0];
var map_embed_id = map_params.map_embed_id;
if (maps.length > 0)
maps.shift();
// create mapbox object
var map = L.mapbox.map('map', map_embed_id, {
zoomAnimation: false
});
// create marker of address entered
L.mapbox.featureLayer({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [
geocode.location.lng,
geocode.location.lat
]
},
properties: {
title: address,
'marker-size': 'medium',
'marker-color': '#f44',
'marker-symbol': 'star'
}
}).addTo(map);
// create polygon layer and add to map from map's geojson
var polygonLayer = L.mapbox.featureLayer().loadURL('https://a.tiles.mapbox.com/v4/' + map_embed_id + '/features.json?access_token=pk.eyJ1IjoiZW5nbGVzaWRldGVycml0b3JpZXMiLCJhIjoiekFIU0NlayJ9.rE9XdicgXc9aIiXJ9yn68w').addTo(map);
// after polygon layer has been added to map
polygonLayer.on('ready', function() {
// featureLayer.getBounds() returns the corners of the furthest-out markers,
// and map.fitBounds() makes sure that the map contains these.
map.fitBounds(polygonLayer.getBounds());
// create a latLng object based on lat/lng of address entered
var latlng = L.latLng(geocode.location.lat, geocode.location.lng);
// create point in layer object
var layer = leafletPip.pointInLayer(latlng, polygonLayer, true);
if (layer.length) {
// found it
return false;
} else {
if (maps.length > 0) {
getMapboxMap(maps, geocode);
}
}
});
}
function getMapsList() {
return $.get('/utility/territories/maps-list');
}
Related
I have a Leaflet map that I am populating with CircleMarkers. I would like to include an additional value (a database ID) with each circle so that when I click on the circle, I can get the value and navigate somewhere else.
I would like to add the value directly to the marker and use a callback function on the entire featureGroup instead of adding a callback function to each marker, since we're dealing with over 500 markers and it would be a performance drag.
Worth mentioning: I'm using Typescript inside an Angular app, but it's still Leaflet.
What I've tried:
var data = [
{lat: 20.45, lng: -150.2, id: 44},
{lat: 23.45, lng: -151.7, id: 45},
]
var points = [];
data.forEach((d) => {
// How do I add an additional variable to this circleMarker?
points.push(circleMarker(latLng(d.lat, d.lng), { radius: 5}));
})
var group = featureGroup(points);
group.on("click", function (e) {
console.log(e);
// This is where I would like to get the ID number of the record
});
FWIW, you have plenty ways of adding your own data to Leaflet Layers (nothing specific to Circle Markers, it is the same for Markers, but also Polygons, Polylines, etc.).
See for instance: Leaflet/Leaflet #5629 (Attach business data to layers)
In short, there are mainly 3 possible ways:
Just directly add some properties to the Leaflet Layer after it has been instantiated. Make sure you avoid collision with library properties and methods. You can add your own prefix to the property name to decrease the chance of collision.
var marker = L.marker(latlng);
marker.myLibTitle = 'my title';
Use the Layer options (usually the 2nd argument of the instantiation factory), as shown by #nikoshr. As previously, avoid collision with library option names.
L.marker(latlng, {
myLibTitle: 'my title'
});
Use the Layer GeoJSON properties. Leaflet does not use those for internal purpose, so you have total freedom of this data, without any risk of collision.
L.Layer.include({
getProps: function () {
var feature = this.feature = this.feature || {}; // Initialize the feature, if missing.
feature.type = 'Feature';
feature.properties = feature.properties || {}; // Initialize the properties, if missing.
return feature.properties;
}
});
var marker = L.marker(latlng);
// set data
marker.getProps().myData = 'myValue';
// get data
myFeatureGroup.on('click', function (event) {
var source = event.sourceTarget;
console.log(source.getProps().myData);
});
Events fired on members of a FeatureGroup are propagated to the FeatureGroup object
Event objects expose a sourceTarget member giving you access to the source marker
Options in a layer can be accessed as marker.options
From there, you could pass your id as a member of the options object when building your markers and retrieve this value when a marker is clicked. For example:
var points = data.map((datum) => {
return L.circleMarker(datum, {radius: 5, id: datum.id});
});
var group = L.featureGroup(points);
group.addTo(map);
group.on("click", (e) => {
console.log(e.sourceTarget.options.id);
});
And a demo
var data = [
{lat: 20.45, lng: -150.2, id: 44},
{lat: 23.45, lng: -151.7, id: 45},
]
var points = [];
var map = L.map('map', {
center: [20.45, -150.2],
zoom: 4
});
var points = data.map(function (datum) {
return L.circleMarker(datum, {radius: 5, id: datum.id});
});
var group = L.featureGroup(points);
group.addTo(map);
group.on("click", function (e) {
console.log(e.sourceTarget.options.id);
});
html, body {
height: 100%;
margin: 0;
}
#map {
width: 100%;
height: 150px;
}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.3.1/leaflet.css"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.3.1/leaflet.js"></script>
<div id='map'></div>
I have a simple google map with multiple markers (4 in this case).
I want to add a "bubble" on click event. Markers are rendered fine, but the bubble (infowindow) show always on the last pin. I mean:
I click marker[1] - infowindow shows up on marker[3]
I click marker[[2] - infowindow shows up on marker[3]
etc.
I think that the problem is in the way I loop my array
Here is my loop, that iterates through 4 elements of array:
var key = 0;
var markers = new Array();
var infowindows = new Array();
for(key in myJson.hotels)
{
var newLatlng = new google.maps.LatLng(myJson.hotels[key].latitude,myJson.hotels[key].longitude);
markers[key] = new google.maps.Marker(
{
position: newLatlng,
map: map,
title: 'Hello World!'
});
// the code above works fine - it renders 4 pins o my map
infowindows[key] = new google.maps.InfoWindow(
{
content: contentString
});
google.maps.event.addListener(markers[key], 'click', function() {
//console.log(key); <-- this always return [3]
infowindows[key].open(map,markers[key]);
});
//console.log(key); <-- this always return the right key - 0,1,2,3
}
}
The function in addListener gets called asynchronously. When it gets called you dont know which value key has.
You can come arround this by storing the key in a closure.
google.maps.event.addListener(markers[key], 'click',
function (k) {
return function() { infowindows[k].open(map,markers[k]);
}(key)
});
I used a #phylax hint and I solved the problem this way:
I made a new function:
function addClickEventToMarker(aMap,aKey){
google.maps.event.addListener(markers[aKey], 'click', function() {
//console.log(key); <-- this always return [3]
infowindows[aKey].open(aMap,markers[aKey]);
});
}
and I call the function in my 'for' loop:
for(key in myJson.hotels)
{
var newLatlng = new google.maps.LatLng(myJson.hotels[key].latitude,myJson.hotels[key].longitude);
markers[key] = new google.maps.Marker(
{
position: newLatlng,
map: map,
title: 'Hello World!'
});
infowindows[key] = new google.maps.InfoWindow(
{
content: contentString
});
addClickEventToMarker(map, key);
}
}
I'm right now building a custom Knockout.js binding to handle drawing of polygons. In this case the Knockout API only gives me a reference to a DOM object to access whatever it is I need to update. However, it looks like by design leaflet.js wants the user to store the map instance in their implementation. I don't have that option.
Trying this gave me an error: var existingMap = L.map('aMapIDGoesHere')
And the error was: map already initialized.
Any way I can use a DOM element or element ID to access the map instance?
By request here's the custom binding, please note it's a work in progress:
ko.bindingHandlers.leafletDraw = {
init: function(element, valueAccessor, allBindingsAccessor) {
var map = L.map(element).setView([40, -90], 3);
var tiles = L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
attribution: 'OSM',
minZoom: 2
}).addTo(map);
// Initialise the FeatureGroup to store editable layers
var editableLayers = new L.FeatureGroup();
map.addLayer(editableLayers);
// Initialise the draw control and pass it the FeatureGroup of editable layers
var drawOptions = {
edit: {
featureGroup: editableLayers,
remove: false
},
draw: {
polyline: false,
circle: false,
marker: false,
polygon: {
allowIntersection: false,
showArea: true
}
}
}
var drawControl = new L.Control.Draw(drawOptions);
map.addControl(drawControl);
// when a shape is first created
map.on('draw:created', function (e) {
var shapeString = $.map(e.layer._latlngs, function(pair) { return pair.lng.toString()+"::"+pair.lat.toString(); }).join(";;;");
var value = valueAccessor();
if (ko.isObservable(value)) {
value(shapeString);
}
editableLayers.addLayer(e.layer);
drawControl.removeFrom(map);
drawOptions.draw.polygon = false;
drawOptions.draw.rectangle = false;
var editControl = new L.Control.Draw(drawOptions);
map.addControl(editControl);
});
// handle when a shape is edited
map.on('draw:edited', function (e) {
var editedLayer = e.layers._layers[Object.keys(e.layers._layers)[0]];
var shapeString = $.map(editedLayer._latlngs, function(pair) { return pair.lng.toString()+"::"+pair.lat.toString(); }).join(";;;");
var value = valueAccessor();
if (ko.isObservable(value)) {
value(shapeString);
}
});
},
update: function(element, valueAccessor) {
// need to figure this out since we can't access leaflet params from
}
};
Special Note You'll notice that I am converting points into a concatenated string. This is necessary for the time being.
As long as you are sure that the DOM element will not be removed, you could just add it as a subproperty on the DOM element itself. Here's a binding handler using the code on the leaflet front page for setting up the leaflet map:
ko.bindingHandlers.leaflet = {
init: function(element, valueAccessor){
var map = L.map(element);
element.myMapProperty = map;
L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
},
update: function(element, valueAccessor){
var existingMap = element.myMapProperty;
var value = ko.unwrap(valueAccessor());
var latitude = ko.unwrap(value.latitude);
var longitude = ko.unwrap(value.longitude);
var zoom = ko.unwrap(value.zoom);
existingMap.setView([latitude, longitude], zoom);
}
};
To use the binding handler you would just bind like the following:
<div data-bind="leaflet: { latitude: latitudeProperty, longitude: longitudeProperty, zoom: zoomProperty }"></div>
Just ensure that you have also styled the div to ensure it has a height and width. I have written a jsfiddle which uses the above leaflet bindingHandler where you could try it out.
I have only tested this jsfiddle in Internet Explorer 11, Firefox 26.0 and Firefox 27.0.1.
noting that in very limited circumstances, this could be a solution: https://stackoverflow.com/a/60836683/1116657
window[Object.keys(window).find(key => key.substr(0,3) === "map")];
Read my original post for comments on it's brittleness and limitations, but thought this could be helpful to someone. Thanks!
I have several markers (in an array) on my map, each with a custom ID tag i've given them.
What I want:
When I click on a marker, i wish to add it's ID to another array.
The problem:
The mouse event from Google does not have a target attribute, only the position, so I can't seem to access the ID directly.
I don't really want to have to resort to using the position to find the closest marker to it and returning it's ID this way, it's rather convoluted.
All help is appreciated
This is really easy, thanks to a feature in JavaScript and many other languages called a closure.
Simply put the code that creates the marker and sets up its event listener(s) insidea function, and call that function for each marker with the data needed for that specific marker. For example:
var places = [
{
id: 'one', lat: 1, lng: -1, name: 'First'
},
{
id: 'two', lat: 2, lng: -2, name: 'Second'
}
];
for( var i = 0; i < places.length; i++ ) {
addPlace( places[i] );
}
function addPlace( place ) {
var marker = new google.maps.Marker({
map: map,
position: new google.maps.LatLng( place.lat, place.lng ),
title: place.name
});
google.maps.event.addListener( 'click', function() {
alert( 'Clicked ' + place.id + ': ' + place.name );
});
}
I didn't test this Maps API code, but the specifics of the code are not important. What is important to understand is that place variable you see used in the code. This is the key part: that variable is accessible inside the event listener, simply because the event listener is nested inside the addPlace() function which has place as a parameter.
Note the difference between that code and code like this, which will not work:
for( var i = 0; i < places.length; i++ ) {
var place = places[i];
var marker = new google.maps.Marker({
map: map,
position: new google.maps.LatLng( place.lat, place.lng ),
title: place.name
});
google.maps.event.addListener( 'click', function() {
alert( 'Clicked ' + place.id + ': ' + place.name );
});
}
The only difference between the two is that the working version puts the loop body in a separate function which is called from the loop, instead of having all that code directly inside the loop. Having that code in a function that you call each time is what creates the closure, and that's what lets the inner event listener function "see" the variables in the outer function.
The great thing about closures is that you can use them in any similar situation. It isn't specific to the Maps API or the objects that the API uses. You may have even used them already and not realized it, for example in a setTimeout() call like this:
// Display an alert 'time' milliseconds after this function is called
function slowAlert( message, time ) {
setTimeout( function() {
alert( message );
}, time );
}
slowAlert( 'Howdy!', 1000 ); // Wait a second and then say Howdy!
Where the alert() call is made inside the setTimeout() callback function is made, it's using the closure on the slowAlert() function to pick up the value of the message variable that was passed into that function.
This should help. I added a customId property to the marker object and then in the marker click event I assign the id property to the new array.
function initialize() {
var map;
var centerPosition = new google.maps.LatLng(38.713107, -90.42984);
var options = {
zoom: 6,
center: centerPosition,
mapTypeId: google.maps.MapTypeId.ROADMAP
};
var bounds = new google.maps.LatLngBounds();
map = new google.maps.Map($('#map')[0], options);
var infoWindow = new google.maps.InfoWindow();
//marker array
var markers = [];
//sencondary array to store markers that were clicked on.
var markerIdArray = [];
for (i = 0; i < 6; i++) {
var lat = 38.713107 + Math.random();
var lng = -90.42984 + Math.random();
var marker = new google.maps.Marker({
map: map,
position: new google.maps.LatLng(lat, lng),
customId: i //add a custom id to the marker
});
bounds.extend(marker.position);
google.maps.event.addListener(marker, 'click', function () {
//add the id to the other array.
markerIdArray.push(this.customId);
//log the content of the array to the console.
console.log(markerIdArray);
});
markers.push(marker);
}
map.fitBounds(bounds);
}
Here is an example of this in action.
I have the following code, and having read this, i understand it wont work because the getJSON call is asynchronous. How do i need to change this so that the MarkerClusterer function gets triggered with a full set of markers? I've tried putting the MarkerClusterer function inside the getJSON call but with no luck...
var mcOptions = {gridSize: 50, maxZoom: 9};
var markers = [];
function parse_json(json) {
if (json.length > 0) {
for (i=0; i<json.length; i++) {
var report = json[i];
var latLng = new google.maps.LatLng(report.latitude, report.longitude);
markers[i] = new google.maps.Marker({
position: latLng,
title: report.name + ' ' + report.surf_size_ft_round,
url: "/place/"+report.slug
});
google.maps.event.addListener(markers[i], 'click', function() {
window.location.href = markers[i].url;
});
markers.push(markers[i]);
}
}
};
$.getJSON('<%= request.fullpath + ".json" %>', function(stream) {
if (stream.length > 0) {
parse_json(stream);
alert(markers[1].title); //sanity check - gives result
}
});
alert(markers[5].title); // sanity check - empty
var mc = new MarkerClusterer(map, markers, mcOptions);
Why not put this code snippet:
mc = new MarkerClusterer(map, markers, mcOptions);
inside the anonymous callback function in your $.getJSON? Just declare var mc; somewhere outside the $.getJSON scope to be able to have access to it elsewhere.
Alternatively, you can fire an event at the end of your parse_json function, listen to that event and then fire up another function that creates your MarkerClusterer object when the event has fired. Check this out: How to trigger event in JavaScript?
EDIT:
Upon inspecting your code a bit more, I can see that you set markers[i] to a new Marker instance and then push onto the markers array that same instance. You probably want to either set markers[i] to a new Marker instance or you want to create a var marker, setting it to a new Marker instance and then pushing on the markers array.
Maybe you need to put it inside the success function you give as an input to $.getJSON?
$.getJSON('<%= request.fullpath + ".json" %>', function(stream) {
if (stream.length > 0) {
parse_json(stream);
alert(markers[1].title); //sanity check - gives result
mc = new MarkerClusterer(map, markers, mcOptions);
}
});
alert(markers[5].title); // sanity check - empty