I'm currently using PlacesService library from Google Maps API. I want to render suggestions for New York City. I added strict_bounds for this matter but I still get results way out of New York as shown in the image.
For the most part it works, but I get many out of bounds suggestions.
/** #private {?this._google.maps.Map} The google map object. */
this._map = new this._google.maps.Map(this._mapEl, {
zoom: 11,
center: this._mapPosition
});
/** #private {this._google.maps.places.Autocomplete}Autocomplete instance */
this._service = new this._google.maps.places.AutocompleteService();
/** #private {this._google.maps.places.PlaceService}PlaceService instance */
this._placeService = new this._google.maps.places.PlacesService(this._map);
Here is the instance in the constructor.
// Attach handler for the autocomplete search box. This updates the map
// position and re-sorts locations around that position.
this._searchEl.addEventListener('keyup', (event) => {
if(event.target.value) {
this._service.getPlacePredictions({
input: event.target.value,
offset: 3,
strictBounds: true,
types: ['geocode'],
bounds: this._map.getBounds()
}, (predictions) => {
if(predictions) {
let results = predictions.map(e => [e['description']]);
event.target.missplete = new MissPlete({
input: event.target,
options: results,
className: 'c-autocomplete'
})
event.target.missplete.select = () => {
let msplt = event.target.missplete;
if (msplt.highlightedIndex !== -1) {
msplt.input.value = msplt
.scoredOptions[msplt.highlightedIndex].displayValue;
msplt.removeDropdown();
console.dir('we did it');
}
};
event.target.predictions = predictions;
console.log(event.target.predictions);
}
});
}
});
There is a method displayPlacesOnMap that gets an array of Google place objects. Here we get the place_id in order to make use of PlaceService library.
displayPlacesOnMap(mapItems) {
if(mapItems) {
mapItems.forEach(place => {
let request = {
placeId: place.place_id,
fields: ['name', 'formatted_address', 'place_id', 'geometry']
}
const officeMap = this;
this._placeService.getDetails(request, function(place, status) {
if (status === 'OK') {
officeMap._mapPosition = place.geometry.location;
officeMap._map.panTo(officeMap._mapPosition);
officeMap.sortByDistance().clearLocations().updateUrl().updateList()
.updateUrl();
$(officeMap._searchEl).blur();
}
})
})
}
};
Related
I've searched high and low for proper documentation on how to configure Azure Maps with Angular and haven't found anything. How do I do this?
(Please look to the comments for my self-answered question)
As documentation for configuring Azure Maps with Angular does not exist, this post will accomplish that instead. By the end of this post, you should have a working Angular version of Azure Maps with map markers. Before adding any code, please follow the steps from the Microsoft website to set up your Azure Map keys: https://learn.microsoft.com/en-us/azure/azure-maps/
The first step to create your Azure Maps component is to create a new Angular component and add the following to your .html file:
<div id="azure-map"></div>
The id can be used for styling your component in the .scss file.
Next, we will work on the .ts file. First, let's set up the map. We'll add the following class variables for the map and coordinates:
map: any;
defaultLat: number = 47.608013; // Seattle coordinates
defaultLng: number = -122.335167;
and this output to emit coordinates to the map's parent component:
#Output() outputCoordinates: EventEmitter<number[]> = new EventEmitter<number[]>();
Now we will make a function called InitMap() and add this code snippet inside to initialize the base map and its properties:
this.map = new atlas.Map('azure-map', {
center: [this.defaultLng, this.defaultLat],
zoom: 12,
language: 'en-US',
showLogo: true,
showFeedbackLink: false,
dragRotateInteraction: false,
authOptions: {
authType: AuthenticationType.subscriptionKey,
subscriptionKey: 'YOUR_SUBSCRIPTION_KEY_HERE'
}
});
Next, we will add this code snippet inside InitMap() to register the map click hander and zoom controls:
this.map.events.add('ready', () => {
// Register the map click handler
this.map.events.add('click', (e) => {
this.outputCoordinates.emit([e.position[0], e.position[1]]); // 0 = longitude, 1 = latitude
});
//Construct a zoom control and add it to the map.
this.map.controls.add(new atlas.control.ZoomControl({
style: ControlStyle.auto,
zoomDelta: 1
}), {position: ControlPosition.BottomLeft});
});
We must also call the InitMap() function inside of ngOnInit().
The next step is to create the functionality to allow the user to drop and move pins on the map. This function will erase the current marker on the map, set the new marker's coordinates, initialize the marker drag handler, and set the boundaries of the map to track the newly placed pin marker. To handle all these operations, we will add this class variable:
markersReference: Marker[] = [];
and this function:
setMarkers(markers: Marker[]) {
if (markers && markers.length > 0) {
this.markersReference = markers;
this.map.markers.clear();
let boundsPositions: Array<{lng: number, lat:number}> = [];
for (let marker of markers) {
if (marker.latitude && marker.longitude) {
let htmlMarker = new atlas.HtmlMarker({
draggable: true,
position: [marker.longitude, marker.latitude] // longitude first
});
// Register the marker drag handler
this.map.events.add('dragend', htmlMarker, (e) => {
var pos = htmlMarker.getOptions().position;
this.outputCoordinates.emit([pos[0], pos[1]]); // 0 = longitude, 1 = latitude
});
boundsPositions.push({lng: marker.longitude, lat: marker.latitude}) // lat, lng
this.map.markers.add(htmlMarker);
}
}
this.map.setCamera({padding: {top: 20, bottom: 20, left: 20, right: 20}, maxZoom: 16,
bounds: atlas.data.BoundingBox.fromLatLngs(boundsPositions)});
}
Now we will add a function that allows us to center the map focus onto the dropped pin:
centerMapWithCoords(lon: number, lat: number) {
this.map.setCamera({zoom: 12, maxZoom: 16, center: [lon, lat]});
}
Lastly, in order to pick up changes that the user makes to the map, we will subscribe to the map subject and its markers. Add these inputs alongside your class variables:
#Input() markerDataSubject: Subject<Marker[]> = new Subject<Marker[]>();
#Input() centerMapSubject: Subject<{lng: number, lat: number}> = new Subject<{lng: number, lat: number}>();
Next, add these subscriptions to your ngOnInit():
this.subscriptions.push((this.centerMapSubject).asObservable().subscribe((coords) =>
this.centerMapWithCoords(coords.lng, coords.lat)));
this.subscriptions.push((this.markerDataSubject).asObservable().subscribe((markers) =>
this.setMarkers(markers)));
And unsubscribe when the component is closed:
ngOnDestroy() {
for (const s of this.subscriptions) {
s.unsubscribe();
}
}
Overall, the class in your .ts file should look similar to the following:
export class AzureMapComponent implements OnInit {
#Input() markerDataSubject: Subject<Marker[]> = new Subject<Marker[]>();
#Input() centerMapSubject: Subject<{lng: number, lat: number}> = new Subject<{lng: number, lat: number}>();
#Output() outputCoordinates: EventEmitter<number[]> = new EventEmitter<number[]>();
subscriptions: Subscription[] = [];
map: any;
markersReference: Marker[] = [];
defaultLat: number = 47.608013; // Seattle coordinates
defaultLng: number = -122.335167;
ngOnInit() {
this.InitMap();
this.subscriptions.push((this.centerMapSubject).asObservable().subscribe((coords) =>
this.centerMapWithCoords(coords.lng, coords.lat)));
this.subscriptions.push((this.markerDataSubject).asObservable().subscribe((markers) =>
this.setMarkers(markers)));
}
//Create an instance of the map control and set some options.
InitMap() {
this.map = new atlas.Map('azure-map', {
center: [this.defaultLng, this.defaultLat],
zoom: 12,
language: 'en-US',
showLogo: true,
showFeedbackLink: false,
dragRotateInteraction: false,
authOptions: {
authType: AuthenticationType.subscriptionKey,
subscriptionKey: 'YOUR_SUBSCRIPTION_KEY_HERE'
}
});
this.map.events.add('ready', () => {
// Register the map click handler
this.map.events.add('click', (e) => {
this.outputCoordinates.emit([e.position[0], e.position[1]]); // 0 = longitude, 1 = latitude
});
//Construct a zoom control and add it to the map.
this.map.controls.add(new atlas.control.ZoomControl({
style: ControlStyle.auto,
zoomDelta: 1
}), {position: ControlPosition.BottomLeft});
});
}
setMarkers(markers: Marker[]) {
if (markers && markers.length > 0) {
this.markersReference = markers;
this.map.markers.clear();
let boundsPositions: Array<{lng: number, lat:number}> = [];
for (let marker of markers) {
if (marker.latitude && marker.longitude) {
let htmlMarker = new atlas.HtmlMarker({
draggable: true,
position: [marker.longitude, marker.latitude] // longitude first
});
// Register the marker drag handler
this.map.events.add('dragend', htmlMarker, (e) => {
var pos = htmlMarker.getOptions().position;
this.outputCoordinates.emit([pos[0], pos[1]]); // 0 = longitude, 1 = latitude
});
boundsPositions.push({lng: marker.longitude, lat: marker.latitude}) // lat, lng
this.map.markers.add(htmlMarker);
}
}
this.map.setCamera({padding: {top: 20, bottom: 20, left: 20, right: 20}, maxZoom: 16,
bounds: atlas.data.BoundingBox.fromLatLngs(boundsPositions)});
}
}
centerMapWithCoords(lon: number, lat: number) {
this.map.setCamera({zoom: 12, maxZoom: 16, center: [lon, lat]});
}
ngOnDestroy() {
for (const s of this.subscriptions) {
s.unsubscribe();
}
}
}
Now that your Azure Maps component is complete, all you have to do is call an instance of your component within the .html of whatever view you'd like to place it in and coordinate the required inputs and output:
<app-azure-map
[markerDataSubject]="locationMarkerSubject"
[centerMapSubject]="centerMapSubject"
(outputCoordinates)="updateCoordinates($event)">
</app-azure-map>
The input subjects on your parent component should look something like this:
locationMarkerSubject: Subject<Marker[]> = new Subject<Marker[]>();
centerMapSubject: Subject<{lng: number, lat: number}> = new Subject<{lng: number, lat: number}>();
And your updateCoordinates() function will handle the marker data sent back from user input upon clicking the map.
I am trying to build an app with ionic 4 and firebase. There are two types of users: simple users and admins. As an admin, I want to see on map where the users are, but only in a certain area. I was able to display my position on map and to update the marker with watchPosition(). I added some locations in my database for the simple users to see if i can access them and then display them on map. Everything worked well, the markers were displayed on map, but now i got this error: Cannot read property 'latitude' of undefined, but i didn't change anything in my code (where i accessed the locations of users) It's strange because a few days ago it worked well, and it worked on android too.
Is this a problem from firebase or from geolocation, anyone have an idea?
This is my code:
getMarkers() {
let usersLocations =[];
firebase
.firestore()
.collection("users")
.get()
.then(userProfileSnapshot => {
userProfileSnapshot.docs.forEach(doc => {
if (doc.data().isAdmin==false)
usersLocations.push(doc.data());
});
}).then((x) => {
if(this.isAdmin==true)
for(let i=0; i<usersLocations.length; i++) {
let userPosition = new google.maps.LatLng(usersLocations[i].Location.latitude, usersLocations[i].Location.longitude);
let userMarker = new google.maps.Marker({
position: userPosition,
icon: {
url: "http://maps.google.com/mapfiles/ms/icons/yellow-dot.png"
},
map: this.map
});
// if(this.haversine_distance(this.locationMarker, userMarker)>1)
// userMarker.setVisible(false);
}
});
}
All we can say from the code you shared is that for some value of i, usersLocations[i] does not have a Location property. If you didn't change your code, you'll want to check which document is missing that property.
Alternatively, you can skip such documents and log their ID by changing the second then to:
for(let i=0; i<usersLocations.length; i++) {
if (!usersLocations[i] || !usersLocations[i].Location) {
console.error("Missing Location data in "+i+": "+JSON.stringify(usersLocations[i]));
continue; // skip further processing of this user
}
let userPosition = new google.maps.LatLng(usersLocations[i].Location.latitude, usersLocations[i].Location.longitude);
let userMarker = new google.maps.Marker({
position: userPosition,
icon: {
url: "http://maps.google.com/mapfiles/ms/icons/yellow-dot.png"
},
map: this.map
});
}
A slightly more modern/idiomatic way of accomplishing this is:
firebase
.firestore()
.collection("users")
.get()
.then(querySnapshot => {
// Convert the snapshot to an array of IDs and data
return querySnapshot.docs.map(doc => { doc.id, ...doc.data() });
})
.then(documents => {
// Remove admins
return documents.filter(doc => doc.isAdmin==false);
})
.then(documents => {
// Remove and log documents that have no location field
return documents.filter(doc => {
if (!doc.Location) {
console.error(`Document ${doc.id} doesn't have a Location`);
return false; // remove this document from the array
}
return true;
});
.then(documents => {
documents.forEach(doc => {
let userPosition = new google.maps.LatLng(doc.Location.latitude, doc.Location.longitude);
let userMarker = new google.maps.Marker({
position: userPosition,
icon: {
url: "http://maps.google.com/mapfiles/ms/icons/yellow-dot.png"
},
map: this.map
});
});
});
I can’t access the values lat , lng from data() in maps() method.
my vue.js component
code link : https://gist.github.com/melvin2016/c8082e27b9c50964dcc742ecff853080
console image of lat,lng
enter image description here
<script>
import Vue from 'vue';
import navbarSec from './navbarSec.vue';
export default {
data(){
return{
lat: '',
lng: '',
mapState: window.mapState,
from:'',
to:'',
placesFrom:[],
placesTo:[]
};
},
components:{
'navbar':navbarSec
},
created(){
var token = this.$auth.getToken();
this.$http.post('http://localhost:3000/book',{},{headers: {'auth':token}}).then(function(data){
this.session = true;
})
.catch(function(data){
this.session = false;
this.$auth.destroyToken();
Materialize.toast(data.body.message, 6000,'rounded');
this.$router.push('/login');
});
if(navigator.geolocation){
navigator.geolocation.getCurrentPosition((data)=>{
this.lat = data.coords.latitude;
this.lng = data.coords.longitude;
this.from=data.coords.latitude+' , '+data.coords.longitude;
});
}else{
Materialize.toast("Cannot Get Your Current Location !", 6000,'rounded');
}
},
mounted(){
if (this.mapState.initMap) {// map is already ready
var val = this.mapState.initMap;
console.log(val);
this.maps();
}
},
watch: {
// we watch the state for changes in case the map was not ready when this
// component is first rendered
// the watch will trigger when `initMap` will turn from `false` to `true`
'mapState.initMap'(value){
if(value){
this.maps();
}
},
from : function(val){
if(val){
var autoComplete = new google.maps.places.AutocompleteService();
autoComplete.getPlacePredictions({input:this.from},data=>{
this.placesFrom=data;
});
}
},
to:function(val){
if(val){
var autoComplete = new google.maps.places.AutocompleteService();
autoComplete.getPlacePredictions({input:this.to},data=>{
this.placesTo=data;
});
}
}
},
methods:{
maps(){
var vm = this;
var lati = vm.lat;
var lngi = vm.lng;
console.log(lati+' '+lngi);
var map;
var latlng = {lat: lati, lng:lngi };
console.log(latlng);
this.$nextTick(function(){
console.log('tickkkk');
map = new google.maps.Map(document.getElementById('maplo'), {
zoom: 15,
center: latlng
});
var marker = new google.maps.Marker({
position: latlng,
map: map
});
});
}
}
}
</script>
This is happening because you're calling maps() in the mounted at which point, the navigator.geolocation.getCurrentPosition((data) => {}) code hasn't resolved. With that in mind, call this.maps() within the getCurrentPosition method i.e:
navigator.geolocation.getCurrentPosition((data)=>{
this.lat = data.coords.latitude;
this.lng = data.coords.longitude;
this.from=data.coords.latitude+' , '+data.coords.longitude;
this.maps()
});
I've not looked in detail but you might be able to change the bits within the maps() method to remove the nextTick stuff when you do this as you'll be calling it a lot later in the cycle at which point everything will have been rendered.
I'm using angular google maps API. My problem is that when my geolocation is turned off and I use the search box to search for generic, not address-bound things or stores like "pizza" or "ikea" the map shows its results in the US by default.
I want to be able to choose my own default location to use when the geolocation is turned off. But i'm not sure how to do this.
This is my code
//SEARCH FEATURES
$scope.searchOpen = false;
$scope.searchModel = {}
//Add and remove css classes on search bar to show and hide the searchbar
$scope.toggleSearch = function () {
$scope.searchModel.searchTerm = null;
var searchFieldInput = document.getElementById('pac-input')
if (searchFieldInput.classList.contains('searchactive')) {
searchFieldInput.classList.remove('searchactive')
searchFieldInput.classList.remove('pac-container')
} else {
searchFieldInput.classList.add('searchactive')
}
}
$scope.$watch("searchModel.searchTerm", function (searchText) {
$scope.filteredMarkers = $filter("filter")($scope.specialMarker, searchText);
if (!$scope.filteredMarkers) {
return;
}
});
$scope.searchbox = {
template: 'searchbox.tpl.html',
position: 'top-left',
options: {
bounds: {}
},
events: {
places_changed: function (searchBox) {
$scope.repositionAllowed = false;
places = searchBox.getPlaces()
if (places.length == 0) {
return;
}
// For each place, get the icon, place name, and location.
newMarkers = [];
var bounds = new google.maps.LatLngBounds();
for (var i = 0, place; place = places[i]; i++) {
// Create a marker for each place.
console.log(place.place_id)
var marker = $scope.createMarker({
id: 's' + i,
title: place.name,
address: place.formatted_address,
lat: place.geometry.location.lat(),
lng: place.geometry.location.lng(),
place: place,
pointType: "search"
})
newMarkers.push(marker);
bounds.extend(place.geometry.location);
}
$scope.map.bounds = {
northeast: {
latitude: bounds.getNorthEast().lat(),
longitude: bounds.getNorthEast().lng()
},
southwest: {
latitude: bounds.getSouthWest().lat(),
longitude: bounds.getSouthWest().lng()
}
}
$scope.map.zoom = 13;
$scope.map.markers = newMarkers;
document.getElementById("pac-input").blur();
}
}
}
$scope.defaultBounds = new google.maps.LatLngBounds(
new google.maps.LatLng(37.9839, 22.7294),
new google.maps.LatLng(38.9839, 24.31715));
$scope.map.bounds = {
northeast: {
latitude: $scope.defaultBounds.getNorthEast().lat(),
longitude: $scope.defaultBounds.getNorthEast().lng()
},
southwest: {
latitude: $scope.defaultBounds.getSouthWest().lat(),
longitude: -$scope.defaultBounds.getSouthWest().lng()
}
}
$scope.searchbox.options.bounds = new google.maps.LatLngBounds($scope.defaultBounds.getNorthEast(), $scope.defaultBounds.getSouthWest());
});
thanks
I have a GoogleMap with EmberJs view. Everything works good except the data binding.
I want to bind the map markers with ember-data. If something changes at data level it must reflect on Map.
I tried to use observer, and re-run the makeMarkers method to set the marker, but that seems to be a bad solution.
What would be the best way to bind data with GoogleMaps?
View is deprecated on Ember 2.0, they will be removed at Ember 2.5, make a component like: {{g-map markers=models}}
This component have a collection of items, here markers.
You can implement something like this:
import Ember from 'ember';
import MarkerSync from '../mixin/marker-synchronizer';
/**
* Basic Component to display a google map,
* Service & Marker have to be improved
**/
export default Ember.Component.extend(MarkerSync, {
classNames: 'google-map',
googleMap: Ember.inject.service(),
map: null,
mapOptions: function () {
return {
center: new google.maps.LatLng(-34.397, 150.644),
zoom: 8
};
},
didInsertElement: function () {
this.$().height('100%');
this.$().width('100%');
this.displayGmap();
jQuery(window).on('resize', Ember.run.bind(this, this.handleResize));
},
willInsertElement: function () {
this.get('googleMap').loadScript();
},
displayGmap: Ember.observer('googleMap.isLoaded', function () {
if (!this.get('googleMap.isLoaded')) {
return;
}
const mapOptions = this.mapOptions();
this.set('map', new google.maps.Map(this.$()[0], mapOptions));
}),
handleResize: function () {
if (!this.get('googleMap.isLoaded')){
return;
}
const map = this.get('map');
const center = map.getCenter();
google.maps.event.trigger(map, 'resize');
map.setCenter(center);
},
});
import Ember from 'ember';
/**
* Synchronize collection with map from component.
* Care about to display or remove marker from map,
* Be careful this is not optimized.
**/
export
default Ember.Mixin.create({
markers: null,
_gHash: Ember.A(),
init() {
this._super.apply(this, arguments);
/*
* observes markers array.
*/
this.get('markers').addArrayObserver({
arrayWillChange: Ember.run.bind(this, this.markersWillChange),
arrayDidChange: Ember.run.bind(this, this.markersDidChange)
});
},
/*
* Remove marker from array and remove from map
*/
markerRemoved(marker) {
let gMarker = this.get('_gHash').find(function(item) {
return item.related === marker;
});
gMarker.native.setMap(null);
this.get('_gHash').removeObject(gMarker);
},
/*
* Add marker to `synchronized` array and display on map
*/
markerAdded(marker) {
const gMarker = new google.maps.Marker({
position: {
lat: marker.lat,
lng: marker.lng
},
title: marker.title,
map: this.get('map'),
});
this.get('_gHash').push({
native: gMarker,
related: marker
});
},
/*
* Take care about removed item
*/
markersWillChange(markers, start, removeCount, addCount) {
if (removeCount > 0) {
for (let i = start; i < start + removeCount; i++) {
this.markerRemoved(markers.objectAt(i));
}
}
},
/*
* Take care about added item
*/
markersDidChange(markers, start, removeCount, addCount) {
if (addCount > 0) {
for (let i = start; i < start + addCount; i++) {
this.markerAdded(markers.objectAt(i));
}
}
},
});
import Ember from 'ember';
const get = Ember.get;
/**
* This service lazy load googleMap api.
* Ugly but do the job
*/
export default Ember.Service.extend({
scriptUrl: 'https://maps.googleapis.com/maps/api/js',
isLoaded: Ember.computed.equal('state', 'loaded'),
state: 'none',
init: function () {
let config = this.container.lookupFactory('config:environment');
var apiKey = get(config, 'googleMap.apiKey');
this.set('apiKey', apiKey);
},
normalizeUrl: function () {
var url = this.get('scriptUrl');
url += '?' + 'v=3' + '&' + 'libraries=places' + '&' + 'callback=loadGmap';
if (this.get('apiKey')) {
url += '&key=' + this.get('apiKey');
}
return url;
},
loadScript: function () {
if (this.get('state') !== 'none'){
return false;
}
this.set('state', 'loading');
window.loadGmap = Ember.run.bind(this, function () {
this.set('state', 'loaded');
});
var url = this.normalizeUrl();
return Ember.$.getScript(url).fail(function(){
console.log('getScript fail');
});
},
});
This implementation work but you have to 'sanitize' this code :)
1) Create a component, not a view
2) Use didInsertElement to render google map and observer to update it. Don't forget that observers are synchronous (http://guides.emberjs.com/v1.13.0/object-model/observers/) and you need to do smth like:
somethingChanged: Ember.observer('something', function () {
Ember.run.once(this, '_somethingChanged');
}).on('init'),
_somethingChanged: function () {
/* do smth about changed property here */
}