universidade federal do rio de janeiro escola polit ecnica...

99
Universidade Federal do Rio de Janeiro Escola Polit´ ecnica DepartamentodeEletrˆonicaedeComputa¸c˜ao Rede Social de Caronas Autor: Guilherme Murad Pim Orientador: Carlos Jos´ e Ribas D’ ´ Avila Examinador: Aloysio de Castro Pinto Pedroza Examinador: Antˆ onio Cl´audio G´ omez de Sousa DEL Mar¸co de 2014

Upload: truongtram

Post on 15-Nov-2018

213 views

Category:

Documents


0 download

TRANSCRIPT

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

6 })

7 })

87