Évolution du jeu Salesforce (2-4) – La page, son contrôleur
On va fournir une page publique Salesforce CarRacerStatistics accessible depuis l’extérieur de Salesforce pour afficher l’état en temps quasi réel de la voiture.
Cette page publique aura comme paramètre d’URL l’ID Salesforce du Racer à afficher
…/CarRacerStatistics?racerId=a041t00000FFEG3
La page va posséder un contrôleur (le code qui s’exécute coté Salesforce qui va :
- Récupérer la dernière position connue de la voiture.
- Récupérer l’historique de la vitesse de la voiture depuis le début de la course
- Récupérer les historiques des autres participants à la même course.
La page va afficher :
- Les dernières informations connues
- Un graphique avec la liste des vitesses
- La carte avec la position de la voiture
Description du contrôleur de la page
Le contrôleur va stocker toutes les informations utiles pour la page
public class CarRacerStatisticsCtrl {
// la position où doit se situer la carte et le niveau de zoom:
// quand il y a un seul participant, c’est lui-même,
// mais quand il y a plusieurs participants
// on va positionner la carte pour tous les voir.
public Decimal mapCenterLatitude {get; set; }
public Decimal mapCenterLongitude {get; set; }
public Integer mapCenterZoom {get; set; }
// l’id du participant, et l’id de la course à laquelle il participe
id racerId;
id raceid;
// les informations générale sur le participant et la course
public String raceName {get; set; }
public String racerName {get; set; }
public String racerCarName {get; set; }
String trackerId;
DateTime startTime= System.now();
DateTime finishTime= System.now();
DateTime currentTime = System.now();
// la frequence à laquelle on veut réafficher la page (et donc redemander le résultat à SF
public integer refreshPeriod {get; set; }
// les informations sur le dernier status récupéré
public Integer elapsedTime {get; set;}
// la liste des données historique du capteur depuis le début de la course
List<Car_Monitoring_Data__b> data;
// the last racer car status
public Car_Monitoring_Data__b theCarStatus { get; set; }
// le check si la voiture à un status connu
public boolean getHasCarStatus() { return theCarStatus!= null; }
// la liste des positions des autres voitures
public List<Map<String, Object>> racerPositions { get; set; }
// la liste des marqueurs à afficher sur la carte Google Map (comme pour le travail de l’an passé)
public String theMapMarkersDataJSON = '{ "type" : "FeatureCollection", "features" : [ ] }';
public String getMapMarkersJSON() { return theMapMarkersDataJSON; }
L’initialisation du contrôleur permet de mettre en place l’environnement de la page.
public CarRacerStatisticsCtrl(){
// on récupère l’ID du participant passé en paramètre
String racerParam = apexpages.currentpage().getparameters().get('racerId');
racerId = racerParam;
Datetime now = System.now();
String pRefreshPeriod = apexpages.currentpage().getparameters().get('refreshPeriod');
if (pRefreshPeriod!=null ) {
refreshPeriod = integer.valueof(pRefreshPeriod);
} else {
refreshPeriod=10;
}
// on cherche le participant avec cet ID
List<Racer__c> racer = [
SELECT
Id,
Name,
Car_Race__c,
Car_Race__r.Name,
Car__c,
Car__r.name,
Start_Date__c,
Finish_Date__c,
Tracker_Mode__c,
Tracker_Big_Object_Tracker_ID__c
FROM Racer__c
where Id =:racerId
];
// si on en a un on mémorise les infos et on provoque le chargement de toutes les informations utile pour la première fois (autres participants, hitorique des vitesse, etc) avec reloadPageData (voir plus loin)
if (racer.size() == 1) {
trackerId = racer[0].Tracker_Big_Object_Tracker_ID__c;
raceid = racer[0].Car_Race__c;
startTime = racer[0].Start_Date__c;
finishTime = racer[0].Finish_Date__c;
raceName = racer[0].Car_Race__r.Name;
racerName = racer[0].Name;
racerCarName = racer[0].Car__r.Name;
reloadPageData();
} else {
racerId = null;
}
}
La fonction reloadPageData va aussi être utilisé périodiquement, pour actualiser la page.
public void reloadPageData() {
// s’il n’y a pas de participant avec l’ID demandé, il n’y a rien à renvoyer (cas d’erreur)
if (racerId==null) {
return;
}
// on memorise l’heure courante
currentTime = System.now();
// on calcule depuis combien de temps le racer observé est dans lma course
elapsedTime = (Integer) ((currentTime.getTime() - startTime.getTime())/1000);
// on charge les données historiques pour ce racer pour cette course
loadCurrentRacerData();
// on charge les données des autres participants
loadOtherRacerPositions();
// on crée la liste des marqueurs à placer sur la carte
theMapMarkersDataJSON = generateAllRacersFeaturesCollection();
}
Le code pour récupérer les infos du participant observé
public void loadCurrentRacerData() {
// pour le racer actif, cherche toutes les infos depuis le début de la course
data = [
SELECT
Id,
GPS_Latitude__c,
GPS_Longitude__c,
GPS_Elevation__c,
GPS_Speed__c,
OBD_Fuel_Level__c,
OBD_RPM__c,
OBD_Speed__c,
Requested_On__c,
Tracker_ID__c,
GPS_Measure_Time__c,
OBD_Measure_Time__c,
GPS_Time__c
FROM Car_Monitoring_Data__b
where
Tracker_ID__c = :trackerId
AND Requested_On__c >= :startTime
AND Requested_On__c <= :finishTime
AND Requested_On__c <= :currentTime
ORDER by Tracker_ID__c desc, Requested_On__c desc
];
// Ici on récupére juste le dernier status en plus avec une requête séparée
List<Car_Monitoring_Data__b> theCarStatusList = [
SELECT
Id,
GPS_Latitude__c,
GPS_Longitude__c,
GPS_Elevation__c,
GPS_Speed__c,
OBD_Fuel_Level__c,
OBD_RPM__c,
OBD_Speed__c,
Requested_On__c,
GPS_Measure_Time__c,
OBD_Measure_Time__c,
GPS_Time__c,
Tracker_ID__c
FROM Car_Monitoring_Data__b
where Tracker_ID__c = :trackerId
AND Requested_On__c >= :startTime
AND Requested_On__c <= :finishTime
AND Requested_On__c <= : currentTime
ORDER by Tracker_ID__c desc, Requested_On__c desc
LIMIT 1
];
if (theCarStatusList.size()>0)
theCarStatus = theCarStatusList[0];
else
theCarStatus = null;
}
Le code pour récupérer les historiques des autres participants est celui du Trape de l’an passé, auquel on ajoute le mode de gestion big data.
// fonction utilitaire pour juste mémoriser le résultat du calcul
// appelé pour la course courante,
// au bout des x secondes que la participant actuel a fait
// mais en excluant le participant courant
public void loadOtherRacerPositions() {
racerPositions = loadRacerPositions(raceid, elapsedTime, racerId);
}
// La fonction qui fait le calcul
public static List<Map<String, Object>> loadRacerPositions(ID raceID, Integer chronoInSeconds, Id currentRacerId) {
// on retrouve la liste des participants
List<Racer__c> lRacers = [
SELECT
Id,
Name,
Comment__c,
Car_Race__c,
Car__c,
Car__r.name,
Start_Date__c,
Finish_Date__c,
Tracker_Mode__c,
Tracker_Big_Object_Tracker_ID__c
FROM Racer__c
where Car_Race__c =:raceID
];
// le tableau où on stocke les position qu'on a trouvées.
List<Map<String, Object>> wrapperList = new List<Map<String, Object>>();
// pour chaque participant, on cherche sa dernière position à cette minute de la course
for (Racer__c racer : lRacers) {
// calcule l'heure pour ce participant (correspondant au x secondes du participant observé ajouté à l’heure de départ ce ce participant
DateTime positionTime = racer.Start_Date__c;
positionTime = positionTime.addSeconds(chronoInSeconds);
if (racer.Tracker_Mode__c == 'Big Object') {
// recherche la dernière position connue pour l’heure actuelle
List<Car_Monitoring_Data__b> carPosition = [
SELECT
Id,
GPS_Latitude__c,
GPS_Longitude__c,
GPS_Measure_Time__c,
Requested_On__c,
Tracker_ID__c
FROM Car_Monitoring_Data__b
where Tracker_ID__c = :racer.Tracker_Big_Object_Tracker_ID__c
AND Requested_On__c >= :racer.Start_Date__c
AND Requested_On__c <= :positionTime
ORDER by Tracker_ID__c desc, Requested_On__c desc
LIMIT 1
];
// si on a trouvé une position, on la met de coté dans la liste wrapperList en retrouvant les données
if (carPosition.size()>0) {
DateTime measureTime = carPosition[0].GPS_Measure_Time__c;
integer raceElapsedTime = (measureTime!=null) ? (integer) ((measureTime.getTime() - racer.Start_Date__c.getTime() ) / 1000) : -1;
integer measureAge = (measureTime!=null) ? (integer) ((positionTime.getTime() - measureTime.getTime() ) / 1000) : -1;
Map<String, Object> wrapp = new Map<String, Object>{
'carName' => racer.Name + ' ('+ racer.Car__r.Name+')',
'comment' => racer.Comment__c,
'racerId' => racer.id,
'longitude' => carPosition[0].GPS_Longitude__c,
'latitude' => carPosition[0].GPS_Latitude__c,
'measureTime' => (measureTime!= null) ? string.valueOfGmt(measureTime) : '',
'measureAge' => measureAge,
'storedTime' => string.valueOfGmt(carPosition[0].Requested_On__c),
'elapsedTime' => raceElapsedTime,
b => (carPosition[0].Requested_On__c > racer.Finish_Date__c) ? true : false,
'currentRacer' => ( currentRacerId == racer.Id ? true : false )
};
wrapperList.add(wrapp);
}
} else {
// cet autre participant fonctionne avec le mode de l’an passé
// code est juste adapté pour remplir les nouveaux champs
// avec des valeurs vide
List<Car_Status__c> carPosition = [
SELECT
Id,
Location__Latitude__s,
Location__Longitude__s,
Requested_On__c,
Car__c,
Car__r.Name
FROM Car_Status__c
WHERE Car__c = :racer.Car__c
AND Requested_On__c >= :racer.Start_Date__c
AND Requested_On__c <= :positionTime
ORDER BY Requested_On__c DESC
LIMIT 1
];
// si on a trouvé une position, on la met de coté
if (carPosition.size()>0) {
DateTime measureTime = carPosition[0].Requested_On__c;
integer raceElapsedTime = (integer) ( (carPosition[0].Requested_On__c.getTime() - racer.Start_Date__c.getTime() ) / 1000 );
integer measureAge = (measureTime!=null) ? (integer) ((positionTime.getTime() - measureTime.getTime() ) / 1000) : -1;
Map<String, Object> wrapp = new Map<String, Object>{
'carName' => racer.Name + ' ('+ racer.Car__r.Name+')',
'comment' => racer.Comment__c,
'racerId' => racer.id,
'longitude' => carPosition[0].Location__Longitude__s,
'latitude' => carPosition[0].Location__Latitude__s,
'measureTime' => string.valueOfGmt(carPosition[0].Requested_On__c),
'measureAge' => measureAge,
'storedTime'=> string.valueOfGmt(carPosition[0].Requested_On__c),
'elapsedTime' => raceElapsedTime,
'finished' => (carPosition[0].Requested_On__c > racer.Finish_Date__c) ? true : false,
'currentRacer' => ( currentRacerId == racer.Id ? true : false )
};
wrapperList.add(wrapp);
}
}
}
// on renvoie les positions qu'on a trouvée
return wrapperList;
}
Ici tous les calculs utiles sont faits. Ici ce sont maintenant les fonctions qui vont fournir à la page les infos à afficher :
// y-a-t-il un participant à afficher (cas d’erreur)
public boolean getHasRacer(){ return racerId != null;}
// l’heure courante
public datetime getCurrentDisplayTime(){return currentTime;}
// les données du participant observé
public Decimal getGPSSpeed() { return (theCarStatus==null) ? 0 : theCarStatus.GPS_Speed__c.round(System.RoundingMode.FLOOR);}
public Decimal getGPSElevation() { return (theCarStatus==null) ? 0 : theCarStatus.GPS_Elevation__c.round(System.RoundingMode.FLOOR);}
public Decimal getOBDSpeed() { return (theCarStatus==null) ? 0 : ((theCarStatus.OBD_Speed__c==null) ? 0 : theCarStatus.OBD_Speed__c.round(System.RoundingMode.FLOOR));}
public Decimal getOBDRPM() { return (theCarStatus==null) ? 0 : ((theCarStatus.OBD_RPM__c==null) ? 0 : theCarStatus.OBD_RPM__c.round(System.RoundingMode.FLOOR));}
public Decimal getOBDFuelLevel() { return (theCarStatus==null) ? 0 : theCarStatus.OBD_Fuel_Level__c;}
public Datetime getLastRequestDate() { return (theCarStatus==null) ? null : theCarStatus.Requested_On__c;}
public Datetime getLastGPSMeasure() { return (theCarStatus==null) ? null : theCarStatus.GPS_Measure_Time__c;}
public Datetime getLastOBDMeasure() { return (theCarStatus==null) ? null : theCarStatus.OBD_Measure_Time__c;}
public Datetime getGPSTime() { return (theCarStatus==null) ? null : theCarStatus.GPS_Time__c;}
public Long getLastRequestAge() { return (theCarStatus==null) ? -1 : getDatetimeAge(theCarStatus.Requested_On__c);}
public Long getLastGPSMeasureAge() { return (theCarStatus==null) ? -1 : getDatetimeAge(theCarStatus.GPS_Measure_Time__c);}
public Long getLastOBDMeasureAge() { return (theCarStatus==null) ? -1 : getDatetimeAge(theCarStatus.OBD_Measure_Time__c);}
Un code particulier prépare les informations à afficher sur le graphe ‘vitesse’
public String getSpeedChartData() {
String content = '{'+
' "type": "scatter",'+
' "data": {'+
' "datasets": [{'+
' "label": "Speed Dataset",'+
' "data": [ '
;
boolean first = true;
// on boucle sur les données observées, et on mémorise heure et vitesse
for (Car_Monitoring_Data__b d:data) {
if (!first) {
content = content + ',';
}
content = content + '{"x": "'+ d.Requested_On__c +'","y": '+ d.OBD_Speed__c +'}';
first = false;
}
content =content +']'+
' }]'+
' },'+
' "options": {'+
' "responsive": "true",' +
' "maintainAspectRatio": "false",'+
' "scales": {'+
' "xAxes": [{'+
' "type": "time",'+
' "position": "bottom"'+
' }]'+
' }'+
' }'+
' }';
// on renvoie le json contenant les caractéristiques du graphe
return content;
}
Un autre code particulier prépare les informations à afficher sur la carte des positions (même code que pour le trape de l’an passé)
// data for json map
public String searchResultFeaturesJSON { get; set; }
// creation du marqueur pour le participant courant
public String generateCurrentRacerFeatureCollection() {
JSONGenerator gen = JSON.createGenerator(true);
gen.writeStartObject();
gen.writeStringField('type', 'FeatureCollection');
gen.writeFieldName('features');
gen.writeStartArray();
gen.writeStartObject();
gen.writeStringField('type', 'Feature');
gen.writeFieldName('geometry');
gen.writeStartObject();
gen.writeStringField('type', 'Point');
gen.writeFieldName('coordinates');
gen.writeStartArray();
gen.writeNumber(theCarStatus.GPS_Longitude__c);
gen.writeNumber(theCarStatus.GPS_Latitude__c);
gen.writeEndArray();
gen.writeEndObject();
gen.writeFieldName('properties');
gen.writeStartObject();
gen.writeStringField('name', theCarStatus.Tracker_ID__c);
gen.writeEndObject();
gen.writeEndObject();
gen.writeEndArray();
gen.writeEndObject();
String pretty = gen.getAsString();
pretty = pretty.replaceAll('\n', ' ');
return pretty;
}
// creation des marqueurs pour les autres participants
public String generateAllRacersFeaturesCollection() {
JSONGenerator gen = JSON.createGenerator(true);
gen.writeStartObject();
gen.writeStringField('type', 'FeatureCollection');
gen.writeFieldName('features');
gen.writeStartArray();
for( Map<String, Object> wrapper : racerPositions) {
// on vérifie d’abord s’il y a bien une position (sinon bug ;-) )
decimal lat = (Decimal)wrapper.get('longitude');
decimal lon = (Decimal)wrapper.get('latitude');
if((lat!=null)&&(lon!=null)&&(lat!=0)&&(lon!=0)) {
gen.writeStartObject();
gen.writeStringField('type', 'Feature');
gen.writeFieldName('geometry');
gen.writeStartObject();
gen.writeStringField('type', 'Point');
gen.writeFieldName('coordinates');
gen.writeStartArray();
gen.writeNumber((Decimal)wrapper.get('longitude'));
gen.writeNumber((Decimal)wrapper.get('latitude'));
gen.writeEndArray();
gen.writeEndObject();
gen.writeFieldName('properties');
gen.writeStartObject();
gen.writeStringField('carName', (String)wrapper.get('carName'));
gen.writeBooleanField('currentRacer', (Boolean)wrapper.get('currentRacer'));
gen.writeStringField('racerId', (String)wrapper.get('racerId'));
gen.writeStringField('storedTime', (String)wrapper.get('measureTime'));
gen.writeStringField('measureTime', (String)wrapper.get('measureTime'));
Integer measureAge = (Integer)wrapper.get('measureAge');
gen.writeNumberField('measureAge', measureAge!=null ? (Integer)wrapper.get('measureAge') : -1);
gen.writeNumberField('elapsedTime', (Integer)wrapper.get('elapsedTime'));
gen.writeBooleanField('finished', (Boolean)wrapper.get('finished'));
gen.writeEndObject();
gen.writeEndObject();
}
}
gen.writeEndArray();
gen.writeEndObject();
String pretty = gen.getAsString();
pretty = pretty.replaceAll('\n', ' ');
return pretty;
} public Long getDatetimeAge(Datetime a) {
if (a==null)
return -1;
Long dt1Long = a.getTime();
Long dt2Long = currentTime.getTime(); // System.now().getTime();
Long milliseconds = dt2Long - dt1Long;
Long seconds = milliseconds / 1000;
return seconds;
}
Pour permettre à la page d’afficher dans des couleurs différentes les mesures ‘trop vieilles’, il a été nécessaire de créer quelques fonctions utilitaires
public boolean getDatetimeDiffTooBig(Datetime a, Datetime b, integer limite) {
if (a==null)
return true;
if (b==null)
return true;
Long dt1Long = a.getTime();
Long dt2Long = b.getTime();
Long milliseconds = dt2Long - dt1Long;
Long seconds = milliseconds / 1000;
if (seconds <0) {
seconds = -seconds;
}
return seconds > limite;
}
// heure de la dernière requête trop vieille
public boolean getLastRequestTooOld() {
return (theCarStatus==null) ? true : getDatetimeDiffTooBig(currentTime, theCarStatus.Requested_On__c, 400);
}
// heure de la dernière data OBD trop vieille
public boolean getObdDataTooOld() {
return (theCarStatus==null) ? true : getDatetimeDiffTooBig(theCarStatus.OBD_Measure_Time__c, theCarStatus.Requested_On__c, 60);
}
// heure de la dernière data GPS trop vieille
public Boolean getGpsDataTooOld() {
return (theCarStatus==null) ? true : getDatetimeDiffTooBig(theCarStatus.GPS_Measure_Time__c, theCarStatus.Requested_On__c, 60);
}
// ecart trop grand entre l’heure récupérére par le GPS, et l’heure du Raspberry (détecter un pbm du Raspberry)
public Boolean getTimeOutOfSync() {
return (theCarStatus==null) ? true : getDatetimeDiffTooBig(theCarStatus.GPS_Measure_Time__c, theCarStatus.GPS_Time__c, 120);
}
}