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.

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
<!--?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;amp; new geocodeaddr($data,$gmpKey); $geocodeResults = $test-&gt;getAddress();
$res = $geocodeResults[0]['address'].' lat: '.
       $geocodeResults[0]['lat'].' long: '.
       $geocodeResults[0]['lng'].'&lt;br ?-->';
// output the result
echo $res;
?&gt;

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:

Pseudo-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.

Ajax: no grazie!Nelle conclusioni del precedente post, dicevo che “… sarebbe interessante utilizzare la tecnologia Ajax per recuperare in modo asincrono le coordinate …”, ebbene, fatti alcuni test, mi sono ritrovato ad affrontare due difficoltà: compatibilità dell’oggetto XMLhttprequest e soprattutto la gestione di richieste multiple contemporanee. Cercando sul web, ho trovato diverse soluzioni, alcune molto complicate, altre troppo “deboli”, una di queste interessante, ma alla fine ne ho trovata una, completamente diversa, che mi ha davvero incuriosito: la simulazione di un comportamento AJAX senza utilizzare… AJAX! La soluzione prospettata dall’autore di questo interessante post su W3 Facile, è tanto semplice quanto geniale: utilizzare il tag <script> per includere nella nostra pagina html, porzioni di codice javascript generati dinamicamente da script server side PHP. In questo modo risulta possibile elaborare richieste a server remoti e ricevere serialmente le risposte, vediamo l’applicazione di questa soluzione al nostro caso:

la pagina iniziale contiene un form per impostare gli indirizzi da georeferenziare e questo codice javascript che effettua le chiamate allo script lato server PHP:

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
<script type="text/javascript">
// Ottieni la base url
url = document.location.href;
xend = url.lastIndexOf("/") + 1;
var base_url = url.substring(0, xend);
function chiama_ajax(url) 
{
  //Inizia l'url con http?
  if (url.substring(0, 4) != 'http') 
  {
    url = base_url + url;
  }
  // Crea un nuovo elemento JS
  var jsel = document.createElement('SCRIPT');
  jsel.type = 'text/javascript';
  jsel.src = url;
  //Appende l'elemento JS e quindi esegue la 'chiamata pseudo-AJAX'
  document.body.appendChild (jsel);
}
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; 
      //per ogni elemento del form, includo lo script js che
      // viene creato dinamicamente dallo script php
      chiama_ajax('pseudoajax.php?query='+params);         
    }        
  }
}
</script>

In pratica, per ogni elemento input di tipo text del form viene richiamata la funzione chiama_ajax() passandogli la url del file php e il valore impostato in querystring.

Lo script lato server pseudoajax.php:

1
2
3
4
5
6
7
<!--?php 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;amp; new geocodeaddr($data,$gmpKey); $geocodeResults = $test-&gt;getAddress();
$res = $geocodeResults[0]['address'].' lat: '.
       $geocodeResults[0]['lat'].' long: '.
       $geocodeResults[0]['lng'].'&lt;br ?-->';
?&gt;
div = document.getElementById('divResults');
div.innerHTML += '<!--?php echo($res); ?-->';

Lo script raccoglie il parametro passato in querystring, valorizza l’array da passare alla classe geocodeaddr, formatta il risultato e lo rispedisce come elemento code>innerHTML
al client HTML sotto forma di script js. Fatto!

Questa è la demo

Conclusioni:

Concordo pienamente con l’autore che, nei commenti al suo articolo dice “Diciamo che come metodo ha i suoi pro e contro…i pro sono un’enorme leggerezza, compatibilità e semplicità d’uso, i contro dei limiti nel suo utilizzo e l’esclusione per la lettura di valori XML, non utilizzando l’XMLhttprequest”, e conclude dicendo: “…nella programmazione non esiste una soluzione migliore di un’altra SEMPRE, dipende dal problema che dobbiamo risolvere”

Riferimenti ed approfondimenti:

PHP: una classe per trovare le coordinate geografiche di una lista di localita’ con Google Maps

 

Questo post ha più di 1 anno. Il contenuto potrebbe essere obsoleto, non più completamente accessibile o mancante di alcune informazioni.

planisferoHo ricavato il codice di questa classe PHP modificando leggermente un chiarissimo esempio disponibile sul sito di Google Maps. L’esempio mostra come sia possibile inserire in un database MySQL le coordinate geografiche relative ad un archivio di località o indirizzi. Ho pensato di renderlo più versatile traducendolo in una classe che prende come input un array di indirizzi/località e restituisce in output un array di coordinate geografiche. Le API di Google Maps mettono a disposizione un servizio per la georeferenziazione che può essere utilizzato attraverso due differenti modalità:

  1. Attraverso una richiesta HTTP
  2. Attraverso l’oggetto GClientGeocoder

Le operazioni di georeferenziazione attraverso GClientGeocoder sono piuttosto onerose in termini di tempo e risorse, per questo motivo Google stesso suggerisce di utilizzare le richieste server side via HTTP.
La URI per accedere al servizio è: http://maps.google.com/maps/geo? e i parametri per completare la richiesta:

  • q – è l’indirizzo o località da georeferenziare
  • key – è la chiave per l’accesso a Google Maps (attivabile qui)
  • output – è il formato dell’output generato, i valori possibili sono: xml, kml, csv, json.

Il codice base è praticamente lo stesso dell’esempio fornito da Google al quale ho soltanto eliminato le parti di accesso al database MySQL. La classe compatibile con PHP5 (che è mostrata qui sotto), utilizza come output un file xml, mentre quella compatibile con PHP4 utilizza un output csv.

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
class geocodeaddr
{
  public $mapsHost = "maps.google.com";
  public $googleKey = "myGoogleMapsKey";
  public $dataArray;
  public $errorMsg = "";
 
  public function __construct($dataArray, $googleKey)
  {
    $this-&gt;dataArray = $dataArray;
    $this-&gt;googleKey = $googleKey;
  }
  public function getAddress()
  {
    $results = array();
    // Initialize delay in geocode speed
    $delay = 0;
    $base_url = "http://" . $this-&gt;mapsHost . "/maps/geo?output=xml" . "&amp;key=" . 
                $this-&gt;googleKey . "&amp;oe=utf-8";
    // Iterate through the rows, geocoding each address
    foreach ($this-&gt;dataArray as $address) {
      $geocode_pending = true;
        while ($geocode_pending) {
          $request_url = $base_url . "&amp;q=" . urlencode($address);
          $xml = simplexml_load_file($request_url) or die("url not loading");
          $status = $xml-&gt;Response-&gt;Status-&gt;code;
          if (strcmp($status, "200") == 0) {
            // Successful geocode
            $geocode_pending = false;
            $coordinates = $xml-&gt;Response-&gt;Placemark-&gt;Point-&gt;coordinates;
            $coordinatesSplit = split(",", $coordinates);
            // Format: Longitude, Latitude
            $lng = $coordinatesSplit[0];
            $lat = $coordinatesSplit[1];
            $results[] = array("address" =&gt; $address, "lat" =&gt; $lat, "lng" =&gt; $lng);
          } else if (strcmp($status, "620") == 0) {
            // sent geocodes too fast
            $delay += 100000;
          } else {
            // failure to geocode
            $geocode_pending = false;
            $results[] = array("address" =&gt; $address, "lat" =&gt; 0, "lng" =&gt; 0);
            $this-&gt;errorMsg .= "Address ".$address.
                  " failed to geocoded. Received status ".$status."\n";
          }
        usleep($delay);
      }
    }
    return $results;
  }
}

Per comprendere meglio come utilizzare la struttura xml generata dal servizio di Google riporto un esempio utilizzando “Roma” come indirizzo di ricerca:

1
2
3
4
5
6
7
<!--?xml version="1.0" encoding="UTF-8"?-->
 
 
  Roma
 
 
    <code>200</code> geocode
Roma, Roma (Lazio), Italy

ITLazioRomaRoma12.482324,41.895466,0

Notate che ho utilizzato un piccolo hack non documentato per evitare “warning” con caratteri particolari, tipo le lettere accentate, che si verifica scegliendo il formato xml. Il trucco consiste nel forzare l’encoding dei caratteri nel formato utf-8 aggiungendo alla querystring questo parametro: &oe=utf-8

Un altro possibile errore potrebbe verificarsi utilizzando la classe versione PHP4. Infatti la funzione utilizzata per “recuperare” il file csv via HTTP è file_get_contents(). Questa funzione è compatibile solo con le versioni di PHP 4.3.x e superiori, e solo se è abilitata la direttiva del php.ini: allow_url_fopen. Per questo motivo nel codice della classe geocoder4.class.php, ho inserito un metodo che utilizza le librerie cURL in alternativa a file_get_contents(). Il metodo deve essere decommentato insieme alla riga di codice che lo richiama nel caso in cui si voglia usare cURL (verificate che almeno cURL sia abilitato!!).

Questo, invece, è uno spezzone di codice che mostra come instanziare la classe in PHP5 e generare una tabella di risultati: (un esempio di utilizzo della classe è visibile qui)

1
2
3
4
5
6
7
8
9
10
11
...
require("geocoder5.class.php");
$data = array("Roma, RM, Italy", 
              "Milano, MI, Italy", 
              "Bologna, BO, Italy", 
              "Napoli, NA, Italy", 
              "Palermo, PA, Italy");
$gmpKey = "ABQIAAAAzr2EBOXUKnm..."; // Inserire la Google key corretta!
$test = new geocodeaddr($data,$gmpKey);
$geocodeResults = $test-&gt;getAddress();
echo("

“); echo(”
“); foreach ($geocodeResults as $rowResult){ echo(”); } echo(”

IndirizzoLatitudineLongitudine
‘.$rowResult[‘address’].’‘.$rowResult[‘lat’]. ‘‘.$rowResult[‘lng’].’

“); echo $test->errorMsg; …

Download

Il pacchetto completo delle classi sia per PHP5 che per PHP4 ed il file di esempio è scaricabile qui.

Conclusioni:

Non è stato difficile adattare l’esempio fornito da Google Maps alle classi, ci è voluto un po’ tempo, invece per assicurarsi che la classe per il PHP4 funzionasse senza errori con diversi tipi di configurazione dei server. Per questo motivo, consiglio a tutti di passare al PHP5 se e ove possibile!
Infine, sarebbe interessante utilizzare la tecnologia Ajax per recuperare in modo asincrono le coordinate, ed eliminare in questo modo il fastidioso stato di attesa del browser finché non sono state georeferenziate tutte le località. Tempo permettendo, è un miglioramento che vorrei implementare.

Riferimenti ed approfondimenti:

P4A framework: utilizzare il widget P4A Google Maps per implementare uno store locator

 

Questo post ha più di 1 anno. Il contenuto potrebbe essere obsoleto, non più completamente accessibile o mancante di alcune informazioni.

P4A esempio di Store LocatorEcco un esempio di utilizzazione del widget P4A Google Maps per creare una mappa di riferimenti (negozi, magazzini, ecc) che siano compresi in un certo raggio di distanza in Km, a partire da un indirizzo prestabilito.
Ho cercato di utilizzare il widget di Alberto Galanti senza effettuare nessuna modifica al codice originale. Ho scelto, quindi di implementare un metodo che fornisce la georeferenza della località che funge da centro, per poi effettuare, attraverso una query, la selezione degli indirizzi compresi nel raggio di distanze prestabilito.
L’esempio utilizzato, per comodità, è lo stesso indicato dalla guida di Google Maps in questo articolo.

I passi principali prima di procedere:

  • Scaricare il widget P4A Google Maps qui
  • Creare un archivio di riferimenti che abbiano un indirizzo nel formato accettato da Google Maps (Es.: “Piazza Venezia, Roma, Italia”)
  • Georeferenziare tutti gli indirizzi (trovare latitudine e longitudine)

Utilizzando MySQL, la struttura dell’archivio suggerita da Google è questa:

1
2
3
4
5
6
7
8
CREATE TABLE `markers` (
  `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(60) NOT NULL,
  `address` VARCHAR(80) NOT NULL,
  `lat` FLOAT(10,6) NOT NULL,
  `lng` FLOAT(10,6) NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1;

Per comodità potete scaricare l’sql completo dei dati di esempio di Google qui

Nel caso non si conoscano già le coordinate dei riferimenti, è possibile utilizzare il metodo suggerito in questo articolo sempre da Google. Questo sistema permette di aggiornare direttamente i dati di un database MySQL con le coordinate fornite da Google Maps per tutti gli indirizzi memorizzati.

La selezione dei record che sono compresi in un reggio di distanza in Km prefissato, avviene tramite una query che utilizza la formula della triangolazione sferica:
cos p = cos a cos b + sen a sen b cos φ,
per maggiori dettagli potete leggere questo post.

Ecco la query in pratica:

1
2
3
4
5
6
7
8
SELECT address, name, lat, lng, 
( 6371 * acos( cos( radians('Center_latitude')) * cos( radians( lat )) 
* cos( radians( lng ) - radians('Center_longitude')) 
+ sin( radians('Center_latitude'))
* sin( radians( lat )))) AS distance
FROM markers
HAVING distance &lt; 'Radius'
ORDER BY distance

dove 'Center_latitude' è la latitudine del centro, 'Center_longitude' è la longitudine del centro e 'Radius' è il raggio in km dal centro.

Ed ecco dunque, il codice della maschera P4A:

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
59
60
61
62
class GoogleMap extends P4A_Mask
{
  function GoogleMap()
  {
    parent::P4A_Mask();
    $this-&gt;setTitle("Google Map Store Locator Test");
    // Frame
    $this-&gt;build("p4a_frame", "frame");
    $this-&gt;frame-&gt;setWidth(800);
    //la chiave per localhost è disponibile nel file p4a_map.php
    $coordArray = $this-&gt;getLocationCoordinates("Mountain View, CA", 
                  "Inserire_chiave_Google"); 
    $radius = 12;
    // DB Source
    $this-&gt;build("p4a_db_source", "source");
    $query = "SELECT address, name, lat, lng, 
     CONCAT_WS('
', name, address) AS info,
     (6371 * acos(cos(radians('$coordArray[2]')) * cos(radians(lat))
     * cos( radians(lng) - radians('$coordArray[3]')) 
     + sin( radians('$coordArray[2]'))
     * sin( radians(lat)))) AS distance
     FROM markers
     HAVING distance &lt; '$radius' ORDER BY distance"; $this-&gt;source-&gt;setQuery($query);
    $this-&gt;source-&gt;setPk("id");
    $this-&gt;source-&gt;load();
    // map
    $map =&amp; $this-&gt;build("p4a_map","mappa");
    $map-&gt;setCenter("Mountain View, CA");
    $map-&gt;setZoom(10);
    $map-&gt;setWidth(750);
    $map-&gt;setHeight(500);
    // markers from datasource
    $map-&gt;setSourceAddressField("address");
    //$map-&gt;setSourceDescriptionField("distance");
    $map-&gt;setSourceDescriptionField("info");
    $map-&gt;setSource($this-&gt;source);
    // Layout
    $this-&gt;frame-&gt;anchorCenter($map);
    // Display
    $this-&gt;display("main", $this-&gt;frame);
   }
 /*
  * Return the coordinates of the location.
  * @param $location		valid Google Maps address of a location.
  * @param $googleMapKey		key for accessing Google Maps.	 
  * @return array  element[0] = status, element[2] = lat, element[3] = long
  */
 function getLocationCoordinates($location, $googleMapKey)
 {
  $base_url = "http://maps.google.com/maps/geo?output=csv&amp;key=".$googleMapKey;
  $request_url = $base_url."&amp;q=".urlencode($location);
  $csv = file_get_contents($request_url);
  if ($csv) {
    $csvSplit = split(",", $csv);
  }
  else{
    $csvSplit = array();
  }
    return $csvSplit;
  }
}

Note:

La query crea la sorgente dati per i markers, che vengono evidenziati automaticamente dalla classe p4a_map.
Al posto delle info, potete visualizzare la distanza di ogni marker dal centro (nell’esempio: “Mountain View, CA”) utilizzando l’istruzione: $map->setSourceDescriptionField("distance");.
Il metodo getLocationCoordinates() recupera le coordinate del centro per poter poi comporre la query.
La chiave per l’accesso a Google Map è quella valida per un server tipo “localhost” e la trovate all’interno del file: p4a_map.php che si trova nella directory libraries. Per avere quella per un dominio pubblico è necessario farla generare a Google Maps qui.
Non ho fatto controlli di errore, ad esempio per quanto riguarda la risposta del metodo getLocationCoordinates() sarebbe opportuno controllare che l’array non sia vuoto e anche lo status della risposta di Google Maps, e si dovrebbe anche limitare il risultato della query al numero di markers massimo gestiti dal widget (17 se non ricordo male…).

Download

Il widget con la maschera sopra descritta è scaricabile qui

Conclusioni:

Il risultato è già soddisfacente, anche se un po’ grezzo, ma credo ci siano molti margini di miglioramento per questo tipo di utilizzo di Google Maps in P4A. Spero di avere tempo per affinare ulteriormente sia questo programma che il widget, per renderlo utilizzabile anche per idee diverse! Suggerimenti e consigli sono molto ben accetti!!

Riferimenti ed approfondimenti: