In this step we will add the ability to send the current location in Google Maps.
To get the devices location (aka geo-location
) we will install a Cordova
plug-in called cordova-plugin-geolocation
:
$ ionic cordova plugin add cordova-plugin-geolocation --save
$ npm install --save @ionic-native/geolocation
If you use
Chromium
you may get the following error:Network location provider at 'https://www.googleapis.com/' : Returned error code 403.
Since chromium 23, some features were slowly moved to require Google API keys in order to function. In a precompiled version of chrome or chromium nightly or canary, everything seems to work, because these builds use default keys. But if we build chromium by ourselves, it won't have any keys and functions like Google Maps Geolocation API, Sync API, etc. won't function properly. See also issue 179686.
Since the location is going to be presented with Google Maps
, we will install a package which will help up interact with it in Angular 2
:
$ npm install --save @agm/core
Before you import the installed package to the app's NgModule
be sure to generate an API key. An API key is a code passed in by computer programs calling an API to identify the calling program, its developer, or its user to the Web site. To generate an API key go to Google Maps API documentation page and follow the instructions. Each app should have it's own API key, as for now we can just use an API key we generated for the sake of this tutorial, but once you are ready for production, replace the API key in the script below:
3
4
5
6
7
8
9
31
32
33
34
35
36
37
38
39
40
import { IonicApp, IonicErrorHandler, IonicModule } from 'ionic-angular';
import { SplashScreen } from '@ionic-native/splash-screen';
import { StatusBar } from '@ionic-native/status-bar';
import { AgmCoreModule } from '@agm/core';
import { MomentModule } from 'angular2-moment';
import { ChatsPage } from '../pages/chats/chats';
import { NewChatComponent } from '../pages/chats/new-chat';
...some lines skipped...
imports: [
BrowserModule,
IonicModule.forRoot(MyApp),
MomentModule,
AgmCoreModule.forRoot({
apiKey: 'AIzaSyAWoBdZHCNh5R-hB5S5ZZ2oeoYyfdDgniA'
})
],
bootstrap: [IonicApp],
entryComponents: [
Before we proceed any further, we will add a new message type to our schema, so we can differentiate between a text message and a location message:
6
7
8
9
10
11
12
13
}
export enum MessageType {
TEXT = <any>'text',
LOCATION = <any>'location'
}
export interface Chat {
We want the user to be able to send a location message through an attachments menu in the MessagesPage
, so let's implement the initial MessagesAttachmentsComponent
, and as we go through, we will start filling it up:
1
2
3
4
5
6
7
8
9
10
11
12
13
import { Component } from '@angular/core';
import { ModalController, ViewController } from 'ionic-angular';
@Component({
selector: 'messages-attachments',
templateUrl: 'messages-attachments.html'
})
export class MessagesAttachmentsComponent {
constructor(
private viewCtrl: ViewController,
private modelCtrl: ModalController
) {}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<ion-content class="messages-attachments-page-content">
<ion-list class="attachments">
<button ion-item class="attachment attachment-gallery">
<ion-icon name="images" class="attachment-icon"></ion-icon>
<div class="attachment-name">Gallery</div>
</button>
<button ion-item class="attachment attachment-camera">
<ion-icon name="camera" class="attachment-icon"></ion-icon>
<div class="attachment-name">Camera</div>
</button>
<button ion-item class="attachment attachment-location">
<ion-icon name="locate" class="attachment-icon"></ion-icon>
<div class="attachment-name">Location</div>
</button>
</ion-list>
</ion-content>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
.messages-attachments-page-content {
$icon-background-size: 60px;
$icon-font-size: 20pt;
.attachments {
width: 100%;
margin: 0;
display: inline-flex;
}
.attachment {
text-align: center;
margin: 0;
padding: 0;
.item-inner {
padding: 0
}
.attachment-icon {
width: $icon-background-size;
height: $icon-background-size;
line-height: $icon-background-size;
font-size: $icon-font-size;
border-radius: 50%;
color: white;
margin-bottom: 10px
}
.attachment-name {
color: gray;
}
}
.attachment-gallery .attachment-icon {
background: linear-gradient(#e13838 50%, #f53d3d 50%);
}
.attachment-camera .attachment-icon {
background: linear-gradient(#3474e1 50%, #387ef5 50%);
}
.attachment-location .attachment-icon {
background: linear-gradient(#2ec95c 50%, #32db64 50%);
}
}
10
11
12
13
14
15
16
27
28
29
30
31
32
33
34
48
49
50
51
52
53
54
55
import { ChatsOptionsComponent } from '../pages/chats/chats-options';
import { LoginPage } from '../pages/login/login';
import { MessagesPage } from '../pages/messages/messages';
import { MessagesAttachmentsComponent } from '../pages/messages/messages-attachments';
import { MessagesOptionsComponent } from '../pages/messages/messages-options';
import { ProfilePage } from '../pages/profile/profile';
import { VerificationPage } from '../pages/verification/verification';
...some lines skipped...
ProfilePage,
ChatsOptionsComponent,
NewChatComponent,
MessagesOptionsComponent,
MessagesAttachmentsComponent
],
imports: [
BrowserModule,
...some lines skipped...
ProfilePage,
ChatsOptionsComponent,
NewChatComponent,
MessagesOptionsComponent,
MessagesAttachmentsComponent
],
providers: [
StatusBar,
We will add a generic style-sheet for the attachments menu since it can also use us in the future:
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
left: calc(100% - #{$options-popover-width} - #{$options-popover-margin}) !important;
top: $options-popover-margin !important;
}
// Attachments Popover Component
// --------------------------------------------------
$attachments-popover-width: 100%;
.attachments-popover .popover-content {
width: $attachments-popover-width;
transform-origin: 300px 30px !important;
left: calc(100% - #{$attachments-popover-width}) !important;
top: 58px !important;
}
Now we will add a handler in the MessagesPage
which will open the newly created menu, and we will bind it to the view:
7
8
9
10
11
12
13
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
import * as _ from 'lodash';
import { MessagesOptionsComponent } from './messages-options';
import { Subscription, Observable, Subscriber } from 'rxjs';
import { MessagesAttachmentsComponent } from './messages-attachments';
@Component({
selector: 'messages-page',
...some lines skipped...
this.message = '';
});
}
showAttachments(): void {
const popover = this.popoverCtrl.create(MessagesAttachmentsComponent, {
chat: this.selectedChat
}, {
cssClass: 'attachments-popover'
});
popover.onDidDismiss((params) => {
// TODO: Handle result
});
popover.present();
}
}
7
8
9
10
11
12
13
<ion-title class="chat-title">{{title}}</ion-title>
<ion-buttons end>
<button ion-button icon-only class="attach-button" (click)="showAttachments()"><ion-icon name="attach"></ion-icon></button>
<button ion-button icon-only class="options-button" (click)="showOptions()"><ion-icon name="more"></ion-icon></button>
</ion-buttons>
</ion-navbar>
A location is a composition of longitude, latitude and an altitude, or in short: long, lat, alt
. Let's define a new Location
model which will represent the mentioned schema:
31
32
33
34
35
36
37
38
39
export interface User extends Meteor.User {
profile?: Profile;
}
export interface Location {
lat: number;
lng: number;
zoom: number;
}
Up next, would be implementing the actual component which will handle geo-location sharing:
3
4
5
6
7
8
9
55
56
57
58
59
60
61
import { IonicApp, IonicErrorHandler, IonicModule } from 'ionic-angular';
import { SplashScreen } from '@ionic-native/splash-screen';
import { StatusBar } from '@ionic-native/status-bar';
import { Geolocation } from '@ionic-native/geolocation';
import { AgmCoreModule } from '@agm/core';
import { MomentModule } from 'angular2-moment';
import { ChatsPage } from '../pages/chats/chats';
...some lines skipped...
providers: [
StatusBar,
SplashScreen,
Geolocation,
{provide: ErrorHandler, useClass: IonicErrorHandler},
PhoneService
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Platform, ViewController } from 'ionic-angular';
import { Geolocation } from '@ionic-native/geolocation';
import { Location } from 'api/models';
import { Observable, Subscription } from 'rxjs';
const DEFAULT_ZOOM = 8;
const EQUATOR = 40075004;
const DEFAULT_LAT = 51.678418;
const DEFAULT_LNG = 7.809007;
const LOCATION_REFRESH_INTERVAL = 500;
@Component({
selector: 'location-message',
templateUrl: 'location-message.html'
})
export class NewLocationMessageComponent implements OnInit, OnDestroy {
lat: number = DEFAULT_LAT;
lng: number = DEFAULT_LNG;
zoom: number = DEFAULT_ZOOM;
accuracy: number = -1;
intervalObs: Subscription;
constructor(private platform: Platform,
private viewCtrl: ViewController,
private geolocation: Geolocation) {
}
ngOnInit() {
// Refresh location at a specific refresh rate
this.intervalObs = this.reloadLocation()
.flatMapTo(Observable
.interval(LOCATION_REFRESH_INTERVAL)
.timeInterval())
.subscribe(() => {
this.reloadLocation();
});
}
ngOnDestroy() {
// Dispose subscription
if (this.intervalObs) {
this.intervalObs.unsubscribe();
}
}
calculateZoomByAccureacy(accuracy: number): number {
// Source: http://stackoverflow.com/a/25143326
const deviceHeight = this.platform.height();
const deviceWidth = this.platform.width();
const screenSize = Math.min(deviceWidth, deviceHeight);
const requiredMpp = accuracy / screenSize;
return ((Math.log(EQUATOR / (256 * requiredMpp))) / Math.log(2)) + 1;
}
reloadLocation() {
return Observable.fromPromise(this.geolocation.getCurrentPosition().then((position) => {
if (this.lat && this.lng) {
// Update view-models to represent the current geo-location
this.accuracy = position.coords.accuracy;
this.lat = position.coords.latitude;
this.lng = position.coords.longitude;
this.zoom = this.calculateZoomByAccureacy(this.accuracy);
}
}));
}
sendLocation() {
this.viewCtrl.dismiss(<Location>{
lat: this.lat,
lng: this.lng,
zoom: this.zoom
});
}
}
Basically, what this component does is refreshing the current geo-location at a specific refresh rate. Note that in order to fetch the geo-location we use Geolocation's
API, but behind the scene it uses `cordova-plugin-geolocation
. The sendLocation
method dismisses the view and returns the calculated geo-location. Now let's add the component's corresponding view:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<ion-header>
<ion-toolbar color="whatsapp">
<ion-title>Send Location</ion-title>
<ion-buttons end>
<button ion-button class="dismiss-button" (click)="viewCtrl.dismiss()"><ion-icon name="close"></ion-icon></button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content class="location-message-content">
<ion-list>
<agm-map [latitude]="lat" [longitude]="lng" [zoom]="zoom">
<agm-marker [latitude]="lat" [longitude]="lng"></agm-marker>
</agm-map>
<ion-item (click)="sendLocation()">
<ion-icon name="compass" item-left></ion-icon>
<h2>Send your current location</h2>
<p *ngIf="accuracy !== -1">Accurate to {{accuracy}} meters</p>
</ion-item>
</ion-list>
</ion-content>
The agm-map
is the component which represents the map itself, and we provide it with lat
, lng
and zoom
, so the map can be focused on the current geo-location. If you'll notice, we also used the agm-marker
component with the
same data-models, so the marker will be shown right in the center of the map.
Now we will add some CSS
to make sure the map is visible:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
.location-message-content {
.scroll-content {
margin-top: 44px;
}
agm-map {
padding: 0;
}
.sebm-google-map-container {
height: 300px;
margin-top: -15px;
}
}
And we will import the component:
13
14
15
16
17
18
19
30
31
32
33
34
35
36
37
52
53
54
55
56
57
58
59
import { MessagesPage } from '../pages/messages/messages';
import { MessagesAttachmentsComponent } from '../pages/messages/messages-attachments';
import { MessagesOptionsComponent } from '../pages/messages/messages-options';
import { NewLocationMessageComponent } from '../pages/messages/location-message';
import { ProfilePage } from '../pages/profile/profile';
import { VerificationPage } from '../pages/verification/verification';
import { PhoneService } from '../services/phone';
...some lines skipped...
ChatsOptionsComponent,
NewChatComponent,
MessagesOptionsComponent,
MessagesAttachmentsComponent,
NewLocationMessageComponent
],
imports: [
BrowserModule,
...some lines skipped...
ChatsOptionsComponent,
NewChatComponent,
MessagesOptionsComponent,
MessagesAttachmentsComponent,
NewLocationMessageComponent
],
providers: [
StatusBar,
The component is ready. The only thing left to do would be revealing it. So we will add the appropriate handler in the MessagesAttachmentsComponent
:
1
2
3
4
5
6
7
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { Component } from '@angular/core';
import { ModalController, ViewController } from 'ionic-angular';
import { NewLocationMessageComponent } from './location-message';
import { MessageType } from 'api/models';
@Component({
selector: 'messages-attachments',
...some lines skipped...
private viewCtrl: ViewController,
private modelCtrl: ModalController
) {}
sendLocation(): void {
const locationModal = this.modelCtrl.create(NewLocationMessageComponent);
locationModal.onDidDismiss((location) => {
if (!location) {
this.viewCtrl.dismiss();
return;
}
this.viewCtrl.dismiss({
messageType: MessageType.LOCATION,
selectedLocation: location
});
});
locationModal.present();
}
}
And we will bind it to its view:
10
11
12
13
14
15
16
<div class="attachment-name">Camera</div>
</button>
<button ion-item class="attachment attachment-location" (click)="sendLocation()">
<ion-icon name="locate" class="attachment-icon"></ion-icon>
<div class="attachment-name">Location</div>
</button>
Now we will implement a new method in the MessagesPage
, called sendLocationMessage
, which will create a string representation of the current geo-location and send it to the server:
1
2
3
4
5
6
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
231
232
233
234
235
236
237
238
239
240
241
242
import { Component, OnInit, OnDestroy, ElementRef } from '@angular/core';
import { NavParams, PopoverController } from 'ionic-angular';
import { Chat, Message, MessageType, Location } from 'api/models';
import { Messages } from 'api/collections';
import { MeteorObservable } from 'meteor-rxjs';
import * as moment from 'moment';
...some lines skipped...
});
}
sendLocationMessage(location: Location): void {
MeteorObservable.call('addMessage', MessageType.LOCATION,
this.selectedChat._id,
`${location.lat},${location.lng},${location.zoom}`
).zone().subscribe(() => {
// Zero the input field
this.message = '';
});
}
showAttachments(): void {
const popover = this.popoverCtrl.create(MessagesAttachmentsComponent, {
chat: this.selectedChat
...some lines skipped...
});
popover.onDidDismiss((params) => {
if (params) {
if (params.messageType === MessageType.LOCATION) {
const location = params.selectedLocation;
this.sendLocationMessage(location);
}
}
});
popover.present();
This requires us to update the addMessage
method in the server so it can support location typed messages:
70
71
72
73
74
75
76
if (!this.userId) throw new Meteor.Error('unauthorized',
'User must be logged-in to create a new chat');
check(type, Match.OneOf(String, [ MessageType.TEXT, MessageType.LOCATION ]));
check(chatId, nonEmptyString);
check(content, nonEmptyString);
The infrastructure is ready, but we can't yet see the message, therefore, we will need to add support for location messages in the MessagesPage
view:
19
20
21
22
23
24
25
26
27
28
29
30
<div *ngFor="let message of day.messages" class="message-wrapper">
<div [class]="'message message-' + message.ownership">
<div *ngIf="message.type == 'text'" class="message-content message-content-text">{{message.content}}</div>
<div *ngIf="message.type == 'location'" class="message-content message-content-text">
<agm-map [zoom]="getLocation(message.content).zoom" [latitude]="getLocation(message.content).lat" [longitude]="getLocation(message.content).lng">
<agm-marker [latitude]="getLocation(message.content).lat" [longitude]="getLocation(message.content).lng"></agm-marker>
</agm-map>
</div>
<span class="message-timestamp">{{ message.createdAt | amDateFormat: 'HH:mm' }}</span>
</div>
</div>
These additions looks pretty similar to the LocationMessage
since they are based on the same core components.
We will now add a method which can parse a string representation of the location into an actual JSON
:
241
242
243
244
245
246
247
248
249
250
251
252
253
254
popover.present();
}
getLocation(locationString: string): Location {
const splitted = locationString.split(',').map(Number);
return <Location>{
lat: splitted[0],
lng: splitted[1],
zoom: Math.min(splitted[2] || 0, 19)
};
}
}
And we will make some final adjustments for the view so the map can be presented properly:
93
94
95
96
97
98
99
100
101
102
103
content: " \00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0";
display: inline;
}
.sebm-google-map-container {
height: 25vh;
width: 35vh;
}
}
.message-timestamp {