{"id":987,"date":"2020-03-14T13:28:00","date_gmt":"2020-03-14T12:28:00","guid":{"rendered":"http:\/\/wollef.org\/?p=987"},"modified":"2020-03-14T13:28:00","modified_gmt":"2020-03-14T12:28:00","slug":"evolution-du-jeu-salesforce-2-3-la-page-son-controleur","status":"publish","type":"post","link":"https:\/\/wollef.org\/blog\/evolution-du-jeu-salesforce-2-3-la-page-son-controleur\/","title":{"rendered":"\u00c9volution du jeu Salesforce (2-4) &#8211; La page, son contr\u00f4leur"},"content":{"rendered":"\n<p>On va fournir une\npage publique Salesforce <strong>CarRacerStatistics<\/strong> accessible depuis l\u2019ext\u00e9rieur de Salesforce\npour afficher l\u2019\u00e9tat en temps quasi r\u00e9el de la voiture.<\/p>\n\n\n\n<p>Cette page publique\naura comme param\u00e8tre d\u2019URL l\u2019ID Salesforce du <strong>Racer<\/strong> \u00e0 afficher<\/p>\n\n\n\n<p>\u2026\/<strong>CarRacerStatistics<\/strong>?<strong>racerId<\/strong>=a041t00000FFEG3<\/p>\n\n\n\n<p>La page va poss\u00e9der un contr\u00f4leur (le code qui s\u2019ex\u00e9cute\ncot\u00e9 Salesforce qui&nbsp;va&nbsp;:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>R\u00e9cup\u00e9rer la derni\u00e8re position connue de la\nvoiture.<\/li><li>R\u00e9cup\u00e9rer l\u2019historique de la vitesse de la\nvoiture depuis le d\u00e9but de la course<\/li><li>R\u00e9cup\u00e9rer les historiques des autres\nparticipants \u00e0 la m\u00eame course.<\/li><\/ul>\n\n\n\n<p>La page va afficher&nbsp;:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>Les derni\u00e8res informations connues<\/li><li>Un graphique avec la liste des vitesses<\/li><li>La carte avec la position de la voiture<\/li><\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Description du contr\u00f4leur de la page<\/h2>\n\n\n\n<p>Le contr\u00f4leur va stocker toutes les informations utiles pour la page<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public class CarRacerStatisticsCtrl {\n\n\/\/ la position o\u00f9 doit se situer la carte et le niveau de zoom:\n \/\/ quand il y a un seul participant, c\u2019est lui-m\u00eame,\n \/\/ mais quand il y a plusieurs participants\n \/\/ on va positionner la carte pour tous les voir.\n\n    public Decimal mapCenterLatitude  {get; set; }\n    public Decimal mapCenterLongitude  {get; set; }\n    public Integer mapCenterZoom  {get; set; }\n\n\t \/\/ l\u2019id du participant, et l\u2019id de la course \u00e0 laquelle il participe\n    id racerId;\n    id raceid;\n\n\/\/ les informations g\u00e9n\u00e9rale sur le participant et la course\n    public String raceName  {get; set; }\n    public String racerName  {get; set; }\n    public String racerCarName  {get; set; }\n\n\n    String trackerId;\n    DateTime startTime= System.now();\n    DateTime finishTime= System.now();\n    DateTime currentTime = System.now();\n\n\n    \/\/ la frequence \u00e0 laquelle on veut r\u00e9afficher la page (et donc redemander le r\u00e9sultat \u00e0 SF\n    public  integer refreshPeriod  {get; set; }\n\n    \/\/ les informations sur le dernier status r\u00e9cup\u00e9r\u00e9\n    public Integer elapsedTime {get; set;}\n\n    \/\/ la liste des donn\u00e9es historique du capteur depuis le d\u00e9but de la course\n    List&lt;Car_Monitoring_Data__b> data;\n    \/\/ the last racer car status\n    public Car_Monitoring_Data__b theCarStatus { get; set; }\n\n\t   \/\/ le check si la voiture \u00e0 un status connu\n    public boolean getHasCarStatus() { return theCarStatus!= null; }\n\n    \/\/ la liste des positions des autres voitures\n    public List&lt;Map&lt;String, Object>> racerPositions { get; set; }\n\n    \/\/ la liste des marqueurs \u00e0 afficher sur la carte Google Map (comme pour le travail de l\u2019an pass\u00e9)\n    public String theMapMarkersDataJSON =  '{ \"type\" : \"FeatureCollection\", \"features\" : [ ] }';\n    public String getMapMarkersJSON() { return theMapMarkersDataJSON; }\n<\/code><\/pre>\n\n\n\n<p>L\u2019initialisation du contr\u00f4leur permet de mettre en place l\u2019environnement de la page.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public CarRacerStatisticsCtrl(){\n\n\n\t   \/\/ on r\u00e9cup\u00e8re l\u2019ID du participant pass\u00e9 en param\u00e8tre\n    String racerParam = apexpages.currentpage().getparameters().get('racerId');\n    racerId = racerParam;\n\n    Datetime now = System.now();\n\n    String pRefreshPeriod = apexpages.currentpage().getparameters().get('refreshPeriod');\n    if (pRefreshPeriod!=null ) {\n        refreshPeriod = integer.valueof(pRefreshPeriod);\n    } else {\n        refreshPeriod=10;\n    }\n\n \t\/\/ on cherche le participant avec cet ID\nList&lt;Racer__c> racer = [\n        SELECT\n            Id,\n            Name,\n            Car_Race__c,\n            Car_Race__r.Name,\n            Car__c,\n            Car__r.name,\n            Start_Date__c,\n            Finish_Date__c,\n            Tracker_Mode__c,\n            Tracker_Big_Object_Tracker_ID__c\n        FROM Racer__c\n        where Id =:racerId\n    ];\n\n\t   \/\/ si on en a un on m\u00e9morise les infos et on provoque le chargement de toutes les informations utile pour la premi\u00e8re fois (autres participants, hitorique des vitesse, etc) avec reloadPageData (voir plus loin)\n    if (racer.size() == 1) {\n        trackerId = racer[0].Tracker_Big_Object_Tracker_ID__c;\n        raceid = racer[0].Car_Race__c;\n        startTime = racer[0].Start_Date__c;\n        finishTime = racer[0].Finish_Date__c;\n        raceName = racer[0].Car_Race__r.Name;\n        racerName = racer[0].Name;\n        racerCarName = racer[0].Car__r.Name;\n        reloadPageData();\n    } else {\n        racerId = null;\n    }\n}\n<\/code><\/pre>\n\n\n\n<p>La fonction reloadPageData va aussi \u00eatre utilis\u00e9 p\u00e9riodiquement, pour actualiser la page.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public void reloadPageData() {\n\/\/ s\u2019il n\u2019y a pas de participant avec l\u2019ID demand\u00e9, il n\u2019y a rien \u00e0 renvoyer (cas d\u2019erreur)\n    if (racerId==null) {\n        return;\n    }\n    \/\/ on memorise l\u2019heure courante\n    currentTime = System.now();\n    \/\/ on calcule depuis combien de temps le racer observ\u00e9 est dans lma course\n    elapsedTime = (Integer) ((currentTime.getTime()  - startTime.getTime())\/1000);\n    \/\/ on charge les donn\u00e9es historiques pour ce racer pour cette course\n    loadCurrentRacerData();\n    \/\/ on charge les donn\u00e9es des autres participants\n    loadOtherRacerPositions();\n    \/\/ on cr\u00e9e la liste des marqueurs \u00e0 placer sur la carte\n    theMapMarkersDataJSON = generateAllRacersFeaturesCollection();\n}\n<\/code><\/pre>\n\n\n\n<p>Le code pour r\u00e9cup\u00e9rer les infos du participant observ\u00e9<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public void loadCurrentRacerData() {\n    \/\/ pour le racer actif, cherche toutes les infos depuis le d\u00e9but de la course\n    data = [\n        SELECT\n        Id,\n        GPS_Latitude__c,\n        GPS_Longitude__c,\n        GPS_Elevation__c,\n        GPS_Speed__c,\n        OBD_Fuel_Level__c,\n        OBD_RPM__c,\n        OBD_Speed__c,\n        Requested_On__c,\n        Tracker_ID__c,\n        GPS_Measure_Time__c,\n        OBD_Measure_Time__c,\n        GPS_Time__c\n        FROM Car_Monitoring_Data__b\n        where\n            Tracker_ID__c = :trackerId\n            AND Requested_On__c >= :startTime\n            AND Requested_On__c &lt;= :finishTime\n            AND Requested_On__c &lt;= :currentTime\n        ORDER by Tracker_ID__c desc, Requested_On__c desc\n    ];\n\n\t  \/\/ Ici on r\u00e9cup\u00e9re juste le dernier status en plus\tavec une requ\u00eate s\u00e9par\u00e9e\n     List&lt;Car_Monitoring_Data__b> theCarStatusList = [\n           SELECT\n                Id,\n                GPS_Latitude__c,\n                GPS_Longitude__c,\n                GPS_Elevation__c,\n                GPS_Speed__c,\n                OBD_Fuel_Level__c,\n                OBD_RPM__c,\n                OBD_Speed__c,\n                Requested_On__c,\n                GPS_Measure_Time__c,\n                OBD_Measure_Time__c,\n                GPS_Time__c,\n                Tracker_ID__c\n           FROM Car_Monitoring_Data__b\n                where Tracker_ID__c = :trackerId\n                    AND Requested_On__c >= :startTime\n                    AND Requested_On__c &lt;= :finishTime\n                    AND Requested_On__c &lt;= : currentTime\n                ORDER by Tracker_ID__c desc, Requested_On__c desc\n                LIMIT 1\n            ];\n            if (theCarStatusList.size()>0)\n                theCarStatus = theCarStatusList[0];\n            else\n                theCarStatus = null;\n    }\n<\/code><\/pre>\n\n\n\n<p>Le code pour r\u00e9cup\u00e9rer les historiques des autres participants est celui du Trape de l\u2019an pass\u00e9, auquel on ajoute le mode de gestion big data.&nbsp;&nbsp;&nbsp; <\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ fonction utilitaire pour juste m\u00e9moriser le r\u00e9sultat du calcul\n\/\/ appel\u00e9 pour la course courante,\n\/\/ au bout des x secondes que la participant actuel a fait\n\/\/ mais en excluant le participant courant\n\n    public void loadOtherRacerPositions() {\n        racerPositions = loadRacerPositions(raceid, elapsedTime, racerId);\n    }\n\n\/\/ La fonction qui fait le calcul\n\n    public static List&lt;Map&lt;String, Object>> loadRacerPositions(ID raceID, Integer chronoInSeconds, Id currentRacerId) {\n        \/\/ on retrouve la liste des participants\n         List&lt;Racer__c> lRacers = [\n            SELECT\n                Id,\n                Name,\n                Comment__c,\n                Car_Race__c,\n                Car__c,\n                Car__r.name,\n                Start_Date__c,\n                Finish_Date__c,\n                Tracker_Mode__c,\n                Tracker_Big_Object_Tracker_ID__c\n           FROM Racer__c\n           where Car_Race__c =:raceID\n        ];\n\n        \/\/ le tableau o\u00f9 on stocke les position qu'on a trouv\u00e9es.\n        List&lt;Map&lt;String, Object>> wrapperList = new List&lt;Map&lt;String, Object>>();\n\n        \/\/ pour chaque participant, on cherche sa derni\u00e8re position \u00e0 cette minute de la course\n        for (Racer__c racer : lRacers) {\n            \/\/ calcule l'heure pour ce participant (correspondant au x secondes du participant observ\u00e9 ajout\u00e9 \u00e0 l\u2019heure de d\u00e9part ce ce participant\n            DateTime positionTime =  racer.Start_Date__c;\n            positionTime = positionTime.addSeconds(chronoInSeconds);\n\n            if (racer.Tracker_Mode__c == 'Big Object') {\n                \/\/ recherche la derni\u00e8re position connue pour l\u2019heure actuelle\n                List&lt;Car_Monitoring_Data__b> carPosition = [\n                    SELECT\n                        Id,\n                        GPS_Latitude__c,\n                        GPS_Longitude__c,\n                        GPS_Measure_Time__c,\n                        Requested_On__c,\n                        Tracker_ID__c\n                    FROM Car_Monitoring_Data__b\n                    where Tracker_ID__c = :racer.Tracker_Big_Object_Tracker_ID__c\n                        AND Requested_On__c >= :racer.Start_Date__c\n                        AND Requested_On__c &lt;= :positionTime\n                    ORDER by Tracker_ID__c desc, Requested_On__c desc\n                    LIMIT 1\n                ];\n\n                \/\/ si on a trouv\u00e9 une position, on la met de cot\u00e9 dans la liste wrapperList en retrouvant les donn\u00e9es\n                if (carPosition.size()>0) {\n                    DateTime measureTime = carPosition[0].GPS_Measure_Time__c;\n\n                    integer raceElapsedTime = (measureTime!=null) ? (integer) ((measureTime.getTime() - racer.Start_Date__c.getTime() ) \/ 1000) : -1;\n                    integer measureAge = (measureTime!=null) ? (integer) ((positionTime.getTime() - measureTime.getTime() ) \/ 1000) : -1;\n\n                    Map&lt;String, Object> wrapp = new Map&lt;String, Object>{\n                        'carName' => racer.Name + ' ('+ racer.Car__r.Name+')',\n                        'comment' => racer.Comment__c,\n                        'racerId' => racer.id,\n                        'longitude' => carPosition[0].GPS_Longitude__c,\n                        'latitude' => carPosition[0].GPS_Latitude__c,\n                        'measureTime' => (measureTime!= null) ? string.valueOfGmt(measureTime) : '',\n                        'measureAge' => measureAge,\n                        'storedTime' =>  string.valueOfGmt(carPosition[0].Requested_On__c),\n                        'elapsedTime' => raceElapsedTime,\n                        b => (carPosition[0].Requested_On__c > racer.Finish_Date__c) ? true : false,\n                        'currentRacer' => ( currentRacerId == racer.Id ? true : false )\n                    };\n                    wrapperList.add(wrapp);\n                }\n            } else {\n                \/\/ cet autre participant fonctionne avec le mode de l\u2019an pass\u00e9\n\t\t\t  \/\/ code est juste adapt\u00e9 pour remplir les nouveaux champs\n        \/\/ avec des valeurs vide\n                List&lt;Car_Status__c> carPosition = [\n                    SELECT\n                        Id,\n                        Location__Latitude__s,\n                        Location__Longitude__s,\n                        Requested_On__c,\n                        Car__c,\n                        Car__r.Name\n                    FROM Car_Status__c\n                    WHERE Car__c = :racer.Car__c\n                        AND Requested_On__c >= :racer.Start_Date__c\n                        AND Requested_On__c &lt;= :positionTime\n                    ORDER BY Requested_On__c DESC\n                    LIMIT 1\n                ];\n\n                \/\/ si on a trouv\u00e9 une position, on la met de cot\u00e9\n                if (carPosition.size()>0) {\n                    DateTime measureTime = carPosition[0].Requested_On__c;\n                    integer raceElapsedTime = (integer) ( (carPosition[0].Requested_On__c.getTime() - racer.Start_Date__c.getTime() ) \/ 1000 );\n                    integer measureAge = (measureTime!=null) ? (integer) ((positionTime.getTime() - measureTime.getTime() ) \/ 1000) : -1;\n\n                    Map&lt;String, Object> wrapp = new Map&lt;String, Object>{\n                        'carName' => racer.Name + ' ('+ racer.Car__r.Name+')',\n                        'comment' => racer.Comment__c,\n                        'racerId' => racer.id,\n                        'longitude' => carPosition[0].Location__Longitude__s,\n                        'latitude' => carPosition[0].Location__Latitude__s,\n                        'measureTime' => string.valueOfGmt(carPosition[0].Requested_On__c),\n                        'measureAge' => measureAge,\n                        'storedTime'=>  string.valueOfGmt(carPosition[0].Requested_On__c),\n                        'elapsedTime' => raceElapsedTime,\n                        'finished' => (carPosition[0].Requested_On__c > racer.Finish_Date__c) ? true : false,\n                        'currentRacer' => ( currentRacerId == racer.Id ? true : false )\n                    };\n                    wrapperList.add(wrapp);\n                }\n            }\n        }\n\n        \/\/ on renvoie les positions qu'on a trouv\u00e9e\n        return wrapperList;\n    }\n\n<\/code><\/pre>\n\n\n\n<p>Ici tous les calculs utiles sont faits. Ici ce sont maintenant les fonctions qui vont fournir \u00e0 la page les infos \u00e0 afficher&nbsp;:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ y-a-t-il un participant \u00e0 afficher (cas d\u2019erreur)\npublic boolean getHasRacer(){ return racerId != null;}\n\n \/\/ l\u2019heure courante\npublic datetime getCurrentDisplayTime(){return currentTime;}\n\n \t\/\/ les donn\u00e9es du participant observ\u00e9\n    public Decimal getGPSSpeed() { return (theCarStatus==null) ? 0 : theCarStatus.GPS_Speed__c.round(System.RoundingMode.FLOOR);}\n\n    public Decimal getGPSElevation() { return (theCarStatus==null) ? 0 :  theCarStatus.GPS_Elevation__c.round(System.RoundingMode.FLOOR);}\n\n    public Decimal getOBDSpeed() { return (theCarStatus==null) ? 0 :  ((theCarStatus.OBD_Speed__c==null) ? 0 : theCarStatus.OBD_Speed__c.round(System.RoundingMode.FLOOR));}\n\n    public Decimal getOBDRPM()   { return (theCarStatus==null) ? 0 :  ((theCarStatus.OBD_RPM__c==null) ? 0 : theCarStatus.OBD_RPM__c.round(System.RoundingMode.FLOOR));}\n\n    public Decimal getOBDFuelLevel()   { return (theCarStatus==null) ? 0 :  theCarStatus.OBD_Fuel_Level__c;}\n\n    public Datetime getLastRequestDate()   { return (theCarStatus==null) ? null :  theCarStatus.Requested_On__c;}\n\n    public Datetime getLastGPSMeasure()   { return (theCarStatus==null) ? null :  theCarStatus.GPS_Measure_Time__c;}\n\n    public Datetime getLastOBDMeasure()   { return (theCarStatus==null) ? null :  theCarStatus.OBD_Measure_Time__c;}\n\n    public Datetime getGPSTime()   { return (theCarStatus==null) ? null :  theCarStatus.GPS_Time__c;}\n\n    public Long getLastRequestAge()   { return (theCarStatus==null) ? -1 :  getDatetimeAge(theCarStatus.Requested_On__c);}\n\n    public Long getLastGPSMeasureAge()   { return (theCarStatus==null) ? -1 :  getDatetimeAge(theCarStatus.GPS_Measure_Time__c);}\n\n    public Long getLastOBDMeasureAge()   { return (theCarStatus==null) ? -1 :  getDatetimeAge(theCarStatus.OBD_Measure_Time__c);}   <\/code><\/pre>\n\n\n\n<p>Un code particulier pr\u00e9pare les informations \u00e0 afficher sur le graphe \u2018vitesse\u2019<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public String getSpeedChartData() {\n    String content = '{'+\n     '   \"type\": \"scatter\",'+\n     '   \"data\": {'+\n     '       \"datasets\": [{'+\n     '           \"label\": \"Speed Dataset\",'+\n     '           \"data\": [ '\n     ;\n\n     boolean first = true;\n\t   \/\/ on boucle sur les donn\u00e9es observ\u00e9es, et on m\u00e9morise heure et vitesse\n     for (Car_Monitoring_Data__b d:data) {\n         if (!first) {\n             content = content + ',';\n         }\n         content = content + '{\"x\": \"'+ d.Requested_On__c +'\",\"y\": '+ d.OBD_Speed__c +'}';\n         first = false;\n     }\n content =content +']'+\n '       }]'+\n '   },'+\n '   \"options\": {'+\n '  \"responsive\": \"true\",' +\n'    \"maintainAspectRatio\": \"false\",'+\n '       \"scales\": {'+\n '           \"xAxes\": [{'+\n '               \"type\": \"time\",'+\n '               \"position\": \"bottom\"'+\n '           }]'+\n '       }'+\n '   }'+\n '   }';\n\n\/\/ on renvoie le json contenant les caract\u00e9ristiques du graphe\nreturn content;\n}\n<\/code><\/pre>\n\n\n\n<p>Un autre code particulier pr\u00e9pare les informations \u00e0 afficher sur la carte des positions (m\u00eame code que pour le trape de l\u2019an pass\u00e9)<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ data for json map\n    public String searchResultFeaturesJSON { get; set; }\n\n\t\/\/ creation du marqueur pour le participant courant\n\n    public String generateCurrentRacerFeatureCollection() {\n        JSONGenerator gen = JSON.createGenerator(true);\n        gen.writeStartObject();\n        gen.writeStringField('type', 'FeatureCollection');\n        gen.writeFieldName('features');\n        gen.writeStartArray();\n                gen.writeStartObject();\n                gen.writeStringField('type', 'Feature');\n                gen.writeFieldName('geometry');\n                gen.writeStartObject();\n                gen.writeStringField('type', 'Point');\n                gen.writeFieldName('coordinates');\n                gen.writeStartArray();\n                gen.writeNumber(theCarStatus.GPS_Longitude__c);\n                gen.writeNumber(theCarStatus.GPS_Latitude__c);\n                gen.writeEndArray();\n                gen.writeEndObject();\n                gen.writeFieldName('properties');\n                gen.writeStartObject();\n                gen.writeStringField('name', theCarStatus.Tracker_ID__c);\n                gen.writeEndObject();\n                gen.writeEndObject();\n        gen.writeEndArray();\n        gen.writeEndObject();\n\n        String pretty = gen.getAsString();\n        pretty = pretty.replaceAll('\\n', ' ');\n        return pretty;\n    }\n\n\t\/\/ creation des marqueurs pour les autres participants\n\n    public String generateAllRacersFeaturesCollection() {\n        JSONGenerator gen = JSON.createGenerator(true);\n        gen.writeStartObject();\n        gen.writeStringField('type', 'FeatureCollection');\n\n        gen.writeFieldName('features');\n        gen.writeStartArray();\n        for( Map&lt;String, Object> wrapper : racerPositions) {\n\t\t    \/\/ on v\u00e9rifie d\u2019abord s\u2019il y a bien une position (sinon bug ;-) )\n            decimal lat = (Decimal)wrapper.get('longitude');\n            decimal lon = (Decimal)wrapper.get('latitude');\n            if((lat!=null)&amp;&amp;(lon!=null)&amp;&amp;(lat!=0)&amp;&amp;(lon!=0)) {\n                gen.writeStartObject();\n                gen.writeStringField('type', 'Feature');\n                gen.writeFieldName('geometry');\n                gen.writeStartObject();\n                gen.writeStringField('type', 'Point');\n                gen.writeFieldName('coordinates');\n                gen.writeStartArray();\n                gen.writeNumber((Decimal)wrapper.get('longitude'));\n                gen.writeNumber((Decimal)wrapper.get('latitude'));\n                gen.writeEndArray();\n                gen.writeEndObject();\n                gen.writeFieldName('properties');\n                gen.writeStartObject();\n                gen.writeStringField('carName', (String)wrapper.get('carName'));\n                gen.writeBooleanField('currentRacer', (Boolean)wrapper.get('currentRacer'));\n                gen.writeStringField('racerId', (String)wrapper.get('racerId'));\n                gen.writeStringField('storedTime', (String)wrapper.get('measureTime'));\n                gen.writeStringField('measureTime', (String)wrapper.get('measureTime'));\n                Integer measureAge = (Integer)wrapper.get('measureAge');\n                gen.writeNumberField('measureAge', measureAge!=null ? (Integer)wrapper.get('measureAge') : -1);\n                gen.writeNumberField('elapsedTime', (Integer)wrapper.get('elapsedTime'));\n                gen.writeBooleanField('finished', (Boolean)wrapper.get('finished'));\n                gen.writeEndObject();\n                gen.writeEndObject();\n            }\n        }\n        gen.writeEndArray();\n        gen.writeEndObject();\n\n        String pretty = gen.getAsString();\n        pretty = pretty.replaceAll('\\n', ' ');\n\n        return pretty;\n    }     public Long getDatetimeAge(Datetime a) {\n        if (a==null)\n            return -1;\n\n        Long dt1Long = a.getTime();\n        Long dt2Long = currentTime.getTime(); \/\/ System.now().getTime();\n        Long milliseconds = dt2Long - dt1Long;\n        Long seconds = milliseconds \/ 1000;\n\n        return seconds;\n    }\n<\/code><\/pre>\n\n\n\n<p>Pour permettre \u00e0 la page d\u2019afficher dans des couleurs diff\u00e9rentes les mesures \u2018trop vieilles\u2019, il a \u00e9t\u00e9 n\u00e9cessaire de cr\u00e9er quelques fonctions utilitaires<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>    public boolean getDatetimeDiffTooBig(Datetime a, Datetime b, integer limite) {\n        if (a==null)\n            return true;\n        if (b==null)\n            return true;\n\n        Long dt1Long = a.getTime();\n        Long dt2Long = b.getTime();\n        Long milliseconds = dt2Long - dt1Long;\n        Long seconds = milliseconds \/ 1000;\n        if (seconds &lt;0) {\n            seconds = -seconds;\n        }\n        return seconds > limite;\n    }\n\n\/\/ heure de la derni\u00e8re requ\u00eate trop vieille\n    public boolean getLastRequestTooOld() {\n        return  (theCarStatus==null) ? true : getDatetimeDiffTooBig(currentTime, theCarStatus.Requested_On__c, 400);\n    }\n\n\/\/ heure de la derni\u00e8re data OBD trop vieille\n    public boolean getObdDataTooOld() {\n        return  (theCarStatus==null) ? true : getDatetimeDiffTooBig(theCarStatus.OBD_Measure_Time__c, theCarStatus.Requested_On__c, 60);\n    }\n\n    \/\/ heure de la derni\u00e8re data GPS trop vieille\n     public Boolean getGpsDataTooOld() {\n        return  (theCarStatus==null) ? true : getDatetimeDiffTooBig(theCarStatus.GPS_Measure_Time__c, theCarStatus.Requested_On__c, 60);\n    }\n\n\/\/ ecart trop grand entre l\u2019heure r\u00e9cup\u00e9r\u00e9re par le GPS, et l\u2019heure du Raspberry (d\u00e9tecter un pbm du Raspberry)\n     public Boolean getTimeOutOfSync() {\n        return  (theCarStatus==null) ? true : getDatetimeDiffTooBig(theCarStatus.GPS_Measure_Time__c, theCarStatus.GPS_Time__c, 120);\n     }\n\n}\n<\/code><\/pre>\n","protected":false},"excerpt":{"rendered":"<p>On va fournir une page publique Salesforce CarRacerStatistics accessible depuis l\u2019ext\u00e9rieur de Salesforce pour afficher l\u2019\u00e9tat en temps quasi r\u00e9el de la voiture. Cette page publique aura comme param\u00e8tre d\u2019URL l\u2019ID Salesforce du Racer \u00e0 afficher \u2026\/CarRacerStatistics?racerId=a041t00000FFEG3 La page va poss\u00e9der un contr\u00f4leur (le code qui s\u2019ex\u00e9cute cot\u00e9 Salesforce qui&nbsp;va&nbsp;: R\u00e9cup\u00e9rer la derni\u00e8re position connue <a class=\"read-more\" href=\"https:\/\/wollef.org\/blog\/evolution-du-jeu-salesforce-2-3-la-page-son-controleur\/\">Continue Reading<\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[31,1],"tags":[53],"class_list":["post-987","post","type-post","status-publish","format-standard","hentry","category-raspberry-cars","category-non-classe","tag-2020-raspberry-cars"],"_links":{"self":[{"href":"https:\/\/wollef.org\/blog\/wp-json\/wp\/v2\/posts\/987","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wollef.org\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/wollef.org\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/wollef.org\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/wollef.org\/blog\/wp-json\/wp\/v2\/comments?post=987"}],"version-history":[{"count":0,"href":"https:\/\/wollef.org\/blog\/wp-json\/wp\/v2\/posts\/987\/revisions"}],"wp:attachment":[{"href":"https:\/\/wollef.org\/blog\/wp-json\/wp\/v2\/media?parent=987"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/wollef.org\/blog\/wp-json\/wp\/v2\/categories?post=987"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/wollef.org\/blog\/wp-json\/wp\/v2\/tags?post=987"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}