Desenvolvimento

28 out, 2016

GeoFire e conceitos de geolocalização – Parte 01

Publicidade

Vamos começar com um pequeno problema. Possuo uma base de dados com alguns poucos estabelecimentos cadastrados. Certo, vamos começar com 5 estabelecimentos.

Então, preciso desenvolver uma feature em meu app, na qual vou visualizar no mapa estabelecimentos que estão próximos a mim.

Fácil, não? Dado que cada estabelecimento em minha lista possui um “GeoPoint” (latitude e longitude), basta eu carregar minha lista de estabelecimentos para o app e sempre que eu mudar minha localização verifico item a item, calculando a distância entre o estabelecimento e minha localização e, por fim, verificar se a distância é menor que a esperada.

Teríamos algo assim:

for (Establishment *item in self.listEstablishment) {
    if ([self getDistance:item.location] <= 500) {
        [self addAnnotationForItem:item];
    }
}

Bem, supondo que nossa lista de estabelecimentos não passe de cinco, até que pode ser uma solução razoável e talvez resolva o problema.

Mas vamos sair do abstrato e entrar no mundo real. Pensando bem baixo, em um raio de cinco metros, nós já teríamos cinco estabelecimentos. Talvez mais, se houver dois ou mais que compartilhem o mesmo prédio/localização. Tendo isso como base, em 500 metros, nossa lista já aumentaria para algo em torno de 2 mil estabelecimentos.

Logo, sempre que eu mudar minha localização no mapa, eu faria 2 mil verificações. Levando em consideração que, mesmo eu estando parado, o método “didUpdateUserLocation” é chamado a cada 1,5 segundo; em apenas 10 segundos, já teríamos feito quase 20 mil verificações.

É, já deu pra notar que essa não é uma boa solução para o nosso problema. Isso que nossa base de dados está apenas começando. Esses 2 mil estabelecimentos facilmente se transformam em 15, 50, 100, 300, 500 mil estabelecimentos.

What can I do?

GeoFire

Lib open source do Firebase, criada com o propósito de permitir consultas à base de dados com base em sua geolocalização.

Sim, de forma simples e prática, o GeoFire cria uma query e um observer para que sempre que sua localização for alterada você receba uma lista de itens (estabelecimentos) que estejam localizados a um raio X de sua localização geográfica.

Mas como isso funciona?

GeoQuery

GeoQuery é um objeto da lib GeoFire que recebe a sua localização atual e o raio (em km) de consulta.

GFCircleQuery *circleQuery = [self.geoFire queryAtLocation:centerLocation withRadius:distance];

GFCircleQuery é uma subclasse de GFQuery (GeoQuery) que contém duas properties:

//
//  GFCircleQuery.h
//  GeoFire
//
//  Created by Jonny Dimond on 7/11/14.
//  Copyright (c) 2016 Firebase. All rights reserved.
//

@interface GFCircleQuery : GFQuery

/**
 * The center of the search area. Update this value to update the query. Events are triggered for any keys that move
 * in or out of the search area.
 */
@property (atomic, readwrite) CLLocation *center;

/**
 * The radius of the geo query in kilometers. Update this value to update the query. 
Events are triggered for any keys
 * that move in or out of the search area.
 */
@property (atomic, readwrite) double radius;

@end

Uma vez instanciado, precisamos adicionar um observer à nossa query para que sempre que um item for adicionado ao snapshot de nossa base de dados um evento seja disparado para que possamos executar alguma ação, como adicionar um annotation em nosso mapa.

Quem já trabalhou com Firebase sabe que, quando instanciado, ele cria um snapshot de sua base de dados dentro da aplicação. Esse snapshot nada mais é que um espelho da sua estrutura de dados, só que vazia, na qual ele trabalha apenas com as informações que sua aplicação precisa.

A lib GeoFire possui a mesma abordagem e, para isso, trabalha com três tipos de evento:

  • GFEventTypeKeyEntered: Quando uma key é adicionada ao snapshot
  • GFEventTypeKeyExited: Quando uma key é removida do snapshot
  • GFEventTypeKeyMoved: Quando alguma informação da key presente no snapshot é alterada

Voltando à nossa aplicação, uma vez que criamos uma referência da nossa base na aplicação e instanciamos nossa query, vamos pedir para a GeoQuery observar esses três eventos:

//ADD
[query observeEventType:GFEventTypeKeyEntered withBlock:^(NSString *key, CLLocation *location) {
        
    if (key && location) {
        //Recebemos uma key que referencia um item em nossa base e recebemos um location com as coordenadas deste item
        //A partir daí, tratamos neste bloco da forma que nossa aplicação espera
    }
}];
//REMOVE
[query observeEventType:GFEventTypeKeyExited withBlock:^(NSString *key, CLLocation *location) {
        
    if (key && location) {
        //Recebemos uma key que referencia um item em nossa base e recebemos um location com as coordenadas deste item
        //A partir daí, tratamos neste bloco da forma que nossa aplicação espera
    }
}];
//UPDATE
[query observeEventType:GFEventTypeKeyMoved withBlock:^(NSString *key, CLLocation *location) {
        
    if (key && location) {
        //Recebemos uma key que referencia um item em nossa base e recebemos um location com as coordenadas deste item
        //A partir daí, tratamos neste bloco da forma que nossa aplicação espera
    }
}];

GFEventTypeKeyEntered

O observer do tipo Entered avisa sempre que uma “key” é adicionada ao nosso snapshot.

Vamos pensar no seguinte cenário. Estou localizado no ponto (x, y) e quero buscar todos os estabelecimentos que estejam dentro de um raio de 500 metros. Sempre que a minha query encontrar um elemento com essa característica, a minha aplicação será avisada por meio do bloco de retorno do observer que criei para o tipo Entered.

Com isso, posso facilmente criar um annotation e adicioná-lo ao mapa.

GFEventTypeKeyExited

Agora (após eu ter adicionado os estabelecimentos ao mapa), vamos imaginar que eu mudei minha localização, me deslocando alguns metros.

Algumas keys que foram adicionadas já não estarão dentro do raio de 500 metros de minha localização, então elas são removidas do meu snapshot.

Ao serem removidas, o observer do tipo Exited me avisa por meio de seu bloco de retorno que uma key foi removida.

Com isso, posso facilmente remover o annotation que havia adicionado anteriormente. Da mesma forma, ao tempo que keys são removidas novas são adicionadas, e assim segue o lifecycle da minha aplicação.

GFEventTypeKeyMoved

Por último, não menos importante, temos o observer do tipo Moved.

Vamos usar como exemplo o aplicativo Waze.

Estou em minha rota e durante o trajeto consigo visualizar meus amigos que estão próximos a mim. Para visualizar, os observers do tipo Entered e Exited são disparados, adicionando e removendo annotations referentes aos meus amigos que estão próximos.

Entretanto, assim como eu, meus amigos também estão se movimentando e atualizando suas geolocalizações. Com isso, sempre que alguma key tem algum atributo atualizado, esse dado também é atualizado em meu snapshot, fazendo o meu observer avisar que a key recebeu uma nova localização.

Desta vez, em vez de adicionar ou remover, eu vou atualizar as coordenadas da minha annotation.

Simples, não?

Sim, assim como o Firebase, o GeoFire possui features muito interessantes e de simples implementação.

A praticidade do Firebase, aliada às características de consulta do GeoFire, forma uma estrutura de backend muito forte, quando precisamos trabalhar com geolocalização em real time.

Mas, e se eu não quiser utilizar nada disso?
Se eu tiver minha própria estrutura de backend e não quiser ficar preso ao Firebase e suas libs?

Nesae caso, vamos precisar olhar um pouco mais profundamente para alguns conceitos de geolocalização que você poderá conferir na segunda parte deste artigo.

Dúvidas e sugestões, postem nos comentários ?. O sample utilizado no artigo pode ser encontrado neste repositório no meu GitHub.

***

Artigo publicado originalmente em http://www.concretesolutions.com.br/2016/10/24/geofire-e-geolocalizacao-1/.