AngularJS Leaflet memory leak - angularjs

There is a clear memory leak in my Angular app wherever I use the following leaflet directive: https://github.com/tombatossals/angular-leaflet-directive.
Note that the directive works fine, however the memory footprint continues growing as I leave and return to any view using the directive.
The directive builds off of the leaflet javascript library found here: https://github.com/Leaflet/Leaflet
I use the directive as follows:
<div ng-controller="Explore">
<div leaflet defaults="defaults" center="center" markers="markers" layers="layers"></div>
</div>
Inside my controller I extend the leaflet directive attributes to the scope:
angular.extend($scope, {
defaults: {
dragging: true,
doubleClickZoom: false,
scrollWheelZoom: false,
maxZoom: 12,
minZoom: 12
},
center: {
lat: $scope.cities[$scope.market.city][1],
lng: $scope.cities[$scope.market.city][0],
zoom: 12
},
markers: {},
layers: {
baselayers: {
google: {
name: 'Google Streets',
layerType: 'ROADMAP',
type: 'google'
}
}
}
});
I am not sure what is causing the memory leak, but I believe it may have to do with event listeners that are not removed when $destroy is called within the leaflet directive:
scope.$on('$destroy', function () {
leafletData.unresolveMap(attrs.id);
});
On destroy the function unresolveMap is called:
this.unresolveMap = function (scopeId) {
var id = leafletHelpers.obtainEffectiveMapId(maps, scopeId);
maps[id] = undefined;
};
This is as far as I got. If anyone has come across anything similar or has any ideas as to how to attack this issue further I would greatly appreciate it :)

You should try to remove completly the map by adding a map.remove() in the on $destroy handler (From leaflet API it should: "Destroys the map and clears all related event listeners").
scope.$on('$destroy', function () {
leafletData.unresolveMap(attrs.id);
map.remove();
});

Have you tried assigning an id attribute to your map? That's what the attrs.id refers to.
<leaflet id="myMap" defaults="defaults" center="center" markers="markers" layers="layers"></leaflet>

Related

Leaflet issues angular woes in showing and hiding? Want to get rid of $timeout

I have a leaflet being created with L.map('mapelement') being called. The issue is that if I click a button that "hides" the leaflet map, then click the button again to show, the leaflet map does not show up. However, when I put in a setTimeout within the link function before the map gets created and set it to 2 seconds, then the map shows every time (though I have to wait 2 seconds). Is there a better alternative to using $timeout in my custom "leaflet-map" directive to show and hide?
I created a naive example of a leaflet-map directive without seeing any of your code and am toggling the display of the map through ng-show. It works without any $timeout. It's hard to diagnose where your problems are stemming from without seeing any code or knowing how you are trying to toggle the map's display.
angular.module('demo', [])
.directive('leafletMap', function() {
return {
restrict: 'E',
scope: {
mapOptions: '&'
},
template: '<div><button ng-click="toggleShow()">Toggle Map</button><div class="demo-map" ng-show="isShown"></div></div>',
link: function(scope, elem, attrs) {
// Find element to bind to map
var mapElem = elem.children().find('div')[0],
// get map options from isolate scope
mapOptions = scope.mapOptions();
// State of hide/show
scope.isShown = true;
// Create Map.
var map = L.map(mapElem, mapOptions);
// Just taken from leaflet example
L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpandmbXliNDBjZWd2M2x6bDk3c2ZtOTkifQ._QA7i5Mpkd_m30IGElHziw', {
maxZoom: 18,
attribution: 'Map data © OpenStreetMap contributors, ' +
'CC-BY-SA, ' +
'Imagery © Mapbox',
id: 'mapbox.streets'
}).addTo(map);
// method to toggle the shown/hidden state of map
scope.toggleShow = function() {
scope.isShown = !scope.isShown;
};
// cleanup on element destroy
elem.on('$destroy', function() {
map.remove();
});
}
};
})
.controller('DemoController', function() {
this.mapOptions = {
center: [51.505, -0.09],
zoom: 13
};
});
.demo-map {
height: 500px;
}
<script src="//cdn.leafletjs.com/leaflet/v0.7.7/leaflet.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<link rel="stylesheet" href="//cdn.leafletjs.com/leaflet/v0.7.7/leaflet.css" />
<div ng-app="demo" ng-controller="DemoController as ctrl">
<leaflet-map map-options="ctrl.mapOptions"></leaflet-map>
</div>
Would CSS help you ?
Create one map in a visible div
visibility: visible
Create the second map in a hidden div
visibility: hidden
Position both your div in the same position
position: absolute
When you want to toggle just change the visibility of your divs
Example: http://plnkr.co/edit/voTjyMLKTxUC183nf8ML?p=preview
(Sorry it's not angular)

Function throws an error if not run from inside a $timeout

I have this code in my controller
$scope.initMap = function() {
var mapOptions = {
center: new google.maps.LatLng(-34.397, 150.644),
zoom: 8
};
var map = new google.maps.Map(document.getElementById("map-canvas"), mapOptions);
}
$timeout(function(){$scope.initMap();}, 1); //This works
//$scope.initMap(); //Doing it like this causes the crash
Using the $timeout line the map is initialized fine, but using the bottom line Angular throws an error a is null, from somewhere inside the minified library.
I'm including the Google Maps js file just above my controller in the HTML
<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?key=MyKey& sensor=false"></script>
<div class="row" ng-controller="VenuesCtrl">
I assume it's something to do with the $timeout function rather than needed to wait for 1 millisecond, but I can't figure it out. Do I need the $timeout or can I make my code work without it?
It happens because Google tries to use id DOM element before map-canvas element is rendered. I would create directive and call Google Map constructor from there
I think even with $timeout 0 it works too.
The function wrapped by $timeout executes after every other piece of code following the call to $timeout is executed.
Why it works?
With timeout you just level down execution priority. Its a trick to tell the browser to run it ASAP when other stuff done and browser is not busy.
You can google enough examples and demos how to write Google Map constructor with directive. I use my own.
Here is a relevant code:
HTML
<map-directive id="map_canvas" >
</map-directive>
directive
app.directive('mapDirective', ['$log', function($log) {
return {
restrict: 'E',
replace: true,
scope: {
/* ... */
},
template: '<div></div>',
link: function(scope, element, attrs) {
var map = false;
//...
initialize(document.getElementById(attrs.id));
function initialize(map_id) {
log.i("initialize Google maps");
var mapZoomLevel = locStorage.get('mapZoomLevel', 12);
var mapLast_lat = locStorage.get('mapLast_lat', 34.060122481016855);
var mapLast_longt = locStorage.get('mapLast_longt', -118.26350324225484);
var options = {
zoom: mapZoomLevel,
center: new google.maps.LatLng(mapLast_lat, mapLast_longt),
mapTypeId: google.maps.MapTypeId.ROADMAP,
panControl: false,
scrollwheel: true,
draggable: true,
rotateControl: false,
mapTypeControl: true,
scaleControl: true,
streetViewControl: true,
zoomControl: true,
disableDoubleClickZoom: false
};
map = new google.maps.Map(document.getElementById(attrs.id), options);
//...
}

angular-google-maps getting GEO location from mouse click

I am using the library angular-google-maps from http://angular-google-maps.org/. I am able to get most everything to work. What I am currently banging my head against is to get the "click" event to work on the map to give me the GEO location of where I clicked. Currently it's not even registering that I am clicking on the map.
Here is my partial:
<google-map
center="map.center"
zoom="map.zoom"
draggable='true'
>
</google-map>
and here is the controller:
// Create Map
$scope.map = {
center: {
latitude: 40.296755,
longitude: -111.696415
},
zoom: 13,
events: {
click: function (mapModel, eventName, originalEventArgs) {
alert("hola?");
}
}
};
Any help would be fantastic!
To add the event to the map, you need to attach an associative array (object) to the events attribute of the directive:
<google-map events="$scope.events" .... ></google-map>
In Controller:
$scope.events = {"click" : function () { console.log('woo-hoo') })

Issue with included JavaScript when running Testacular unit test on Angular.js directive

I'm trying to write a unit test for a directive that I use to insert a Leaflet.js map.
This tag
<div ng-controller="WorldMapCtrl">
<sap id="map"></sap>
</div>
Is used with this controller
function WorldMapCtrl($scope) {}
And the following directive
angular.module('MyApp').
directive('sap', function() {
return {
restrict: 'E',
replace: true,
template: '<div></div>',
link: function(scope, element, attrs) {
var map = L.map(attrs.id, {
center: [40, -86],
zoom: 2
});
//create a CloudMade tile layer and add it to the map
L.tileLayer('http://{s}.tile.cloudmade.com/57cbb6ca8cac418dbb1a402586df4528/997/256/{z}/{x}/{y}.png', {
maxZoom: 4, minZoom: 2
}).addTo(map);
}
};
});
This correctly inserts the map into the page. When I run the following unit test, it crashes out with
TypeError: Cannot read property '_leaflet' of null
describe('directives', function() {
beforeEach(module('MyApp'));
// Testing Leaflet map directive
describe('Testing Leaflet map directive', function() {
it('should create the leaflet dir for proper injection into the page', function() {
inject(function($compile, $rootScope) {
var element = $compile('<sap id="map"></sap>')($rootScope);
expect(element.className).toBe('leaflet-container leaflet-fade-anim');
})
});
});
});
From what I can tell from some Googling, the error seems to be fairly common. Perhaps not putting the tag in properly (With an id="map") or something similar can cause the problem. However, I'm not seeing what I can change with the test for this directive. Any ideas what I'm doing wrong here?
I've also included the necessary JS files in testacular.conf.js in the same order that they're included in index.html. So, it's not an issue of the files not being there (I assume at least).
One result from Googling (sadly, it doesn't help): https://groups.google.com/forum/#!msg/leaflet-js/2QH7aw8uUZQ/cWD3sxu8nXsJ

angularjs with Leafletjs

Following directie code is from http://jsfiddle.net/M6RPn/26/
I want to get a json feed that has many lat and long.. I can get a json with $resource or $http in Angular easily but How can I feed it to this directive to map thing on the map?
module.directive('sap', function() {
return {
restrict: 'E',
replace: true,
template: '<div></div>',
link: function(scope, element, attrs) {
var map = L.map(attrs.id, {
center: [40, -86],
zoom: 10
});
//create a CloudMade tile layer and add it to the map
L.tileLayer('http://{s}.tile.cloudmade.com/57cbb6ca8cac418dbb1a402586df4528/997/256/{z}/{x}/{y}.png', {
maxZoom: 18
}).addTo(map);
//add markers dynamically
var points = [{lat: 40, lng: -86},{lat: 40.1, lng: -86.2}];
for (var p in points) {
L.marker([points[p].lat, points[p].lng]).addTo(map);
}
}
};
});
I don't know a lot about Leaflet or what you're trying to do, but I'd assume you want to pass some coordinates in from your controller to your directive?
There are actually a lot of ways to do that... the best of which involve leveraging scope.
Here's one way to pass data from your controller to your directive:
module.directive('sap', function() {
return {
restrict: 'E',
replace: true,
template: '<div></div>',
link: function(scope, element, attrs) {
var map = L.map(attrs.id, {
center: [40, -86],
zoom: 10
});
//create a CloudMade tile layer and add it to the map
L.tileLayer('http://{s}.tile.cloudmade.com/57cbb6ca8cac418dbb1a402586df4528/997/256/{z}/{x}/{y}.png', {
maxZoom: 18
}).addTo(map);
//add markers dynamically
var points = [{lat: 40, lng: -86},{lat: 40.1, lng: -86.2}];
updatePoints(points);
function updatePoints(pts) {
for (var p in pts) {
L.marker([pts[p].lat, pts[p].lng]).addTo(map);
}
}
//add a watch on the scope to update your points.
// whatever scope property that is passed into
// the poinsource="" attribute will now update the points
scope.$watch(attr.pointsource, function(value) {
updatePoints(value);
});
}
};
});
Here's the markup. In here you're adding that pointsource attribute the link function is looking for to set up the $watch.
<div ng-app="leafletMap">
<div ng-controller="MapCtrl">
<sap id="map" pointsource="pointsFromController"></sap>
</div>
</div>
Then in your controller you have a property you can just update.
function MapCtrl($scope, $http) {
//here's the property you can just update.
$scope.pointsFromController = [{lat: 40, lng: -86},{lat: 40.1, lng: -86.2}];
//here's some contrived controller method to demo updating the property.
$scope.getPointsFromSomewhere = function() {
$http.get('/Get/Points/From/Somewhere').success(function(somepoints) {
$scope.pointsFromController = somepoints;
});
}
}
I recently built an app using Angular JS and Leaflet. Very similar to what you've described, including location data from a JSON file. My solution is similar to blesh.
Here's the basic process.
I have a <map> element on one of my pages. I then have a Directive to replace the <map> element with the Leaflet map. My setup is slightly different because I load the JSON data in a Factory, but I've adapted it for your use case (apologies if there are errors). Within the Directive, load your JSON file, then loop through each of your locations (you'll need to setup your JSON file in a compatible way). Then display a marker at each lat/lng.
HTML
<map id="map" style="width:100%; height:100%; position:absolute;"></map>
Directive
app.directive('map', function() {
return {
restrict: 'E',
replace: true,
template: '<div></div>',
link: function(scope, element, attrs) {
var popup = L.popup();
var southWest = new L.LatLng(40.60092,-74.173508);
var northEast = new L.LatLng(40.874843,-73.825035);
var bounds = new L.LatLngBounds(southWest, northEast);
L.Icon.Default.imagePath = './img';
var map = L.map('map', {
center: new L.LatLng(40.73547,-73.987856),
zoom: 12,
maxBounds: bounds,
maxZoom: 18,
minZoom: 12
});
// create the tile layer with correct attribution
var tilesURL='http://tile.stamen.com/terrain/{z}/{x}/{y}.png';
var tilesAttrib='Map tiles by Stamen Design, under CC BY 3.0. Data by OpenStreetMap, under CC BY SA.';
var tiles = new L.TileLayer(tilesURL, {
attribution: tilesAttrib,
opacity: 0.7,
detectRetina: true,
unloadInvisibleTiles: true,
updateWhenIdle: true,
reuseTiles: true
});
tiles.addTo(map);
// Read in the Location/Events file
$http.get('locations.json').success(function(data) {
// Loop through the 'locations' and place markers on the map
angular.forEach(data.locations, function(location, key){
var marker = L.marker([location.latitude, location.longitude]).addTo(map);
});
});
}
};
Sample JSON File
{"locations": [
{
"latitude":40.740234,
"longitude":-73.995715
},
{
"latitude":40.74277,
"longitude":-73.986654
},
{
"latitude":40.724592,
"longitude":-73.999679
}
]}
Directives and mvc in angularJs are different technologies. Directives are usually executed when the page loads. Directives are more for working on/with html and xml. Once you have JSON, then its best to use the mvc framework to do work.
After the page has rendered, to apply directives you often need to do $scope.$apply() or $compile to register the change on the page.
Either way, the best way to get a service into a directive is by using the dependency injection framework.
I noticed the scope:true, or scope:{} was missing from your directive. This has a big impact on how well the directive plays with parent controllers.
app.directive('mapThingy',['mapSvc',function(mapSvc){
//directive code here.
}]);
app.service('mapSvc',['$http',function($http){
//svc work here.
}])
Directives are applied by camelCase matching. I would avoid using or because of an issue with IE. Alternative would be
<div map-thingy=""></div>
Assuming that in your controller you got
$scope.points = // here goes your retrieved data from json
and your directive template is:
<sap id="nice-map" points="points"/>
then inside your directive definition you can use the "=" simbol to setup a bi-directional binding between your directive scope and your parent scope
module.directive('sap', function() {
return {
restrict: 'E',
replace: true,
scope:{
points:"=points"
},
link: function(scope, element, attrs) {
var map = L.map(attrs.id, {
center: [40, -86],
zoom: 10
});
L.tileLayer('http://{s}.tile.cloudmade.com/57cbb6ca8cac418dbb1a402586df4528/997/256/{z}/{x}/{y}.png', {
maxZoom: 18
}).addTo(map);
for (var p in points) {
L.marker([p.lat, p.lng]).addTo(map);
}
}
};
});
Also instead of adding the markers right into the map, it's recomended to add your markers to a L.featureGroup first, and then add that L.featureGroup to the map because it has a clearLayers() metho, which will save you some headaches when updating your markers.
grupo = L.featureGroup();
grupo.addTo(map);
for (var p in points) {
L.marker([p.lat, p.lng]).addTo(grupo);
}
// remove all markers
grupo.clearLayers();
I hope this helps, cheers

Resources