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
Related
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
I'm calling a chain of functions through one click function, like below (simplified from my actual code). The problem is, initMap() is running before the user clicks the <button>, throwing an error, because the latitude/longitude are not defined until the user inputs an address through the click() function.
How can I prevent initMap() from running before the click() function?
$("button").click(function() {
var user_search = $("input").val();
var url = "https://api.mapbox.com/geocoding/v5/mapbox.places/" + user_search + ".json?access_token=MYAPIKEY";
$.getJSON(url, function(data) {
var number = data.features[0].address;
var route = data.features[0].text;
var latitude = data.features[0].geometry.coordinates[1];
var longitude = data.features[0].geometry.coordinates[0];
initMap(latitude, longitude);
});
});
var map;
var infowindow;
// Create Google Map with location at center
function initMap(latitude, longitude) {
var location = {lat: latitude, lng: longitude};
console.log(location);
map = new google.maps.Map(document.getElementById('map'), {
center: location,
zoom: 15
});
var service = new google.maps.places.PlacesService(map);
service.nearbySearch({
location: location,
radius: 3200,
types: ['school']
}, callback);
}
// List nearby places
function callback(results, status) {
if (status === google.maps.places.PlacesServiceStatus.OK) {
for (var i = 0; i < results.length; i++) {
listPlaces(results[i]);
}
}
else {
alert("There was an error finding address data.")
}
}
// ONLY list places with characteristics below
function listPlaces(place) {
if (place.name.indexOf('High ') > -1 && place.name.indexOf('Junior') == -1) {
$('body').append('<br>' + place.name);
}
}
ur initMap function is not getting closed properly.the error is because of syntax error not because of initMap() running without button click
$("button").click(function() {
initMap(latitude, longitude);
});
function initMap(latitude, longitude) {
//do stuff
callback();
}
function callback(results, status) {
//do stuff
}
function listPlaces(place) {
//do more stuff
}
Did this is the real code you want?
$("button").click(function() {
initMap(latitude, longitude);
});
function initMap(latitude, longitude) {
//do stuff
}
function callback(results, status) {
//do stuff
}
function listPlaces(place) {
//do more stuff
}
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'm doing a mobile app using AngularJS.
What I'm trying to achieve is to update the geolocation data as soon as I turn on the GPS. How do I achieve this? The problem I'm facing is, in order for the data to be updated I have to navigate to other page. These are the codes. I'm sharing data from one controller to the other.
.factory('sharedProperties', function () {
var coordinates = {};
var getC = function () {
return coordinates;
};
var setC = function (value) {
coordinates = value;
return coordinates;
};
return {
getCoords: getC,
setCoords: setC
};
})
First Controller
.controller('MessageController', ['$scope', 'sharedProperties', function ($scope, sharedProperties) {
var nObj = sharedProperties.getCoords();
console.log(nObj);
$scope.message = "This is my location: " + nObj.lat + ", " + nObj.lng + ". I'm around " + nObj.acc + " meters from point.";
}])
Second Controller
.controller("mainController", ['$scope', 'sharedProperties', function ($scope, sharedProperties) {
$scope.lat = "0";
$scope.lng = "0";
$scope.accuracy = "0";
$scope.error = "";
$scope.model = {
myMap: undefined
};
$scope.myMarkers = [];
$scope.showResult = function () {
return $scope.error == "";
}
$scope.mapOptions = {
center: new google.maps.LatLng($scope.lat, $scope.lng),
zoom: 15,
mapTypeId: google.maps.MapTypeId.ROADMAP,
disableDefaultUI: true
};
$scope.showPosition = function (position) {
$scope.lat = position.coords.latitude;
$scope.lng = position.coords.longitude;
$scope.accuracy = position.coords.accuracy;
$scope.$apply();
sharedProperties.setCoords({
'lat': position.coords.latitude,
'lng': position.coords.longitude,
'acc': position.coords.accuracy
});
var latlng = new google.maps.LatLng($scope.lat, $scope.lng);
$scope.model.myMap.setCenter(latlng);
$scope.myMarkers.push(new google.maps.Marker({
map: $scope.model.myMap,
position: latlng,
title: 'You are here'
}));
}
$scope.showMarkerInfo = function (marker) {
$scope.myInfoWindow.open($scope.model.myMap, marker);
};
$scope.showError = function (error) {
switch (error.code) {
case error.PERMISSION_DENIED:
$scope.error = "User denied the request for Geolocation."
break;
case error.POSITION_UNAVAILABLE:
$scope.error = "Location information is unavailable."
break;
case error.TIMEOUT:
$scope.error = "The request to get user location timed out."
break;
case error.UNKNOWN_ERROR:
$scope.error = "An unknown error occurred."
break;
}
$scope.$apply();
}
$scope.getLocation = function () {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition($scope.showPosition, $scope.showError,
{ enableHighAccuracy: true});
} else {
$scope.error = "Geolocation is not supported by this browser.";
}
}
$scope.getLocation();
}])
EDIT:
Somehow I managed to get it to work like this.
.controller('MessageController', ['$scope', 'sharedProperties', function ($scope, sharedProperties) {
$scope.getLoc = function () {
var nObj = sharedProperties.getCoords();
console.log(nObj);
var numbers = [nObj.lat, nObj.lng, nObj.acc];
return "This is my location: " + numbers[0].toFixed(6) + ", " + numbers[1].toFixed(6) + ". I'm around " + numbers[2].toFixed(0) + " meters from point.";
}])
And in the view, I put it like this.
<textarea style="width: 100%; height: 183px;" placeholder="Message">{{getLoc()}}</textarea>
but it displays the {{getLoc()}} in the textarea. Is there anyway that I can hide this and show only when it gets the data?
You may use ng-bind-html
So it will be like
<textarea style="width: 100%; height: 183px;" placeholder="Message" ng-bind-html="getLoc()"></textarea>
I am using google API to autocomplete feature of user address. and its working fine.
But now I want to use Nokia OVI Map for address autocomplete feature.
Please help me how to implement the same.
I am using below code
<div id="customSearchBox" class="main-search">
<span class ="caption">Search For Places:</span>
<div module="SearchBox">
<input rel="searchbox-input" class="search-box-bckgrnd" type="text" />
<div rel="searchbox-list" class="search-list"></div>
</div>
</div>
<script>
var customSearchBox = new nokia.places.widgets.SearchBox({
targetNode: 'customSearchBox',
template: 'customSearchBox',
searchCenter: function () {
return {
latitude: 52.516274,
longitude: 13.377678
}
},
onResults: function (data) {
//here you have access to data
alert(data);
}
});
</script>
How to get lat, long in this code
Thanks
Shivam
I got answer of my question by reading document of OVI map.
<script>
var customSearchBox = new nokia.places.widgets.SearchBox({
targetNode: 'customSearchBox',
template: 'customSearchBox',
searchCenter: function () {
return {
latitude: 52.516274,
longitude: 13.377678
}
},
onResults: function (data) {
//here you have access to data
//var a=getData();
renderResults(data);
//alert(data.results[0]);
}
});
function renderResults (data) {
var previewList = document.getElementById ('results');
previewList.innerHTML = '';
var results = data.results;
for (var i = 0, l = results.length; i < l; i++) {
var result = results[i];
var resultLi = document.createElement ('li');
resultLi.innerHTML = result.place.name+" - Lat:"+result.place.location.position.latitude+" Long:"+result.place.location.position.longitude;
//alert(result.place.location.position.longitude);
previewList.appendChild (resultLi);
}
}
</script>
I hope this will help to someone.
Thanks
Shivam
for jQuery you can do it like this with a textfield, because i don't wanted to use the predefined "searchbox widget":
$('#location_address').autocomplete({
focus:function (event, ui) {
return false;
},
search:function () {
$(this).addClass('working');
},
open:function () {
$(this).removeClass('working');
},
select:function (event, ui) {
var position = ui.item.id.split(";");
var coord = new nokia.maps.geo.Coordinate(parseFloat(position[0]), parseFloat(position[1]))
$('#location_address').val(ui.item.label)
$('#location_longitude')[0].value = coord.longitude;
$('#location_latitude')[0].value = coord.latitude;
map.setCenter(coord, "default");
map.setZoomLevel(16);
},
source:function (request, response) {
var searchCenter = {
latitude:52.516274,
longitude:13.377678
};
nokia.places.search.manager.findPlaces({
searchTerm:request.term,
onComplete:function (data, status) {
var nData = data.results.map(function (val, i) {
return {
value:val.place.name,
id:val.place.location.position.latitude + ";" + val.place.location.position.longitude
};
})
response(nData);
},
searchCenter:searchCenter,
didYouMean:5
});
},
minLength:2
});