Find out if a leaflet control has already been added to the map - leaflet

I wrote a custom Leaflet control. It's some kind of legend that may be added for each layer. The control itself has a close button to remove it from the map (like a popup).
The control can be added by clicking a button.
My problem is that the user may add the same control to the map several times. So what I need is to test if this specific control has already been added to the map and, if so, don't add it again.
I create a control for each layer, passing some options
var control = L.control.customControl(mylayer);
and add it to my map on button click
control.addTo(map);
Now imagine the control has a close button and may be closed. Now if the user clicks the button again, I only want to add the control if it's not already on the map - something like this (hasControl is pseudocode, there is afaik no such function)
if(!(map.hasControl(control))) {
control.addTo(map);
}
For simplicity I made an example where I create a zoom control and add it twice here.

Easiest way is to check for the existence of the _map property on your control instance:
var customControl = new L.Control.Custom();
console.log(customControl._map); // undefined
map.addControl(customControl);
console.log(customControl._map); // returns map instance
But please keep in mind, when using the _map property, that the _ prefix of the property implies that it's a private property, which you are normally not supposed to use. It could be changed or removed in future versions of Leaflet. You're not going to encounter that if you use the follow approach:
Attaching a reference of your custom control to your L.Map instance:
L.Control.Custom = L.Control.extend({
options: {
position: 'bottomleft'
},
onAdd: function (map) {
// Add reference to map
map.customControl = this;
return L.DomUtil.create('div', 'my-custom-control');
},
onRemove: function (map) {
// Remove reference from map
delete map.customControl;
}
});
Now you can check for the reference on your map instance like so:
if (map.customControl) { ... }
Or create a method and include it in L.Map:
L.Map.include({
hasCustomControl: function () {
return (this.customControl) ? true : false;
}
});
That would work like this:
var customControl = new L.Control.Custom();
map.addControl(customControl);
map.hasCustomControl(); // returns true
map.removeControl(customControl);
map.hasCustomControl(); // returns false
Here's a demo of the concept on Plunker: http://plnkr.co/edit/nH8pZzkB1TzuTk1rnrF0?p=preview

Related

How to access control from the popup fragment by ID

I want my text area to be empty after I press OK button.
I have try this line this.byId("id").setValue("")
onWorkInProgress: function (oEvent) {
if (!this._oOnWorkInProgressDialog) {
this._oOnWorkInProgressDialog = sap.ui.xmlfragment("WIPworklist", "com.sap.FinalAssestments.view.WorkInProgress", this);
//this.byId("WIP").value = "";
//this.byId("WIP").setValue();
this.getView().addDependent(this._oOnWorkInProgressDialog);
}
var bindingPath = oEvent.getSource().getBindingContext().getPath();
this._oOnWorkInProgressDialog.bindElement(bindingPath);
this._oOnWorkInProgressDialog.open();
},
//function when cancel button inside the fragments is triggered
onCancelApproval: function() {
this._oOnWorkInProgressDialog.close();
},
//function when approval button inside the fragments is triggered
onWIPApproval: function() {
this._oOnWorkInProgressDialog.close();
var message = this.getView().getModel("i18n").getResourceBundle().getText("wipSuccess");
MessageToast.show(message);
},
The text area will be in popup in the fragment. I am expecting the text area to be empty.
If you instantiate your fragment like this:
sap.ui.xmlfragment("WIPworklist", "com.sap.FinalAssestments.view.WorkInProgress", this);
You can access its controls like this:
Fragment.byId("WIPworklist", "WIP").setValue(""); // Fragment required from "sap/ui/core/Fragment"
Source: How to Access Elements from XML Fragment by ID
The better approach would be to use a view model. The model should have a property textAreaValue or something like that.
Then bind that property to your TextArea (<TextArea value="{view>/textAreaValue}" />). If you change the value using code (e.g. this.getView().getModel("view").setProperty("/textAreaValue", "")), it will automatically show the new value in your popup.
And it works both ways: if a user changes the text, it will be automatically updated in the view model, so you can access the new value using this.getView().getModel("view").getProperty("/textAreaValue");.
You almost have it, I think. Just put the
this.byId("WIP").setValue("") line after the if() block. Since you are adding the fragment as a dependent of your view, this.byId("WIP") will find the control with id "WIP" every time you open the WIP fragment and set its value to blank.
You are likely not achieving it now because A. it is not yet a dependent of your view and B. it is only getting fired on the first go-around.

How to use the .openPopup method in a feature group?

I have a set of markers that have binded popups, but I can't figure out how to show all the popups when the marker group is toggled in the layers control.
For example, I have my markers like so:
var testMarker = L.marker([32.9076,33.35449]).bindPopup('Marker 1');
var testMarkerTwo = L.marker([33.58259,34.64539]).bindPopup('Marker 2');
Then, I put it in a freature group and append the openPopup method:
var markerGroup = L.featureGroup([testMarker,testMarkerTwo]).openPopup().addTo(map);
This doesn't work.
My final goal is to add that featureGroup to my layers control where I can toggle the group off/on. Before I get to that part, I need to first understand why the openPopup method is not working.
Edit: The answer below appears to only work with the plain Leaflet API, not the Mapbox API.
There are a couple problems here. The first is that by default, Leaflet closes the previously opened popup each time another is opened. The second is that, while your markers have popups bound to them, markerGroup does not, and even if it did, markerGroup.openPopup would only cause a single popup to open.
To get around the first problem, you can use the hack from this answer. If you place the following code at the beginning of your script (before you define your map) it will override the default behavior and allow you to open multiple popups at once:
L.Map = L.Map.extend({
openPopup: function(popup) {
this._popup = popup;
return this.addLayer(popup).fire('popupopen', {
popup: this._popup
});
}
});
Then, once you are able to open multiple popups, you can open all popups in markerGroup using the eachLayer method:
var markerGroup = L.featureGroup([testMarker,testMarkerTwo]).addTo(map);
markerGroup.eachLayer(function(layer) {
layer.openPopup();
});
Here is an example fiddle:
http://fiddle.jshell.net/nathansnider/02gsb1Lt/

Having trouble attaching event listener to a kml layer's polygon

Using Google Earth I have a loaded kml layer that displays polygons of every county in the US. On click a balloon pop's up with some relevant info about the state (name, which state, area, etc) When a user clicks the polygon I want the information to also pop up on a DIV element somewhere else.
This is my code so far.
var ge;
google.load("earth", "1");
function init() {
google.earth.createInstance('map3d', initCB, failureCB);
}
function initCB(instance) {
ge = instance;
ge.getWindow().setVisibility(true);
ge.getNavigationControl().setVisibility(ge.VISIBILITY_AUTO);
ge.getNavigationControl().setStreetViewEnabled(true);
ge.getLayerRoot().enableLayerById(ge.LAYER_ROADS, true);
//here is where im loading the kml file
google.earth.fetchKml(ge, href, function (kmlObject) {
if (kmlObject) {
// show it on Earth
ge.getFeatures().appendChild(kmlObject);
} else {
setTimeout(function () {
alert('Bad or null KML.');
}, 0);
}
});
function recordEvent(event) {
alert("click");
}
// Listen to the mousemove event on the globe.
google.earth.addEventListener(ge.getGlobe(), 'click', recordEvent);
}
function failureCB(errorCode) {}
google.setOnLoadCallback(init);
My problem is that when I change ge.getGlobe() to kmlObject or ge.getFeatures() it doesn't work.
My first question is what should I change ge.getGlobe() to to be able to get a click listener when a user clicks on a kml layer's polygon?
After that I was planning on using getDescription() or getBalloonHtml() to get the polygons balloons information. Am I even on the right track?
...what should I change ge.getGlobe() to...
You don't need to change the event object from GEGlobe. Indeed it is the best option as you can use it to capture all the events and then check the target object in the handler. This means you only have to set up a single event listener in the API.
The other option would be to somehow parse the KML and attach specific event handlers to specific objects. This means you have to create an event listener for each object.
Am I even on the right track?
So, yes you are on the right track. I would keep the generic GEGlobe event listener but extend your recordEvent method to check for the types of KML object you are interested in. You don't show your KML so it is hard to know how you have structured it (are your <Polygon>s nested in <Placemarks> or ` elements for example).
In the simple case if your Polygons are in Placemarks then you could just do the following. Essentially listening for clicks on all objects, then filtering for all Placmark's (either created via the API or loaded in via KML).
function recordEvent(event) {
var target = event.getTarget();
var type = target.getType();
if(type == "KmlPolygon") {
} else if(type == "KmlPlacemark") {
// get the data you want from the target.
var description = target.getDescription();
var balloon = target.getBalloonHtml();
} else if(type == "KmlLineString") {
//etc...
}
};
google.earth.addEventListener(ge.getGlobe(), 'click', recordEvent);
If you wanted to go for the other option you would iterate over the KML Dom once it has loaded and then add events to specific objects. You can do this using something like kmldomwalk.js. Although I wouldn't really recommend this approach here as you will create a large number of event listeners in the api (one for each Placemark in this case). The up side is that the events are attached to each specific object from the kml file, so if you have other Plaemarks, etc, that shouldn't have the same 'click' behaviour then it can be useful.
function placeMarkClick(event) {
var target = event.getTarget();
// get the data you want from the target.
var description = target.getDescription();
var balloon = target.getBalloonHtml();
}
google.earth.fetchKml(ge, href, function (kml) {
if (kml) {
parseKml(kml);
} else {
setTimeout(function () {
alert('Bad or null KML.');
}, 0);
}
});
function parseKml(kml) {
ge.getFeatures().appendChild(kml);
walkKmlDom(kml, function () {
var type = this.getType();
if (type == 'KmlPlacemark') {
// add event listener to `this`
google.earth.addEventListener(this, 'click', placeMarkClick);
}
});
};
Long time since i have worked with this.. but i can try to help you or to give you some tracks...
About your question on "google.earth.addEventListener(ge.getGlobe(), 'click', recordEvent);"
ge.getGlobe can not be replaced with ge.getFeatures() : if you look in the documentation ( https://developers.google.com/earth/documentation/reference/interface_g_e_feature_container-members) for GEFeatureContainer ( which is the output type of getFeatures() , the click Event is not defined!
ge.getGlobe replaced with kmlObject: waht is kmlObject here??
About using getDescription, can you have a look on the getTarget, getCurrentTarget ...
(https://developers.google.com/earth/documentation/reference/interface_kml_event)
As I told you, i haven't work with this since a long time.. so I'm not sure this can help you but at least, it's a first track on which you can look!
Please keep me informed! :-)

What is the proper way in OpenLayers (OSM) to trigger a popup for a feature?

I have the feature ID, I can grab the marker layer on GeoRSS loadend, but I'm still not sure how to cause the popup to appear programmatically.
I'll create the popup on demand if that's necessary, but it seems as though I should be able to get the id of the marker as drawn on the map and call some event on that. I've tried using jQuery and calling the $(marker-id).click() event on the map elements, but that doesn't seem to be working. What am I missing?
Since I was asked for code, and since I presumed it to be boilerplate, here's where I am so far:
map = new OpenLayers.Map('myMap');
map.addLayer(new OpenLayers.Layer.OSM());
map.addLayer(new OpenLayers.Layer.GeoRSS(name,url));
//I've done some stuff as well in re: projections and centering and
//setting extents, but those really don't pertain to this question.
Elsewhere I've done a bit of jQuery templating and built me a nice list of all the points that are being shown on the map. I know how to do a callback from the layer loadend and get the layer object, I know how to retrieve my layer out of the map manually, I know how to iter over the layers collection and find my layer. So I can grab any of those details about the popup, but I still don't know how to go about using the built-in methods of the DOM or of this API to make it as easy as element.click() which is what I would prefer to do.
You don't have to click the feature to open a popup.
First you need a reference to the feature from the feature id. I would do that in the loadend event of the GeoRSS layer, using the markers property on the layer.
Assuming you have a reference to your feature, I would write a method which handles the automatic popup:
var popups = {}; // to be able to handle them later
function addPopup(feature) {
var text = getHtmlContent(feature); // handle the content in a separate function.
var popupId = evt.xy.x + "," + evt.xy.y;
var popup = popups[popupId];
if (!popup || !popup.map) {
popup = new OpenLayers.Popup.Anchored(
popupId,
feature.lonlat,
null,
" ",
null,
true,
function(evt) {
delete popups[this.id];
this.hide();
OpenLayers.Event.stop(evt);
}
);
popup.autoSize = true;
popup.useInlineStyles = false;
popups[popupId] = popup;
feature.layer.map.addPopup(popup, true);
}
popup.setContentHTML(popup.contentHTML + text);
popup.show();
}
fwiw I finally came back to this and did something entirely different, but his answer was a good one.
//I have a list of boxes that contain the information on the map (think google maps)
$('.paginatedItem').live('mouseenter', onFeatureSelected).live('mouseleave',onFeatureUnselected);
function onFeatureSelected(event) {
// I stuff the lookup attribute (I'm lazy) into a global
// a global, because there can be only one
hoveredItem = $(this).attr('lookup');
/* Do something here to indicate the onhover */
// find the layer pagination id
var feature = findFeatureById(hoveredItem);
if (feature) {
// use the pagination id to find the event, and then trigger the click for that event to show the popup
// also, pass a null event, since we don't necessarily have one.
feature.marker.events.listeners.click[0].func.call(feature, event)
}
}
function onFeatureUnselected(event) {
/* Do something here to indicate the onhover */
// find the layer pagination id
var feature = findFeatureById(hoveredItem);
if (feature) {
// use the pagination id to find the event, and then trigger the click for that event to show the popup
// also, pass a null event, since we don't necessarily have one.
feature.marker.events.listeners.click[0].func.call(feature, event)
}
/* Do something here to stop the indication of the onhover */
hoveredItem = null;
}
function findFeatureById(featureId) {
for (var key in map.layers) {
var layer = map.layers[key];
if (layer.hasOwnProperty('features')) {
for (var key1 in layer.features) {
var feature = layer.features[key1];
if (feature.hasOwnProperty('id') && feature.id == featureId) {
return feature;
}
}
}
}
return null;
}
also note that I keep map as a global so I don't have to reacquire it everytime I want to use it

duplicate layer/ show permanantly on click

I'm starting to learn how to use leaflet. I'm trying to create a map with markers. If you hover them they should display a route. if the mouse leaves the marker the route should be deleted. (This part works)
When you click on the marker the route should stay on the map even when the mouse leaves the marker.
Therefore I would need to duplicate the route layer so that it doesn't get deleted when the mouse leaves the marker. Or there is a better method that I don't know.
function Route() {
DirectionsLayerLong = omnivore.gpx('GPX/ Route_long.gpx');
DirectionsLayerLong.on('ready', function() {
this.setStyle(style_long);
});
DirectionsLayerShort = omnivore.gpx('GPX/Route_short.gpx');
DirectionsLayerShort.on('ready', function() {
this.setStyle(style_short);
});
return DirectionsLayer = L.featureGroup([DirectionsLayerLong, DirectionsLayerShort]);
};
var Marker = L.marker([50, -100], {
icon: iconfu
}).addTo(map);
Marker.on('mouseover', function(e) {
Route();
DirectionsLayer.addTo(map);
});
Marker.on('mouseout', function(e) {
DirectionsLayer.remove()
});
Marker.on('click', function(e) {
DirectionsPermaLayer.remove();
Route();
DirectionsPermaLayer = DirectionsLayer;
DirectionsPermaLayer.addTo(map);
});
I could simply use omnivore with another variable but I'd like to reuse the function.
The simplest solution is just to remove the mouseout event listener when you click on the marker:
Marker.on('click', function(e) {
Marker.off('mouseout');
});
Cloning your route layer would be a little more complicated (not to mention unnecessary, if removing the event listener solves your problem), but it's worth exploring how one might do that. First of all, a concise explanation of why you can't just create a copy using DirectionsPermaLayer = DirectionsLayer can be found on w3schools:
Objects are mutable: They are addressed by reference, not by value.
If y is an object, the following statement will not create a copy of
y:
var x = y; // This will not create a copy of y.
The object x is not a copy of y. It is y. Both x and y points to the
same object.
Any changes to y will also change x, because x and y are the same
object.
There are many ways to go about cloning an object in Javascript, but I suspect that most of these will not work for cloning leaflet layers, as all Leaflet's internal ids will be copied along with the object, causing unpredictable behavior. The best strategy would probably be to convert DirectionsLayerShort and DirectionsLayerLong to GeoJSON using the .toGeoJSON method, then read them back in using L.geoJson:
var Short2 = L.geoJson(DirectionsLayerShort.toGeoJSON()).setStyle(style_short);
var Long2 = L.geoJson(DirectionsLayerLong.toGeoJSON()).setStyle(style_long);
var Directions2 = L.featureGroup([Long2, Short2]).addTo(map);
This could require a little refactoring of your code, but it should do the job.