Trouble passing functions to other objects in Angular - javascript

I am having trouble passing functions to other objects in Angular. Specifically, I have created a function generateTile(coords) that populates a tile that will then be given to leaflet. This function is in a MapComponent method. I think I understand why this is an issue as this refers to a different context. However I don't know how to work around this issue.
generateTile(coords) {
...
return image;
}
private initMap(): void {
this.map = L.map('map', {
crs: L.CRS.Simple,
center: [0, 0],
zoom: 5
});
L.TileLayer.CustomMap = L.TileLayer.extend({
getTileUrl: function(coords) {
var img = this.generateTile(coords);
return img.src;
},
getAttribution: function() {
return "<a href='https://example.com'>Example</a>"
}
});
...
}

Use arrow function so you don't create a scope for this. You can read more about how this works in JavaScript, here is a link, for example.
getTileUrl: (coords) => {
var img = this.generateTile(coords);
return img.src;
};

Related

Leaflet: Including metadata with CircleMarkers

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>

not able to get angular 2 scope inside call back functions

I need to update a array object inside a call back function ,i used the following lines but the values are set in the scope of call back loop not as angular variable so my view is not updated.(deviceval) value is changed if i print it inside the callback but outside the value is still the old one.
export class DashboardComponent implements OnInit {
hideTable: boolean = true;
public deviceVal:any;
constructor(private ref: ChangeDetectorRef) {}
ngOnInit() {
this.deviceVal = deviceData;
console.log(this.deviceVal);
var container = $('.map-canvas');
var options = {
center: new google.maps.LatLng(41.676258, -99.683199),
zoom: 4,
mapTypeId: google.maps.MapTypeId.ROADMAP
};
gmap = new google.maps.Map(container[0], options);
this.drawChart(deviceData);
this.plotMarkers();
}
plotMarkers(){
$.each(deviceData, function(key, val) {
var controller=this;
var marker = new google.maps.Marker({
position: new google.maps.LatLng(parseInt(val.lat), parseInt(val.lon)),
map: gmap,
});
google.maps.event.addListener(marker, 'click', function() {
this.deviceVal = val;
});
markerCache.push(marker);
})
}
}
The problem is here:
$.each(deviceData, function(key, val) {
var controller=this;
var marker = new google.maps.Marker({
position: new google.maps.LatLng(parseInt(val.lat), parseInt(val.lon)),
map: gmap,
});
google.maps.event.addListener(marker, 'click', function() {
this.deviceVal = val;
});
markerCache.push(marker);
})
when you use function() as a callback function, the 'this' value is changed. You better read here about this.
You can fix this using arrow functions:
plotMarkers(){
$.each(deviceData, (key, val) => {
var marker = new google.maps.Marker({
position: new google.maps.LatLng(parseInt(val.lat), parseInt(val.lon)),
map: gmap,
});
google.maps.event.addListener(marker, 'click', () => {
this.deviceVal = val;
});
})
}
But you have a lot of other problems, like: you don't need to use jQuery (to be honest, you should avoid jQuery in an ng2 app), the 'gmap' variable is not defined (you can set it as an property of the class, as you have done with 'deviceVal' for example), 'markerCache' was not defined too, there is no drawChart method, 'deviceData' is not defined inside plotMarkers().
I solved it by declaring a global variable before export component like
var controller;
and initialized it in ngoninit(),
controller = this;
and passed the controller to addlistener,
google.maps.event.addListener(marker, 'click', () => {
controller.deviceVal=[];
controller.deviceVal.push(val);
//console.log(controller.deviceVal+"end....................................")
});

Value from inside for loop overwriting data outside loop

Everytime I run this loop, each marker in the markers array has it's icon overwritten by the results from let icon = iconLoader.getIcon(data[index][5]);.
leaving each marker having the last loaded icon, instead of the icon loading during each pass of the for loop.
I thought that passing icon into the closure would essentially pass it by value, preventing it from being overwritten outside the scope of the closure, but this doesn't seem to be working for me. What am I missing?
var markers = []
for (var index in data) {
let icon = iconLoader.getIcon(data[index][5]);
var createMarker = (function (i) {
return function () {
var marker = new L.marker([data[index][2], data[index][3]])
.setIcon(i)
markers.push(marker);
}
})(icon);
createMarker();
}
var iconLoader = (function () {
var icon = L.icon({
// options
});
return {
getIcon: function (iconName) {
// do stuff to get icon
return icon;
}
};
}());
JSFiddle
So, as I mentioned in my original comment, JavaScript objects and arrays are always passed by reference unless you explicitly create and pass a copy. Nothing in your code is inherently wrong and is not causing this issue - it is actually an issue with how leaflet is handling the object references internally. The way to avoid this is to do a deep copy on the result from iconLoader.getIcon(). If you are using jQuery, you can do it very simply by using $.extend().
for (var index in data) {
let icon = $.extend(true, {}, iconLoader.getIcon(data[index][2]));
var marker = new L.marker([data[index][0], data[index][1]])
.setIcon(icon);
markers.push(marker);
}
If not, you can look into non-jQuery solutions - it's not ideal, but they're everywhere.
I was typing this up as mhodges was writing his answer. I'll go ahead and post it as it's a different solution that also solved the issue for me.
After looking at mhodges' working demo, I noticed he had set up the iconloader a bit differently than I had. He had the iconloader as an object...
var iconLoader = {
getIcon: function (elem) {
return elem;
}
}
while mine was set up as a closure...
var iconLoader = (function () {
var icon = L.icon({
// options
});
return {
getIcon: function (iconName) {
// do stuff to get icon
return icon;
}
};
}());
I thought that maybe I could try setting it up a bit differently and see if that made a difference, and VOILA!
const proto = {
getIcon (iconName) {
var icon = L.icon({
// options
});
// do stuff to get icon
return icon;
}
};
function factoryIcon() {
return Object.create(proto);
}
and then grabbed the icon with
const iconFactory = factoryIcon();
let icon = iconFactory.getIcon(data[index][5]);

How can I get a leaflet.js instance using only a DOM object?

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!

Problems calling fitbounds outside jquery.each()

I'm working on a Google Map in JavaScript(v3).
I need to show some markers from XML, for which I use jQuery.
Here's the object and function, might save me time explaining:
var VX = {
map:null,
bounds:null
}
VX.placeMarkers = function(filename) {
$.get(filename, function(xml) {
$(xml).find("marker").each(function() {
var lat = $(this).find('lat').text();
var lng = $(this).find('lng').text();
var point = new google.maps.LatLng(parseFloat(lat),parseFloat(lng));
VX.bounds.extend(point);
VX.map.fitBounds(VX.bounds); //this works
var marker = new google.maps.Marker({
position: point,
map: VX.map,
zoom: 10,
center: point
});
});
});
//VX.map.fitBounds(VX.bounds); //this shows me the ocean east of Africa
}
So basically my problem is that I can't figure out how to do fitbounds from outside of the .each function, and doing it inside the function calls it for every marker which looks bad.
I declare the bounds when I initialize the map... haven't included the entire code because its like 300 lines.
Shouldn't I be able to use a value that I passed to a global object?
Edit: ah, I was calling it from outside of the get function!
The second call doesn't work because it is firing before the ajax get() returns.
Place the fitBounds inside the get() handler, but outside the each() function. Like so:
var VX = {
map:null,
bounds:null
}
VX.placeMarkers = function(filename)
{
$.get
(
filename,
function(xml)
{
$(xml).find("marker").each
(
function()
{
var lat = $(this).find('lat').text();
var lng = $(this).find('lng').text();
var point = new google.maps.LatLng(parseFloat(lat),parseFloat(lng));
VX.bounds.extend(point);
//VX.map.fitBounds(VX.bounds); //this works
var marker = new google.maps.Marker
({
position: point,
map: VX.map,
zoom: 10,
center: point
});
}
);
VX.map.fitBounds(VX.bounds); //-- This should work.
}
);
//VX.map.fitBounds(VX.bounds); //this shows me the ocean east of africa
}

Categories