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);
     }
    
}