#Partie "Web" pour le projet R&D
TODO : Pensez à créer les callback lors des créations / update d'événements pour notifier les invités
##SOMMAIRE
- Models
- Utilisateurs
- Evenements
- Now
- Routes
- /user
- /event
- /friend
- /now
- RECHERCHE : Calcul des trajets
- RECHERCHE : Notifications Push
- Essais avec Postman
var UserSchema = new Schema({
username : {type: String, required: true, index: { unique: true }},
email : {type: String, required: true, index: { unique: true }},
password : {type: String, required: true},
telephone : {type: String},
adresse : {type: String, required: true},
gps : {type: String},
loginAttempts : {type: Number, required: true, default: 0},
lockUntil : {type: Number},
friends : {type: [String], default: []}, // accepted friends
askedToBeFriend : {type: [String], default: []}, // people I asked to be friend with me
requestFrom : {type: [String], default: []}, // people WHO asked to be friend with me
bannedBy : {type: [String], default: []}, // people who refused to be friend with me
GCMid : {type: String, required: true}
});
- User.comparePassword(PasswordATester, callback)
- User.incLoggingAttempts(callback)
- getAuthenticated(username, password, callback)
var EventSchema = new Schema({
title : {type: String, required: true},
date : {type: Date, required: true, default: Date.now},
owner : {type: String},
guests : {type: [String]},
coming : {type: [String]},
refusedBy : {type: [String]},
place_name : {type: String, required: true},
place_gps : {type: String, required: true},
date_locked : {type: Date, required: true, default: Date.now},
version : {type: Number, required: true, default: 1},
type : {type: String, enum: ['Bar', 'Lunch', 'Meeting', 'Restaurant', 'Sport', 'Hangout', 'Walk', 'Other'], default: 'Other'}
});
- Event.ownedBy(username)
- Event.imInvited(username)
- Event.iRefused(username)
- Event.iWillgo(username)
var NowSchema = new Schema({
titleMessage : {type: String},
responseMessage : {type: String},
guestStatus : {type: Number, default:0},
eventStatus : {type: Number, default:0},
travelMode : {type: String, required:true, enum: ['walking', 'driving', 'transit'], default: 'transit'},
radius : {type: Number, default:200},
rankBy : {type: String, enum: ['DISTANCE', 'PROMINENCE'], default: 'PROMINENCE'},
date : {type: Date, required: true, default: Date.now},
owner : {type: String, required:true},
guest : {type: String, required:true},
latOwner : {type: Number, default:0},
lonOwner : {type: Number, default:0},
latGuest : {type: Number, default:0},
lonGuest : {type: Number, default:0},
latMiddlePoint : {type: Number, default:0},
lonMiddlePoint : {type: Number, default:0},
type : {type: String, enum: ['bar', 'cafe', 'library', 'movie\_theater', 'museum', 'night\_club', 'parking', 'restaurant', 'subway_station', 'none'], default: 'bar'},
// Type from https://developers.google.com/places/documentation/supported_types
placesAround : {type: Array, default:[]},
version : {type: Number, default:1}
});
/users
avec la methodePOST
pour avoir la liste des utilisateurs/user/<USERNAME>
avec la methodePOST
pour avoir les infos de l'utilisateur ayant l'username<USERNAME>
/user
avec la methodePOST
pour créer un nouvel utilisateur/user/<USERNAME>
avec la methodeDELETE
pour supprimer l'utilisateur ayant l'username<USERNAME>
/user
avec la methodePUT
pour mettre à jour l'utilisateur ayant l'username passé en paramètre (myUsername)/user/nick/<USERNAME>
avec la methodeGET
pour regarder si l'username est déjà pris (retourne 0 ou 1);/auth/logout
avec la methodePOST
pour supprimer le GCMid courant (passé dans les paramètres)/auth/login
avec la methodePOST
pour ajouter le GCMid courant (passé dans les paramètres)
/events
avec la methodePOST
pour avoir la liste des evenements/event/<ID>
avec la methodePOST
pour avoir les infos de l'evenement ayant l'id<ID>
/event
avec la methodePOST
pour créer un nouvel événement/event/<ID>
avec la methodeDELETE
pour supprimer l'événement ayant l'id<ID>
/event/<ID>
avec la methodePUT
pour mettre à jour l'événement ayant l'id<ID>
/friend/add/username/<USERNAME>
avec la methodePOST
pour demander l'utilisateur<USERNAME>
en ami/friend/add/email/<EMAIL>
avec la methodePOST
pour demander l'utilisateur<EMAIL>
en ami/friend/add/email/<EMAIL>
avec la methodePOST
pour demander l'utilisateur<EMAIL>
en ami/friend/accept/<USERNAME>
avec la methodePOST
pour accepter la demande de<USERNAME>
/friend/refuse/<USERNAME>
avec la methodePOST
pour refuser la demande de<USERNAME>
/friend/remove/<USERNAME>
avec la methodePOST
pour supprimer<USERNAME>
de sa liste d'amis
/nows
avec la methodePOST
pour avoir la liste des 'Nows'/now/show/<ID>
avec la methodePOST
pour avoir les infos du 'Now' ayant l'id<ID>
/now
avec la methodePOST
pour créer un nouveau 'Now'/now/<ID>
avec la methodeDELETE
pour supprimer le 'Now' ayant l'id<ID>
/now/accept/<ID>
avec la methodePUT
pour mettre à jour le 'Now' ayant l'id<ID>
et l'accepter/now/refuse/<ID>
avec la methodePUT
pour mettre à jour le 'Now' ayant l'id<ID>
et le refuser
Un des points importants de notre projet est la recherche d'un point de rencontre optimal entre les participants.
Nous utilisons l'API google Map afin de nous aider pour le calcul des trajets et découvrir les lieux à proximité. En effet, leur base de donnée en OPEN DATA nous permet d'obtenir très rapidement des éléments d'aide pour nos calculs.
- Recherche documentée sur les API google (map, direction, places)
- Première itération avec la création d'un script côté client
- Tentatives pour appliquer le code créé côté serveur
- Deuxième itération avec la création d'un script côté serveur et recherche d'une solution tendant vers l'optimal
Très naturellement, nous nous sommes tournés vers l'utilisation de l'immense base de données accessible par tous que propose Google via ses API. Nous avons dû nous documenter pour mieux savoir : ce qu'il était possible de faire et ce que nous ne pouvions pas faire avec les outils à notre disposition.
Le résultat suivant semblait être une bonne solution :
- Utilisation de l'API google map pour placer les coordonnées GPS de nos clients sur un repère de type carte
- Utilisation de l'API google direction pour obtenir le chemin optimal entre les clients
- Utilisation d'une librairie complémentaire (epoly.js) pour obtenir le "route halfway" entre les clients
- Utilisation de l'API google places pour obtenir les établissements situés autour du point obtenu précédement
Puisque l'API ne peut s'exécuter que côté client, les pages /map (version illustrée) /map/json (version rendant le JSON) ont été créées. Elles servent à récupérer les données qui seraient ensuite traitées par le serveur et envoyées au client mobile.
Actuellement il est possible de calculer :
- Le trajet le plus court à pied, en voiture, en transport en commun
- Trouver le "mi chemin"
- Trouver dans un rayon donné un type d'établissement donné
Tous les paramètres se passent directement dans l'URL (méthode GET). L'adresse ressemble à :
/map?firstAddress=LAT_1, LONG_1&secondAddress=LAT_2, LONG_2&travelModeParam=TYPE_OF_TRAVEL&typeOfPlaces=TYPE_OF_PLACE[&radius=NUMBER&openNow=yes&rankBy=DISTANCE]
- Les coordonnées des adresses doivent être séparées par une virgule suivi d'un espace (ex :
48.581073, 7.749145
ce qui donne encodé :48.581073,%207.749145
) - Le paramètre travelModeParam peut prendre les valeurs
DRIVING
,WALKING
,TRANSIT
(documentation) - Le paramètre typeOfPlaces peut prendre les valeurs présentées à cette adresse
- Le paramètre openNow prend la valeur
yes
ouno
en fonction du choix souhaité (documentation) Par défaut : no - Le paramètre radius prend un nombre (mètre) comme valeur (documentation) Par défaut : 200
- Le paramètre rankBy peut être ajouté avec la valeur
DISTANCE
. A ce moment, le radius n'est plus pris en compte mais une liste est créée en cherchant les établissements les plus proches (pourra être utilisé lorsque le lieu calculé est pauvre en établissement)
Exemple :
- http://aftersoon.herokuapp.com/map?firstAddress=48.581073,%207.749145&secondAddress=48.583483,%207.746404&travelModeParam=TRANSIT&typeOfPlaces=bar&radius=100&openNow=yes (sans rankBy)
- http://aftersoon.herokuapp.com/map?firstAddress=48.501073,%207.749145&secondAddress=48.483483,%207.746404&travelModeParam=TRANSIT&typeOfPlaces=bar&rankBy=DISTANCE (avec rankBy)
Actuellement la page affiche une carte pour être sur des résultats. L'objet est visible dans la console. En utilisant la même URL mais en ajoutant
/json
après/map
l'objet JSON apparait
Une fois notre codé réalisé, nous avons tenté de récupérer les données côté serveur. Nous espérions appeler la page /map/json
et récupérer son contenu pour l'ajouter à notre objet.
Néanmoins nous n'avions pas pensé qu'une requête de type GET n'exécute pas le Javascript présent sur la cible. Nous ne pouvions donc pas obtenir nos données.
Nous avons essayé de plusieurs façon d'intégrer notre code côté serveur. Google ne permet pas d'utiliser ses scripts côté serveur. Malgré nos recherches durant plusieurs jours, en essayant d'utiliser d'autres plateformes, nous étions donc dans une impasse.
Deuxième itération avec la création d'un script côté serveur et recherche d'une solution tendant vers l'optimal
Nous avons donc décidé de réflechir à nouveau à une solution côté serveur. Nos ressources étaient limité à l'utilisation des API google retournant directement du JSON. Nous avions donc :
- La possibilité d'utiliser les coordonnées GPS des clients
- Utiliser l'API google direction pour récupérer un objet JSON de notre requête
- Utiliser l'API google places pour récupérer un objet JSON de notre requête
Malheureusement l'API principale, celle de Google Map n'était plus disponible tout comme la librairie de calcul (epoly.js). Nous ne pouvions donc plus trouver le point de rencontre.
Nous avons néanmoins trouvé une solution permettant d'approximer ce point de rencontre de manière fiable : Le JSON retourné par l'API google direction contient : + la distance totale du trajet + un tableau contenant toutes les étapes pour naviguer d'un point à l'autre qui contient pour chaque étape : + la distance de l'étape + un polyline (structure qui encode un tableau de coordonnées GPS définissant la courbe du trajet du début de l'étape à la fin de l'étape)
Notre solution est donc la suivante :
- Nous parcourons le tableau des étapes jusqu'à dépasser le milieu du trajet (distance totale / 2),
- Nous gardons l'étape où se trouve le milieu du trajet, la distance totale parcourue au début et à la fin de celui-ci,
- Nous décodons le polyline de l'étape pour créer un tableau de coordonnées GPS qui correspondent à la forme de l'étape,
- Nous calculons la distance entre le début de l'étape et le milieu du trajet pour établir un pourcentage qui servira à obtenir l'index correspondant au mieux au milieu du trajet :
GPSarray[0] GPSarray[calculé] GPSarray[length]
DistanceDebutEtape Milieu Trajet DistanceFinEtape
|------------------------------|------------------------|
En effet, sur des grandes étapes, nous pouvons admettre que la disposition des différentes coordonnées GPS du polyline sont placées de façon continue et régulière (loi des grands nombres et probabilité). Pour des petites étapes, même si cette disposition est moins bien respectée, l'impact est négligeable de par la petite distance. Avec cette solution nous pouvons obtenir des résultats très convaincants pour le milieu d'un trajet (de plus le rayon de recherche d'un établissement effacera également les disparités avec le vrai milieu du trajet).
- Une fois le milieu obtenu, nous formons une URL qui ira chercher un objet JSON via l'API google places.
Un des défis techniques consiste en la communication en temps réel entre le serveur web et l'application Android. L'objectif est de rendre notre application la plus réactive possible (nécessaire vu le projet) et d'ajouter de l'interractivité pour les utilisateurs.
- Appel au serveur à un intervalle régulier par l'application Android
- Recherche de documentations sur les solutions proposées par Heroku (Pubnub)
- Recherche de documentations sur la solution de Google (GCM)
Dans un premier temps, n'ayant aucune expérience dans le domaine, nous imaginions que le serveur appelle une page du serveur listant les nouveautés depuis son dernier passage. Plusieurs problèmes apparaissaient alors :
- Cette solution nécessite que l'application reste tout le temps en marche
- Cette solution est énergivore pour la batterie du téléphone puisqu'une action est effectuée de façon répétée (une fois par 30 sec au grand maximum pour avoir un semblant de réactivité)
- La gestion des nouveautés à afficher côté serveur était complexe (mise en place d'un genre de flux pour chaque personne, des appels à la BDD de façon récurrente)
Nous avons très vite abandonné l'idée d'utiliser cette solution qui semblait être la plus simple au premier abord.
Heroku propose plusieurs plugins à ajouter à son serveur. Ces solutions permettent de communiquer facilement en temps réel avec le serveur et l'application côté client. Pour beaucoup il s'agit de requetes AJAX. Pubnub se rapprochaient le plus de nos besoins. Néanmoins sa documentation et ses exemples rendaient son implémentation difficile.
Google propose un service qui permet de communiquer entre un serveur et un appareil Android grâce au Google Cloud Messaging (GCM). En utilisant cette solution, le serveur se voit allouer un ID unique et privé qui lui permet d'entrer en contact avec les serveurs Google. En parallèle, lors de la connexion d'un utilisateur à notre application, nous récupérons son GCM id user unique. Celui-ci est alors stocké dans notre base de données et servira à communiquer avec lui via des notifications push. La gestion des GCM id user est géré à la connexion et la déconnexion d'un utilisateur (gestion de plusieurs appareils connectés en même temps à l'application par exemple). Lors d'événements précis, notre serveur est donc capable de transmettre à Google que nous souhaitons envoyer un message à un utilisateur unique. Nous le faisons lors de plusieurs événements précis côté serveur : -FRIEND_INVITATION_ASKED -FRIEND_INVITATION_ACCEPTED -NOW_INVITATION (quelqu'un m'a invité) -NOW_REMINDER_NO_ANSWER (push envoyé au guest car il n’a pas repondu au bout de 5min) -NOW_NO_ANSWER_FROM_GUEST (push envoyé aux 2, comme pas de réponse : event annulé) -NOW_CANCELLED (push envoyé à l’owner car le guest a dit non) -PLACE_FOUND (place trouvée) -PLACE_ERROR (place non trouvée car une des coordonnées ou le calcul ne fonctionne pas)
Aperçu disponible sur http://aftersoon.herokuapp.com
Utiliser Postman pour faire des tests (Postman sur le store)
####Configurer Postman : Dans l'url : http://aftersoon.herokuapp.com puis :
Pour l'encart Headers
(bouton en haut à droite), ajouter :
Header
: Content-TypeValue
: application/json Les données sont traitées en JSON côté serveur
Les données à transférer pour chacune des urls se fait via l'onglet raw
(dernier de la liste des choix). Il faut ensuite choisir dans le menu déroulant JSON
Pour chaque requête autre que l'ajout d'un utilisateur, pour des raisons de sécurité, il faut passer en plus en parametre
myUsername
etmyPassword
(à voir à l'avenir pour améliorer la méthode avec un système de session par exemple)
####Exemple pour l'affichage d'un utilisateur :
- URL : http://aftersoon.herokuapp.com/user/Hugo
- Methode : POST
- Header: Content-Type : application/json
- raw JSON :
{
"myUsername": "test2",
"myPassword": "test"
}
Différents exemples pré-écrits : POSTMAN - Aftersoon.json à la racine du projet, à importer dans Postman