I'm working on an angularjs single-page application, and I'm trying to build a mapping system for the application. The map is loading fine, however whenever I attempt to use the geocode functionality, I get the error referenceError: Google is not defined.
Map controller
(function () {
'use strict';
angular
.module('CityWits')
.controller('mapCtrl', mapCtrl);
mapCtrl.$inject = ['$scope', '$http', 'mapApi', '$q'];
function mapCtrl($scope, $http, mapApi, $q){
var vm = this;
vm.setQuery = setQuery;
// todo: Switch this out with deals that are loaded depending on the radius of the map
getBranches();
function setQuery(query) {
console.log("business deal filter controller : query=" + query);
vm.query = query;
vm.focus = false;
}
function getBranches(){
$http.get('app/cwitsTestData/branchData.json').then(function(data){
vm.branches = sortBranches(data.data.branches);
$scope.$broadcast("branchesSorted", vm.branches);
});
}
}
function sortBranches(branches){
var locations, address, text;
locations = [];
for(var branch in branches){
address = branches[branch].address;
text = address.street_line1 + " " + address.city+ " " +address.state;
locations.push(text);
}
return locations;
}
})();
Here's the google factory I wrote to handle the api:
(function() {
'use strict';
angular
.module('CityWits')
.factory('mapApi', mapApi);
function mapApi () {
var mapApi = {}
var markers = [];
var geocoder;
var service;
mapApi.geocode = geocode;
mapApi.marker = marker;
mapApi.distance = distance;
return mapApi;
function geocode (addresses){
geocoder = new google.maps.Geocoder();
var coords = [];
if(geocoder){
for(var i in addresses){
geocoder.geocode( { 'address': addresses[i]}, function(results, status) {
if (status === 'OK') {
coords.push(results[0].geometry.location);
} else {
alert('Geocode was not successful for the following reason: ' + status);
}
});
}
}
}
function distance(start, end, method="DRIVING"){
service = new google.maps.DistanceMatrixService;
service.getDistanceMatrix({
origins: start,
destinations: end,
travelMode: method
}, function (status, response){
if(status ==! "OK"){
console.log("Error: "+status);
} else {
console.log("distance measured");
var result = {};
for(var i in response.rows){
result = response.rows[i].element;
}
return result;
}
});
}
function marker(positions, json){
if(markers.length > 0){
for(o in markers){
markers[o].setMap(null);
}
}
for(x in positions){
}
}
}
})();
And lastly this is the directive that initiates the api:
(function () {
'use strict';
angular
.module('CityWits')
.directive('dealMap', dealMap);
dealMap.$inject = ['$timeout', '$http', 'mapApi'];
function dealMap($timeout, $http, mapApi){
var directive = {
link: link,
templateUrl: 'app/map/map.directive.html',
scope: {
deals: '=',
branches: '='
},
restrict: 'EA'
};
return directive;
function link(scope, element, attrs) {
var script = document.createElement('script');
script.type = 'text/javascript';
script.async = true;
script.defer = true;
script.src = "https://maps.googleapis.com/maps/api/js?key=AIzaSyB4RaOArTNm9C7crfutMVc0KkWIoQG-ZE0";
document.body.appendChild(script);
$timeout(function(){
scope.initialize();
}, 500);
// todo: Do stuff after deals are loaded based on map radius
scope.$on('branchesSorted', function(event, data) {
console.log('deals loaded');
console.log(data);
var points = mapApi.geocode(data);
console.log(points);
});
scope.initialize = function() {
scope.mapOptions = {
zoom: 8,
center: new google.maps.LatLng(22.649907498685803, 88.36255413913727)
};
scope.map = new google.maps.Map(document.getElementById('map'), scope.mapOptions);
};
console.log(scope);
}
}
})();
Apparently this error occurs since the Google Maps API is not yet loaded.
The moment when the data is getting loaded:
$http.get('app/cwitsTestData/branchData.json').then(function(data){
vm.branches = sortBranches(data.data.branches);
$scope.$broadcast("branchesSorted", vm.branches);
});
and afterwards once Geocoder is utilized, there is no any guarantee that Google Maps API is already loaded at that moment:
scope.$on('branchesSorted', function(event, data) {
console.log('deals loaded');
console.log(data);
var points = mapApi.geocode(data); //<--Google Maps API could be still not loaded at that moment
console.log(points);
});
since Google Maps library is getting loaded asynchronously in your example like this:
var script = document.createElement('script');
script.type = 'text/javascript';
script.async = true;
script.defer = true;
script.src = "https://maps.googleapis.com/maps/api/js?key=AIzaSyB4RaOArTNm9C7crfutMVc0KkWIoQG-ZE0";
document.body.appendChild(script);
I would propose the following solution instead.
Let's introduce the following service to load Google Maps API, create the map and notify once it is ready:
.factory('googleMapsApi', function ($rootScope,$window, $q) {
return {
load: function (key) {
var deferred = $q.defer()
if ($window.google && $window.google.maps) {
deferred.resolve($window.google);
}
else {
var url = 'https://maps.googleapis.com/maps/api/js?callback=googleMapsLoad';
if (key) url += "&key=" + key;
var script = document.createElement('script');
script.type = 'text/javascript'
script.src = url;
$window.googleMapsLoad = function () {
deferred.resolve($window.google);
}
document.body.appendChild(script);
}
return deferred.promise;
},
createMap : function(scope,id,options){
var mapObject = new google.maps.Map(document.getElementById(id), options);
scope.$emit('google-maps-loaded',mapObject);
},
onMapReady : function(scope, ready){
var handler = $rootScope.$on('google-maps-loaded', function(evnt,data){ return ready(data);});
scope.$on('$destroy', handler);
}
}
})
Then the map could be created like this via link function of directive:
link: function (scope, element, attributes) {
googleMapsApi.load(scope.key)
.then(function () {
var mapOptions = {
center: scope.center,
zoom: scope.zoom,
mapTypeId: google.maps.MapTypeId.ROADMAP
};
googleMapsApi.createMap(scope,attributes.id,mapOptions);
});
}
and data is loaded in controller like this:
.controller('MyCtrl', function ($scope, googleMapsApi) {
googleMapsApi.onMapReady($scope, function(mapInst) {
console.log('Google Map is ready');
mapInst.data.loadGeoJson('https://storage.googleapis.com/mapsdevsite/json/google.json');
});
});
JSFiddle example
Related
This's my service. I've got variable isPolygonDrawingEnded there.
angular
.module('gillie.clients.app')
.service('mapDrawingService', mapDrawingService);
mapDrawingService.$inject = ['$ngBootbox', '$translate', 'fitPolygonService', 'mapCommonService'];
function mapDrawingService($ngBootbox, $translate, fitPolygonService, mapCommonService) {
var self = this;
var map;
var polygonFeatures;
var stopDrawingAction;
var isPolygonDrawingEnded = false;
var isDrawingMode = true;
self.startNewPolygonDrawing = function (afterDrowed) {
fitPolygonService.suggestPoint(isDrawingMode);
var modify = createModifyInteractionToPolygonFeatures();
var draw = createDrawInteraction();
attachDrawEvents(draw, afterDrowed);
map.addInteraction(draw);
stopDrawingAction = function() {
map.removeInteraction(draw);
map.removeInteraction(modify);
};
};
var attachDrawEvents = function (draw, afterDrowed) {
draw.on('drawend', function (e) {
e.preventDefault();
afterDrowed();
isPolygonDrawingEnded = true;
if (fitPolygonService.isPolygonsExist()) {
var geometry = e.feature.getGeometry();
e.feature.setGeometry(fitPolygonService.fitPolygon(geometry));
}
});
}
This is my controller. Here I need to change $scope.showCountButton when mapDrawingService.isPolygonDrawingEnded == true. So how can I know that value in service is changed ?
angular
.module('gillie.clients.app')
.controller('regionController', regionController);
regionController.$inject = ['$q', '$filter', '$scope', '$ngBootbox', '$translate', 'regionService', 'mapService', 'mapDrawingService', 'modalService', 'regionCommonService', 'alertService', 'nominatimCommonService', 'populationService', 'provinceCommonService', 'provinceService', 'rx'];
function regionController($q, $filter, $scope, $ngBootbox, $translate, regionService, mapService, mapDrawingService, modalService, regionCommonService, alertService, nominatimCommonService, populationService, provinceCommonService, provinceService, rx) {
$scope.showCountButton = false;
$scope.startRegionSelection = function (region) {
$scope.isSavingAvailable = true;
$scope.showCountButton = false;
if (currentEditingRegion && region && currentEditingRegion.Id === region.Id) {
return;
}
if(region)
mapService.clearRegion(region);
if (currentEditingRegion && !region) {
mapDrawingService.continuePolygonDrawing();
return;
}
if (region) {
mapDrawingService.stopPolygonDrawing();
mapDrawingService.clearMap();
currentEditingRegion = region;
mapDrawingService.startExistPolygonDrawing(region.Name, region.CoordinatesJson);
return;
}
mapDrawingService.startNewPolygonDrawing(function () {
$scope.showCountButton = true
});
};
When I use mapDrawingService.startNewPolygonDrawing function - the value of showCountButton changes but view doesn't. $scope.$apply produces this exception:
[$rootScope:inprog] $digest already in progress
When you are passing the callback function it is no longer in the scope of the controller which is why the view is not updating. You can use promises instead since you are asynchronous draw event.
Service
mapDrawingService.$inject = ['$q', '$ngBootbox', '$translate', 'fitPolygonService', 'mapCommonService'];
function mapDrawingService($q, $ngBootbox, $translate, fitPolygonService, mapCommonService) {
var self = this;
var map;
var polygonFeatures;
var stopDrawingAction;
var isPolygonDrawingEnded = false;
var isDrawingMode = true;
self.startNewPolygonDrawing = function() {
fitPolygonService.suggestPoint(isDrawingMode);
var modify = createModifyInteractionToPolygonFeatures();
var draw = createDrawInteraction();
var promiseObj = attachDrawEvents(draw);
map.addInteraction(draw);
stopDrawingAction = function() {
map.removeInteraction(draw);
map.removeInteraction(modify);
};
return promiseObj;
};
var attachDrawEvents = function(draw) {
return $q(function(resolve, reject) {
draw.on('drawend', function(e) {
e.preventDefault();
if (fitPolygonService.isPolygonsExist()) {
var geometry = e.feature.getGeometry();
e.feature.setGeometry(fitPolygonService.fitPolygon(geometry));
}
resolve();
});
});
}
Controller
mapDrawingService.startNewPolygonDrawing().then(function() {
$scope.showCountButton = true
});
I'm new to JavaScript and was really getting along well with it until a few hours ago and this has got me stumped.
My question is with the callback which i don't think i'm doing right. Essentially, on the Init of the App.Map which is called from the html page, the script will load the google maps script and execute the code in the callback. This all works fine. The problem is when i'm stepping through the script in the load function, the location should be null but isn't. However, if i use App.Map.location in place of location the variable is correct and equal to null.
Secondly, the commented out line ClearMarkers(); throws an error that it is not recognised when i step through the script.
I'm not doing something right, what would be the correct way to implement a callback this way?
Namespaces:
var App = App || {};
App.Map = App.Map || {};
App namespace:
(function(ns) {
ns.LoadScript = function(url, callback) {
var script = document.createElement('script');
script.type = 'text/javascript';
if (script.readyState) {
script.onreadystatechange = function() {
if (script.readyState === 'loaded' ||
script.readyState === 'complete') {
script.onreadystatechange = null;
callback();
}
};
} else {
script.onload = function() {
callback();
}
}
script.src = url;
document.head.appendChild(script);
};
})(App);
Map namespace:
(function(ns) {
ns.position = {
lat: -34.397,
lng: 150.644
};
ns.location = null;
var key = '';
var map;
var markers = [];
ns.ClearMarkers = function () {
if (markers.length !== 0) {
markers.forEach(function (marker) {
marker.setMap(null);
});
markers = [];
}
};
var load = function () {
map = new google.maps.Map(document.getElementById('map'), {
center: this.position
});
//ClearMarkers();
if (location) {
alert("perform search with this location");
} else {
alert("find current location");
}
};
ns.Init = function (data, options) {
this.location = data && data.location ? data.location : null;
App.LoadScript('https://maps.googleapis.com/maps/api/js?key=' + key + '&libraries=places', load);
};
})(App.Map);
Thanks for any help and if the question needs reworking please let me know.
I have this variable that is being controlled by the Factory and to update the Controller but it's not happening.
Here is what I have:
var app = angular.module('plunker', []);
app.controller('AppController', function($scope, AppFactory) {
var vm = this;
$scope.serverStatus = AppFactory.getStatus();
});
app.factory('AppFactory', function($timeout) {
var AppFactory = {};
var vm = this;
vm.serverStatus = true;
// Execute after 2 seconds of page start
$timeout(function() {
AppFactory.setStatus(false);
}, 2000);
AppFactory.setStatus = function(status) {
console.log('Server set to ' + status);
vm.serverStatus = status;
// Getting server status = false
AppFactory.getStatus();
};
AppFactory.getStatus = function() {
console.log('Getting server status: ' + vm.serverStatus);
return vm.serverStatus;
};
return AppFactory;
});
LIVE PLUNKER DEMO: https://plnkr.co/edit/62xGw7Klvbywp9TODWF4?p=preview
Do you think Directives would work better with 2-way-communication between a factory and controller?
Check this edited the plunkr https://plnkr.co/edit/z6tdr5?p=preview
var app = angular.module('plunker', []);
app.controller('AppController', function($scope,$timeout, AppFactory) {
var vm = this;
$timeout(function() {
AppFactory.setStatus(false);
$scope.serverStatus = AppFactory.getStatus();
}, 2000);
$scope.serverStatus = AppFactory.getStatus();
});
app.factory('AppFactory', function($timeout) {
var AppFactory = {};
var serverStatus = true;
// Execute after 2 seconds of page start
return {
getStatus: function () {
//console.log('Getting server status: ' + vm.serverStatus);
return serverStatus;
},
setStatus : function(status) {
var vm = this;
console.log('Server set to ' + status);
serverStatus = status;
// Getting server status = false
vm.getStatus();
}
};
});
Here's a solution that uses events, e.g.:
app.controller('AppController', function($scope, AppFactory) {
var vm = this;
$scope.$on('messageOne', function(event, data){
console.log(data);
$scope.serverStatus = data;
$scope.$apply(); //I think $apply() is not needed here!
});
$scope.serverStatus = AppFactory.getStatus();
});
app.factory('AppFactory', function($timeout, $rootScope) {
var AppFactory = {};
var vm = this;
vm.serverStatus = true;
// Execute after 2 seconds of page start
$timeout(function() {
AppFactory.setStatus(false);
}, 2000);
AppFactory.setStatus = function(status) {
console.log('Server set to ' + status);
vm.serverStatus = status;
// Getting server status = false
//AppFactory.getStatus();
$rootScope.$broadcast('messageOne', status);
};
AppFactory.getStatus = function() {
console.log('Getting server status: ' + vm.serverStatus);
return vm.serverStatus;
};
return AppFactory;
});
https://plnkr.co/edit/pARMnE3Wl0OeJezKuvLT?p=info
There is the following Angular code:
$scope.clickByPoint = function(marker, eventName, point) {
var geocoder, location;
$scope.options.info.point = point;
$scope.options.info.show = true;
$scope.searched = false;
$scope.address = "";
geocoder = new google.maps.Geocoder();
location = {
lat: parseFloat(point.latitude),
lng: parseFloat(point.longitude)
};
geocoder.geocode({location: location}, function(results, status) {
$scope.searched = true;
if (status === google.maps.GeocoderStatus.OK) {
$scope.address = results[0].formatted_address;
}
$scope.$digest();
});
};
And my Jasmine test:
describe('$scope.clickByPoint', function() {
var point;
beforeEach(inject(function(_Point_) {
point = _Point_.build('PointName', { latitude: 0, longitude: 0 });
}));
describe('try to find the address', function() {
it('initialize google maps info window', function() {
$scope.clickByPoint(null, null, point)
expect($scope.searched).toEqual(true);
});
})
});
As you can see I'm trying to test 'scope.searched' variable is changed, but it's always 'false', because function is asynchronous. How can I test this code properly? Thanks in advance.
In this case, use a mock of google.maps.Geocoder() in test, using jasmine, because, you are testing clickByPoint() logic and not Google maps api.
var geocoder = new google.maps.Geocoder();
jasmine.createSpy("geocode() geocoder").andCallFake(function(location, callback) {
// no wait time ...
var results = {fake:'', data:''};
var status = google.maps.GeocoderStatus.OK; // get OK value and set it OR redefine it with a Spy.
callback(result, status);
});
Now you can use your test :
describe('try to find the address', function() {
it('initialize google maps info window', function() {
$scope.clickByPoint(null, null, point);
expect($scope.searched).toEqual(true);
});
})
I have an application that uses an AngularJS service and using Angular-Google-Maps and I do get multiple markers on my map but I can't get the click on each marker to work. The only marker that allows a click is the last one which doesn't allow me to close it after opening the window or if I only have one address the marker works as expected. I think I'm close but can't figure out what I might be missing to have the click on the markers work for all of them. Any ideas on what I'm missing or need to do differently?
Here is the markup on my page.
<div ng-app="myMapApp" ng-controller="mapController">
<ui-gmap-google-map center='map.center' zoom='map.zoom' options="options">
<ui-gmap-markers models="directoryMarkers" coords="'self'" icon="'icon'" click="'onClick'">
<ui-gmap-windows show="show">
<div ng-non-bindable>{{organization}}</div>
</ui-gmap-window>
</ui-gmap-markers>
</ui-gmap-google-map>
</div>
The code in myMapApp.js
var app = angular.module("myMapApp", ['uiGmapgoogle-maps', 'ngStorage']);
The code in mapController.js
app.controller('mapController', function ($scope, Geocoder) {
$scope.map = { center: { latitude: 45, longitude: -73 }, zoom: 10 };
var hfValue = $("#ucDirectory_UcResults_hfResults");
$scope.directoryMarkers = [];
var createMarker = function (organization, address, latitude, longitude, i) {
var ret = {
latitude: latitude,
longitude: longitude,
address: address,
organization: organization,
show: false
};
ret.onClick = function () {
console.log("Clicked!");
ret.show = !ret.show;
};
ret["id"] = i;
return ret;
};
var json = jQuery.parseJSON(hfValue[0].value);
var markers = [];
var i = 0;
var org;
for (var key in json) {
if (json.hasOwnProperty(key)) {
org = json[key].organization;
if (json[key].address.length > 0) {
Geocoder.geocodeAddress(json[key].address).then(function (data) {
markers.push(createMarker(org, json[key].address, data.lat, data.lng, i))
$scope.map.center.latitude = data.lat;
$scope.map.center.longitude = data.lng;
});
i++;
}
}
}
$scope.directoryMarkers = markers;
});
The code in geocoder-service.js
* An AngularJS Service for intelligently geocoding addresses using Google's API. Makes use of
* localStorage (via the ngStorage package) to avoid unnecessary trips to the server. Queries
* Google's API synchronously to avoid `google.maps.GeocoderStatus.OVER_QUERY_LIMIT`.
*
* #author: benmj
* #author: amir.valiani
*
* Original source: https://gist.github.com/benmj/6380466
*/
/*global angular: true, google: true, _ : true */
'use strict';
//angular.module('geocoder', ['ngStorage']).factory('Geocoder', function ($localStorage, $q, $timeout, $rootScope) {
app.factory('Geocoder', function ($localStorage, $q, $timeout, $rootScope) {
var locations = $localStorage.locations ? JSON.parse($localStorage.locations) : {};
var queue = [];
// Amount of time (in milliseconds) to pause between each trip to the
// Geocoding API, which places limits on frequency.
var QUERY_PAUSE = 250;
/**
* executeNext() - execute the next function in the queue.
* If a result is returned, fulfill the promise.
* If we get an error, reject the promise (with message).
* If we receive OVER_QUERY_LIMIT, increase interval and try again.
*/
var executeNext = function () {
var task = queue[0],
geocoder = new google.maps.Geocoder();
geocoder.geocode({ address: task.address }, function (result, status) {
if (status === google.maps.GeocoderStatus.OK) {
var parsedResult = {
lat: result[0].geometry.location.lat(),
lng: result[0].geometry.location.lng(),
formattedAddress: result[0].formatted_address
};
locations[task.address] = parsedResult;
$localStorage.locations = JSON.stringify(locations);
queue.shift();
task.d.resolve(parsedResult);
} else if (status === google.maps.GeocoderStatus.ZERO_RESULTS) {
queue.shift();
task.d.reject({
type: 'zero',
message: 'Zero results for geocoding address ' + task.address
});
} else if (status === google.maps.GeocoderStatus.OVER_QUERY_LIMIT) {
if (task.executedAfterPause) {
queue.shift();
task.d.reject({
type: 'busy',
message: 'Geocoding server is busy can not process address ' + task.address
});
}
} else if (status === google.maps.GeocoderStatus.REQUEST_DENIED) {
queue.shift();
task.d.reject({
type: 'denied',
message: 'Request denied for geocoding address ' + task.address
});
} else {
queue.shift();
task.d.reject({
type: 'invalid',
message: 'Invalid request for geocoding: status=' + status + ', address=' + task.address
});
}
if (queue.length) {
if (status === google.maps.GeocoderStatus.OVER_QUERY_LIMIT) {
var nextTask = queue[0];
nextTask.executedAfterPause = true;
$timeout(executeNext, QUERY_PAUSE);
} else {
$timeout(executeNext, 0);
}
}
if (!$rootScope.$$phase) { $rootScope.$apply(); }
});
};
return {
geocodeAddress: function (address) {
var d = $q.defer();
if (_.has(locations, address)) {
d.resolve(locations[address]);
} else {
queue.push({
address: address,
d: d
});
if (queue.length === 1) {
executeNext();
}
}
return d.promise;
}
};
});
As an aside, if you don't have a lot of windows open at the same time, you shouldn't use the windows directive, instead use the window directive and define it as a sibling to your markers. As recommended by the documentation.
But to answer the original question, this plnkr uses your code, minus the geocoding, to produce markers with windows. It takes two clicks on a marker to get to where you want it to be because the click happens before the value is changed.
I think to get the behavior you want it would look more like the following:
html:
<ui-gmap-google-map center='map.center' zoom='map.zoom' options="options">
<ui-gmap-markers fit="true" models="directoryMarkers" coords="'self'" icon="'icon'" click="'onClick'">
</ui-gmap-markers>
<ui-gmap-window show="selected.show" coords="selected">
<div>{{selected.organization}}</div>
</ui-gmap-window>
controller:
$scope.map = {
center: {
latitude: 45,
longitude: -73
},
zoom: 10
};
$scope.directoryMarkers = [];
$scope.selected = null;
var createMarker = function(latitude, longitude, i) {
var ret = {
latitude: latitude,
longitude: longitude,
organization: "Foo",
show: false
};
ret.onClick = function() {
console.log("Clicked!");
$scope.selected = ret;
ret.show = !ret.show;
};
ret["id"] = i;
return ret;
};
var markers = [];
var org;
var coords = chance.coordinates().split(",");
$scope.map.center.latitude = coords[0];
$scope.map.center.longitude = coords[1];
for (var i = 0; i < 20; i++) {
coords = chance.coordinates().split(",");
markers.push(createMarker(coords[0], coords[1], i));
}
$scope.directoryMarkers = markers;
Which can be seen tied together in this plnkr: http://plnkr.co/edit/rT4EufIGcjplgd8orVWu?p=preview