Verificare l’esistenza di una destinazione (javascript, ajax e scriptacolous)
Ci troviamo di fronte ad un problema comune in siti che trattano di vacanze o che hanno a che fare con posizioni geografiche (geoname) per alcune destinazioni. I requisiti ci chiedono di fornire un servizio di ricerca di una posizione geografica, come fare? Si potrebbe tentare di definire una ricerca guidata, fornendo all’inizio una form per selezionare il continente e lo stato, per poi raffinarla. E’ un classico dilemma di information retrival quando si cerca di rispondere alla domanda: “E’ meglio usare un motore di ricerca o una web directory per ottenere le informazioni che sto cercando?”. La faccenda è abbastanza controversa, perchè noi potremmo cercare una destinazione di cui non conosciamo il continente o lo stato.In questo articolo fornirò un modo per interrogare un webservice esterno tramite javascript per farci restituire una o più destinazioni geografiche che corrisponderanno meglio alla nostra keyword inserita. Partiamo subito con il codice che ci servirà per fare la chiamata asincrona lato client.
/** * funzione javascript per una richiesta asincrona al webservice geoname * (gestita da uno script php) * * @param destinazione * string prende in input la distanizione di un utente * @param IdDivElement * string prende l'id dell'elemento div dove visualizzare il * contenuto * @param cntx * string specifica il contesto della ricerca del geoname * ossia per la locazione corrente o per le destinazione preferite etc.. * * * */ function verifyDestination(destinazione, IdDivElement,cnxt) { // Elemento dove visualizzare i risultati IdContent = IdDivElement; context = cnxt; if(destinazione == "" || destinazione == undefined){ Ext.MessageBox.alert('Error', 'Please, insert a location'); return; } //alert(destinazione); // e' possibile crittografare l'url per renderlo // illegibile, oltre ad utilizzare l'offuscamento del codice var geonameWS = "verify_destination.php?q="; geonameWS = geonameWS + destinazione; //alert(geonameWS); if (window.XMLHttpRequest) { // Mozilla based request = new XMLHttpRequest(); } else if (window.ActiveXObject) { request = new ActiveXObject("Msxml2.XMLHTTP"); if (!request) { request = new ActiveXObject("Microsoft.XMLHTTP"); } } if (typeof request == 'undefined') { alert("Ajax not supported!"); return; } // TODO: Visualizzare un messaggio di attesa dei risultati var indicator = document.getElementById('indicator'+context); indicator.style.display = ""; request.onreadystatechange = callback; request.open("GET", geonameWS, true); request.send(null); }
Fino a questo punto le operazioni sono ovvie e classiche per ciascuna chiamata asincrona con javascript. E’ da notare come il nostro file php che inoltra la chiamata al webservice esterno si chiama verify_destination.php, al qual passiamo un parametro che rappresenterà la nostra destinazione ricercata. Infatti, come da titolo dell’articolo noi siamo interessati a validare una località, ossia verificare che l’utente inserisce effettivamente il nome di una località esistente, non ad esempio Topolinia. Vi sono altri aspetti sul codice, come il contesto (cntx) e l’id dell’elemento div per la visualizzazione che saranno trattati in seguito. Una volta inoltrata la richiesta il nostro script php ci restituirà i risultati in formato xml che noi andremo a parsare. Lo script verify_destination è molto semplice, poichè il webservice accetta chiamate http.
define ( "MAXROWS", 5 ); define ( "FEATURECLASS", "P" ); if(!isset($_SESSION['user']['username'])){ echo 'You are trying a DoS or DDoS attack, your IP address is tracked'; exit(); } $destination = $_GET ['q']; //$destination = addcslashes($destination,"'"); //se uso il web service $destination = stripcslashes($destination); include_once ("include/ws_geoname.inc.php");
Il file incluso nello script fa la semplice chiamata http e restituisce il document xml.
header ( 'Content-Type: text/xml' ); $url = 'http://ws.geonames.org/search?q='; $destination = urlencode($destination); $param = $destination.'&maxRows=5'.'&featureClass='.FEATURECLASS.'&style=FULL'; $url.=$param; $req = file_get_contents ( $url ); echo $req;
Come si può vedere con la funzione file_get_contents($url) viene fatta una richiesta http per scaricare il documento xml dal webservice e con una semplice echo ridirezioniamo il contenuto al client. Come da codice, nel sorgente javascript mostrato abbiamo dichiarato una funziona callback che verrà richiamata quando il client riceverà la richiesta. La funziona javascript si chiama semplicemente callback.
function callback() { if (request.readyState == 4) { if (request.status == 200) { // XML xmlDoc = request.responseXML; // document.write(xmlDoc); //TODO: disabilitare il messaggio di attesa dei risultati var indicator = document.getElementById('indicator'+context); indicator.style.display = "none"; display(); } else { alert("Service no working.."); } } }
Il codice precedente è quasi standard in una chiamata asincrona con ajax, quando la richiesta è stata ricevuta il browser prende il contenuto e viene dichiarato che è in formato xml con il metodo request.responseXML. Ora che abbiamo il contenuto salvato in una variabile con scope globale all’interno dello script è possibile parsarlo è recuperare il codice che ci serve.
/** * Visualizza i risultati nella div predefinita */ function display() { var results; var name; var countryName; var adminName; var numRes; var lat; var lng; var geonameId; var continentCode; document.getElementById(IdContent).innerHTML=""; try { var totalRes = xmlDoc.getElementsByTagName("totalResultsCount"); numRes = totalRes[0].firstChild.nodeValue; results = "Number of results " + numRes + " "; results += " <ul>"; var geonames = xmlDoc.getElementsByTagName("geoname"); // alert(geonames[0].childNodes[1].nodeName); for ( var i = 0; i < geonames.length; i++) { var childs = geonames[i].childNodes; // Scorro tutti nodi figli dell'elemento geoname for ( var j = 0; j < childs.length; j++) { if (childs[j].nodeName == "name") { name = childs[j].firstChild.nodeValue; } else if (childs[j].nodeName == "countryName") { countryName = childs[j].firstChild.nodeValue; } else if (childs[j].nodeName == "adminName1") { adminName = childs[j].firstChild.nodeValue; break; // Se sono arrivato qui stop } else if (childs[j].nodeName == "lat") { lat = childs[j].firstChild.nodeValue; } else if (childs[j].nodeName == "lng") { lng = childs[j].firstChild.nodeValue; } else if (childs[j].nodeName == "geonameId") { geonameId = childs[j].firstChild.nodeValue; } else if (childs[j].nodeName == "continentCode") { continentCode = childs[j].firstChild.nodeValue; } } // end for name = addslashes(name); adminName = addslashes(adminName); countryName = addslashes(countryName); results += "<li><a href=\"javascript:selectDestination("; results += "'" + name + "'," + "'" + adminName + "','" + countryName + "','" + lat + "','" + lng + "','" + geonameId + "','" + continentCode +"')\">"; results += name + "," + adminName + "," + countryName + "</a> </li>"; }// end for results += "</ul>"; //alert(results); } catch (e) { alert("An exception occurred at:" + e.name + " " + " Error msg:" + e.message); } // creo il div con le informazioni var div = document.getElementById(IdContent); subdiv = document.createElement("div"); if(numRes == '0'){ results+="please insert a different location"; } subdiv.innerHTML = results; div.appendChild(subdiv); /*************************************************************************** * Chiamata al framework scriptaculous **************************************************************************/ Effect.SlideDown(IdContent); return; }
Ho cercato di formattare il codice il meglio possibile, ma i limiti del plugin di wordpress non permettono di fare di più (almeno per ora). Per sopperire a tale inconveniente allegherò il codice a fondo artcolo. Lo script precendente visualizza i risultati tramite scriptacolous dando un effetto a tendina scorrevole che appare una volta ricevuti i risultati. Nel caso di poter usare questo script in più elementi di una form, come ad esempio due input form. In ultimo abbiamo bisogno di un modo per permettere all’utente di selezionare una destinazione e modificare gli elementi html predefiniti.
/** * @author Gianfranco Murador * @param name * string nome del geoname * @param adminName * string regione del geoname * @param countryName * string stato del geoname * @param lat * string latitudine * @param lng * string longitudine * @param geonameId * string identificatore * * * Selezione il geoname e setta alcuni campi di input nascosti per la form * **/ function selectDestination(name, adminName, countryName, lat, lng, geonameId, continentCode) { var inputGeoname = document.getElementById('inputGeoname'+context); inputGeoname.value = name + "," + adminName + "," + countryName; alert("lat"+context); var latitudine = document.getElementById('lat'+context); latitudine.value = lat; var longitudine = document.getElementById('lng'+context); longitudine.value = lng; var geoId = document.getElementById('geonameId'+context); geoId.value = geonameId; var n = document.getElementById('name'+context); n.value = name; var cName = document.getElementById('countryName'+context); cName.value = countryName; var cCode = document.getElementById('continentCode'+context); cCode.value = continentCode; var aName = document.getElementById('adminName'+context); aName.value = adminName; /*************************************************************************** * Chiamata al framework scriptaculous **************************************************************************/ $(IdContent).hide(); // Pulisco il contenuto dell'elemento div var div = document.getElementById(IdContent); inputGeoname.setAttribute('readonly','readonly'); div.innerHTML = ""; var buttonChange = document.getElementById('buttonChange'+context); buttonChange.style.display = ""; var button = document.getElementById('button'+context); button.style.display ="none"; inputGeoname.style.borderColor="red"; }
Con questo script bloccheremo l’input text per la validazione e useremo un’altro button per sbloccarlo se l’utente ha sbagliato a selezionare la destinazione. Da notare come il codice è quasi del tutto indipendente, nel funzionamento, da scriptacolous, infatti potremmo usare un diverso controllo per la visualizzazione delle risposte. Per cambiare la destinazione uso la seguente function:
function changeDestination(cntxt){ var inputGeoname = document.getElementById('inputGeoname'+cntxt); inputGeoname.removeAttribute('readonly'); inputGeoname.style.borderColor = ""; var buttonChange = document.getElementById('buttonChange'+cntxt); buttonChange.style.display = "none"; var button = document.getElementById('button'+cntxt); button.style.display =""; } /**** * Questa funzione aggiunge un carattere * di escape per permettere l'inserimento * di carattere contente un apice. Lato server * php deve ripristinare gli apici * * ****/ function addslashes(str) { return (str+'').replace(/([\\"'])/g, "\\$1").replace(/\0/g, "\\0"); }
Il nostro file html per funzionare deve contenere dei tag del genere per far funzionare gli script definiti precedentemente:
<!-- GESTIONE LOCAZIONI FUTURE --> <input id="inputGeonameDestinazioniFuture" name="inputGeonameDestinazioniFuture" type="text" />; <div class="buttonLink"> <input id="buttonChangeDestinazioniFuture" class="formButton" style="display: none" onclick="javascript:changeDestination('DestinazioniFuture')" type="button" value="Change" /> <input id="buttonDestinazioniFuture" class="formButton" onclick="javascript:verifyDestination(document.getElementById('inputGeonameDestinazioniFuture').value,'geonameDestinazioniFuture','DestinazioniFuture')" type="button" value="Validate Location" /></div> <div id="uploadProgressFuture" style="display: none;"> <img style="vertical-align: middle;" src="profile_edit_locations.php_files/loading.gif" alt="" /> <strong>Validating, please wait...</strong></div>
Come ultimo accorgimento è bene ricordarsi di includere lo script nell’header o in qualche altra parte, come includere anche gli script di scriptacolous necessari per l’effetto desiderato. Allegherò il codice non appena possibile.