Universidade Federal do Rio de Janeiro Escola Polit ecnica...
-
Upload
truongtram -
Category
Documents
-
view
213 -
download
0
Transcript of Universidade Federal do Rio de Janeiro Escola Polit ecnica...
Universidade Federal do Rio de Janeiro
Escola Politecnica
Departamento de Eletronica e de Computacao
Rede Social de Caronas
Autor:
Guilherme Murad Pim
Orientador:
Carlos Jose Ribas D’Avila
Examinador:
Aloysio de Castro Pinto Pedroza
Examinador:
Antonio Claudio Gomez de Sousa
DEL
Marco de 2014
UNIVERSIDADE FEDERAL DO RIO DE JANEIRO
Escola Politecnica - Departamento de Eletronica e de Computacao
Centro de Tecnologia, bloco H, sala H-217, Cidade Universitaria
Rio de Janeiro - RJ CEP 21949-900
Este exemplar e de propriedade da Universidade Federal do Rio de Janeiro, que
podera incluı-lo em base de dados, armazenar em computador, microfilmar ou adotar
qualquer forma de arquivamento.
E permitida a mencao, reproducao parcial ou integral e a transmissao entre bibli-
otecas deste trabalho, sem modificacao de seu texto, em qualquer meio que esteja
ou venha a ser fixado, para pesquisa academica, comentarios e citacoes, desde que
sem finalidade comercial e que seja feita a referencia bibliografica completa.
Os conceitos expressos neste trabalho sao de responsabilidade do(s) autor(es) e
do(s) orientador(es).
ii
DEDICATORIA
Gostaria de dedicar este trabalho a meus familiares, que me auxiliaram durante
toda minha vida. Dedico tambem a meus amigos, que sempre tornaram momentos
difıceis em momentos mais leves e muitas vezes proveitosos.
iii
AGRADECIMENTO
Agradeco a minha famılia, por toda sua compreensao e forca durante essa jor-
nada. Alem dela, agradeco a meus professores, sem os quais nao haveria agregado
o conhecimento apresentado neste trabalho. Agradeco tambem a meus amigos, que
tornam mais leves os desafios no caminho. Agradeco igualmente ao povo brasileiro,
que investiu na minha educacao atraves desta faculdade publica.
iv
RESUMO
Este trabalho trata do desenvolvimento de uma rede social de caronas em forma
de aplicativo para Web. A ferramenta e totalmente automatizada e integrada com
o Google Maps.
Usuarios entram na rede atraves do acesso com a rede social Facebook e entao
cadastram seus horarios e encontram amigos e amigos de amigos que podem com-
partilhar o transporte.
Palavras-Chave: caronas, sistemas distribuıdos, aplicativos web, nosql, node
v
ABSTRACT
This paper addresses the development of a ridesharing social network in the form
of a web application. This tool is totally integrated with Google Maps.
Users login in the network through Facebook and then register their schedules.
They then find friends and friends of friends who may share a ride.
Key-words: ridesharing, distributed systems, web applications, nosql, node
vi
SIGLAS
UFRJ Universidade Federal do Rio de Janeiro
PUC Pontifıcia Universidade Catolica
URL Uniform Resource Locator
HTTP Hypertext Transfer Protocol
HTTPS Hypertext Transfer Protocol Secure
query string Parametros em uma URL
TCP/IP Transmission Control Protocol / Internet Protocol
API Application Programming Interface - definicoes publicas para que desenvolve-
dores possam criar programas usando um dado servico
Node Software interpretador de JavaScript
Redis Software de servidor de memoria compartilhada
MongoDB Software de banco de dados nao-relacional
MySQL Software de banco de dados relacional
Google Provedor de servicos na internet gigante, como busca, email e mapas
Google Maps Software de mapas internacional do Google
Directions API API para buscar dados de rotas do Google Maps
Geocoding API API para buscar dados de pontos do Google Maps
Facebook Rede social internacional com bilhoes de usuarios
Facebook Graph API API para realizar consultas no Facebook
WhatsApp Aplicativo de chat para celulares
Lyft Companhia internacional de caronas
vii
Amazon Companhia com solucoes para armazenamento e processamento em nu-
vem
S3 Simples Storage Service, da Amazon - servico de armazenamento de arquivos
estaticos
viii
Sumario
1 Introducao 1
1.1 Motivacao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2 Delimitacao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.3 Solucoes ja existentes . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.3.1 Carona.com.vc . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.3.2 Zaznu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.3.3 Carona Facil . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.4 Proposta . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2 Arquitetura da Aplicacao 8
2.1 Estrutura da Aplicacao . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.2 Arquitetura do Servidor . . . . . . . . . . . . . . . . . . . . . . . . . 8
3 Estrutura de Dados 10
3.1 Escolha do Banco de Dados . . . . . . . . . . . . . . . . . . . . . . . 10
3.2 Arquitetura do Banco de Dados . . . . . . . . . . . . . . . . . . . . . 11
3.2.1 User . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
3.2.2 Friendship . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
3.2.3 UserPreference . . . . . . . . . . . . . . . . . . . . . . . . . . 15
3.2.4 BasePoint . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
3.2.5 BaseRoute . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
3.2.6 RoutePointDistance . . . . . . . . . . . . . . . . . . . . . . . . 19
3.2.7 Schedule . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
3.2.8 Group . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
3.2.9 Ride . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
ix
3.2.10 Notification . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
4 Tarefas Assıncronas 27
4.1 Implementacao do Servidor . . . . . . . . . . . . . . . . . . . . . . . 27
4.2 A estrutura de uma tarefa . . . . . . . . . . . . . . . . . . . . . . . . 28
4.3 Tarefas do Minha Carona . . . . . . . . . . . . . . . . . . . . . . . . 28
4.3.1 baseRoute/syncFromRoute . . . . . . . . . . . . . . . . . . . . 29
4.3.2 baseRoute/processDistances . . . . . . . . . . . . . . . . . . . 34
4.3.3 basePoint/syncFromLocation . . . . . . . . . . . . . . . . . . 35
4.3.4 basePoint/processDistances . . . . . . . . . . . . . . . . . . . 38
4.3.5 user/importData . . . . . . . . . . . . . . . . . . . . . . . . . 40
4.3.6 user/updateMutual . . . . . . . . . . . . . . . . . . . . . . . . 41
4.3.7 user/updateFriends . . . . . . . . . . . . . . . . . . . . . . . . 41
5 Fluxos do Sistema 43
5.1 Login . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
5.1.1 Novos usuarios . . . . . . . . . . . . . . . . . . . . . . . . . . 44
5.1.2 Estabelecendo a sessao . . . . . . . . . . . . . . . . . . . . . . 45
5.2 Criar Horario . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
5.2.1 Novos locais - sincronizacao e processamento . . . . . . . . . . 48
5.2.2 Locais ja existentes . . . . . . . . . . . . . . . . . . . . . . . . 50
5.2.3 Finalizacao . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
5.3 Buscar Motorista para Horario . . . . . . . . . . . . . . . . . . . . . . 51
5.3.1 Filtros de Busca . . . . . . . . . . . . . . . . . . . . . . . . . . 52
5.3.2 Agregacao de Filtros . . . . . . . . . . . . . . . . . . . . . . . 56
5.4 Enviar Pedido de Carona . . . . . . . . . . . . . . . . . . . . . . . . . 56
5.5 Criar Grupo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
5.5.1 Escolhendo o trajeto . . . . . . . . . . . . . . . . . . . . . . . 59
5.5.2 Privacidade do grupo . . . . . . . . . . . . . . . . . . . . . . . 60
5.6 Buscar Passageiros . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
5.6.1 O Mecanismo de Busca . . . . . . . . . . . . . . . . . . . . . . 63
5.7 Enviar Convite de Carona . . . . . . . . . . . . . . . . . . . . . . . . 64
5.8 Compartilhar Grupo . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
x
6 Conclusao 68
6.1 Estudos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
6.1.1 Inconsistencia do banco de dados . . . . . . . . . . . . . . . . 68
6.1.2 Escalabilidade de uma rede social . . . . . . . . . . . . . . . . 69
6.2 Planos futuros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
6.2.1 Tarefas de consistencia . . . . . . . . . . . . . . . . . . . . . . 69
6.2.2 Escalabilidade na nuvem . . . . . . . . . . . . . . . . . . . . . 70
Bibliografia 71
A Tarefas Assıncronas 72
A.1 Implementacao das Tarefas . . . . . . . . . . . . . . . . . . . . . . . . 72
A.1.1 baseRoute/syncFromRoute . . . . . . . . . . . . . . . . . . . . 72
A.1.2 baseRoute/processDistances . . . . . . . . . . . . . . . . . . . 76
A.1.3 basePoint/syncFromLocation . . . . . . . . . . . . . . . . . . 77
A.1.4 basePoint/processDistances . . . . . . . . . . . . . . . . . . . 84
A.1.5 user/importData . . . . . . . . . . . . . . . . . . . . . . . . . 85
A.1.6 user/updateMutual . . . . . . . . . . . . . . . . . . . . . . . . 86
A.1.7 user/updateFriends . . . . . . . . . . . . . . . . . . . . . . . . 86
xi
Lista de Figuras
3.1 Diagrama de Associacoes no banco de dados . . . . . . . . . . . . . . 12
5.1 Interface para login . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
5.2 Notificacao de boas-vindas . . . . . . . . . . . . . . . . . . . . . . . . 45
5.3 Interface para criacao de horarios . . . . . . . . . . . . . . . . . . . . 47
5.4 Horario pendente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
5.5 Busca de motoristas para horario . . . . . . . . . . . . . . . . . . . . 52
5.6 Filtros para busca de motoristas . . . . . . . . . . . . . . . . . . . . . 52
5.7 Filtro de distancias no mapa . . . . . . . . . . . . . . . . . . . . . . . 54
5.8 Interface com informacoes sobre um grupo . . . . . . . . . . . . . . . 56
5.9 Interface para enviar pedidos de carona . . . . . . . . . . . . . . . . . 57
5.10 Pedidos e Convites do horario . . . . . . . . . . . . . . . . . . . . . . 58
5.11 Interface para criar grupos . . . . . . . . . . . . . . . . . . . . . . . . 58
5.12 Diferentes trajetos na criacao de grupos . . . . . . . . . . . . . . . . . 59
5.13 Alerta para usuario sem permissao de ver rota do grupo . . . . . . . . 61
5.14 Ferramenta para buscar passageiros . . . . . . . . . . . . . . . . . . . 61
5.15 Passageiros encontrados para um grupo . . . . . . . . . . . . . . . . . 62
5.16 Interface para enviar convites de carona . . . . . . . . . . . . . . . . . 65
5.17 Pedidos e Convites do horario . . . . . . . . . . . . . . . . . . . . . . 66
5.18 Ferramenta para compartilhamento de grupo . . . . . . . . . . . . . . 67
5.19 Grupo compartilhado no Facebook . . . . . . . . . . . . . . . . . . . 67
xii
Capıtulo 1
Introducao
O Minha Carona trata de uma solucao que visa ajudar pessoas a encontrarem
alternativas de transporte e diminuir o transito da cidade atraves da pratica de
caronas. O sistema e uma rede social que junta amigos e amigos de amigos em
um grupo quando os trajetos forem compatıveis, para que essas pessoas possam
combinar caronas.
1.1 Motivacao
A motivacao para a criacao desta ferramenta foi naturalmente extraıda do cotidi-
ano de um estudante da UFRJ na Ilha do Fundao. O transporte para a faculdade
costuma ser difıcil por causa da falta de opcoes de transporte publico. A saıda e,
muitas vezes, chegar a Cidade Universitaria atraves de um carro proprio. Carro
este que esta frequentemente tripulado com apenas uma pessoa, como nota-se nos
estacionamentos da faculdade. Essa vacancia no veıculo pode ser aproveitada pelo
motorista para ajudar amigos e conhecidos que precisam de uma mao para chegar
a Ilha, e tambem para dividir as despesas do condutor.
1.2 Delimitacao
A motivacao inicial do projeto veio do dia-a-dia na faculdade, mas a carona nao e
uma atividade exclusiva desse nicho, e portanto o Minha Carona nao foi concebido
com a ideia de restringir-se tecnicamente ao escopo universitario. O sistema funciona
1
para a pratica de carona em qualquer lugar do mundo. Isso so foi possıvel usando
um servico de mapas robusto como o Google Maps, para comparar trajetos, e uma
rede social abrangente como o Facebook, para gerenciar cadastros e amizades.
Uma delimitacao de escopo deste projeto e a criacao de um aplicativo Web apenas.
Nao sera contemplada a criacao de aplicativos para celulares, pois o projeto ficaria
muito maior e muito mais complexo.
1.3 Solucoes ja existentes
Ja existem diversas solucoes no mercado nacional para auxiliar a pratica de ca-
ronas. Todas elas foram avaliadas e foi concluıdo que o Minha Carona deve ser
diferente das alternativas principalmente atraves do uso de tecnologia de ponta e do
emprego de muitas funcionalidades nao encontradas nas demais plataformas.
Nesta secao, serao abordadas algumas das solucoes existentes e algumas de suas
caracterısticas interessantes. Os seguintes projetos serao avaliados:
• Carona.com.vc
• Zaznu
• Carona Facil
1.3.1 Carona.com.vc
Esta rede social tambem surgiu em uma Faculdade - a PUC-Rio. O Carona.com.vc
obtem o cadastro do usuario atraves do login com o Facebook, e, em seguida, pede
para que o recem-cadastrado informe seu endereco e algumas informacoes como
se possui WhatsApp e se suporta cigarros. Para concluir o fluxo de cadastro da
plataforma, o internauta precisa indicar que horas deve entrar e sair da faculdade
para cada dia da semana.
Concluıdo o cadastro, o usuario depara-se com uma lista de pessoas que poderiam
pegar caronas e outra lista de pessoas que poderiam dar caronas a ele, para cada dia
2
da semana. Para cada pessoa nessas listas, o usuario pode ver a distancia entre seu
local cadastrado e os locais dos demais. Alem disso, o usuario tambem tem acesso a
quantos amigos em comum ele possui com cada um, e algumas peculiaridades como
se a pessoa tem WhatsApp e se a pessoa suporta cigarro.
O proximo passo e enviar um pedido ou convite de carona para uma das pessoas
da lista. O pedido ou convite de carona e enviado automaticamente atraves de um
email quando voce aperta um botao na lista. Com esse email, os usuarios podem
entao entrar em contato e comecar a combinar caronas.
Pontos positivos
• Cadastro rapido atraves do Facebook
• O sistema e simples (pouca manutencao e facil de usar)
Pontos negativos
• Nao possui aplicativos para celular
• Inflexibilidade de origem e destino (o usuario esta preso a PUC e apenas um
local)
• Inflexibilidade de horarios (o usuario esta preso a um horario de ida e volta
por dia da semana, e a uma semana fixa para sempre)
• Combinacao de trajetos levam em conta apenas os locais cadastrados pelos
usuarios (nao e possıvel saber se existem pessoas no caminho)
• Nao ha filtros de seguranca na listagem de caronas
• O contato entre usuarios e feito atraves de email (pedidos e convites nao podem
ser cancelados)
• Nao ha controle de lotacao para as caronas
3
1.3.2 Zaznu
O Zaznu e uma solucao de caronas que busca promover a carona como uma ma-
neira de ganhar dinheiro - como uma forma de trabalho e possivelmente de sustento.
Em muitos momentos na descricao do aplicativo, a plataforma e comparada com sis-
temas de taxi. A chamada usada no cadastro de motoristas e a seguinte: ”Ganhe
ate R$6000 por mes dando caronas para pessoas maneiras.”. Isso indica tambem
que o alvo da rede e a faixa jovem e ”descolada”.
O modelo comercial adotado pelo Zaznu e baseado no modelo estrangeiro de uma
rede chamada Lyft. Essa ideia tem dado certo la fora ha algum tempo, apesar de di-
versos entraves legais associados a ausencia de licencas para que os motoristas atuem
como uma especie de taxistas. Para se blindar contra problemas judiciais, o modelo
indica que a solucao nao se trata de um servico de transporte, mas de um servico
que simplesmente une pessoas que podem se ajudar atraves de caronas. Assim, a
plataforma nao cobra pela corrida de uma maneira tradicional de pagamentos, pois
teoricamente nao estao oferecendo o servico de trasporte que seria cobrado.
A remuneracao do condutor e feita atraves de ”doacoes”, ao inves de ”pagamen-
tos”, e o usuario que pegou a carona pode decidir exatamente quanto ele deseja dar
a seu motorista - assim como pode nao dar nada. Ao final da corrida, porem, os
usuarios sao avaliados, e aquele que nao ”doou”tera uma nota ruim. Dessa forma,
usuarios que nao pagam bem seus motoristas sao mal avaliados e portanto tem
menos chances de usufruir do transporte no futuro. Para sustentar-se, essa rede
reserva-se o direito de tomar uma parte da doacao para si.
O conceito de carona nessa rede e deturpado, uma vez que os condutores nao estao
auxiliando pessoas no seu dia-a-dia comum, mas sim realizando ”corridas”(como
as de taxi) para busca-los em qualquer canto. A rede, nesse caso, atua como uma
especie de cooperativa de taxi comunitaria e sem regulacao governamental de acordo.
O Zaznu ainda esta em desenvolvimento, portanto nao ha como avalia-lo em fun-
cionamento. Toda a avaliacao realizada neste item foi feita sobre o que e anunciado
pela rede em construcao, e pela proposta de seu modelo base Lyft.
4
Pontos positivos
• Usuarios sao motivado a adotar o servico pelos possıveis lucros
• Os motoristas sao cadastrados a partir de uma serie de condicoes de seguranca
• A rede e segura, pois os usuarios sao validados em seu ingresso na rede
• Avaliacao de usuarios
Pontos negativos
• Possıveis problemas legais por atuar como taxistas sem licenca
• O conceito de carona apresentado e desfigurado
1.3.3 Carona Facil
O Carona Facil e um sistema de caronas extremamente simples e direto. Voce
faz login usando o Facebook, e entao publica uma oferta ou pedido de carona. Essa
plataforma e voltada para caronas em viagens de longas distancias - para publicar
uma oferta ou um pedido de carona, voce so pode escolher cidades, ou seja, caronas
dentro de uma mesma cidade nao sao cobertas pelo sistema.
Alem de publicar pedidos e ofertas de caronas, os usuarios tambem tem acesso as
listas de pedidos e ofertas ja publicadas. Nessas listas, os usuarios podem escolher
quaisquer das entradas e estabelecer contato com o usuario que a publicou atraves
de mensagens no Facebook. A partir desse contato, os usuarios podem combinar a
carona.
Pontos positivos
• Autenticacao pelo Facebook
• O sistema e extremamente simples e direto
• Quando ha uma oferta ou um pedido compatıvel, voce recebe uma notificacao
no Facebook
5
Pontos negativos
• O sistema cobre apenas caronas de longas distancias
• Nao ha filtros de seguranca - qualquer pessoa pode ver os pedidos e ofertas
publicadas
• Nao e possıvel dar mais informacoes sobre a carona que nao a origem, destino
e data
• Nao e possıvel cancelar caronas
• Nao ha controle de lotacao para as caronas
1.4 Proposta
Apos a extensa avaliacao realizada sobre as solucoes ja existentes, foi estipulada
uma serie de funcionalidades necessarias a rede. Sao elas:
• Login com o Facebook
• Agenda Semanal com uma visao geral dos horarios do usuario
• Usuarios criam horarios com origem, destino e hora indicando seus compro-
missos
• Nao deve haver restricao de origem e destino - a rede deve funcionar para
qualquer parte do mundo
• Nao deve haver restricao de horarios - os usuarios devem cadastrar quantos
desejarem
• Usuarios que dirigem criam grupos para juntar passageiros
• Usuarios que querem carona buscam grupos para seus horarios
• Usuarios que encontram grupos enviam pedidos para entrar
• Usuarios podem cancelar pedidos e convites sem que este seja notado
6
• Motoristas que encontram passageiros para seus grupos enviam convites para
que entrem neles
• Todas as informacoes importantes geram notificacoes para o usuario, cha-
mando sua atencao
• A busca de passageiros e motoristas devem possuir os seguintes filtros:
– Grau de amizade: ”Amigos”ou ”Amigos + Amigos de Amigos”
– Tolerancia de tempo: faixa de tempo para combinacao dos horarios
– Tolerancia de distancia: motoristas e passageiros so devem ser combi-
nados quando o caroneiro estiver no caminho, ate uma distancia limite
escolhida pelo usuario
• O estado do usuario com o servidor deve permanecer sempre atualizado, ca-
racterizando uma experiencia de ”tempo-real”
• Usuarios devem poder compartilhar seus grupos
A funcionalidade mais complexa dentre as listadas e o filtro de combinacao para
passageiros e motoristas. Essa e a feature que mais distingue o projeto das demais
implementacoes existentes, pois e um ponto crucial para combinar usuarios de uma
forma satisfatoria. Em termos tecnicos, e uma funcionalidade interessante e com-
plexa porque envolve a integracao com outros sistemas - o Facebook e o Google
Maps. Alem disso, ela oferece desafios porque a complexidade deve ser tratada de
forma a nao impactar a experiencia do usuario com respostas demoradas.
O proximo capıtulo enuncia a estrutura usada para implementar as funcionalides
dispostas. Nos capıtulos que seguem, sao elucidadas as implementacoes de cada
parte da estrutura.
7
Capıtulo 2
Arquitetura da Aplicacao
2.1 Estrutura da Aplicacao
O desenvolvimento da rede social pode ser dividido em duas grandes partes: a
parte do servidor e a parte do cliente.
A parte do servidor acolhe as partes ’escondidas’ do usuario, como as regras de
negocio sensıveis da aplicacao e bancos de dados. A parte do cliente diz respeito a
tudo diretamente ligado a interface apresentada ao usuario. A interface trata-se das
’paginas’ visitadas.
Neste trabalho, a parte do cliente nao e abordada porque e muito grande e carac-
terizaria outro trabalho sozinha. Este texto trata da estrutura de dados e da grande
maioria dos fluxos do usuario.
2.2 Arquitetura do Servidor
O servidor e a parte que recolhe toda a logica sensıvel da aplicacao. Toda a logica
que nao deve ser externalizada ao usuario sem que antes haja validacao e sanitizacao
de dados.
A arquitetura de servidores e disposta da seguinte forma:
• 1x Servidor Web usando Node
8
• 1x Consumidor de Jobs Assıncronos usando Redis e Node
• 1x Servidor de Banco de Dados MongoDB
• 1x Servidor de arquivos estaticos na Amazon, usando S3 (Simple Storage Ser-
vice)
O servidor de arquivos estaticos e usado simplesmente para armazenar arquivos
que serao servidos sem qualquer processamento. Sao armazenadas nesse servidor,
por exemplo, as imagens.
O servidor de banco de dados MongoDB e usado para manter estrutura de dados
do Capıtulo 3. No Capıtulo 4, sao listadas e explicadas as tarefas assıncronas,
usadas para implementar processamentos complexos que poderiam atrapalhar a ex-
periencia do usuario. O servidor Web e usado para processar pedidos do usuario e
assim implementar os fluxos descritos no Capıtulo 5.
9
Capıtulo 3
Estrutura de Dados
3.1 Escolha do Banco de Dados
O banco de dados escolhido foi o banco nao-relacional chamado MongoDB. A
decisao de usar MongoDB ao inves de um banco relacional comum como o MySQL foi
de carater experimental, para aprofundamento no assunto e aquisicao de experiencia.
O MongoDB e um software recente, lancado em 2007. O MySQL e uma solucao mais
robusta e confiavel, lancada em 1995.
Os pontos mais interessantes sobre o banco escolhido sao:
• E um banco de dados orientado a colecoes e documentos, e nao a linhas e
tabelas com estruturas pre definidas, como em bancos relacionais tradicionais.
A colecao e o analogo de uma tabela, e um documento e o analogo de uma
linha de uma tabela. A grande diferenca e que a estrutura do MongoDB e
flexıvel enquanto a estrutura dos bancos relacionais tem de ser definidas ante-
riormente. Uma colecao pode conter dois tipos de documentos completamente
diferentes, embora isso nao seja usual.
• Documentos podem conter vetores incorporados. Isso acaba com as tabelas
de associacao em muitos casos (onde os dados desta sao simples) e tambem
reduz o tempo de leitura do banco, uma vez que a leitura e feita em um lugar
apenas e nao ha JOINs entre tabelas.
10
• Nao e possıvel fazer JOINs entre colecoes. Isso reduz a complexidade dos me-
canismos de busca, mas tambem muda radicalmente os metodos de modelagem
do banco de dados. Esse fator deve ser levado em conta quando a arquitetura
e definida.
• E facil de escalar o banco de dados atraves de sharding (fragmentacao dos
dados) distribuıdo em diversas maquinas de um cluster.
• Servidores de replica podem ser facilmente configurados.
3.2 Arquitetura do Banco de Dados
O banco de dados e composto pelas seguintes entidades, que serao explicadas no
decorrer do capıtulo:
User
Usuarios do sistema
UserPreference
Preferencias de usuarios
Friendship
Relacoes de amizades
BasePoint
Ponto geografico com LatLng, sincronizado com o Google Maps
BaseRoute
Rota entre BasePoints, sincronizadas com o Google Maps
RoutePointDistance
Relacoes de distancia entre pontos e rotas
Schedule
Horarios de usuarios
Ride
Episodios de carona pontuais
11
Group
Grupo de carona de um usuario
Notification
Notificacoes para usuarios
Diagrama de Associacoes O seguinte diagrama representa as associacoes entre
as entidades:
Figura 3.1: Diagrama de Associacoes no banco de dados
3.2.1 User
1 {
2 // Dados basicos do usuario
3 name:String ,
4 firstName:String ,
5 gender:String ,
6 email:{type:String , select:false},
12
7 // Codigo do usuario no Facebook
8 facebookId:Number ,
9 // Nome de usuario no Facebook
10 facebookUsername:String ,
11 // Token de acesso do Facebook
12 fbAccessToken :{type:String , select:false},
13 // Data de expiracao do token de acesso
14 fbAccessTokenExpires :{type:Date , select:false},
15 // Indica se o usuario ja entrou no sistema
16 hasJoined:Boolean ,
17 // Indica a ultima vez que os dados foram importados do Facebook
18 dataImportedAt:Date ,
19 // Indica a ultima vez que os amigos foram sincronizados com o Facebook
20 lastFriendsImport:Date ,
21 // Indica a ultima vez que as amizades em comum foram atualizadas
22 lastMutualFriends:Date ,
23 // Indica quando o usuario foi criado
24 createdAt:Date ,
25 // Contador de notificacoes nao lidas
26 unreadNotifications:Number ,
27 // Codigo identificador unico do usuario
28 userHash :{
29 type:String ,
30 select:false ,
31 default:function () {
32 // Gera uma chave aleatoria de 64 caracteres
33 return utils.randomString (64)
34 }
35 }
36 }
A entidade User e responsavel por agregar todas as informacoes de usuarios do
sistema. Os dados sao atualizados diretamente do Facebook atraves das tarefas
user/importData, user/updateMutual e user/updateFriends.
Nesse modelo, dados basicos do usuario como nome, sexo e email sao armazenados.
Para manter uma ligacao entre o usuario e seu perfil no Facebook, o campo face-
bookId contem seu identificador na rede social (e um numero como ”71029333311”).
O nome de usuario no Facebook e armazenado no campo facebookUsername, para
ser usado como um nome de usuario no Minha Carona.
Uma contagem de notificacoes nao lidas e mantida no campo unreadNotifications.
Seu valor e recalculado a cada vez que uma notificacao e enviada ou lida por um
13
usuario. Dessa forma, garante-se que o valor da contagem esta sempre disponıvel
e atualizado. Esse campo pre-calculado na entidade e interessante porque permite
facil acesso ao dado. Em um banco relacional, o dado seria acessado atraves de
um calculo mais complexo feito em tempo de execucao. O seguinte pseudocodigo
representa a possıvel consulta:
1 SELECT COUNT (*) as unreadNotifications
2 FROM Notifications
3 WHERE user = "USUARIO"
4 AND readAt is NULL
O campo userHash guarda uma chave aleatoria unica para cada usuario do sis-
tema. Essa chave e usada para identificar os usuarios publicamente em URLs (como,
por exemplo, na URL de perfil de usuario: http://minhacarona.com/#/profiles/¡userHash¿).
3.2.2 Friendship
1 {
2 // Referencia aos dois usuarios representados na amizade
3 users:[
4 {type:Schema.Types.ObjectId , ref:’User’}
5 ],
6 // Chave unica para um par de amigos
7 key:String ,
8 // Contador de amigos em comum
9 counters :{
10 mutual:Number
11 },
12 // Usuarios que sao amigos em comum
13 mutualFriends :[
14 {type:Schema.Types.ObjectId , ref:’User’}
15 ]
16 }
A entidade Friendship representa uma relacao de amizade entre dois usuarios
distintos.
Uma chave unica e usada no campo key, para garantir que dois usuarios tenham
no maximo 1 registro de amizade. A chave unica para dois usuarios userA e userB
e gerada atraves do seguinte algoritmo:
1 function (userA , userB) {
2 userA = userA._id || userA
14
3 userB = userB._id || userB
4 return (userA < userB) ? userA + ’:’ + userB : userB + ’:’ + userA
5 }
Isso garante que dois usuarios distintos tenham sempre a mesma chave, indepen-
dente da ordem com que a funcao e executada. Dessa forma, e garantido que o valor
do campo key e sempre o mesmo para dois usuarios.
O campo counters.mutual e usado para manter uma contagem de amigos em
comum pre-calculada para os usuarios da relacao. No campo mutualFriends, sao
guardados todos os amigos em comum entre os usuarios. O processamento feito
para atualizar os dados de amigos em comum entre usuarios e realizado atraves da
tarefa user/updateMutual.
3.2.3 UserPreference
1 {
2 // Referencia ao User dono das preferencias
3 user:{type:Schema.Types.ObjectId , ref:’User’},
4 // Informacoes (settings) do ultimo grupo criado,
5 // para preenchimento automatico
6 lastGroupCreate :{},
7 // Preferencias de privacidade de grupos (groupPrivacy e routePrivacy)
8 groupShare :{}
9 }
A entidade UserPreference persiste algumas preferencias de um usuario.
O campo lastGroupCreate mantem uma copia das ultimas settings de um grupo
criado, para que essa informacao esteja disponıvel da proxima vez que o usuario
quiser criar um grupo e assim facilitar o processo.
O campo groupShare guarda informacoes sobre a ultima configuracao de priva-
cidade de grupos (visibilidade de grupo, groupPrivacy, e visibilidade do trajeto,
routePrivacy) escolhidas pelo usuario.
3.2.4 BasePoint
15
1 {
2 // Enderecos extraıdos do Google Maps
3 address:String ,
4 sublocality:String ,
5 locality:String ,
6 // Todos os dados extraıdos do Google Maps, sem modificacoes
7 addressComponents :{type:{}, select:false},
8 // Latitude e Longitude
9 lat:Number ,
10 lng:Number ,
11 // Pares de latitude e longitude que podem usar este ponto
12 latlngs :[ String],
13 // Indica a ultima vez que este ponto foi sincronizado com o Google Maps
14 syncedAt:Date ,
15 // Indica a ultima vez que este ponto foi processado pelo sistema
16 // para calcular rotas proximas
17 processedAt:Date
18 }
A entidade BasePoint representa um ponto geografico unico no sistema, atraves
dos campos lat (latitude) e lng (longitude). Essa entidade guarda todas as in-
formacoes do ponto e e atraves dela que comparacoes de distancia entre pontos e
rotas sao feitas.
No campo latlngs e mantida uma lista unica de pares de coordenadas que devem
ser representados pelo BasePoint. Isso e feito para poupar processamento e espaco
no banco. Para que dois pontos sejam representados pelo mesmo BasePoint, eles
devem estar muito proximos (em um raio de 1 metro). Sempre que um par de
coordenadas for buscado do banco de dados, ele deve ser buscado nas listas latlngs
dos BasePoints, e nao nos campos lat e lng.
Sempre que um ponto precisa ser usado no sistema mas seu BasePoint ainda nao
existe, a tarefa basePoint/syncFromLocation e executada. Essa tarefa consulta
o Google Maps para criar um BasePoint associado ao ponto e com as informacoes
retornadas do servico.
O campo addressComponents guarda todos os dados retornados do Google Maps,
sem qualquer tipo de formatacao. Os campos address, sublocality e locality guardam,
respectivamente, o endereco completo, a cidade e o bairro. Segue um exemplo dessas
informacoes:
16
1 {
2 "address" : "Avenida Brasil - Bangu , Rio de Janeiro , Brazil",
3 "locality" : "Rio de Janeiro",
4 "sublocality" : "Bangu",
5 "addressComponents" : [{
6 "long_name" : "Avenida Brasil",
7 "short_name" : "Av. Brasil",
8 "types" : ["route"]
9 }, {
10 "long_name" : "Bangu",
11 "short_name" : "Bangu",
12 "types" : ["sublocality", "political"]
13 }, {
14 "long_name" : "Rio de Janeiro",
15 "short_name" : "Rio de Janeiro",
16 "types" : ["locality", "political"]
17 }, {
18 "long_name" : "Rio de Janeiro",
19 "short_name" : "Rio de Janeiro",
20 "types" : ["administrative_area_level_1", "political"]
21 }, {
22 "long_name" : "Brazil",
23 "short_name" : "BR",
24 "types" : ["country", "political"]
25 }],
26 "lat" : -22.8559544 ,
27 "lng" : -43.4922148 ,
28 "latlngs" : [" -22.8559544 , -43.4922148"]
29 }
Quando um BasePoint e criado, a tarefa basePoint/processDistances e lancada
para calcular as distancias entre a nova entidade e as BaseRoutes existentes. Essa
operacao e complexa e deve ser evitada sempre que possıvel.
3.2.5 BaseRoute
1 {
2 // Referencias aos BasePoints de origem e destino da rota
3 baseOrigin :{type:Schema.Types.ObjectId , ref:’BasePoint ’},
4 baseDestination :{type:Schema.Types.ObjectId , ref:’BasePoint ’},
5 // Waypoints
6 waypoints :[ String],
7 // Ponto medio da rota, em Latitude e Longitude
8 centerPoint :{
9 lat:Number ,
10 lng:Number
17
11 },
12 // Duracao e distancia extraıdos do Google Maps
13 duration:Number ,
14 distance:Number ,
15 // Pontos que formam a rota, codificado
16 polyline:String ,
17 // Pontos que formam este trajeto, em Latitude e Longitude
18 points:Array ,
19 // Indica a ultima vez que esta rota foi sincronizada com o Google Maps
20 syncedAt:Date ,
21 // Indica a ultima vez que esta rota foi processada pelo sistema
22 // para buscar pontos proximos
23 processedAt:Date
24 }
A entidade BaseRoute guarda informacoes sobre uma rota unica no sistema. A
unicidade desta entidade e definida por uma chave unica no campo polyline. Isso
quer dizer que nao e possıvel que existam duas BaseRoutes com a mesma polyline.
Dessa forma, e poupado processamento de distancias para BasePoints e BaseRoutes,
visto que elas sao reaproveitadas.
Uma BaseRoute e basicamente definida por um ponto de origem, baseOrigin, um
ponto de destino baseDestination e uma lista de pontos waypoints que devem ser
passados no caminho. As demais informacoes na entidade sao usadas para que
mecanismos do sistema funcionem.
No campo centerPoint, e guardado o ponto medio do trajeto. Isso e usado no pro-
cessamento de pontos e rotas das tarefas */processDistances para poupar esforco
- apenas pontos proximos o suficiente de uma rota tem suas distancias calculadas.
O campo polyline mantem uma lista compactada de pontos. A compactacao dos
pontos e definida pelo servico Google Maps [2]. Um exemplo do dado contigo neste
campo segue:
1 {
2 "polyline": "xddkC ‘gufGhBQhCUXCpAInAGF?r@AL@R@NBd@H ‘@JˆPˆRTPn@n@bAdAbAbAxBvBp@r@HHbA
|@‘A˜@‘A˜@|@‘Ap@p@NNdA˜@n@l@RRd@ \\ HFHDJDJBH@J@J?JAFAXGlA]
lA_@r@UXGnA_@JCHEHELKFGDIBIBIDU@K?KCUCICIkCoGKU_@w@Uc@[q@s@kBCOESAEC]A[?]
Ak@Bw@Hu@@GFYRq@N]Tc@Xa@fCmCDEZWTSLMv@k@pAw@x@c@ZOrAu@PKl@a@RKbAq@TKRIVEPEJ?
JAH@T@TDB@h@NDBTL \\TˆXRRHNHLHTFRL \\@F@B\\ bB@JPz@b@dCBLf@lCh@nCv@rEp@rDBTJh@BR@H ‘
@hBBPLr@?BH˜@BˆBf@BdA?BAtA?VEdBCbA"
18
3 }
Este dado e recebido e processado pela tarefa baseRoute/syncFromRoute.
Mais informacoes podem ser encontradas na descricao da tarefa, na pagina 29.
3.2.6 RoutePointDistance
1 {
2 // Referencia ao BaseRoute comparado
3 baseRoute :{type:Schema.Types.ObjectId , ref:’BaseRoute ’},
4 // Referencia ao BasePoint comparado
5 basePoint :{type:Schema.Types.ObjectId , ref:’BasePoint ’},
6 // Ponto mais proximo entre o BasePoint e o BaseRoute, em Latitude e Longitude
7 routePoint :{
8 lat:Number ,
9 lng:Number
10 },
11 // Em que altura da rota o ponto de intersecao esta, de 0 a 1 (100%)
12 progress:Number ,
13 // Distancia do BasePoint a BaseRoute, em metros
14 distance:Number ,
15 // Indica a ultima vez que esta comparacao foi processada
16 processedAt:Date
17 }
A entidade RoutePointDistance guarda informacoes sobre proximidade entre rotas
e pontos, para que seja possıvel saber quando um ponto esta proximo o suficiente de
uma rota. Com essa entidade implementada, e possıvel combinar caronas de forma
que um usuario so encontre pessoas que estao a uma distancia razoavel (ajustada
atraves de filtros pelo proprio internauta).
O campo routePoint guarda o ponto na rota baseRoute mais proximo do ponto
basePoint.
O campo progress indica em que altura da rota baseRoute o ponto routePoint
esta, atraves de um numero de 0 a 1, onde 0 indica que ele esta na origem e 1 indica
que esta no destino. Qualquer valor intermediario indica que o ponto routePoint
esta no meio da rota. Multiplicando o numero por 100, encontra-se a porcentagem
de conclusao da rota no ponto.
19
O campo distance guarda a distancia direta entre basePoint e routePoint. E im-
portante lembrar que essa distancia e uma reta entre os pontos - qualquer obstaculo
no caminho, como predios, nao e considerado.
Entidades RoutePointDistance sao criadas apos a comparacao de rotas e pontos
nas tarefas */processDistances.
3.2.7 Schedule
1 {
2 // Referencia ao User dono deste Schedule
3 user:{type:Schema.Types.ObjectId , ref:’User’},
4 // Origem e Destino do horario
5 origin :{
6 address:String ,
7 lat:Number ,
8 lng:Number
9 },
10 destination :{
11 address:String ,
12 lat:Number ,
13 lng:Number
14 },
15 // Referencias aos BasePoints de origem e destino do horario
16 baseOrigin :{type:Schema.Types.ObjectId , ref:’BasePoint ’},
17 baseDestination :{type:Schema.Types.ObjectId , ref:’BasePoint ’},
18 // Data quando o horario foi criado, no formato "YYYY-MM-DD"
19 date:String ,
20 // Indica se o horario e periodico
21 periodic:Schema.Types.Mixed ,
22 // Hora de chegada
23 arriveTime:Number ,
24 // Hora de partida
25 leaveTime:Number ,
26 // Referencias a Groups para este horario
27 groups :[
28 {type:Schema.Types.ObjectId , ref:’Group’}
29 ],
30 // Referencia a um Group deste horario
31 group:{type:Schema.Types.ObjectId , ref:’Group’},
32 // Pontos ja processados para este horario
33 processedPoints :[ String]
34 }
A entidade Schedule representa um horario criado por um usuario. Um horario
contem informacoes que constituem demandas de carona no sistema. Essa demanda
20
e definida por um ponto de origem baseOrigin, um ponto de destino baseDestination
e um horario de saıda ou de chegada (leaveTime/arriveTime).
Os campos origin e destination sao os mesmos pontos de baseOrigin e baseDes-
tination, mas contem mais informacoes incorporadas na entidade para facil acesso.
Dessa forma, nao e preciso realizar consultas a outras colecoes quando apenas dados
dos Schedules sao necessarios.
Um Schedule pode ter sua frequencia definida atraves do campo periodic. A
frequencia e um objeto que indica de quantos em quantos dias a demanda repete-se
em um campo frequency e em qual dia index desses N dias ocorre o evento. Por
exemplo, para um horario semanal em todas as segundas, o campo periodic de um
Schedule sera:
1 {
2 // 7 dias na semana
3 "frequency": 7,
4 // 0 = domingo e 6 = sabado
5 "index": 1
6 }
Com esse mecanismo, e possıvel ser flexıvel nos perıodos de repeticao, nao restringindo-
os a faixas semanais.
No campo date, e guardada a data na qual o horario foi criado.
O campo groups guarda os grupos nos quais o usuario foi aceito para o horario.
Nesses grupos, o usuario ja pode pegar caronas. Ja o campo group guarda um grupo
que o usuario possa ter criado para o horario, indicando que podera dirigir.
O campo processedPoints indica quais dos pontos baseOrigin e baseDestination
ja foram sincronizados com o Google Maps e processados, como esta descrito na
pagina 48.
3.2.8 Group
21
1 {
2 // Referencia a um User que dirige para este grupo
3 driver :{type:Schema.Types.ObjectId , ref:’User’},
4 // Referencia ao Schedule que originou este grupo
5 schedule :{type:Schema.Types.ObjectId , ref:’Schedule ’},
6 // Horario de chegada
7 arriveTime:Number ,
8 // Horario de partida
9 leaveTime:Number ,
10 // Membros deste grupo
11 members :[
12 {type:Schema.Types.ObjectId , ref:’User’}
13 ],
14 // Mapa de locais para embarque e desembarque de cada membro
15 memberLocation :{},
16 // Configuracoes do grupo
17 settings :{
18 // Numero maximo de passageiros
19 maxPassengers:Number ,
20 // Contribuicao esperada de cada membro
21 contribution :{
22 value:Number ,
23 optional:Boolean
24 }
25 },
26 // Referencia a um BaseRoute usado no grupo
27 baseRoute :{type:Schema.Types.ObjectId , ref:’BaseRoute ’},
28 // Indica que tipos de usuario podem ver o grupo
29 groupPrivacy:String ,
30 // Indica que tipos de usuario podem ver a rota
31 routePrivacy:String ,
32 // Data quando o horario foi criado, no formato "YYYY-MM-DD"
33 date:String ,
34 // Indica se o grupo e periodico
35 periodic:Schema.Types.Mixed ,
36 // Indica quando este grupo foi criado
37 createdAt :{type:Date , default:Date.now}
38 }
A entidade Group representa grupos de usuarios que podem dirigir para seus
horarios. O grupo sempre deve possuir um motorista driver e um Schedule schedule
associado a ele.
Os horarios leaveTime e arriveTime indicam as horas previstas de saıda da origem
e de chegada ao destino, respectivamente.
22
No campo members, e guardada uma lista nao-ordernada de usuarios que fazem
parte do grupo. Em memberLocations, e armazenado um objeto indexado pelo
identificador de usuario de membros do grupo, e com objetos que informam: o local
e hora na qual um membro gostaria de ser pego e o local onde seria deixado. Por
exemplo:
1 {
2 // Identificador de um usuario
3 "IDENTIFICADOR_DO_USUARIO" : {
4 // Local onde o membro sera pego
5 pickPos: LatLng ,
6 // Hora de encontro, no formato HHMM
7 pickTime: Number ,
8 // Local onde o membro sera deixado
9 dropPos: LatLng
10 },
11 ..
12 "ID_DE_OUTRO_USUARIO": ...
13 }
Com essa implementacao, cada membro do grupo tem a capacidade de indicar
um local de encontro diferente para cada grupo de seus horarios.
No campo settings estao configuracoes do grupo definidas pelo usuario. Essas
configuracoes incluem o numero maximo de passageiros (settings.maxPassengers) e
a contribuicao que e desejada pelo motorista (settings.contribution). Outras con-
figuracoes tambem estao definidas nos campos routePrivacy e groupPrivacy, que
definem, respectivamente, que tipo de relacao um usuario deve ter com o motorista
do grupo para ver a rota feita e o grupo.
Os campos periodic e date sao analagos a suas versoes da entidade Schedule.
3.2.9 Ride
1 {
2 // Referencia ao Group da carona
3 group:{type:Schema.Types.ObjectId , ref:’Group’},
4 // Referencia ao Motorista da carona
5 driver :{type:Schema.Types.ObjectId , ref:’User’},
6 // Data da carona, no formato "YYYY-MM-DD"
7 date:String ,
23
8 // Data de confirmacao da carona,
9 confirmedAt:Date ,
10 // Horario de chegada
11 arriveAt:Date ,
12 // Horario de saıda
13 leaveAt:Date ,
14 // Configuracoes da carona
15 settings:Schema.Types.Mixed ,
16 // Usuarios confirmados para a carona
17 confirmedUsers :[
18 {type:Schema.Types.ObjectId , ref:’User’}
19 ],
20 // Referencias a usuarios na lista de espera
21 standbyUsers :[
22 {type:Schema.Types.ObjectId , ref:’User’}
23 ],
24 // Mapa de locais para embarque e desembarque de cada membro
25 memberLocation :{}
26 }
O Ride representa ”Episodios de Carona”. Cada carona que ocorre no sistema e
registrada atraves da criacao de um item nesta colecao. Esta estrutura esta sempre
relacionada a um Group, pois nao podem haver caronas se nao houver um grupo.
Para indicar se o motorista esta confirmado ou nao nesta carona, o campo confir-
medAt e usado. Quando um motorista confirma que ira dirigir para este episodio,
o campo confirmedAt e marcado com o momento exato da confirmacao. Se ele
desconfirma, o campo passa para null.
Este modelo guarda duas listas importantes para a carona: uma lista de usuarios
confirmados, confirmedUsers, e uma lista de usuarios na espera, standbyUsers. A
lista de espera e populada quando o motorista ainda nao esta confirmado ou quando
o limite de passageiros e atingido, e nao tem limite. Quando um usuario da lista
de confirmados desconfirma, o primeiro usuario da lista de espera e confirmado
automaticamente. Da mesma forma, quando o motorista confirma, usuarios da lista
de espera sao confirmados ate esgotar as vagas para passageiros na carona. Quando
o motorista desconfirma, todos os usuarios da lista de passageiros confirmados sao
passados a frente da lista de espera.
24
Os campos settings, memberLocation, arriveAt, leaveAt e driver sao copiados do
grupo que originou o Ride para que o episodio de carona possa ser customizado.
Dessa forma, o motorista podera alterar os horarios de chegada e saıda, o numero
maximo de passageiros e outras configuracoes para um dia da carona apenas. A
copia do campo memberLocation permite que todos os usuarios da carona possam
escolher outros locais de embarque e desembarque para um dia apenas, sem alterar
seu padrao para a rota.
3.2.10 Notification
1 {
2 // Referencia ao User notificado
3 user:{type:Schema.Types.ObjectId , ref:’User’},
4 // Dados da notificacao,
5 data:Schema.Types.Mixed ,
6 // Tipo de notificacao (identifica o evento)
7 type:String ,
8 // Indica quando o usuario leu a notificao (e se leu)
9 readAt:Date ,
10 // Indica quando a notificacao foi enviada
11 sentAt :{type:Date , default:Date.now}
12 }
A entidade Notification guarda notificacoes para usuarios. As notificacoes sao
alertas lancados em determinados eventos do sistema.
Atraves do campo type, podemos identificar o tipo de evento que disparou a
notificacao para o usuario. Os seguintes tipos de notificacao existem no sistema:
user welcome
Mensagem de boas-vindas
user first import
Indica que os dados foram importados do Facebook pela primeira vez
user data imported
Indica que os dados foram sincronizados do Facebook
group request received
Pedidos para entrar no grupo recebido
25
group invite received
Convite para entrar no grupo recebido
generic
Notificacao generica
Para cada tipo de notificacao, o campo data deve ser preenchido de acordo com as
informacoes esperadas. Em alguns casos, o campo nao precisa de nenhum dado, pois
informacoes adicionais para a notificacao nao sao necessarias. Esse e o caso da noti-
ficacao user welcome, que simplesmente exibe uma mensagem dando boas-vindas
ao usuario e portanto nao precisa de dados extras. Porem, no caso da notificacao
group invite received, o campo data deve ser preenchido para indicar quem con-
vidou o usuario para um grupo, e qual grupo foi. Isso e feito atraves de dados
como:
1 {
2 user: "<IDENTIFICADOR DO USUARIO >",
3 group: "<IDENTIFICADOR DO GRUPO >"
4 }
Um tipo de notificacao interessante e a notificacao generic. Esse tipo de noti-
ficacao possibilita a exibicao de qualquer tıtulo e texto atraves dos dados:
1 {
2 title: "Titulo",
3 text: "Texto"
4 }
Essa arquitetura de notificacoes lhes confere grande flexibilidade porque qualquer
tipo de dado pode ser atribuıdo a elas no campo data.
26
Capıtulo 4
Tarefas Assıncronas
Tarefas assıncronas sao rotinas usadas para processar dados sem atrasar o
fluxo de pedidos do usuario.
Uma tarefa assıncrona deve ser usada quando o processamento a ser realizado
pode levar muito tempo e portanto e capaz de prejudicar a experiencia do usuario.
Um cenario de exemplo e a consulta de um servico externo, que nao esta sob o
controle da aplicacao, como o Google Maps. Por questoes de latencia da rede e
imprevisibilidade, e ideal usar uma tarefa assıncrona e dar uma resposta ao usuario
assim que os processos no domınio da aplicacao forem concluıdos (como validacao
de dados e persistencia no banco).
No Anexo A estao incorporados os codigos para cada tarefa listada neste capıtulo.
4.1 Implementacao do Servidor
Usando o servidor de memoria compartilhada chamado Redis, podemos orquestrar
uma lista de tarefas assıncronas a serem processadas.
Usamos o Node para programar processos workers, que ficam conectados ao ser-
vidor de memoria esperando novas tarefas a serem processadas. Cada processo
conectado esta atuando como um cliente do servidor de memoria e esta escutando
por novas tarefas.
27
Quando uma tarefa e inserida na fila, um dos processos que esta na escuta ”con-
some”a tarefa. Uma tarefa deve ser consumida por apenas um processo para que
operacoes nao sejam realizadas varias vezes. Por exemplo, nao e aceitavel que uma
tarefa de enviar emails seja consumida duas vezes, resultando no envio emails du-
plicados. Esse controle de unicidade na fila e realizado pelo proprio Redis, atraves
de um mecanismo de listas unicas.
4.2 A estrutura de uma tarefa
Como o codigo das tarefas esta incluıdo em apendice, e interessante dispor de um
manual de sua estrutura. Uma tarefa assıncrona do Minha Carona e definida da
seguinte forma:
1 jobs.process(’NOME’, FUNCAO)
Onde o NOME e o identificador da funcao, a ser usado para executar a tarefa, e
FUNCAO e o codigo a ser executado, que define a funcionalidade da tarefa.
A FUNCAO deve ser definida da seguinte forma:
1 function(job , next){}
Onde
job
Objeto de informacoes sobre a tarefa sendo executada. job.data define o/os
parametros passados para a tarefa.
next
Funcao que deve ser executada uma vez que a tarefa for finalizada, para re-
tornar o fluxo de controle ao gerenciador de tarefas
4.3 Tarefas do Minha Carona
As seguintes tarefas assıncronas foram usadas no projeto:
28
baseRoute/syncFromRoute
Sincroniza dados de uma rota com o Google Maps
baseRoute/processDistances
Processa distancias de uma rota
basePoint/syncFromLocation
Sincroniza dados de um ponto com o Google Maps
basePoint/processDistances
Processa distancias de um ponto
user/importData
Importa dados de um usuario do Facebook
user/updateMutual
Atualiza o cache de amigos em comum de um usuario
user/updateFriends
Atualiza relacoes de amizade de um usuario atraves do Facebook
A seguir, cada uma das tarefas e explicada.
4.3.1 baseRoute/syncFromRoute
Esta tarefa e responsavel por sincronizar dados de uma rota com o Google Maps e
atualizar as informacoes no banco de dados. Especificamente, esta tarefa consulta o
servico Directions API do Google Maps, usando os pontos de latitude e longitude de
origem e destino, e pontos a serem atravessados na rota, e entao persiste no banco
uma nova entidade BaseRoute.
Parametros
baseOrigin BasePoint de origem da rota
baseDestination BasePoint de destino da rota
waypoints Lista [LatLng] de pontos a serem passados no caminho
29
Passos
1. Uma chamada ao Directions API do Google Maps e feita para obter as in-
formacoes sobre a rota. O pedido e feito a uma URL e seus parametros sao
passados em sua query string, como no exemplo de chamada a seguir:
https://maps.googleapis.com/maps/api/directions/json?origin=-22.964015
6,-43.202246&destination=-22.8552649,-43.2313681&waypoints=&sensor=fal
se
Os parametros dessa chamada sao:
(a) origin Par de latitude e longitude de origem
(b) destination Par de latitude e longitude de destino
(c) waypoints Pares de latitude e longitude que devem ser passados no
caminho da rota, separados por ’,’ (waypoints)
(d) sensor (true—false) Indica se os dados passados foram obtidos de um
GPS
Os parametros da chamada sao extraıdos dos parametros da tarefa, da se-
guinte forma:
• origin (baseOrigin.lat,baseOrigin.lng)
• destination (baseDestination.lat,baseDestination.lng)
• waypoints waypoints
• sensor sempre false, porque o dado nunca vem de GPS
Um exemplo das informacoes obtidas do servico, para origin -22.9640156,-
43.202246, destination -22.8552649,-43.2313681”, sem waypoints e com sensor
false segue:
1 {
2 // Lista de possıveis rotas a serem usadas para a demanda
3 "routes" : [
4 {
30
5 // Limites geograficos da rota (define um retangulo que cobre toda a rota)
6 "bounds" : {
7 // Ponto geografico representando o canto direito cima
8 "northeast" : {
9 "lat" : -22.854895 ,
10 "lng" : -43.2021953
11 },
12 // Ponto geografico representando o canto esquerdo baixo
13 "southwest" : {
14 "lat" : -22.9640739 ,
15 "lng" : -43.23465179999999
16 }
17 },
18 "copyrights" : "Map data C2014 Google",
19 // Partes da viagem (ha multiplas apenas quando a rota exige paradas)
20 "legs" : [
21 {
22 // Distancia percorrida na rota
23 "distance" : {
24 "text" : "14.3 km",
25 "value" : 14292
26 },
27 // Duracao aproximada para percorrer a rota toda
28 "duration" : {
29 "text" : "13 mins",
30 "value" : 791
31 },
32 // Endereco de destino
33 "end_address" : "Avenida Horacio Macedo , 2051 -2577 - Cidade
Universitaria , Rio - Rio de Janeiro , Brazil",
34 "end_location" : {
35 "lat" : -22.855223 ,
36 "lng" : -43.2312972
37 },
38 // Endereco de origem
39 "start_address" : "Rua Carvalho Azevedo , 125 -205 - Lagoa , Rio - Rio de
Janeiro , Brazil",
40 "start_location" : {
41 "lat" : -22.9640298 ,
42 "lng" : -43.2022428
43 },
44 // Passos que formam a rota toda -- Esse dado nao e usado pelo sistema
45 "steps" : [
46 ...
47 {
48 "distance" : {
49 "text" : "0.2 km",
50 "value" : 250
51 },
31
52 "duration" : {
53 "text" : "1 min",
54 "value" : 29
55 },
56 "end_location" : {
57 "lat" : -22.9618447 ,
58 "lng" : -43.2030307
59 },
60 // Instrucoes para o usuario, como, ex: "Vire a direita na Rua Fonte da
Saudade"
61 "html_instructions" : "Turn \u003cb\u003eright\u003c/b\u003e onto
\u003cb\u003eRua Fonte da Saudade\u003c/b\u003e",
62 "maneuver" : "turn -right",
63 "polyline" : {
64 "points" : "lddkCd ‘ufGe@BiAFy@D_@Bc@Ba@@QB_AJi@Dc@DC?I?"
65 },
66 "start_location" : {
67 "lat" : -22.9640739 ,
68 "lng" : -43.2027451
69 },
70 "travel_mode" : "DRIVING"
71 },
72 ...
73 ],
74 "via_waypoint" : []
75 }
76 ],
77 // Pontos do trajeto, codificados
78 "overview_polyline" : {
79 "points" : "dddkC˜| tfGFxA?Je@BcCLcAF}CVg@DI?CESQm@i@m@_@e@IMXQJ]RC\\
@FFLNLfB?rAB ‘BIFHDRJtAJ \\NTFNI?g@@yCDq@DcBP[BgFXyDDmA?
yCOkX_B_EQeQuAqGe@yFc@_T_BmHUuESyA?mALi@J[F}@XcAˆgAf@y@n@aA˜@Yd@mC ‘IkF
‘O{BfEwBbCc@ ‘@e@Zi@Ni@Dq@AiEm@{@UoAc@mBcAmAe@o@IaAByCh@qB ‘
@sBX_CLsBHkDJyQ ‘@eMXuc@ ‘AuA@{CK_AGoAM[@[AuAOgB_@sAYsA?_@BQDOBeA \\[P{
@j@e@h@q@fA]n@Yd@e@r@cA ‘Ak@ ‘@oB˜@gDx@i@P[Lw@b@s@j@WRwA|
Ae@d@_@XWPoGxDiIzEwQfKiDjB}Al@k@Ju@N_CLwBHUD[Fq@JsCdAs@XODq@V{
DlCoFzDqFhEaI ‘GmJjHuInG{EfDc@\\ m@f@wAfAiBvAwBjBcCzBy@l@oHjE}BpAwBz@{
@VgBb@cCZw@LuAHkB@aDEsM[mBCu@@{CLmCR}QjAqGd@cBVgE˜@o@TaBx@a@DmAˆ
q@Do@GWKg@]sF_FuAmAUIYEUCY?WD{@\\ mBbAg@PgBp@kBpAoEnCiDtBSDK?QE{CsF_D{
Fi@kAEM?K@MDKv@g@"
80 }
81 }
82 ],
83 // Indica se o pedido foi atendido com sucesso
84 "status" : "OK"
85 }
2. Os dados acima sao interpretados e uma nova BaseRoute e criada. Dos dados
32
retornados do servico, sao extraıdos os seguintes campos para a nova Base-
Route:
• distance Distancia total da rota
• duration Duracao estimada da rota
• polyline Coordenadas que formam a rota (ainda codificada)
• centerPoint Ponto medio do trajeto
• points Pontos decodificados da polyline
A decodificacao e feita usando o seguinte algoritmo:
1 // GMaps polyline decoding from http://doublespringlabs.blogspot.com.br/2012/11/
2 // decoding-polylines-from-google-maps.html
3 decodePolyline:function (encoded) {
4 // array that holds the points
5 var points = [ ]
6 var index = 0, len = encoded.length;
7 var lat = 0, lng = 0;
8 while (index < len) {
9 var b, shift = 0, result = 0;
10 do {
11
12 b = encoded.charAt(index ++).charCodeAt (0) - 63;//finds ascii //and
substract it by 63
13 result |= (b & 0x1f) << shift;
14 shift += 5;
15 } while (b >= 0x20);
16
17
18 var dlat = (( result & 1) != 0 ? ˜( result >> 1) : (result >> 1))
19 lat += dlat;
20 shift = 0;
21 result = 0;
22 do {
23 b = encoded.charAt(index ++).charCodeAt (0) - 63;
24 result |= (b & 0x1f) << shift;
25 shift += 5;
26 } while (b >= 0x20);
27 var dlng = (( result & 1) != 0 ? ˜( result >> 1) : (result >> 1));
28 lng += dlng;
29
30 points.push({lat:( lat / 1E5), lng:( lng / 1E5)})
31
32 }
33 return points
33
34 },
O produto do codigo acima e uma lista de coordenadas, em ordem,
formando o trajeto.
3. Com essas informacoes na BaseRoute, ja e possıvel inferir distancias entre
pontos e rotas com a tarefa baseRoute/processDistances. Sendo assim,
uma nova tarefa do tipo e lancada.
4.3.2 baseRoute/processDistances
Esta tarefa calcula distancias entre uma BaseRoute e todos os BasePoints proximos
o suficiente dela.
Parametros
baseRoute BaseRoute para o qual calcular as distancias
Passos
1. Sao buscados todos os BasePoints dentro de um raio de 50km do centerPoint
de baseRoute, dessa forma processamento desnecessario e poupado
2. Para cada BasePoint encontrado, como basePoint :
(a) E calculada a distancia mınima ate qualquer um dos pontos points de
baseRoute. O seguinte algoritmo, levando como parametros o ponto ba-
sePoint e os pontos points do trajeto, e usado para o calculo:
1 closestPoint:function (point , points) {
2
3 var closestPoints = {}
4 , routesAt = {}
5 , totalDistance = 0
6
7 for (var i = 0, len = points.length - 1; i < len; i++) {
8 var closest = utils.gmaps.closestLinePoint(points[i], points[i +
1], point)
9 , dist = utils.gmaps.pointsDistance(point , closest)
10 if (closestPoints[dist]) continue
11 closestPoints[dist] = closest
34
12 routesAt[dist] = totalDistance + utils.gmaps.pointsDistance(
points [0], closest)
13 totalDistance += parseFloat(utils.gmaps.pointsDistance(points[i],
points[i + 1]) * 1000)
14 }
15
16 var min = Math.min.apply(Math , Object.keys(closestPoints))
17 return {
18 point:closestPoints[min],
19 progress:routesAt[min] / totalDistance ,
20 distance:min
21 }
22 }
23 }
O codigo acima retorna um objeto com os seguintes atributos:
• point Par de coordenadas do ponto em points mais proximo do ponto
basePoint
• progress Em que altura do trajeto o ponto encontrado point esta
no trajeto (de 0 - 0% a 1 - 100%)
• distance Distancia entre o ponto na rota point e o ponto original
basePoint
(b) Uma nova entidade RoutePointDistance e criada para baseRoute e base-
Point, com seus campos routePoint, progress e distance obtidos do objeto
acima.
Se uma entidade RoutePointDistance ja existia para baseRoute e base-
Point, ela e atualizada e outra nao precisa ser criada.
4.3.3 basePoint/syncFromLocation
Esta tarefa cria um novo BasePoint para as coordenadas.
Parametros
lat Latitude
lng Longitude
35
Passos
1. Uma chamada ao Geocoding API do Google Maps e feita para obter as in-
formacoes sobre o ponto. O pedido e feito a uma URL e seus parametros sao
passados em sua query string, como no exemplo de chamada a seguir:
http://maps.googleapis.com/maps/api/geocode/json?latlng=-22.9640156,-
43.202246&sensor=false
Os parametros dessa chamada sao:
(a) latlng Par de latitude e longitude do ponto
(b) sensor (true—false) Indica se os dados passados foram obtidos de um
GPS
Os parametros da chamada sao extraıdos dos parametros da tarefa, da se-
guinte forma:
• latlng (lat,lng)
• sensor sempre false, porque o dado nunca vem de GPS
Um exemplo das informacoes obtidas do servico, para latlng -22.9640156,-
43.202246”e com sensor false segue:
1
2 {
3 // Lista de resultados encontrados para o ponto
4 "results" : [
5 {
6 // Lista de componentes que formam o endereco, como bairro, cidade e paıs
7 "address_components" : [
8 {
9 "long_name" : "126 -206",
10 "short_name" : "126 -206",
11 "types" : [ "street_number" ]
12 },
13 {
14 "long_name" : "Rua Carvalho Azevedo",
15 "short_name" : "Rua Carvalho Azevedo",
16 "types" : [ "route" ]
36
17 },
18 {
19 "long_name" : "Lagoa",
20 "short_name" : "Lagoa",
21 "types" : [ "neighborhood", "political" ]
22 },
23 {
24 "long_name" : "Rio",
25 "short_name" : "Rio",
26 "types" : [ "locality", "political" ]
27 },
28 {
29 "long_name" : "Rio de Janeiro",
30 "short_name" : "Rio de Janeiro",
31 "types" : [ "administrative_area_level_2", "political" ]
32 },
33 {
34 "long_name" : "Rio de Janeiro",
35 "short_name" : "RJ",
36 "types" : [ "administrative_area_level_1", "political" ]
37 },
38 {
39 "long_name" : "Brazil",
40 "short_name" : "BR",
41 "types" : [ "country", "political" ]
42 },
43 {
44 "long_name" : "22471",
45 "short_name" : "22471",
46 "types" : [ "postal_code_prefix", "postal_code" ]
47 }
48 ],
49 // Endereco completo, formatado
50 "formatted_address" : "Rua Carvalho Azevedo , 126 -206 - Lagoa , Rio - Rio
de Janeiro , Brazil",
51 // Tipo de endereco
52 "types" : [ "street_address" ]
53 },
54 ...
55 ],
56 "status" : "OK"
57 }
Ha uma lista de resultados para o par de coordenadas porque um mesmo
ponto pode representar varias coisas geograficamente. Por exemplo, um ponto
pode representar uma rua e seu paıs tambem.
37
2. Os dados acima sao interpretados e um novo BasePoint e criado. Dos dados
retornados do servico, sao extraıdos os seguintes campos para o novo Base-
Point:
• address Endereco formatado (formatted address)
• sublocality Bairro (o primeiro item da lista address components com
type ”sublocality”)
• locality Cidade (o primeiro item da lista address components com type
”locality”)
• addressComponents Dados do ponto extraıdos sem qualquer trata-
mento (address components)
3. Agora uma tarefa basePoint/processDistances e lancada para calcular a
distancia do ponto com as rotas do sistema.
4.3.4 basePoint/processDistances
Esta tarefa calcula distancias entre um BasePoint e todas as BaseRoutes proximas
o suficiente dele.
Parametros
basePoint BasePoint
Passos
1. Sao buscados todos os BaseRoutes com o campo centerPoint dentro de um raio
de 50km de basePoint, dessa forma processamento desnecessario e poupado.
2. Para cada BaseRoute encontrado, como baseRoute:
(a) E calculada a distancia mınima ate qualquer um dos pontos points de
baseRoute. O seguinte algoritmo, levando como parametros o ponto ba-
sePoint e os pontos points do baseRoute, e usado para o calculo:
1 closestPoint:function (point , points) {
2
3 var closestPoints = {}
38
4 , routesAt = {}
5 , totalDistance = 0
6
7 for (var i = 0, len = points.length - 1; i < len; i++) {
8 var closest = utils.gmaps.closestLinePoint(points[i], points[i +
1], point)
9 , dist = utils.gmaps.pointsDistance(point , closest)
10 if (closestPoints[dist]) continue
11 closestPoints[dist] = closest
12 routesAt[dist] = totalDistance + utils.gmaps.pointsDistance(
points [0], closest)
13 totalDistance += parseFloat(utils.gmaps.pointsDistance(points[i],
points[i + 1]) * 1000)
14 }
15
16 var min = Math.min.apply(Math , Object.keys(closestPoints))
17 return {
18 point:closestPoints[min],
19 progress:routesAt[min] / totalDistance ,
20 distance:min
21 }
22 }
23 }
O codigo acima retorna um objeto com os seguintes atributos:
• point Par de coordenadas do ponto em points mais proximo do ponto
basePoint
• progress Em que altura do trajeto o ponto encontrado point esta
no trajeto (de 0 - 0% a 1 - 100%)
• distance Distancia entre o ponto na rota point e o ponto original
basePoint
(b) Uma nova entidade RoutePointDistance e criada para baseRoute e base-
Point, com seus campos routePoint, progress e distance obtidos do objeto
acima.
Se uma entidade RoutePointDistance ja existia para baseRoute e base-
Point, ela e atualizada e outra nao precisa ser criada.
39
4.3.5 user/importData
Esta tarefa atualiza os dados de um usuario atraves do Facebook Graph API.
Parametros
user User para o qual atualizar os dados
Passos
1. Uma consulta ao Facebook Graph API pelos dados do usuario e feita no se-
guinte endereco:
https://graph.facebook.com/:facebookId:
Nessa URL, o facebookId representa o id de usuario no Facebook. Um
exemplo de resposta a essa chamada e:
1 {
2 "id": "742310501",
3 "name": "Guilherme Pim",
4 "first_name": "Guilherme",
5 "last_name": "Pim",
6 "link": "https ://www.facebook.com/pimguilherme",
7 "birthday": "08/26/1988",
8 "hometown": {
9 "id": "110346955653479",
10 "name": "Rio de Janeiro , Rio de Janeiro"
11 },
12 "location": {
13 "id": "110346955653479",
14 "name": "Rio de Janeiro , Rio de Janeiro"
15 },
16 "gender": "male",
17 "email": "[email protected]",
18 "timezone": -2,
19 "verified": true ,
20 "updated_time": "2013 -12 -05 T23 :00:43+0000",
21 "username": "pimguilherme"
22 }
2. O usuario user e entao atualizado com os dados obtidos do servico Facebook
Graph API.
40
3. Se essa e a primeira vez que o usuario tem seus dados atualizados, uma Noti-
fication do tipo user welcome e enviada a ele.
4.3.6 user/updateMutual
Esta tarefa atualiza as relacoes de amizade em comum de um usuario.
Parametros
user User para o qual atualizar os amigos
Passos
1. Sao buscados todos os amigos de user e armazenados na variavel friends
2. Sao buscados todos os amigos de amigos de user e armazenados na variavel
all
3. Para cada item da lista all, armazenado na variavel friend :
(a) Sao buscados os amigos em comum de user e friend e guardados mutual
(b) A lista mutual e persistida na amizade Friendship entre user e friend, em
seu campo mutualFriends
(c) A quantidade de amigos em comum e obtida da lista mutual e guardada
na Friendship, em seu campo counters.mutual
4.3.7 user/updateFriends
Esta tarefa e responsavel por consultar o Facebook Graph API e atualizar as
relacoes de amizade de um usuario.
Parametros
user User para o qual atualizar os amigos
Passos
1. Uma consulta ao Facebook Graph API pelos amigos do usuario e feita no
seguinte endereco:
41
https://graph.facebook.com/:facebookId:/friends
Nessa URL, o facebookId representa o id de usuario no Facebook. Um
exemplo de resposta a essa chamada e:
1 {
2 "data": [
3 {
4 "name": "Amigo 1",
5 "id": "10000000"
6 },
7 {
8 "name": "Amigo 2",
9 "id": "20000000"
10 },
11 {
12 "name": "Amigo 3",
13 "id": "30000000"
14 },
15 ...
16 ]
17 }
Como pode ser visto, a resposta contem uma lista de amigos do usuario.
Para cada item da lista, temos um amigo, representado pelo seu nome name
e pelo seu facebookId id.
2. Para cada amigo encontrado:
(a) E criada uma entidade Friendship para representar a amizade. Se uma
ja existe, nada e feito.
(b) Se um User com o campo facebookId ainda nao existe com o id do amigo,
e criado um novo User para ele. O novo User tera apenas seus campos
facebookId e name preenchidos da lista.
Esse novo usuario nao tem o campo hasJoined marcado. Isso indica
que ele e um usuario que ainda nao entrou no sistema, mas que foi criado
para representar um usuario amigo de algum outro que ja faz parte do
Minha Carona.
42
Capıtulo 5
Fluxos do Sistema
Neste capıtulo sao listados e explicados os principais fluxos que compoem o Minha
Carona. Sao eles:
• Login
• Criar Horario
• Buscar Motorista para Horario
• Enviar Pedido de Carona
• Criar Grupo
• Buscar Passageiros
• Enviar Convite de Carona
• Compartilhar Grupo
5.1 Login
O fluxo inicial do sistema e o login atraves do Facebook. E preciso que o usuario
possua uma conta no Facebook e esteja disposto a autorizar o aplicativo do Minha
Carona no gigante social para que possa usar a rede. Isso e necessario porque,
para que filtros de seguranca possam ser aplicados, e preciso conhecer os amigos do
usuario. Dessa forma, podemos restringir a visibilidade das caronas apenas a seus
amigos e amigos de amigos que fazem parte do sistema.
43
Figura 5.1: Interface para login
Ao final da autenticacao via Facebook, e obtido um identificador do usuario cha-
mado facebookId (como ”742310501”, por exemplo). O facebookId e um identificador
unico do Facebook que sempre e cedido ao fim do fluxo de autenticacao e sempre
estara associado aquele usuario. Esse codigo e usado para buscar um registro de
usuario no Minha Carona. Se nao houver um registro para o identificador, isso quer
dizer que o usuario e novo.
5.1.1 Novos usuarios
Quando um usuario que nunca fez login no sistema e portanto nao possui um
cadastro ainda faz o login, criamos uma entidade User para ele. A nova entidade
e associada ao facebookId, para que nas proximas vezes que o usuario fizer o lo-
gin, seu registro seja identificado. Quando esse usuario e criado, uma notificacao
user welcome e gerada para ele.
44
Sempre que um usuario entra pela primeira vez na plataforma, uma tarefa assıncrona
user/importData e criada para que seus dados sejam atualizados. A tarefa e res-
ponsavel por conectar-se ao Facebook Graph API e usar a autenticacao que o usuario
concedeu para buscar suas informacoes e atualizar a base de dados. Quando essa
tarefa e finalizada, uma notificacao user first import e gerada, indicando que seus
dados foram importados.
Figura 5.2: Notificacao de boas-vindas
5.1.2 Estabelecendo a sessao
Para que a sessao do usuario seja estabelecida, ela e guardada em cookies do nave-
gador, na variavel fbsr. A sessao do usuario e identificada atraves de um codigo cha-
mado signed request, que e cedido pelo Facebook no fim da autenticacao. Esse codigo
e um pacote de dados criptografado que so pode ser decodificado com uma chave
que apenas os administradores do aplicativo no Facebook tem acesso. Essa chave e
chamada de Secret Key pelo Facebook. Um exemplo de codigo signed request e:
”H1K6xNBSm-3f1-QOJUj6p1XXdKCkFWU1eg69OjmU8T0.eyJhbGdvcml0aG0i
OiJITUFDLVNIQTI1NiIsImNvZGUiOiJBUUNpWmk1WGFlQk1IdHE5YVp6SVR
YSDZseXdMVWZqZWZRekJkSWVkOTFsU2VnN096V0FCejA1MU5BeENLVmh
BRlBoRnUyMVZjd29MS2M1T2hiUTY5YWJtRGxBR0RPQWFYTF9obEJpUmZ
fekNSY2xrd0dLN0ZZbHhzTVduRVhDemZFQlhNZXYtVVV6Qm5EeXJDTWt4b
mVybmZfRGp6MGVpNURENWlySzZrUlZWX1lnNTF0YnBJUlVNblo1WGthb2p
Ydm1FbVRkemMzUWlYWFdxamcxMUdma19JVEpSb25vMGVjRVFXOGMzNF
45
JQYTI3WVFfUjNua0prVDMySklGUHpTVnFpNDJEcjJVdHJxZk9admw4V1MxZ
EtJUjluWnlDdGNPcUZWbmhPQlFUcWtxQ0FBVjY5ZnhOb0tOTDdnUWRLNm
FHZyIsImlzc3VlZF9hdCI6MTM4ODY4NzMzMiwidXNlcl9pZCI6Ijc0MjMxMDUw
MSJ9”
Para decodificar este codigo com a chave FACEBOOK SECRET (a chave de
fato nao pode ser cedida sem comprometer a aplicacao), o seguinte algoritmo foi
usado:
1 function parseSignedRequest(payload_string) {
2 try {
3 var payload = payload_string.split(’.’);
4 var sig = payload [0];
5 var data = JSON.parse(new Buffer(payload [1], ’base64 ’).toString ());
6
7 if (data[’algorithm ’]. toUpperCase () !== ’HMAC -SHA256 ’) {
8 console.log(’Unknown signed_request hash algorithm: ’ + data[’algorithm ’]);
9 return null;
10 }
11 var expected_sig = crypto.createHmac(’sha256 ’, FACEBOOK_SECRET);
12 expected_sig.update(payload [1]);
13 expected_sig = expected_sig.digest(’base64 ’);
14 if (sig !== expected_sig) {
15 console.log(’Bad signed_request encoding ’);
16 return null;
17 }
18
19 return data;
20 } catch (e) {
21 return null;
22 }
23 }
Apos a decodificacao do signed request, os seguintes dados sao extraıdos:
1 {
2 // Algoritmo usado para criptografar os dados
3 algorithm: ’HMAC -SHA256 ’,
4 // Codigo de sessao do Facebook
5 code: ’...’,
6 // Indica quando a autenticacao foi solicitada
7 issued_at: 1388687332 ,
8 // Identificador do usuario
9 user_id: ’742310501 ’
10 }
46
O atributo user id (facebookId no Minha Carona), e entao usado para identificar
o usuario do Facebook no sistema, como dito acima.
A cada requisicao do usuario, o cookie fbsr deve ser decodificado novamente para
identificar o internauta.
5.2 Criar Horario
O fluxo inicial para qualquer usuario do sistema e a criacao de horarios, pois
estes representam as demandas da rede. Para criar um horario, o usuario acessa a
Agenda e clica com o botao esquerdo em algum dia da semana para abrir a seguinte
ferramenta:
Figura 5.3: Interface para criacao de horarios
47
Preenchendo as informacoes de origem, destino, hora de chegada ou de saıda e se
o horario e semanal, o usuario pode entao prosseguir com a criacao do horario. O
pacote de dados enviado ao servidor para essa requisicao segue o seguinte formato:
1 {
2 // Coordenada geografica indicando origem
3 origin: LatLng ,
4 // Coordenada geografica indicando destino
5 destination: LatLng ,
6 // Horario de chegada no formato HHMM (0000 a 2359)
7 arriveTime: Number ,
8 // Horario de saıda no formato HHMM (0000 a 2359)
9 leaveTime: Number ,
10 // Data para a criacao do horario no formato YYYY-MM-DD
11 date: String ,
12 // Indica se o horario e periodico
13 periodic: Boolean
14 }
O servidor valida os dados e entao segue o processamento. Sao buscadas entidades
de BasePoint no banco de dados para ambos os locais de origem e destino atraves
de um filtro no campo latlngs.
5.2.1 Novos locais - sincronizacao e processamento
Quando um local nao esta presente no banco ainda, um novo BasePoint associado
as coordenadas do lugar e criado e uma tarefa basePoint/syncFromLocation e
lancada para que o novo ponto tenha seus dados sincronizados com o Google Maps.
Quando essa tarefa for concluıda, a tarefa basePoint/processDistances e usada
para calcular as distancias entre o novo BasePoint e todos os BaseRoutes da rede.
Esse segundo processo e chamado de processamento do ponto. Quando finalizado,
e dito que o ponto esta processado.
O horario estara pendente ate que a sincronizacao com o Google Maps e o processa-
mento do ponto estejam finalizados (esse processo todo leva no maximo 3 segundos):
48
Figura 5.4: Horario pendente
O campo processedPoints do Schedule e usado para verificar se ele ainda esta em
processamento. Esse campo contem referencia aos pontos ja processados do horario.
Se ambos os pontos ja foram processados, o Schedule tera a lista como no exemplo:
1 {
2 baseOrigin: "ID1",
3 baseDestination: "ID2",
4 processedPoints: [
5 "ID1",
6 "ID2"
7 ]
8 }
Como pode ser visto, ambos os pontos estao listados em processedPoints. Isso
indica que eles foram processados. Caso apenas um deles tenha sido processado ate
o momento, apenas o identificador do ponto processado aparecera. Por exemplo:
1 {
2 baseOrigin: "ID1",
3 baseDestination: "ID2",
4 processedPoints: [
5 "ID1"
6 ]
7 }
Caso nenhum ponto tenha sido processado ainda, processedPoints contera uma
lista vazia.
Quando a tarefa basePoint/syncFromLocation acabar, ela lancara a tarefa
basePoint/processDistances para processar o ponto recem-sincronizado. Uma
49
vez que essa tarefa acabar de calcular a distancia para o novo ponto e todos os
trajetos do sistema, todos os horarios que contiverem o BasePoint como origem ou
destino terao a entidade adicionada a lista processedPoints. Dessa forma, o fluxo de
processamento de pontos em horarios esta concluıda.
O usuario nao pode usar um horario ate que ambos seus pontos estejam processa-
dos. Isso porque a informacao de distancia entre o ponto e os trajetos do sistema nao
existe ate que o ponto seja processado, e, portanto, nao e possıvel buscar caronas
baseadas na distancia entre a demanda e os trajetos.
5.2.2 Locais ja existentes
Quando um local ja existe no banco de dados, o BasePoint e obtido e associ-
ado ao horario. Se esse BasePoint ja foi processado, como pode ser visto em seu
campo processedAt, ele e adicionado imediatamente ao campo processedPoints do
Schedule. Caso o ponto ainda nao esteja processado, ele nao e adicionado ao campo
processedPoints, pois espera-se que uma tarefa basePoint/syncFromLocation ou
basePoint/processPoints esteja em execucao, ao final das quais o ponto estara
processado e sera adicionado a lista.
5.2.3 Finalizacao
A parte complexa de criacao dos horarios e essa relacionada aos pontos. As de-
mais informacoes do horario nao requerem qualquer fluxo assıncrono e portanto sao
apenas validadas e persistidas no banco de dados em uma nova entidade Schedule.
Um exemplo concreto de um Schedule criado segue:
1 {
2 "__v" : 1,
3 "_id" : ObjectId("51 ce11a85f13390d00000007"),
4 "date" : "2013 -06 -29",
5 "destination" : {
6 "address" : "Rua Reseda , Lagoa , Rio de Janeiro , 22471 -230 , Brazil",
7 "lat" : -22.9638865 ,
8 "lng" : -43.2023063
9 },
10 "group" : null ,
11 "groups" : [ ],
50
12 "leaveTime" : 1200,
13 "origin" : {
14 "address" : "Avenida Horacio Macedo , 1212 -1356 - Cidade Universitaria ,
Rio de Janeiro , 21941 -598 , Brazil",
15 "lat" : -22.8552649 ,
16 "lng" : -43.2313681
17 },
18 "periodic" : {
19 "index" : 6,
20 "frequency" : "weekly"
21 },
22 "processedPoints" : [
23 "51 cd291402b225f51b31fb21",
24 "51 cd291402b225f51b31fb22"
25 ],
26 "baseDestination" : ObjectId("51 cd291402b225f51b31fb22"),
27 "baseOrigin" : ObjectId("51 cd291402b225f51b31fb21"),
28 "user" : ObjectId("51 cd28ebad70db0900000002"),
29 "weeklyIndex" : 6
30 }
Esse e um horario saindo as 12h da UFRJ e indo para a Rua Reseda. O horario e
semanal para todos os Sabados (periodic.index=6) e foi criado no dia ”2013-06-29”.
Ambos os pontos ja foram processados, como pode ser visto em processedPoints.
Com a criacao do horario finalizada, o proximo passo e buscar motoristas ou criar
um grupo para encontrar passageiros.
5.3 Buscar Motorista para Horario
Com demandas cadastradas no sistema, e possıvel comecar a buscar motoristas
para um horario. Buscar motoristas, na realidade, significa buscar entidades Groups
que possam ser usadas pelo usuario para um determinado horario. A busca de grupos
pode ser feita apenas para um horario por vez.
Com grupos encontrados, o usuario usa entao o fluxo de Enviar Pedido de
Carona para enviar pedidos e, mediante aprovacao do motorista, entrar nos grupos
e comecar a pegar caronas.
51
Para iniciar a busca, o usuario deve clicar em algum horario da Agenda, abrindo
a seguinte tela:
Figura 5.5: Busca de motoristas para horario
5.3.1 Filtros de Busca
Para melhorar a busca de motoristas, sao disponibilizados filtros ao usuario, como
visto na seguinte imagem:
Figura 5.6: Filtros para busca de motoristas
A seguir, e explicado como cada filtro funciona.
52
5.3.1.1 Tolerancia de Tempo
O filtro de tolerancia de tempo garante que o usuario encontrara apenas motoristas
que devem sair ou chegar dentro de uma faixa de tolerancia em comparacao as horas
cadastradas nos horarios. O filtro permite valores de 15m, 30m, 1h, 1h30m e 2h
de tolerancia temporal. Na busca do banco de dados, esse filtro age nos campos
arriveTime e leaveTime de grupos e do horario em questao.
Supondo o tempo de tolerancia T e as horas leaveTimeHorario e arriveTimeHo-
rario conhecidas do horario, sao buscados no banco de dados entidades Group com:
1 {
2 arriveTime: entre arriveTimeHorario + T e arriveTimeHorario - T,
3 leaveTime: entre leaveTimeHorario + T e leaveTimeHorario - T
4 }
5.3.1.2 Grau de Amizade
Para promover a seguranca e a privacidade da busca, ha um filtro que permite que
o usuario encontre apenas grupos criados por usuario com algum nıvel de amizade
definido. Os graus de amizade permitidos para a busca sao ”Amigos”e ”Amigos +
Amigos de Amigos”.
A implementacao desse filtro nao e otima, e e lenta - a demora e de ate 5s em
alguns casos. Os dois passos do algoritmo sao:
1. Primeiro, todos os amigos do usuario sao buscados da colecao Friendships e
armazenados na memoria em uma variavel friends. A busca em Friendships e
feita consultando a lista no campo users, de forma que o usuario esteja nela.
Essa lista users tem sempre dois usuarios - no caso, um deles tera que ser o
usuario em questao, e o outro seu amigo representado na relacao). Para cada
amizade Friendship obtida na busca, o amigo (o outro usuario da lista users)
e adicionado a variavel friends.
Se a busca e por ”Amigos + Amigos de Amigos”, apos armazenar todos os
amigos do usuario em friends, sao entao buscados todos os amigos de amigos
atraves de outra consulta em Friendships. Essa consulta e analoga a anterior,
53
porem sao buscadas todas as relacoes de amizade para TODOS os amigos
do usuario. Esse e um processo custoso, como pode ser visto ao final dessa
subsecao. Todos os amigos encontrados sao adicionados a variavel friends.
2. Em seguida, a busca no banco e feita para encontrar todos os grupos para os
quais seu campo user esta dentro da lista friends. Na notacao do MongoDB,
este filtro e representado por:
1 {
2 user: {$in: friends}
3 }
5.3.1.3 Distancia maxima a Origem e Destino
Esse filtro da ao usuario a habilidade de indicar o quanto ele ”gostaria de an-
dar”para pegar uma carona. O filtro pode ser feito para as distancias fixas 250m,
500m, 1km, 1.5km e 2km a origem e destino. O filtro pode tambem ser aplicado no
mapa:
Figura 5.7: Filtro de distancias no mapa
1. Sao buscadas todas as entidades RoutePointDistance com distance menor do
que o valor do filtro da origem e com o basePoint igual ao baseOrigin do
horario. Para cada RoutePointDistance encontrado, sao guardados seus valo-
res de baseRoute e progress em uma lista routes.
54
A lista routes e necessaria para garantir que estamos encontrando trajetos
na mesma direcao do usuario. A lista fica como no seguinte exemplo:
1 [
2 ...
3 {
4 baseRoute: "ID1",
5 progress: 0.2
6 },
7 {
8 baseRoute: "ID2",
9 progress: 0.7
10 },
11 ...
12 ]
O campo progress e um decimal de 0 a 1, indicando em que altura do trajeto
o ponto - no caso, de origem - esta. Nesse cenario, 0 indica que a origem do
horario coincide com a origem da baseRoute e 1 indica que a origem do horario
coincide com o destino da baseRoute.
2. Sao buscadas todas as entidades RoutePointDistance com seu campo distance
menor do que o valor do filtro do destino, com o campo basePoint igual ao
baseDestination do horario, e com o campo progress maior do que o progress
em cada uma das rotas routes encontradas no passo 1. Ao final desse passo,
encontramos varias entidades RoutePointDistance cujos baseRoutes sao as ro-
tas proximas o suficiente para o horario com os filtros dados. Essas rotas sao
extraıdas para uma variavel filteredRoutes.
3. Sao buscados todos os grupos com o campo baseRoute contido na lista filtere-
dRoutes.
O filtro mais interessante e complexo do sistema e este. Por tras deste filtro, ha
um mecanismo de processamento assıncrono e distribuıdo no banco de dados para
garantir que seja possıvel encontrar a distancia mınima entre pontos e trajetos sem
atrapalhar o usuario e sem desperdicar processamento. Esse mecanismo e descrito
nas tarefas basePoint/* e baseRoute/*, e nas estruturas de dado BasePoint,
BaseRoute e RoutePointDistance.
55
5.3.2 Agregacao de Filtros
Todos os filtros apresentados acima sao agregados - eles sao aditivos, e nao sao
exclusivos.
5.4 Enviar Pedido de Carona
Uma vez que o usuario encontrou um motorista disponıvel para algum de seus
horarios, ele pode entao enviar um convite para entrar no grupo. Isso e feito atraves
da seguinte interface:
Figura 5.8: Interface com informacoes sobre um grupo
A tela exibe diversas informacoes sobre o grupo para que o usuario decida se quer
entrar no grupo. Sao listadas todas as pessoas no grupo, a contribuicao desejada pelo
motorista, as peculiaridades (como: se e permitido fumar, se ha ar, etc). Tambem
sao exibidas informacoes sobre a proxima carona: se o motorista esta confirmado e
quantas vagas ha. Se houver caronas passadas, essas tambem sao mostradas, para
indicar o grau de atividade do grupo - um grupo as moscas e menos preferıvel do
que um grupo ativo.
56
Apos decidir-se, o usuario pode clicar em ”Entrar no grupo”para enviar um pe-
dido. Fazendo isso, ele tem acesso a seguinte tela:
Figura 5.9: Interface para enviar pedidos de carona
Nessa etapa, o usuario deve preencher uma mensagem e escolher onde gostaria
de entrar e sair da carona, e a que horas encontraria o motorista. Para auxiliar o
motorista a entender onde ele gostaria de ser pego, o usuario pode tambem escrever
onde deve ser o ponto de encontro.
Apos enviar o pedido, este fica imediatamente disponıvel na aba de Pedidos e
Convites do horario, esperando a aprovacao do motorista. Quando o motorista
aceita ou rejeita o pedido, o usuario que o enviou recebe uma notificacao.
57
Figura 5.10: Pedidos e Convites do horario
5.5 Criar Grupo
Para comecar a organizar caronas e buscar passageiros, apos criar um horario,
o usuario deve criar um grupo. Clicando em ”POSSO DIRIGIR!”no dashboard de
horarios, a seguinte interface e disponibilizada:
Figura 5.11: Interface para criar grupos
58
Preenchendo as informacoes de hora de chegada e de saıda, o trajeto usado, e se
o horario e semanal, o usuario pode entao prosseguir com a criacao do grupo. O
pacote de dados enviado ao servidor para essa requisicao segue o seguinte formato:
1 {
2 // Horario de chegada no formato HHMM (0000 a 2359)
3 arriveTime: Number ,
4 // Horario de saıda no formato HHMM (0000 a 2359)
5 leaveTime: Number ,
6 // Lista de coordenadas para formar o trajeto
7 route: [LatLng],
8 // Indica quem pode ver a rota
9 routePrivacy: Enum [’fof’,’friends ’,’group’],
10 // Indica quem pode ver o grupo
11 groupPrivacy: Enum [’fof’,’friends ’]
12 }
5.5.1 Escolhendo o trajeto
Para escolher o trajeto, o usuario deve usar o mapa disponibilizado. Arrastando a
linha que forma o trajeto, ele pode ser modificado. No exemplo seguem dois trajetos
diferentes criados dessa forma:
Figura 5.12: Diferentes trajetos na criacao de grupos
Modificando o trajeto, o usuario esta configurando o parametro route que sera
enviado ao servidor. Cada bola branca exibida no mapa e chamada de waypoint e
representa um item na lista route. A denominacao waypoint e definida no Google
Maps e representa uma coordenada que deve ser visitada no trajeto calculado. A
59
ordem dos itens na lista e importante, pois ela define a ordem de visita dos waypoints
na viagem simulada pelo Google Maps.
Um exemplo para o parametro route:
1 [
2 {
3 lat: ’23.102300 ’,
4 lng: ’48.123222 ’
5 },
6 {
7 lat: ’23.102650 ’,
8 lng: ’48.123239 ’
9 }
10 ]
5.5.2 Privacidade do grupo
Para promover a seguranca e a privacidade do grupo, existem os parametros
routePrivacy e groupPrivacy. Eles sao responsaveis por definir que grau de amizade
um usuario precisa ter para, respectivamente, ver a rota que o motorista faz no
grupo e ver o grupo na busca de motoristas.
Atraves do parametro routePrivacy, o usuario pode definir que apenas membros
do grupo (’group’), apenas amigos (’friends’) ou apenas amigos e amigos de amigos
(’fof’) podem ver o trajeto feito por ele no mapa.
Com o parametro groupPrivacy, o usuario pode definir que apenas amigos (’fri-
ends’) ou apenas amigos e amigos de amigos (’fof’) podem ver seu grupo na busca
de motoristas.
Usuario sem permissoes de rota Quando um usuario nao tem permissoes para
ver as rotas de um grupo, porem pode ver o grupo na busca, o seguinte alerta e
exibido a ele sobre o mapa:
60
Figura 5.13: Alerta para usuario sem permissao de ver rota do grupo
5.6 Buscar Passageiros
Com um grupo criado, o usuario pode entao comecar a buscar por passageiros.
Isso e feito atraves da seguinte ferramenta, na interface do grupo:
Figura 5.14: Ferramenta para buscar passageiros
No exemplo acima, nao ha nenhum usuario amigo ou amigo de amigos que poderia
fazer parte do grupo do motorista. Uma grande diferenca entre a busca de passagei-
ros e a busca de motoristas e que nao ha filtros para a busca de passageiros.
Ou melhor, os filtros existem mas nao podem ser alterados pelo usuario.
61
Compartilhamento Quando nao ha ninguem para quem o motorista pode ofere-
cer caronas, ele e estimulado a compartilhar seu grupo no Facebook para que mais
pessoas conhecam o sistema e seu horario. O compartilhamento e explicado no fluxo
Compartilhar Grupo.
Quando ha passageiros que podem aproveitar caronas do motorista para o grupo,
a seguinte lista e detalhes sao apresentados:
Figura 5.15: Passageiros encontrados para um grupo
Para cada passageiro da lista, o motorista pode ver quanto, no maximo, aquela
pessoa teria que andar para pegar a carona. Alem disso, ele ve tambem a diferenca
de horarios maxima entre seu grupo e o horario do passageiro. Clicando sobre o
nome do passageiro, ele tem acesso a seu perfil no Facebook. Se os usuarios nao
sao amigos, os amigos em comum entre eles sao listados. Quando um passageiro e
selecionado, diversas informacoes sao apresentadas no mapa.
62
No exemplo acima, o passageiro selecionado teria que andar no maximo 121m e
se atrasaria ou se adiantaria no maximo 15min em seu horario desejado. No mapa,
podem ser vistas informacoes mais detalhadas - o passageiro quer sair as 18h45 do
mesmo lugar que o motorista e quer saltar em algum lugar do trajeto que esta a
121m do seu destino desejado.
Com todas as informacoes disponibilizadas, o motorista pode decidir se deseja
convidar o usuario ou nao. O convite e explicado no fluxo Enviar Convite de
Carona.
5.6.1 O Mecanismo de Busca
Filtros de busca Como dito anteriormente, os filtros da busca de passageiros sao
fixos. Sao eles:
• Tolerancia de Tempo: 15min
• Grau de Amizade: Amigos + Amigos de Amigos
• Distancia a Origem e Destino: 300m
Nota Os filtros estao explicados no fluxo Buscar Motorista para Horario.
A implementacao da busca de passageiros e similar a da busca de motoristas,
porem mais simples. A maior diferenca e que sao buscados horarios que podem usar
o grupo do motorista, ao inves de grupos que podem usar o horario do usuario. Em
termos mais tecnicos, sao buscados Schedules que podem usar o Group do usuario,
ao inves de Groups que podem ser usado pelo Schedule do usuario.
1. Sao buscadas todas as RoutePointDistances que tem o campo baseRoute igual
ao baseRoute do grupo para o qual os passageiros estao sendo buscados e o
campo distance menor que 300m. As entidades encontradas sao guardadas em
points. Em points estao todos os BasePoints que estao proximos o suficiente
do trajeto feito no grupo.
2. E necessario encontrar todos horarios que tem um desses pontos como origem
e outro como destino. Mas, antes disso, e necessario garantir que a combinacao
63
dos pontos esta na mesma direcao do trajeto. Para isso, a lista em points e
percorrida e outra lista e gerada, com combinacoes de pontos que estao na
direcao do trajeto. Isso e feito juntando um ponto com todos os pontos que
tem o campo progress maior que ele (isso indica que esses pontos estao a sua
frente no trajeto e, portanto, na mesma direcao do trajeto).
O resultado desse processo e outra lista, combinedPoints, com o seguinte
formato:
1 [
2 ...
3 {
4 origin: "ID1" (BasePoint),
5 destination: "ID2" (BasePoint)
6 }
7 ...
8 ]
3. Buscar todos os Schedules que possuem nos campos baseOrigin e baseDesti-
nation uma combinacao da lista combinedPoints. Alem do filtro de distancia,
tambem sao aplicados os filtros de tolerancia de tempo para 15min e de grau de
amizade para Amigos + Amigos de Amigos, como explicado no fluxo Buscar
Motorista para Horario.
5.7 Enviar Convite de Carona
Uma vez que o usuario encontrou um passageiro disponıvel para algum de seus
grupos, ele pode entao enviar um convite de carona. Isso e feito atraves da seguinte
interface:
64
Figura 5.16: Interface para enviar convites de carona
A ferramenta de convites da ao motorista a opcao de enviar uma mensagem para
um usuario escolhido da lista.
Apos enviar o convite, este fica imediatamente disponıvel na aba de Pedidos e
Convites do horario, esperando a aceitacao do usuario. Quando o usuario aceita ou
rejeita o pedido, o usuario que o enviou recebe uma notificacao.
65
Figura 5.17: Pedidos e Convites do horario
5.8 Compartilhar Grupo
Para compartilhar um grupo, o usuario deve usar a seguinte ferramenta disponi-
bilizada em sua interface:
66
Figura 5.18: Ferramenta para compartilhamento de grupo
Clicando no botao de compartilhar, o usuario compartilha seu grupo no Facebook.
Um grupo compartilhado aparece na rede social da seguinte forma:
Figura 5.19: Grupo compartilhado no Facebook
67
Capıtulo 6
Conclusao
Este capıtulo trata de estudos realizados e planos futuros desenhados para o
projeto.
6.1 Estudos
Nesta secao, serao apresentados alguns topicos estudados no software imple-
mentado.
6.1.1 Inconsistencia do banco de dados
Por nao haver consultas com associacao entre colecoes no banco escolhido, a arqui-
tetura foi desenhada de forma nao normalizada. Isso quer dizer que ha informacao
replicada no banco, para que nao sejam necessarias varias consultas separadas. Um
exemplo dessa replicacao de dados esta na entidade Schedule, que contem os campos
origin e destination como copias dos dados mantidos nos BasePoints relativos a eles
(respectivamente baseOrigin e baseDestination).
Essa duplicidade dos dados diminui o tempo de consulta no banco, pois nao mais
e necessario consultar varias colecoes (seria necessario consultar os Schedules para
buscar um horario, e consultar tambem os BasePoints para obter as informacoes
relativas a origin e destination).
68
Se as entidades dos BasePoints forem atualizadas, isso nao sera automaticamente
replicado para seus dados analogos nos Schedules. Portanto algum processamento a
mais deve ser implementado para sanar esse advento. Isso sera resolvido nos Planos
futuros, ao fim deste capıtulo, na subsecao Tarefas de consistencia.
6.1.2 Escalabilidade de uma rede social
Importar para o sistema as relacoes de amizades de uma rede social gigantesca
como e o Facebook provou-se um desafio. A cada vez que um usuario novo entra
no sistema, todos seus amigos sao importados com ele. Usando a media de 200
amigos por usuario [1], cada novo usuario no Minha Carona traz consigo mais 200
usuarios para o banco. Com uma base de 1000 usuarios, ja existem 1000 * 200 =
200000 registros de usuarios em Users e mais 200000 registros de amizade na colecao
Friendships.
Conforme a base cresce, as consultas ficam mais demoradas e e necessario mais
espaco no disco rıgido e memoria. Para solucionar esse problema, algumas opcoes
foram avaliadas:
• Mudar a tecnologia usada, usando um banco de dados de grafo
• Escalar o servico horizontalmente, usando mais servidores, em nuvem
Para esse projeto, a solucao aplicada seria a escalabilidade em nuvem. Ela parece
menos custosa ja que a arquitetura dos servidores detalhada na pagina 8 ja esta
distribuıda.
6.2 Planos futuros
6.2.1 Tarefas de consistencia
Para acabar com a inconsistencia do banco, algumas tarefas assıncronas serao
usadas. Elas serao responsaveis por realizar operacoes no banco, transportando
dados de uma colecao a outra.
69
Para o problema apresentado no item Inconsistencia do banco de dados
acima, uma tarefa com o nome consistency/schedule/addresses sera criada. Ela
garantira que os dados necessarios da colecao BasePoints estao presentes na colecao
Schedule.
Para orquestrar essa manutencao de consistencia, um agendador de tarefas sera
implementado. Esse agendador fara disparos de tarefas de consistencia em horarios
de baixo movimento, para o processamento extra nao afete a experiencia do usuario.
6.2.2 Escalabilidade na nuvem
Para acabar com os problemas de escalabilidade dispostos no capıtulo anterior,
mais servidores serao usados na infra-estrutura da Amazon. Como o problema so
apresentou gargalo no banco de dados, serao necessarios mais servidores deste tipo
de software apenas.
Dada sua inerente caracterıstica de alta escalabilidade, a operacao de crescer
a rede de servidores de banco de dados resume-se a configuracao de servidores e
contratacao de mais maquinas na nuvem.
70
Bibliografia
[1] Facebook. Facebook Friends Average Study. url: http://www.facebook.com/
notes / facebook - data - team / anatomy - of - facebook / 10150388519243859
(acesso em 11/01/2014).
[2] Google. Encoded Polyline Algorithm Format. url: https://developers.google.
com/maps/documentation/utilities/polylinealgorithm (acesso em 08/01/2014).
71
Apendice A
Tarefas Assıncronas
A.1 Implementacao das Tarefas
Esse anexo contem todo o codigo usado para implementar cada uma das tarefas
assıncronas listadas no Capıtulo 4.
A.1.1 baseRoute/syncFromRoute
1 jobs.process(’baseRoute/syncFromRoute ’, function (job , cb) {
2
3 var baseOrigin = job.data.baseOrigin
4 , baseDestination = job.data.baseDestination
5 , waypoints = job.data.waypoints
6
7 async.waterfall ([
8 // Expanding our baseOrigin and baseDestinations
9 function (cb) {
10 models.BasePoint.find({_id:{$in:[baseOrigin , baseDestination ]}}, function (
err , docs) {
11 if (err) return cb(err)
12 if (!docs.length) {
13 return cb(’missing_basepoints ’)
14 }
15 if (docs [0].id == baseOrigin) {
16 baseOrigin = docs [0]
17 baseDestination = docs [1]
18 } else {
19 baseOrigin = docs [1]
20 baseDestination = docs [0]
21 }
22 if (! baseOrigin || !baseDestination) {
23 return cb(’missing_basePoint ’)
72
24 }
25 else if (! baseOrigin.lat || !baseOrigin.lng || !baseDestination.lng || !
baseDestination.lat) {
26 return cb(’missing_coordinates ’)
27 }
28 cb()
29 })
30 },
31 // Requesting GMaps
32 function (cb) {
33 models.BaseRoute.findOne(
34 {
35 baseOrigin:baseOrigin ,
36 baseDestination:baseDestination ,
37 waypoints:models.BaseRoute.getWaypointsKey(waypoints)
38 },
39 function (err , doc) {
40 if (err) return cb(err)
41 gmaps.directions(
42 baseOrigin.lat + ’,’ + baseOrigin.lng ,
43 baseDestination.lat + ’,’ + baseDestination.lng ,
44 function (err , data) {
45 if (err) return cb(err)
46
47 if (gmaps.limitsExceeded(data)) {
48 return cb(’limits_exceeded ’)
49 }
50
51 if (!( data.routes instanceof Array) || !data.routes.length)
{
52 return cb(’invalid_results ’)
53 }
54
55 var route = data.routes [0]
56 if (!( route.legs instanceof Array) || route.legs.length > 1)
{
57 return cb(’invalid_legs ’)
58 }
59 var leg = route.legs [0]
60
61 // Attributes for the new baseRoute
62 var points = utils.decodePolyline(route.overview_polyline.
points)
63 , attrs = {
64 baseOrigin:baseOrigin ,
65 baseDestination:baseDestination ,
66 centerPoint :{
67 lat:(route.bounds.northeast.lat + route.bounds.
southwest.lat) / 2,
73
68 lng:(route.bounds.northeast.lng + route.bounds.
southwest.lng) / 2
69 },
70 duration:leg.duration.value ,
71 distance:leg.distance.value ,
72 polyline:route.overview_polyline.points ,
73 points:points ,
74 totalPoints:points.length ,
75 syncedAt:new Date()
76 }
77 cb(null , attrs)
78 },
79 ’false’,
80 ’driving ’,
81 GMapsWaypoints(waypoints)
82 )
83 })
84 },
85 // Creating/Updating BaseRoute
86 function (attrs , cb) {
87 models.BaseRoute.update(
88 {baseOrigin:baseOrigin , baseDestination:baseDestination , polyline:attrs.
polyline},
89 {
90 $addToSet :{
91 waypoints:models.BaseRoute.getWaypointsKey(waypoints)
92 },
93 $set:attrs
94 },
95 {upsert:true , multi:true},
96 function (err) {
97 if (err) return cb(err)
98 // Find our newly updated/created document
99 models.BaseRoute.findOne ({ baseOrigin:baseOrigin.id, baseDestination:
baseDestination.id, polyline:attrs.polyline}, cb)
100 }
101 )
102 },
103 // Consistency
104 function (baseRoute , next) {
105
106 async.series ([
107 function (next) {
108 // Updating groups with the proper baseRoute
109 models.UserRoute.distinct(’_id’,
110 {
111 baseOrigin:baseOrigin ,
112 baseDestination:baseDestination ,
113 waypointsKey:models.BaseRoute.getWaypointsKey(waypoints)
74
114 },
115 function (err , routes) {
116 if (err) return cb(err)
117 models.Group.update ({
118 route:{$in:routes}
119 }, {
120 $set:{
121 baseRoute:baseRoute
122 }
123 },
124 {multi:true},
125 function (err) {
126 if (err) return next(err)
127 models.Group.find(
128 {
129 baseRoute:baseRoute
130 },
131 function (err , docs) {
132 if (err) return next(err)
133 API.modelsUpdated(docs)
134 next()
135 }
136 )
137
138 })
139 }
140 )
141 },
142 function (next) {
143 models.UserRoute.update(
144 {
145 baseOrigin:baseOrigin ,
146 baseDestination:baseDestination ,
147 waypointsKey:models.BaseRoute.getWaypointsKey(waypoints)
148 },
149 {
150 $set:{
151 baseRoute:baseRoute ,
152 polyline:baseRoute.polyline
153 }
154 },
155 {multi:true}, next)
156 },
157 function (next) {
158
159 jobs.create(’baseRoute/processDistances ’, {
160 baseRoute:baseRoute.id
161 }).unique(false).save()
162
75
163 jobs.create(’baseRoute/downloadMap ’, {
164 id:baseRoute.id
165 }).unique(false).save()
166
167 next()
168 }
169 ], next)
170
171 }
172 ], cb)
173
174 })
A.1.2 baseRoute/processDistances
1 jobs.process(’baseRoute/processDistances ’, function (job , cb) {
2 models.BaseRoute.findById(job.data.baseRoute , function (err , doc) {
3 if (err) return cb(err)
4 if (!doc) return cb(’missing_baseRoute ’)
5
6 async.series ([
7 function (cb) {
8 // We first look for all the points which are already processed, so we don’t process
9 // them again
10 models.RoutePointDistance.find({ baseRoute:doc}, function (err ,
processedPoints) {
11 if (err) return cb(err)
12 // And then look for the points which are not yet processed, and process them.
13 models.BasePoint.find({
14 _id:{$nin:processedPoints},
15 ’lat’:{$lt:doc.centerPoint.lat + 5, $gt:doc.centerPoint.lat -
5},
16 ’lng’:{$lt:doc.centerPoint.lng + 5, $gt:doc.centerPoint.lng - 5}
17 }, function (err , points) {
18 if (err) return cb(err)
19 if (! points.length) return cb()
20
21 // Parallel processing of routes and points distances
22 var q = async.queue(function (point , cb) {
23 models.RoutePointDistance.calculateDistances(doc , point , cb)
24 }, 2)
25
26 q.drain = cb
27 q.push(points)
28 })
29 })
30 },
31 // Updates the BaseRoute to be processedAt now
32 function (cb) {
76
33 doc.processedAt = Date.now()
34 doc.save(cb)
35 },
36 function (cb) {
37 // Consistency to Groups
38 models.Group.update(
39 { baseRoute:doc.id, processedAt:null },
40 { $set:{ processedAt:Date.now()} },
41 {multi:true},
42 function (err , total) {
43 if (err) return cb(err)
44 if (!total) return cb()
45 models.Group.find({
46 baseRoute:doc.id
47 }, function (err , docs) {
48 if (err) return next(err)
49 API.modelsUpdated(docs)
50 cb()
51 })
52 }
53 )
54 }
55 ], cb)
56 })
57 })
A.1.3 basePoint/syncFromLocation
1
2 var VALID_GEOCODE_TYPES = [’street_address ’, ’route’, ’locality ’]
3
4 // Creates a BasePoint from its location, if it doesn’t yet exists
5 jobs.process(’basePoint/syncFromLocation ’, function (job , cb) {
6
7 var originalLat = job.data.lat
8 , originalLng = job.data.lng
9 , useOriginal = job.data.useOriginal
10
11 if (! originalLat || !originalLng) {
12 return cb(’invalid_params ’)
13 }
14
15 // We have to make sure the user knows this is an invalid schedule
16 var invalid = function (error) {
17
18 var users
19 async.series ([
20 // We’ll just remove the UserPlaces
21 function (next) {
77
22 models.UserPlace.distinct(
23 ’_id’,
24 {
25 $or:[
26 {’origin.lat’:originalLat , ’origin.lng’:originalLng},
27 {’destination.lat’:originalLat , ’destination.lng’:
originalLng}
28 ]
29 },
30 function (err , ids) {
31 if (err) return next(err)
32 API.modelsDestroyed(ids , ’UserPlace ’)
33 models.UserPlace.remove ({_id:{$in:ids}}, next)
34 }
35 )
36 },
37 // We’ll send the users notifications indicating his
38 // locations were invalid
39 function (next) {
40 models.Schedule.find(
41 {
42 $or:[
43 {’origin.lat’:originalLat , ’origin.lng’:originalLng},
44 {’destination.lat’:originalLat , ’destination.lng’:
originalLng}
45 ]
46 },
47 function (err , docs) {
48 users = _.unique(_.pluck(docs , ’user’))
49 models.Schedule.remove(
50 {
51 _id:{$in:docs}
52 },
53 function (err) {
54 if (err) return next(err)
55 API.modelsDestroyed(docs)
56 next()
57 }
58 )
59 }
60 )
61 },
62 // Notifications for the users
63 function (next) {
64 users.each(function (user) {
65 models.User.sendNotification(
66 user ,
67 {
68 type:’invalid_schedule_location ’,
78
69 data:{
70 type:error
71 }
72 }
73 )
74 })
75 next()
76 }
77
78 ],
79 cb
80 )
81 }
82
83 models.BasePoint.findOne(
84 {
85 latlngs:models.BasePoint.getLatLngKey(originalLat , originalLng)
86 },
87 function (err , doc) {
88 if (err) return cb(err)
89
90 // Let’s go to google and check their reverse geocoding for the address
91 gmaps.reverseGeocode(gmaps.checkAndConvertPoint ([ originalLat , originalLng ]),
function (err , data) {
92 if (err) return cb(err)
93
94 if (gmaps.limitsExceeded(data)) {
95 return cb(’limits_exceeded ’)
96 }
97
98 if (!( data.results instanceof Array) || !data.results.length) {
99 return cb(’invalid_results ’)
100 }
101
102 /**
103 * Place type checking
104 */
105 var place = data.results.find(function (result) {
106 return result.types.intersect(VALID_GEOCODE_TYPES).length
107 })
108 if (!place) {
109 return invalid(’invalid_place_type , results[’ + data.results.length
+ ’]’)
110 }
111
112 /**
113 * Place Location & Address
114 */
79
115 if (!place.geometry || !place.geometry.location || !place.geometry.
location.lat || !place.geometry.location.lng) {
116 return invalid(’invalid_place_location ’)
117 }
118 if (!place.formatted_address) {
119 return invalid(’invalid_place_address ’)
120 }
121
122 /**
123 * Sublocality (Bairros)
124 */
125 var sublocality = place.address_components.find(function (a) {
126 return ˜a.types.indexOf(’sublocality ’)
127 })
128 sublocality = (sublocality && sublocality.short_name) || null
129
130 /**
131 * Locality (Cidade)
132 */
133 var locality = place.address_components.find(function (a) {
134 return ˜a.types.indexOf(’locality ’)
135 })
136 locality = (locality && locality.short_name) || null
137
138 /**
139 * Data
140 */
141
142 var address = place.formatted_address
143 , lat = place.geometry.location.lat
144 , lng = place.geometry.location.lng
145
146 if (useOriginal) {
147 lat = originalLat
148 lng = originalLng
149 }
150
151 async.waterfall ([
152
153 // Atomic update
154 function (next) {
155 models.BasePoint.update(
156 {lat:lat , lng:lng},
157 {
158 $addToSet :{
159 latlngs :{$each:[
160 models.BasePoint.getLatLngKey(originalLat ,
originalLng),
161 models.BasePoint.getLatLngKey(lat , lng)
80
162 ]}
163 },
164 $set:{
165 lat:lat ,
166 lng:lng ,
167 address:address ,
168 sublocality:sublocality ,
169 locality:locality ,
170 syncedAt:new Date ,
171 addressComponents:place.address_components
172 }
173 },
174 {
175 upsert:true ,
176 multi:true
177 },
178 function (err) {
179 next(err)
180 }
181 )
182 },
183
184 // Find our newly updated/created document
185 function (next) {
186 models.BasePoint.findOne ({lat:lat , lng:lng}, next)
187 },
188
189 /**
190 * Consistency Updates
191 */
192 //
193 function (basePoint , next) {
194 async.parallel ([
195 function (next) {
196 models.UserPlace.update(
197 {lat:originalLat , lng:originalLng},
198 {$set:{ address:address , lat:lat , lng:lng}},
199 {multi:true},
200 next
201 )
202 },
203 function (next) {
204 models.Schedule.update(
205 {’origin.lat’:originalLat , ’origin.lng’:originalLng
},
206 {$set:{’origin.address ’:address , ’origin.lat’:
originalLat , ’origin.lng’:originalLng ,
baseOrigin:basePoint}},
207 {multi:true},
81
208 next
209 )
210 },
211 function (next) {
212 models.Schedule.update(
213 {’destination.lat’:originalLat , ’destination.lng’:
originalLng},
214 {$set:{’destination.address ’:address , ’destination.
lat’:originalLat , ’destination.lng’:originalLng ,
baseDestination:basePoint}},
215 {multi:true},
216 next
217 )
218 }],
219 function (err) {
220 next(err , basePoint)
221 }
222 )
223
224 },
225 // Let’s get a hold of our Schedules which will be changed and send
226 // them via the socket
227 function (basePoint , next) {
228
229 job.data.basePoint = basePoint
230
231 async.parallel ([
232 function (next) {
233 models.Schedule.find({
234 $or:[
235 {baseDestination:basePoint},
236 {baseOrigin:basePoint}
237 ]
238 }, function (err , docs) {
239 if (err) return next(err)
240 API.modelsUpdated(docs)
241 next()
242 })
243 },
244 function (next) {
245 models.UserPlace.find(
246 {
247 lat:lat , lng:lng
248 },
249 function (err , docs) {
250 if (err) return next(err)
251 API.modelsUpdated(docs)
252 next()
253 }
82
254 )
255 }
256 ],
257 function (err) {
258 if (err) return next(err)
259 next()
260 }
261 )
262 },
263
264 function (next) {
265
266 var basePoint = job.data.basePoint
267
268 if (job.data.processDistances !== false) {
269 if (!job.data.sync) {
270 jobs
271 .create(’basePoint/processDistances ’, {
272 basePoint:basePoint.id
273 })
274 .unique(false)
275 .save()
276 next()
277 } else {
278 jobs.run(
279 ’basePoint/processDistances ’,
280 {
281 basePoint:basePoint.id
282 },
283 next
284 )
285 }
286 } else {
287 next()
288 }
289 }
290 ],
291 // We’ll try to end this returning the basePoint
292 function (err) {
293 if (err) return cb(err)
294 cb(null , job.data.basePoint)
295 }
296 )
297
298 })
299
300 })
301 })
83
A.1.4 basePoint/processDistances
1 jobs.process(’basePoint/processDistances ’, function (job , cb) {
2 models.BasePoint.findById(job.data.basePoint , function (err , doc) {
3 if (err) return cb(err)
4 if (!doc) return cb(’missing_basePoint ’)
5
6 async.series ([
7 function (cb) {
8 // We first look for all the routes which are already processed, so we don’t process
9 // them again
10 models.RoutePointDistance.find({ basePoint:doc}, function (err ,
processedRoutes) {
11 if (err) return cb(err)
12 // And then look for the routes which are not yet processe, and process them.
13 models.BaseRoute.find({
14 _id:{$nin:processedRoutes},
15 ’centerPoint.lat’:{$lt:doc.lat + 5, $gt:doc.lat - 5},
16 ’centerPoint.lng’:{$lt:doc.lng + 5, $gt:doc.lng - 5}
17 }, function (err , routes) {
18 if (err) return cb(err)
19 if (! routes.length) return cb()
20
21 // Parallel processing of routes and points distances
22 var q = async.queue(function (route , cb) {
23 models.RoutePointDistance.calculateDistances(route , doc , cb)
24 }, 2)
25
26 q.drain = cb
27 q.push(routes)
28 })
29 })
30 },
31 // Updates the BasePoint to be processedAt now
32 function (cb) {
33 doc.processedAt = Date.now()
34 doc.save(cb)
35 },
36 // Update Schedules to be processed and send them via socket
37 function (cb) {
38 models.Schedule.update(
39 {
40 $or:[
41 {baseDestination:doc},
42 {baseOrigin:doc}
43 ],
44 processedPoints :{$ne:doc.id.toString ()}
45 },
46 { $addToSet :{ processedPoints:doc.id.toString ()} },
84
47 {multi:true},
48 function (err , total) {
49 if (err) return cb(err)
50 if (!total) return cb()
51 models.Schedule.find({
52 $or:[
53 {baseDestination:doc},
54 {baseOrigin:doc}
55 ]
56 }, function (err , docs) {
57 if (err) return next(err)
58 API.modelsUpdated(docs)
59 cb()
60 })
61 }
62 )
63 }
64 ], cb)
65
66 })
67 })
A.1.5 user/importData
1 jobs.process(’user/importData ’, function (job , cb) {
2
3 models.User.findById(job.data.id, function (err , user) {
4
5 // Oops!
6 if (err) return cb(err)
7 if (!user || !user.facebookId) {
8 return cb(’invalid_user ’)
9 }
10 if (user.dataImportedAt && Date.create(user.dataImportedAt).daysUntil () < 2) {
11 return cb()
12 }
13
14 async.parallel(
15 [
16 // We have to import all the data from Facebook
17 user.updateDataFromFacebook.bind(user),
18 // We have to import all the data from Facebook
19 user.updateFriendshipsFromFacebook.bind(user)
20 ],
21 function (err) {
22 if (err) {
23 user.set({ lastImportErrorAt:new Date()})
24 if (err.type == ’FacebookApiException ’) {
25 user.save(function () {
85
26 return cb(’fb_permission_error ’)
27 })
28 } else {
29 user.save(function () {
30 cb(err)
31 })
32 return
33 }
34 }
35 var firstTime = !user.dataImportedAt
36 // Okay, we have cb everything!
37 user.set({ dataImportedAt:new Date()})
38 user.save(function (err) {
39 if (err) return cb(err)
40 models.User.sendNotification(user ,
41 {
42 type:firstTime ? ’user_first_import ’ : ’user_data_imported ’
43 },
44 cb
45 )
46 API.modelsUpdated(user)
47 // Time to Update mutual friends
48 jobs.create(’user/updateMutual ’, {
49 id:job.data.id
50 }).save()
51 })
52 }
53 )
54
55 })
56 })
A.1.6 user/updateMutual
1 jobs.process(’user/updateMutual ’, function (job , next) {
2 models.User.findById(job.data.id, function (err , user) {
3 if (err) return next(err)
4 if (!user) return next(’invalid_user ’)
5 models.Friendship.updateUserMutual(user , next)
6 })
7 })
A.1.7 user/updateFriends
1 jobs.process(’user/updateFriends ’, function (job , next) {
2 models.User.findById(job.data.id, function (err , user) {
3 if (err) return next(err)
4 if (!user) return next(’invalid_user ’)
5 user.updateFriendshipsFromFacebook(next)
86