Desenvolvimento

22 fev, 2018

Brincando com Ionic, Lumen, Firebase, Google Maps, Raspberry Pi e geolocalização de background

Publicidade

Eu quero fazer um projeto simples de estimação. A ideia é criar um aplicativo móvel. Esse aplicativo acompanhará minha localização do GPS e enviará essas informações para um banco de dados Firebase. Eu nunca brinquei com Firebase e quero aprender um pouco.

Com essa informação, vou criar um aplicativo web simples, hospedado no meu Raspberry Pi. Esse aplicativo web mostrará um mapa do Google com a minha última localização. Eu colocarei esse aplicativo web na minha TV, e qualquer pessoa na minha casa poderá sempre ver onde eu estou.

Essa é a ideia. Eu quero um MVP. Primeiro, o aplicativo móvel. Eu usarei o framework Ionic, do qual sou grande fã.

O aplicativo móvel é muito simples. Só tem uma alternância para ativar-desativar a geolocalização de background (às vezes eu não quero ser rastreado).

<ion-header>
    <ion-navbar>
        <ion-title>
            Ionic Blank
        </ion-title>
    </ion-navbar>
</ion-header>
 
<ion-header>
    <ion-toolbar [color]="toolbarColor">
        <ion-title>{{title}}</ion-title>
        <ion-buttons end>
            <ion-toggle color="light"
                        checked="{{isBgEnabled}}"
                        (ionChange)="changeWorkingStatus($event)">
            </ion-toggle>
        </ion-buttons>
    </ion-toolbar>
</ion-header>
 
<ion-content padding>
</ion-content>

E o controller:

import {Component} from '@angular/core';
import {Platform} from 'ionic-angular';
import {LocationTracker} from "../../providers/location-tracker/location-tracker";
 
@Component({
    selector: 'page-home',
    templateUrl: 'home.html'
})
export class HomePage {
    public status: string = localStorage.getItem('status') || "-";
    public title: string = "";
    public isBgEnabled: boolean = false;
    public toolbarColor: string;
 
    constructor(platform: Platform,
                public locationTracker: LocationTracker) {
 
        platform.ready().then(() => {
 
                if (localStorage.getItem('isBgEnabled') === 'on') {
                    this.isBgEnabled = true;
                    this.title = "Working ...";
                    this.toolbarColor = 'secondary';
                } else {
                    this.isBgEnabled = false;
                    this.title = "Idle";
                    this.toolbarColor = 'light';
                }
        });
    }
 
    public changeWorkingStatus(event) {
        if (event.checked) {
            localStorage.setItem('isBgEnabled', "on");
            this.title = "Working ...";
            this.toolbarColor = 'secondary';
            this.locationTracker.startTracking();
        } else {
            localStorage.setItem('isBgEnabled', "off");
            this.title = "Idle";
            this.toolbarColor = 'light';
            this.locationTracker.stopTracking();
        }
    }
}

Como você pode ver, o botão de alternância irá ativar-desativar a geolocalização de background e também alterar a cor de fundo da barra de ferramentas.

Para a geolocalização de background, usarei um plugin cordova disponível como plugin ionic nativo.

Aqui você pode ver um artigo muito legal, explicando como usar o plugin com Ionic. Como o artigo explica, eu criei um provedor.

import {Injectable, NgZone} from '@angular/core';
import {BackgroundGeolocation} from '@ionic-native/background-geolocation';
import {CONF} from "../conf/conf";
 
@Injectable()
export class LocationTracker {
    constructor(public zone: NgZone,
                private backgroundGeolocation: BackgroundGeolocation) {
    }
 
    showAppSettings() {
        return this.backgroundGeolocation.showAppSettings();
    }
 
    startTracking() {
        this.startBackgroundGeolocation();
    }
 
    stopTracking() {
        this.backgroundGeolocation.stop();
    }
 
    private startBackgroundGeolocation() {
        this.backgroundGeolocation.configure(CONF.BG_GPS);
        this.backgroundGeolocation.start();
    }
}

A ideia do plugin é enviar uma solicitação POST para uma URL com os dados do GPS no corpo da solicitação. Então, vou criar um servidor api web para lidar com essa solicitação. Usarei meu Raspberry Pi3 para atender ao aplicativo. Vou criar um aplicativo PHP/Lumen simples. Esse aplicativo irá lidar com a solicitação POST do aplicativo móvel e também servirá uma página HTML com o mapa (usando o Google Maps).

As solicitações móveis serão autenticadas com um token no cabeçalho, e o aplicativo web usará uma autenticação HTTP básica. Por isso, vou criar dois middlewares para lidar com as diferentes maneiras de autenticar.

<?php
require __DIR__ . '/../vendor/autoload.php';
 
use App\Http\Middleware;
use App\Model\Gps;
use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Http\Request;
use Laravel\Lumen\Application;
use Laravel\Lumen\Routing\Router;
 
(new Dotenv\Dotenv(__DIR__ . '/../env/'))->load();
 
$app = new Application(__DIR__ . '/..');
$app->singleton(ExceptionHandler::class, App\Exceptions\Handler::class);
$app->routeMiddleware([
    'auth'  => Middleware\AuthMiddleware::class,
    'basic' => Middleware\BasicAuthMiddleware::class,
]);
 
$app->router->group(['middleware' => 'auth', 'prefix' => '/locator'], function (Router $route) {
    $route->post('/gps', function (Gps $gps, Request $request) {
        $requestData = $request->all();
        foreach ($requestData as $poi) {
            $gps->persistsData([
                'date'             => date('YmdHis'),
                'serverTime'       => time(),
                'time'             => $poi['time'],
                'latitude'         => $poi['latitude'],
                'longitude'        => $poi['longitude'],
                'accuracy'         => $poi['accuracy'],
                'speed'            => $poi['speed'],
                'altitude'         => $poi['altitude'],
                'locationProvider' => $poi['locationProvider'],
            ]);
        }
 
        return 'OK';
    });
});
 
return $app;

Como podemos ver, rout/locator/gps irá lidar com a solicitação POST. Eu criei um modelo para persistir os dados do GPS no banco de dados do Firebase:

<?php
 
namespace App\Model;
 
use Kreait\Firebase\Factory;
use Kreait\Firebase\ServiceAccount;
 
class Gps
{
    private $database;
 
    private const FIREBASE_CONF = __DIR__ . '/../../conf/firebase.json';
 
    public function __construct()
    {
        $serviceAccount = ServiceAccount::fromJsonFile(self::FIREBASE_CONF);
        $firebase       = (new Factory)
            ->withServiceAccount($serviceAccount)
            ->create();
 
        $this->database = $firebase->getDatabase();
    }
 
    public function getLast()
    {
        $value = $this->database->getReference('gps/poi')
            ->orderByKey()
            ->limitToLast(1)
            ->getValue();
 
        $out                 = array_values($value)[0];
        $out['formatedDate'] = \DateTimeImmutable::createFromFormat('YmdHis', $out['date'])->format('d/m/Y H:i:s');
 
        return $out;
    }
 
    public function persistsData(array $data)
    {
        return $this->database
            ->getReference('gps/poi')
            ->push($data);
    }
}

O projeto está quase concluído. Agora só precisamos criar o mapa do Google.

Aqui está a API:

<?php
$app->router->group(['middleware' => 'basic', 'prefix' => '/map'], function (Router $route) {
    $route->get('/', function (Gps $gps) {
        return view("index", $gps->getLast());
    });
 
    $route->get('/last', function (Gps $gps) {
        return $gps->getLast();
    });
});

E o HTML:

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="initial-scale=1.0, user-scalable=no">
    <meta charset="utf-8">
    <title>Locator</title>
    <style>
        #map {
            height: 100%;
        }
 
        html, body {
            height: 100%;
            margin: 0;
            padding: 0;
        }
    </style>
</head>
<body>
<div id="map"></div>
<script>
 
    var lastDate;
    var DELAY = 60;
 
    function drawMap(lat, long, text) {
        var CENTER = {lat: lat, lng: long};
        var contentString = '<div id="content">' + text + '</div>';
        var infowindow = new google.maps.InfoWindow({
            content: contentString
        });
        var map = new google.maps.Map(document.getElementById('map'), {
            zoom: 11,
            center: CENTER,
            disableDefaultUI: true
        });
 
        var marker = new google.maps.Marker({
            position: CENTER,
            map: map
        });
        var trafficLayer = new google.maps.TrafficLayer();
 
        trafficLayer.setMap(map);
        infowindow.open(map, marker);
    }
 
    function initMap() {
        lastDate = '{{ $formatedDate }}';
        drawMap({{ $latitude }}, {{ $longitude }}, lastDate);
    }
 
    setInterval(function () {
        fetch('/map/last', {credentials: "same-origin"}).then(function (response) {
            response.json().then(function (data) {
                if (lastDate !== data.formatedDate) {
                    drawMap(data.latitude, data.longitude, data.formatedDate);
                }
            });
        });
    }, DELAY * 1000);
</script>
<script async defer src="https://maps.googleapis.com/maps/api/js?key=my_google_maps_key&callback=initMap">
</script>
</body>
</html>

E isso é o suficiente! O código-fonte está disponível na minha conta do Github.

***

Gonzalo Ayuso faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela Redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: https://gonzalo123.com/2018/02/19/playing-with-ionic-lumen-firebase-google-maps-raspberry-pi-and-background-geolocation/