Angular is a cross-platform JavaScript framework that can be used to build applications for different platforms such as web, desktop, and mobile. Moreover, it allows developers to use the same code base and apply the same web techniques to each platform, enjoying the same experience and performance. In this chapter, we will investigate how we can build mobile applications using Angular.
Ionic is a popular UI toolkit that allows us to build mobile applications using web technologies such as Angular. The Capacitor library greatly enhances Ionic applications by enabling them to run natively on Android and iOS devices. In this chapter, we will use both technologies to build a mobile application that can take geotagged photos and display them on a 3D map.
We will cover the following topics in detail:
Capacitor is a native mobile runtime that enables us to build native Android and iOS applications with web technologies, including Angular. It provides an abstraction API layer for web applications to interact with the native resources of a mobile OS. It does not include a UI layer or any other way of interacting with the user interface.
Ionic is a mobile framework that contains a collection of UI components that we can use in an application built with Capacitor. The main advantage of Ionic is that we maintain a single code base across all native mobile platforms. That is, we write the code once, and it works everywhere. Ionic supports all popular JavaScript frameworks, including Angular.
Important note
When we create a new Ionic application from scratch, we also get Capacitor installed and configured out of the box.
Firebase is a Backend-as-a-Service (BaaS) platform provided by Google that contains a set of tools and services for building applications. Cloud Firestore is a database solution provided by Firebase that features a flexible and scalable NoSQL document-oriented database that can be used in web and mobile applications. Storage is a Firebase service that allows us to interact with a storage mechanism and upload or download files.
CesiumJS is a JavaScript library for creating interactive 3D maps in the browser. It is an open source, cross-platform library that uses WebGL and allows us to share geospatial data on multiple platforms. It is empowered by Cesium, a platform for building high-quality and performant 3D geospatial applications.
In this project, we will build a mobile application that can take photos according to the current location and preview them on a map. Initially, we will learn how to create a mobile application using Angular and Ionic. We will then use Capacitor to take photos using the camera of the mobile device and tag them with the current location via the GPS. We will upload those photos in Firebase along with their location data. Finally, we will use CesiumJS to load location data on a 3D globe along with a preview of the photo.
Important note
In this chapter, you will learn how to build a mobile application with Angular and Ionic. To follow up with the project and preview your application, you will need to follow the getting started guide for your development environment (Android or iOS), which you can find in the Further reading section.
Build time: 2 hours
You will need the following software and tools to complete the project:
The first step toward building our application is creating a new mobile application using the Ionic toolkit. We will start building our application with the following tasks:
Ionic has a pretty straightforward process for creating a new mobile application from scratch, which can be done from the Ionic website without entering a single line of code.
To create a new Ionic application, we need to head over to https://www.ionicframework.com and complete the following steps:
You will be redirected to the Create your Ionic App page to fill in all the necessary details to create your Ionic application.
Tip
Ionic will create a repository for your application automatically and connect it with your Ionic dashboard if you select the latter.
For this project, it is recommended to select one of the free Git services available. When you choose to do so, you will be asked to be authorized with the selected Git service.
After the build has finished, you will be redirected to the Ionic dashboard of your application. From there, you can accomplish specific tasks for your application, such as setting up a Continuous Integration/Development (CI/CD) pipeline, building, and previewing your application.
Ionic has created a sample application for us with some ready-made data. To modify it according to our needs, we need to get a copy of the application locally. Let's see how we can accomplish that in the following section.
Ionic provides a command-line tool called the Ionic CLI that is used to build and run an Ionic mobile application. Let's see how we can install it and start building the main menu of our application:
npm install -g @ionic/cli cordova-res
The cordova-res library is used to generate the icons and splash screens of our application for native mobile devices.
npm install
<title>Phototag App</title>
<ion-list-header>Phototag</ion-list-header>
<ion-note>Capture geotagged photos</ion-note>
An ion-list-header element is the header of a list. An ion-note element is a text element that is used to provide additional information, such as the subtitle of a list.
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: 'app.component.html',
styleUrls: ['app.component.scss'],
})
export class AppComponent {
public appPages = [
{
title: 'Take a photo',
url: '/capture',
icon: 'camera'
},
{
title: 'View gallery',
url: '/view',
icon: 'globe'
}
];
}
The appPages property contains all the pages of our application. Each page has a title, the URL from which it is accessible, and an icon. Our application will consist of two pages, one that will be used for taking photos using the camera and another for displaying them on a map.
ionic serve
The preceding command will build your application and open your default browser at http://localhost:8100.
Click on the Menu button, and you should see the following output:
Tip
Try to adjust your browser window size to achieve a more realistic view for a mobile device, or use the Device toolbar in the Google Chrome developer tools. You can find more details about the Device toolbar at https://developers.google.com/web/tools/chrome-devtools/device-mode#viewport.
We have learned how to create a new Ionic application using the getting started page of the Ionic website. We also saw how to get the application locally on our machine and make modifications according to our needs.
If we now try to click on a menu item, we will notice that nothing happens since we have not created the necessary pages that will be activated in each case. In the following section, we will learn how to complete this task by building the functionality of the first page.
The first page of our application will allow the user to take photos using the camera. We will use the Capacitor runtime to get access to the native resource of the camera. To implement the page, we need to take the following actions:
Let's start building the user interface of the page.
Each page in our application is a different Angular module. To create an Angular module in Ionic, we can use the generate command of the Ionic CLI:
ionic generate page capture
The previous command will perform the following actions:
Let's start building the logic of our new page now:
{
path: '',
redirectTo: 'capture',
pathMatch: 'full'
}
The empty path is called the default routing path, and it is activated when our application starts up. The redirectTo property tells Angular to redirect to the capture path, which will load the page we created.
Tip
You can also remove the folder/:id path as it is no longer needed, and the whole folder module from the application, which is part of the template layout.
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-menu-button color="primary">
</ion-menu-button>
</ion-buttons>
<ion-title>Take a photo</ion-title>
</ion-toolbar>
</ion-header>
The ion-toolbar element is part of the ion-header element, which is the top navigation bar of the page. It contains an ion-menu-button element for toggling the main menu of the application and an ion-title element that depicts the title of the page.
<ion-content>
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">Take a photo</ion-title>
</ion-toolbar>
</ion-header>
</ion-content>
The header will be displayed when the page is expanded and the main menu is displayed on the screen. The size attribute of the ion-title element is set to large for supporting collapsible large tiles on iOS devices.
<div id="container">
<strong class="capitalize">Take a nice photo with
your camera</strong>
<ion-fab vertical="center" horizontal="center"
slot="fixed">
<ion-fab-button>
<ion-icon name="camera"></ion-icon>
</ion-fab-button>
</ion-fab>
</div>
It contains an ion-fab-button element, which, when clicked, will open the camera of the device to take a photo.
#container {
text-align: center;
position: absolute;
left: 0;
right: 0;
top: 50%;
transform: translateY(-50%);
}
#container strong {
font-size: 20px;
line-height: 26px;
}
#container ion-fab {
margin-top: 60px;
}
Let's run the application using ionic serve to get a quick preview of what we have built so far:
The camera button on the page needs to open the camera to take a photo. In the following section, we will learn how to use Capacitor to interact with the camera.
Taking photos in our application involves using two APIs from the Capacitor library. The Camera API will open the camera to take a photo, and the Geolocation API will read the current location from the GPS. Let's see how we can use both in our application:
npm install @capacitor/camera @capacitor/geolocation
ionic generate service photo
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
import { Geolocation } from '@capacitor/geolocation';
private async getLocation() {
const location = await
Geolocation.getCurrentPosition();
return location.coords;
}
The getCurrentPosition method of the Geolocation object contains a coords property with various location-based data such as the latitude and the longitude.
async takePhoto() {
await this.getLocation();
await Camera.getPhoto({
resultType: CameraResultType.DataUrl,
source: CameraSource.Camera,
quality: 100
});
}
We use the getPhoto method of the Camera object and pass a configuration object to define the properties for each photo. The resultType property indicates that the photo will be in a data URL format to easily save it later to the cloud. The source property indicates that we will use the camera device to get the photo, and the quality property defines the quality of the actual photo.
import { Component, OnInit } from '@angular/core';
import { PhotoService } from '../photo.service';
@Component({
selector: 'app-capture',
templateUrl: './capture.page.html',
styleUrls: ['./capture.page.scss'],
})
export class CapturePage implements OnInit {
constructor(private photoService: PhotoService) { }
ngOnInit() {
}
}
openCamera() {
this.photoService.takePhoto();
}
<ion-fab vertical="center" horizontal="center" slot="fixed">
<ion-fab-button (click)="openCamera()">
<ion-icon name="camera"></ion-icon>
</ion-fab-button>
</ion-fab>
We have now added all the necessary pieces to take a photo using the camera of the device. Let's try to run the application on a real device to test the interaction with the camera:
ionic build
The preceding command will create a www folder in the root folder of your project that contains your application bundle.
npm install @capacitor/<os>
In the previous command, the <os> parameter can be either android or ios. If your application is targeted at both platforms, you must execute the command twice.
ionic cap add <os>
The cap command is the executable file of the Capacitor library.
The command will create one folder for each platform and will also add the required npm package in the dependencies section of the package.json file.
Important note
You must add the specific platform folder to your source control for it to be available to the rest of the project.
ionic cap open <os>
In the previous command, <os> can be either android or ios. Upon execution, it will open the native mobile project in the respective IDE, Android Studio or Xcode, depending on the platform that you are targeting. The IDE must then be used to run the native application.
Important note
Every time you re-build the application, you need to run the npx cap copy command to copy the application bundle from the www folder into the native mobile project.
Important note
You may need to add additional permissions in the native mobile project of your development environment. Check the respective documentation of the APIs on the Capacitor website.
The first page of our application now has a sleek interface that allows the user to interact with the camera. We have also created an Angular service that ensures a seamless interaction with Capacitor to get location-based data and take photos. In the following section, we will see how to save them in the cloud using Firebase.
The application will be able to store photos and their location in Firebase. We will use the Storage service to upload our photos and the Cloud Firestore database to keep their location. We will further expand our application in the following tasks:
First, we need to set up a new Firebase project for our application.
We can set up and configure a Firebase project using the Firebase console at https://console.firebase.google.com:
Important note
Firebase generates a unique identifier for your project, which is located underneath the project name and is used in various Firebase services.
Click on the third option with the code icon to add Firebase to a web application.
var firebaseConfig = {
apiKey: "<Your API key>",
authDomain: "<Your project auth domain>",
projectId: "<Your project ID>",
storageBucket: "<Your storage bucket>",
messagingSenderId: "<Your messaging sender ID>",
appId: "<Your application ID>"
};
Take a note of the firebaseConfig object and click the Continue to console button.
Tip
The Firebase configuration can also be accessed later at https://console.firebase.google.com/project/<project-id>/settings/general where project-id is the ID of your Firebase project.
Choosing a mode is nothing less than setting rules for your database. Test mode allows faster setup and keeps your data public for 30 days. When you are ready to move your application into production, you can modify the rules of your database accordingly to make your data private.
Congratulations! You have created a new Cloud Firestore database. In the next section, we will learn how to put the new database into saving data with our mobile application.
The AngularFire library is an Angular library that we can use in an Angular application to interact with Firebase family products such as Cloud Firestore and the Storage service. To install it in our application, run the following command of the Angular CLI:
ng add @angular/fire
The preceding command will ask you to select the Firebase project that we created in the previous section and will modify the structure of the application accordingly to use AngularFire. Let's see now how we can use the AngularFire library in our application:
export const environment = {
production: false,
firebaseConfig: {
apiKey: '<Your API key>',
authDomain: '<Your project auth domain>',
projectId: '<Your project ID>',
storageBucket: '<Your storage bucket>',
messagingSenderId: '<Your messaging sender ID>',
appId: '<Your application ID>'
}
};
The environment.ts file is used when we are running our application in development mode.
import { AngularFireModule } from '@angular/fire';
import { AngularFirestoreModule } from '@angular/fire/firestore';
import { AngularFireStorageModule } from '@angular/fire/storage';
import { environment } from '../environments/environment';
AngularFireModule is the main module of the AngularFire library. AngularFirestoreModule is a specific module of AngularFire that we need when working with Cloud Firestore databases. AngularFireStorageModule is a module that we can use to interact with the Storage service.
In the preceding code, we also import the environment object that contains the Firebase configuration.
Important note
We import the environment object from the development environment file and not the production one. The Angular framework is smart enough to understand which environment we are currently running our application in and use the appropriate file during runtime.
imports: [BrowserModule, IonicModule.forRoot(),
AppRoutingModule,
AngularFireModule.initializeApp
(environment.firebaseConfig),
AngularFirestoreModule,
AngularFireStorageModule
]
In the preceding code, we use the initializeApp method of the AngularFireModule class to register our application with our Firebase project.
ionic generate interface photo
export interface Photo {
url: string;
lat: number;
lng: number;
}
The url property will be the URL of the actual photo, and the lat/lng properties represent the latitude and longitude of the current location.
import { AngularFirestore } from '@angular/fire/firestore';
import { AngularFireStorage } from '@angular/fire/storage';
import { Photo } from './photo';
The AngularFirestore service contains all the necessary methods that we will need to interact with our Cloud Firestore database. The AngularFireStorage service contains methods for uploading files to the Storage service.
constructor(private firestore: AngularFirestore, private storage: AngularFireStorage) {}
private async savePhoto(dataUrl: string, latitude: number, longitude: number) {
const name = new
Date().getUTCMilliseconds().toString();
const upload = await
this.storage.ref(name).putString(dataUrl,
'data_url');
const photoUrl = await upload.ref.getDownloadURL();
await
this.firestore.collection<Photo>('photos').add({
url: photoUrl,
lat: latitude,
lng: longitude
});
}
First, we create a random name for our photo and use the putString method of the storage variable to upload it to Firebase storage. As soon as uploading has been completed, we get a downloadable URL using the getDownloadURL method, which can be used to access that photo. Finally, we use the add method to add a new Photo object in the collection property of the firestore variable. We use the collection property because we want to work with a list of photos in our application.
Tip
The firestore variable also contains a doc property that can be used when we want to work with single objects. The collection property internally keeps a list of doc objects.
async takePhoto() {
const {latitude, longitude} = await
this.getLocation();
const cameraPhoto = await Camera.getPhoto({
resultType: CameraResultType.DataUrl,
source: CameraSource.Camera,
quality: 100
});
await this.savePhoto(cameraPhoto.dataUrl, latitude,
longitude);
}
We are now ready to check the full functionality of the photo-shooting process:
ionic build
ionic cap copy
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write;
}
}
}
In the preceding screenshot, the file named 669 is the physical file of the photo that you have taken.
In the preceding screenshot, the 1oFxxWgQseIwqWUrYBkN entry is the logical object of the photo that contains the URL of the actual file and its location data.
The first page of our application is now feature-complete. We have gone through the full process of taking a photo and uploading it to the cloud along with its location data. We started by setting up and configuring a Firebase project and finished by learning how to use the AngularFire library to interact with that project. In the next section, we will reach our final destination by implementing the second page of our application.
The next feature of our application will be to display all the photos that we have taken with the camera on a 3D map. The CesiumJS library provides a viewer with a 3D globe that we can use to visualize various things, such as images in specific locations. This new feature of our application will consist of the following:
We will begin by learning how to set up the CesiumJS library.
The CesiumJS library is an npm package that we can install to start working with 3D maps and visualizations:
npm install cesium
"assets": [
{
"glob": "**/*",
"input": "src/assets",
"output": "assets"
},
{
"glob": "**/*.svg",
"input":
"node_modules/ionicons/dist/ionicons/svg",
"output": "./svg"
},
{
"glob": "**/*",
"input": "node_modules/cesium/Build/Cesium",
"output": "/assets/cesium"
}
]
The preceding entry will copy runtime all CesiumJS source files into a cesium folder inside the assets folder of our application.
"styles": [
"node_modules/cesium/Build/Cesium/Widgets/
widgets.css",
"src/theme/variables.scss",
"src/global.scss"
]
The viewer of CesiumJS contains a toolbar with widgets, including a search bar and a dropdown for selecting a specific type of map, such as Bing Maps or Mapbox.
// eslint-disable-next-line @typescript-eslint/dot-
// notation
window['CESIUM_BASE_URL'] = '/assets/cesium/';
The CESIUM_BASE_URL global variable indicates the location of the CesiumJS source files.
npm install -D @angular-builders/custom-webpack
A builder is an Angular library that extends the default functionality of the Angular CLI. The @angular-builders/custom-webpack builder allows us to provide an additional webpack configuration file while building our application. It is beneficial in cases where we want to include additional webpack plugins or override existing functionality.
module.exports = {
resolve: {
fallback: {
fs: "empty",
Buffer: false,
http: "empty",
https: "empty",
zlib: "empty"
}
},
module: {
unknownContextCritical: false
}
};
The configuration file will ensure that webpack will not try to load CesiumJS code that cannot understand. CesiumJS uses modules in a format that cannot be statically analyzed from webpack.
"builder": "@angular-builders/custom-webpack:browser"
"customWebpackConfig": {
"path": "./extra-webpack.config.js"
}
"serve": {
"builder": "@angular-builders/custom-webpack:
dev-server",
"options": {
"browserTarget": "app:build"
},
"configurations": {
"production": {
"browserTarget": "app:build:production"
},
"ci": {
"progress": false
}
}
}
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": ["cesium"]
}
Now that we have completed the configuration of the CesiumJS library, we can start creating the page for our feature:
ionic generate page view
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-menu-button color="primary">
</ion-menu-button>
</ion-buttons>
<ion-title>View gallery</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<div #mapContainer></div>
</ion-content>
#mapContainer is a template reference variable and we use it to declare an alias for an element in our template.
div {
height: 100%;
width: 100%;
}
import { AfterViewInit, Component, OnInit, ElementRef, ViewChild } from '@angular/core';
import { Viewer } from 'cesium';
@Component({
selector: 'app-view',
templateUrl: './view.page.html',
styleUrls: ['./view.page.scss'],
})
export class ViewPage implements OnInit, AfterViewInit {
@ViewChild('mapContainer') content: ElementRef;
constructor() { }
ngOnInit() {
}
ngAfterViewInit() {
const viewer = new
Viewer(this.content.nativeElement);
}
}
We create a new Viewer object inside the ngAfterViewInit method of the component. The ngAfterViewInit method is called when the view of the component has finished loading, and it is defined in the AfterViewInit interface. The constructor of the Viewer class accepts as a parameter the native HTML element on which we want to create the viewer. In our case, we want to attach it to the map container element that we created earlier. Thus, we use the @ViewChild decorator to reference that element by passing the template reference variable name as a parameter.
Tip
If the map on the viewer is not displayed correctly, try to select a different provider from the map button on the viewer toolbar, next to the question button.
We have now successfully configured the CesiumJS library in our application. In the next section, we will see how to benefit from it and display our photos on the 3D globe of the CesiumJS viewer.
The next thing that we need to do for our application to be ready is to display our photos on the map. We will get all the photos from Firebase and add them to the viewer in the specified locations. Let's see how we can accomplish that:
ionic generate service cesium
import { AngularFirestore } from '@angular/fire/firestore';
import { Cartesian3, Color, PinBuilder, Viewer } from 'cesium';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Photo } from './photo';
export class CesiumService {
private viewer: Viewer;
constructor(private firestore: AngularFirestore) { }
}
register(viewer: Viewer) {
this.viewer = viewer;
}
private getPhotos(): Observable<Photo[]> {
return this.firestore.collection<Photo>('photos'). snapshotChanges().pipe(
map(actions => actions.map(a => a.payload.doc.data() as Photo))
);
}
In the preceding method, we call the snapshotChanges method to get the data of the photos collection. We have already learned that a collection consists of doc objects. Thus, we can reach the actual photo object using the data method on the doc property for each action object.
addPhotos() {
const pinBuilder = new PinBuilder();
this.getPhotos().subscribe(photos => {
photos.forEach(photo => {
const entity = {
position: Cartesian3.fromDegrees(photo.lng,
photo.lat),
billboard: {
image: pinBuilder.fromColor
(Color.fromCssColorString('#de6b45'),
48).toDataURL()
},
description: `<img width="100%"
style="margin:auto; display: block;"
src="${photo.url}" />`
};
this.viewer.entities.add(entity);
});
});
}
The location of each photo on the viewer will be displayed as a pin. Thus, we need to initialize a PinBuilder object first. The preceding method subscribes to the getPhotos method to get all photos from Cloud Firestore. For each photo, it creates an entity object that contains the position, which is the location of the photo in degrees, and a billboard property that displays a pin of 48 pixels in size. It also defines a description property that will display the actual image of the photo when we click on the pin.
Each entity object is added to the entities collection of viewer using its add method.
.cesium-infoBox, .cesium-infoBox-iframe {
height: 100% !important;
width: 100%;
}
import { AfterViewInit, Component, OnInit, ElementRef, ViewChild } from '@angular/core';
import { Viewer } from 'cesium';
import { CesiumService } from '../cesium.service';
@Component({
selector: 'app-view',
templateUrl: './view.page.html',
styleUrls: ['./view.page.scss'],
})
export class ViewPage implements OnInit, AfterViewInit {
@ViewChild('mapContainer') content: ElementRef;
constructor(private cesiumService: CesiumService) {}
ngOnInit() {
}
ngAfterViewInit() {
const viewer = new
Viewer(this.content.nativeElement);
}
}
ngAfterViewInit() {
this.cesiumService.register(new
Viewer(this.content.nativeElement));
this.cesiumService.addPhotos();
}
We are now set to view our photos on the map:
We now have a complete mobile application for taking geotagged photos and displaying them on a map. We saw how to set up the CesiumJS library and get our photos from Cloud Firestore. The API of the CesiumJS viewer provided us with an easy way to visualize our photos on the map and interact with them.
In this chapter, we built a mobile application for taking photos, tagging them with the current location, and displaying them on a 3D map. Initially, we learned how to create a new mobile application using the Ionic framework. We built the application locally, and then we integrated Capacitor to interact with the camera and the GPS device. The camera was used to take photos and the GPS to mark them with the location.
Later on, we used Firebase services to store our photo files and data in the cloud. Finally, we learned how to retrieve the stored photos from Firebase and displayed them on a 3D globe using the CesiumJS library.
In the next chapter, we will investigate another way to prerender content in Angular. We will use server-side rendering techniques to create a GitHub portfolio website.
3.138.114.38