Ajax e PHP: una progress bar V-meter style

Progress bar V-meter styleEcco un’idea per una progress bar un po’ diversa dal solito. L’aspetto è quello dei V-meter digitali degli apparecchi audio. Solo che l’andamento della progressione non è logaritmico ma decimale e percentuale.
Può essere utilizzata come strumento di monitoraggio di operazioni lunghe eseguite sul server perché, grazie alla tecnologia Ajax, è possibile recuperare valori ad intervelli regolari da uno script PHP.

I requisiti per un corretto funzionamento sono:

  • Javascript abilitato
  • PHP con supporto GD 2.0.1 o successivi
  • FreeType library

La progress bar viene generata dinamicamente da uno script PHP che esegue l’eco di un’immagine in formato PNG. I valori passati in querystring servono per colorare le barre. La percentuale viene calcolata normalizzando il valore sul minimo e il massimo, con un’approssimazione per difetto alla decina inferiore. (p.e.: 52% accende 5 barre). Il default per il minimo e il massimo è rispettivamente 0 e 100

Ecco il codice:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//==============================================================================
// PROGRESS BAR V-METER STYLE
//==============================================================================
if (!empty($_GET['value'])){
  $value = intval($_GET['value']);
  $min = empty($_GET['min']) ? 0 : intval($_GET['min']);
  $max = empty($_GET['max']) ? 100 : intval($_GET['max']);
}
else{
  $value = 0;
  $min = 0;
  $max = 100;
}
  $nValue = floor((($value-$min) / ($max-$min)) * 100);
  header("Content-type: image/png");
  $string = $nValue." %";
  $font = 'arial.ttf';
  $width  = 110; 
  $height = $width;
  $im = @imagecreatetruecolor ($width,$height);
  $width -=5;
  $height -=5;
  $background_color = imageColorAllocate($im, 0, 0, 0);
  $color = imageColorAllocate($im, 0, 255, 0);
  imagefill($im, 0, 0, $background_color);
  for($i=0;$i<10;$i++){
    $x1 = $width;
    $y1 = $height-2 - ($i*10);
    $x2 = $width-10 - ($i*10);
    $y2 = $height-10 - ($i*10);
    if ($i<(floor($nValue/10))){
      imageFilledRectangle($im, $x1, $y1, $x2,  $y2, $color);
    }
    else{
      imagerectangle($im, $x1, $y1, $x2,  $y2, $color);
    }
  }
  $text_color = imagecolorallocate ($im, 255, 255, 255);
  imagettftext($im, 14, 0, 5, $height-10, $text_color, $font, $string);
  imagepng($im);
  imagedestroy($im);

Volendo una più ampia compatibilità, è possibile evitare di utilizzare la funzione imagettftext() sostituendola con imagestring(), in questo caso vengono utilizzati i font interni con codifica latin2. Però non sono altrettanto belli e le dimensioni possibili sono solo 5. Il valore più grande restituisce una dimensione di carattere piuttosto piccola… ove possibile è preferibile usare i TTF

Questo è il codice di esempio per rappresentare la progress bar in una pagina web con un po’ di Ajax per rigenerare dinamicamente l’immagine ogni 2 secondi:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
  <meta http-equiv="content-type" content="text/html; charset=utf-8">
  <meta name="generator" content="PSPad editor, www.pspad.com">
    <title> Test della progress bar V-meter style </title>
  <script type="text/javascript" language="javascript">
    <!--
      var xml = "";
      function getPage() {
        var url = "./randgen.php";
        if (window.XMLHttpRequest) {
          xml = new XMLHttpRequest();
        } else if (window.ActiveXObject) {
          xml = new ActiveXObject("Microsoft.XMLHTTP");
        } else {
          alert("Your browser lacks the needed ability to use Ajax");
          return false;   
        }
        xml.onreadystatechange = processPage;
        xml.open("GET", url, true);
        xml.send("");
        setTimeout('getPage()', 2*1000);
      }
 
      function processPage() {
        if (xml.readyState == 4) {
          if (xml.status == 200) {
            document.getElementById("myProgBar").src="./progbar.php?value="
                                                      + xml.responseText;        
          } else {
            alert("There was a problem retrieving the XML data:\n"
                   + xml.statusText);
          }
        }  
      }    
    //-->  
  </script>
  </head>
  <body>
  <script type="text/javascript" language="javascript">
  getPage();
  </script>
      <h1>Test della progress bar V-meter style</h1>   
      <div>
 
        <img id="myProgBar" src="./black.png" alt="progress bar" />
      </div>
  </body>  
</html>

Lo script randgen.php genera dei numeri casuali da 0 a 100 solo a scopo dimostrativo, nella pratica dovrà generare il valore da rappresentare nella progress bar.

randgen.php:

1
2
3
4
5
6
7
8
$min = empty($_GET['min']) ? 0 : intval($_GET['min']);
$max = empty($_GET['max']) ? 100 : intval($_GET['max']);
// headers are sent to prevent browsers from caching
header('Expires: Fri, 25 Dec 1980 00:00:00 GMT'); // time in the past
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . 'GMT'); 
header('Cache-Control: no-cache, must-revalidate'); 
header('Pragma: no-cache');
echo rand($min,$max);

Il pacchetto completo è scaricabile qui

Conclusioni:

Siccome adoro i V-meter analogici, per capirci quelli che si trovavano sui grandi amplificatori HiFi degli anni ’70, stavo pensando anche a qualcosa del genere per il futuro, peccato che con la grafica ho qualche difficoltà…!

Riferimenti ed approfondimenti:

AJAX, JavaScript, PHP e Google maps: Georeferenziazione indirizzi

Questo post ha più di 1 anno. Il contenuto potrebbe essere obsoleto, non più completamente accessibile o mancante di alcune informazioni. In questo articolo ci sono informazioni simili, ma più recenti.

screenshot del programma georeferenziazione indirizziPer concludere il panorama dei possibili utilizzi della classe PHP per georeferenziare indirizzi multipli, vediamo l’impiego della tecnologia AJAX vera e propria.
Come avevo accennato nel precedente articolo, l’approccio non è così lineare per la necessità di invocare più volte l’oggetto XMLHttpRequest, e ancora peggio, per dover gestire le risposte multiple ed asincrone del server. Si rende infatti necessario, non solo creare tanti oggetti AJAX quante sono le richieste, ma anche altrettanti oggetti che gestiscano separatamente le risposte, mantenendo l’handle della chiamata.
Una buona soluzione mi è sembrata quella illustrata in questo articolo, ed è quella che ho adottato per questo esempio.

La pagina html iniziale comprende un form per l’immissione degli indirizzi e un semplice script javascript che, per ogni tag di tipo input, invoca una richiesta AJAX. Ecco il codice della funzione:

<script type="text/javascript" src="MultipleAjaxClassSimple.js"></script> 
<script type="text/javascript">
 
function getFormValues() 
{
  document.getElementById("divResults").innerHTML = '';
  var myForm = document.getElementById("myForm").getElementsByTagName('input');
  var len = myForm.length;
  var params = '';
  for (i = 0; i < len; i++) {
    if(myForm[i].type == 'text'){
      params = myForm[i].value; 
      ajaxSaveTag(params);         
    }        
  }
}    
</script>

Lo script AJAX è composto da quattro funzioni:

  • ajaxSaveTag()
  • httpRequest()
  • initRequest()
  • ReturnSaveTag()

La prima funzione prende come parametro l’indirizzo da georeferenziare, compone una url con la querystring da passare allo script server e infine chiama la seconda funzione httpRequest() che si occupa di instanziare l’oggetto XMLHttpRequest. La terza funzione, initRequest() ha il compito di gestire le risposte del server, invocando i metodi della funzione di callback (la quarta: ReturnSaveTag()), che in realtà è un oggetto. Questo permette di creare tante istanze di callback quante sono le chiamate AJAX.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// JavaScript Document
function ajaxSaveTag(tag) {
  var url  = 'ajaxSaveTags.php';
  url += '?query='+encodeURIComponent(tag);
  httpRequest("GET",url,1,new ReturnSaveTag());
}
function httpRequest(type,url,asynch,respHandle) {
  var request;
  // mozilla
  if (window.XMLHttpRequest) {
    request = new XMLHttpRequest();
  }
  // internet explorer
  else if (window.ActiveXObject) {
    request = new ActiveXObject("Msxml2.XMLHTTP");
    if (!request) {
      request = new ActiveXObject("Microsoft.XMLHTTP");
    }
  }
  // send request | error
  if (request) {
    initRequest(request,type,url,asynch,respHandle);
  }
  else {
  // ERROR!
  }
}
function initRequest(request,type,url,asynch,respHandle) {
  try {
    respHandle.setReqObj(request);
    request.onreadystatechange = respHandle.goResponse;
    request.open(type,url,asynch);
    request.send(null);
  } catch (errv) {
    // ERROR!
  }
}
function ReturnSaveTag() {
  var reqObj = null;
  this.setReqObj = setReqObj;
  this.goResponse = goResponse;
  function setReqObj(myVal) { reqObj = myVal; }
  function goResponse() {
    var request = reqObj;
    if (request.readyState == 4) {
      if (request.status == 200) {
        // Success!
        var mytext = request.responseText;
        var myDiv = document.getElementById("divResults");
        // display the HTML output
        myDiv.innerHTML = myDiv.innerHTML + mytext;
      } else {
        // ERROR (couldn't find ajax file)
      }
    }
    return true;
  } // end method
}

Lo script lato server ajaxSaveTags.php crea un header che non consenta al browser di utilizzare la cache, raccoglie i dati dalla querystring, utilizza la classe PHP geocodeaddr
per georeferenziare l’indirizzo, ed infine stampa il risultato formattato.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
// make sure the user's browser doesn't cache the result
header('Expires: Wed, 23 Dec 1980 00:30:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-cache, must-revalidate');
header('Pragma: no-cache');
require("geocoder4.class.php");
$data = array();
if (!empty($_GET['query']))
 {
  $toGeocode = $_GET['query'];
  $data = array($toGeocode);
 }      
//localhost key:
$gmpKey = "ABQIAAAAzr2EBOXUKnm_...";// Inserire la Google key corretta!
$test = &amp; new geocodeaddr($data,$gmpKey);
$geocodeResults = $test->getAddress();
$res = $geocodeResults[0]['address'].' lat: '.
       $geocodeResults[0]['lat'].' long: '.
       $geocodeResults[0]['lng'].'<br />';
// output the result
echo $res;
?>

Questa è la demo

Conclusioni:

L’utilizzo della tecnologia AJAX ha richiesto un codice un po’ più sofisticato e meno facilmente intuibile rispetto alla soluzione pseudo-AJAX. Sarebbe interessante fare un confronto anche in altre situazioni in cui sia necessario effettuare richieste asincrone al server, e verificare efficienza e robustezza di entrambi con carichi gravosi. Un’ultima curiosità: poiché le risposte del server sono asincrone, può capitare che l’ordine dei risultati non sia lo stesso delle chiamate.

Riferimenti ed approfondimenti:

Calcolare le distanze geodetiche fra comuni d’Italia in php

coordinate.pngIn un precedente articolo avevo pubblicato il codice di una funzione in ANSI C per il calcolo delle distanze sulla superficie terrestre. Poiché il C e il PHP sono due linguaggi dalla sintassi assai simile, ho deciso di fare la traduzione del codice. Inoltre, per dare un senso pratico al lavoro, ho trovato sul web l’archivio completo dei comuni italiani con le relative coordinate geografiche e su questo ho costruito un programmino PHP interattivo che permette di selezionare due comuni ed ottenere la relativa distanza geodetica, ovvero la lunghezza dell’arco massimo che unisce i due punti rappresentati dalle coordinate dei due comuni.
Va sottolineato che, come spiegato in questo ottimo articolo, per distanze molto piccole la formula che utilizzo fornisce un errore piuttosto grosso, quindi i risultati vanno presi come puramente indicativi.

Ecco il codice della funzione:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function disgeod($latA, $lonA, $latB, $lonB){
  define ("R","6371");
  /* Converte i gradi in radianti */
  $lat_alfa = M_PI * $latA / 180;
  $lat_beta = M_PI * $latB / 180;
  $lon_alfa = M_PI * $lonA / 180;
  $lon_beta = M_PI * $lonB / 180;
  /* Calcola l'angolo compreso fi */
  $fi = abs($lon_alfa - $lon_beta);
  /* Calcola il terzo lato del triangolo sferico */
  $p = acos(sin($lat_beta) * sin($lat_alfa)
   + cos($lat_beta) * cos($lat_alfa) * cos($fi));
  /* Calcola la distanza sulla superficie terrestre R = ˜6371 Km */
  $d = $p * R;
  return($d);
}

E questa invece è la funzione che converte i gradi sessagesimali in decimali:

1
2
3
4
5
6
7
8
function deg2dec($d=0,$m=0,$s=0,$direction)
{
  $decimal=($d+($m/60)+($s/3600));
  //South latitudes and West longitudes need to return a negative result
  if (($direction=="S") or ($direction=="W"))
          { $decimal=$decimal*(-1);}
  return $decimal;
}

Per evitare di dover caricare le due select con tutti gli 8104 comuni, cosa che avrebbe rallentato il caricamento della pagina in modo improponibile, ho utilizzato uno script AJAX che provvede a popolare le select dinamicamente con i soli nomi dei comuni di una certa provincia preselezionata.

Potete provare il programma su questo link (Il programma è stato testato con Firefox 2 e con Internet Explorer 7)

Download

L’intero pacchetto (circa 1Mb) comprende anche l’archivio in formato sql, txt, xls e csv.

Conclusioni

Lo sviluppo di questo programmino è stato un esercizio interessante, che mi ha permesso di approfondire anche l’utilizzo e il funzionamento degli script AJAX sui quali credo che tornerò in futuro.

Riferimenti ed approfondimenti: