L'idea di partenza
L'XMap è un'applicazione che usa AJAX per visualizzare un'immagine grande dividendola in frammenti e mostrando all'utente solo alcuni di questi, permettendogli di muoversi "sull'immagine" tramite un semplice pannello di controllo.
Per sviluppare questa applicazione ci rifacciamo alle tecniche ed alle librerie già usate nell'articolo Xchat. Le librerie usate sono comunque incluse nel file dell'esempio scaricabile e sono:
- net.ContentLoader libreria JavaScript che permette di gestire in maniera totalmente trasparente la creazione dell'oggetto XMLHttpRequest e il suo utilizzo.
- Json.class.php Libreria che permette di codificare in modo rapido qualsiasi dato PHP nella relativa entità JSON.
I punti chiave di questa applicazione, soprattutto per quanto riguarda il funzionamento logico, sono tre. Una volta compresi questi aspetti la scrittura del codice non è altro che un esercizio.
- Il primo punto è relativo alla gestione dei frammenti. Ho scelto di frammentare un immagine relativamente grossa, in quadrati di 100px per lato e di mostrarne solo 9 insieme (in modo da avere un quadrato di 300px a lato).
- Il secondo punto riguarda l'interfaccia utente, ovvero come permettere all'utente di muoversi. Ho deciso di creare una sorta di joypad testuale tramite una tabella contenete dei link, ai quali è collegata una delle 8 direzioni possibili.
- Il terzo punto è quello più difficile concettualmente. Infatti a questo punto manca che permetta di sapere lo stato corrente dell'immagine visualizzata, ovvero "quale parte dell'immagine grande l'utente sta visualizzando". Questa è un informazione necessaria. Infatti non basta sapere che l'utente vuole scrollare l'immagine a destra; a destra rispetto a cosa? Questo problema è stato risolto utilizzando una variabile che tiene traccia del frammento centrale (d'ora in poi "frammento master") nel quadrato 3x3. Questo concetto viene ripreso successivamente.
Queste tre soluzioni sono semplicemente una mia scelta, sicuramente non le migliori in assoluto (basti pensare allo scrolling fatto con il drag&drop per rendersi conto di qualche altra possibilità), ma credo permettano di realizzare un'applicazione funzionale ma anche di facile comprensione.
Il funzionamento teorico
Innanzitutto è necessario chiamare ciascun frammento dell'immagine grande con un nome relativo alla sua posizione nell'immagine di partenza. Per comodità le immagini sono chiamate X_Y.jpg dove X corrisponde al posizionamento orizzontale e Y al posizionamento verticale.
L'immagine è stata divisa in 64 frammenti (quindi le immagini andranno da 1_1.jpg a 8_8.jpg) e la parte che l'utente sta visualizzando in questo momento è segnata da un quadrato più scuro. Il frammento azzurro è il frammento master, ovvero quello centrale, quello che permette di gestire lo stato dell'applicazione.
Ipotizziamo la situazione in figura a sinistra come quella di partenza.
Quando l'utente preme sul link per spostare l'immagine visualizzata verso destra, il motore dell'applicazione, in base al frammento master, capisce quali immagini visualizzare (in caso dello spostamento a destra, basterà aumentare di uno l'indice X). Per questo è utile tenere le informazioni sul frammento master.
Una volta compresi i meccanismi, deleghiamo ciascuno compito alla parte che si occuperà di eseguirlo.
- La parte client (XHTML, DOM e JavaScript) si occupererà di visualizzare le immagini, gestire il frammento master, gestire l'interazione con l'utente e inviare richieste al server (due informazioni fondamentali: posizione del frammento master e direzione dello spostamento).
- La parte server invece si occuperà di elaborare la richiesta, analizzando i due parametri ottenuti dal client ed inviando i "nuovi" 9 frammenti da visualizzare.
Questa breve introduzione era necessaria a comprendere bene cosa stiamo per sviluppare. Ora possiamo passare a "smanettare"!
La parte client
La parte grafica: XHTML
La parte XHTML è relativamente banale: l'intera applicazione è composta da un elenco puntato (che corrisponde alla mappa) e da una tabella (che corrisponde al joypad direzionale).
L'elenco puntato nel momento del caricamento della pagina non contiene nessun elemento, in quanto sarà l'evento onload dell'oggetto window a riempire la mappa con i frammenti di partenza. L'elenco puntato verrà riempito con 9 elementi, ognuno dei quali composto dal frammento di immagine appropriato:
Listato 1. La mappa
<div id="map">
<ul id="mapUl">
</ul>
</div>
Listato 2. Il markup del joypad
<table>
<tr>
<td><a href="#" onclick="getImages('nw')">NW</a></td>
<td><a href="#" onclick="getImages('n')">N</a></td>
<td><a href="#" onclick="getImages('ne')">NE</a></td>
</tr>
<tr>
<td><a href="#" onclick="getImages('w')">W</a></td>
<td> </td>
<td><a href="#" onclick="getImages('e')">E</a></td>
</tr>
<tr>
<td><a href="#" onclick="getImages('sw')">SW</a></td>
<td><a href="#" onclick="getImages('s')">S</a></td>
<td><a href="#" onclick="getImages('se')">SE</a></td>
</tr>
</table>
All'interno delle celle vengono inseriti dei link posizionati secondo i punti cardinali (N, S, E, W e valori intermedi) associati alla funzione getImages
, che esamineremo in dettaglio nella parte dedicata al motore dell'applicazione client-side, che viene invocata passando un parametro con la direzione.
Può essere utile esaminare il CSS che permette di impostare un elenco puntato facendolo sembrare una tabella:
Listato 3. Foglio di stile della mappa
#map { width:300px; height:300px; padding:0px; }
#map ul { margin:0px; padding: 0px; }
#map ul li { display:block; float:left; margin:0px; list-style-type:none; }
#map ul li img { border:0px; padding:0px; margin:0px; }
I primi due stili servono per impostare correttamente il div e l'elemento ul presenti nella pagina, mentre i successivi verranno "utilizzati" nel momento in cui verranno caricate le immagini. È quindi utile immaginare la pagina completa e non solo basandoci sull'HTML riportato.
Nella dimostrazione online è possibile anche notare un'anteprima dell'immagine rimpicciolita. Non essendo questo elemento parte del tutorial ma solo una semplice immagine posizionata su una pagina html, ho preferito tralasciarlo e puntare solo sugli aspetti essenziali.
Il motore del client: JavaScript
La parte JavaScript è composta essenzialmente due variabili globali e da due funzioni.
Listato 4. Variabili globali
var images = new Array();
var mapUl = document.getElementById("mapUl");
Images
è semplicemente un array vuoto che riempiremo successivamente con i riferimenti alle 9 immagini che dei frammenti, mentre mapUl
contiene un riferimento all'elemento <ul>
. Entrambe le variabili globali sono state istanziate per comodità e non per motivi funzionali.
Listato 5. Le funzioni lato client
function getImages(direction) {
if(images[4])
{
param = "current="+images[4].src+"&direction="+direction;
new net.ContentLoader("server.php", getImagesCallback, param);
}
else
{
for(i=0;i<9;i++) {
li = document.createElement("LI");
images[i] = document.createElement("IMG");
li.appendChild(images[i])
mapUl.appendChild(images[i]);
}
new net.ContentLoader("server.php", getImagesCallback);
}
}
function getImagesCallback(txt) {
new_images = eval('(' + txt + ')');
if(images.length != new_images.length)
{
alert("ERRORE!!!");
return;
}
for(i=0;i<new_images.length;i++) {
images[i].src = new_images[i];
}
}
La funzione getImages
si occupa di creare la connessione con il server, mentre la funzione getImagesCallback
si occupa di gestire la response.
La prima funzione controlla che l'array images
sia già inizializzato e in caso positivo effettua una richiesta inviando come parametro il quinto elemento (ovvero il frammento centrale: il frammento master) dell'array images
e la direzione scelta dall'utente.
Nel caso in cui images
non sia inizializzato, la stessa funzione riempie l'elemento <ul>
della pagina con 9 elementi <li>
ciascuno con una immagine, infine associa il riferimento dell'oggetto immagine all'array images
. Successivamente effettua una richiesta al server senza inviare alcun parametro.
Questo secondo caso sussiste solamente nel momento della prima richiesta, la quale non necessita né di una direzione né di un frammento master, essendo l'immagine non ancora "montata". Questa funzione verrà successivamente associata all'evento onload dell'oggetto intrinseco window.
La seconda funzione si occupa invece di gestire la risposta del server. Questo invierà un array di 9 elementi: i 9 percorsi dei frammenti da visualizzare.
La funzione costruisce, tramite la funzione eval
, l'array ricevuto dal server in JSON, controlla che gli elementi siano davvero 9 e in caso positivo cambia l'attributo src
ai frammenti contenuti nell'array images
aggiornando quindi l'immagine visualizzata.
La parte server
I principali compiti del server sono due:
- controllare che la direzione scelta dall'utente sia valida (controllando che non si superino i limiti dell'immagine);
- determinare le "nuove" immagini da visualizzare a seconda sia del frammento master corrente, sia della direzione scelta dall'utente.
Riguardo il primo punto ho scelto di permettere uno spostamento di un unità oltre al limite imposto dall'immagine. Infatti se l'utente vorrà spostarsi verso nord in un immagine che abbia raggiunto il limite visualizzerà un frammento "non disponibile" per i 3 frammenti che rimangono fuori dall'immagine.
Questo frammento può essere una qualsiasi immagine di 100x100 pixel da visulizzare in caso di errore (nell'esempio è semplicemente un immagine bianca con la scritta apposita). Questo spostamento "off-limit" sarà possibile solo una volta.
Dalla posizione evidenziata sarà possibile spostarsi verso nord o verso ovest solo una volta. Supponiamo uno spostamento verso nord. L'utente visualizzerà 3 frammenti "non-disponibile", i primi 3 frammenti della prima riga e i primi 3 frammenti della seconda riga. Se nel caso volesse insistere verso nord, l'applicazione genererà un errore.
Questo comportamento deriva da fatto che spostandosi per 2 volte consecutive verso una direzione non permessa, si genererebbe una circostanza inaspettata: il frammento master (che ricordo è il frammento centrale dei 9) diventerebbe un frammento "non disponibile" e questo comporterebbe una imperfezione nella gestione dello stato corrente dell'applicazione rendendone impossibile il normale ciclo di esistenza.
Tornando alla parte più tecnica, questa controllo viene effettuato tramite due funzioni:
Listato 6. Funzioni lato server
function getName($w,$h)
{
global $filename;
if(is_file(sprintf($filename,$w,$h)))
return sprintf($filename,$w,$h);
else return "no.jpg";
}
function checkCentral($w,$h)
{
global $filename;
return is_file(sprintf($filename,$w,$h));
}
$filename = "map_images/%d_%d.jpg";
Entrambe queste funzioni utilizzano la variabile globale $filename
che contiene il percorso per costruire il nome dei frammenti (sfruttando sprinf
che sostituisce le sequenze %d
con le coordinate ricevute).
La funzione getName
si occupa di generare il nome del file a partire dagli indici di posizione ($w
e $h
). Questa funzione controlla l'esistenza del file: se non lo trova ritorna il percorso dell'immagine "non disponibile" che nell'esempio si chiama no.jpg.
La seconda funzione si occupa di controllare che un frammento possa essere utilizzato come frammento master. Se questa funzione dovesse ritornare un valore negativo, lo spostamento non verrebbe per evitare di avere come frammento master un "non disponibile".
La seconda macro-funzione della parte server, come detto in precendenza, è quella di determinare i frammenti "nuovi" derivanti dallo spostamento. Questo aspetto è gestito da uno switch, che in base alla direzione scelta dall'utente e dal frammento master, costruisce un array contenente i 9 frammenti da comunicare al client.
Innanzi tutto è necessario determinare il posizionamento del frammento master all'interno dell'immagine completa:
Listato 7. Posizione del frammento master
if(isset($_POST['current'])
{
$current = $_POST['current'];
$chr = strrchr($current,"/");
$current = substr(,1,strpos($chr,".")-1);
list($w,$h) = explode("_",$current);
}
Con 4 righe abbiamo estrapolato dal nome del frammento master il suo posizionamento orizzontale ($w
) e verticale ($h
) all'interno dell'immagine. Ora all'interno dello switch andremo a riempire l'array:
Listato 8. Impostare i nomi delle immagini
$direction = isset($_POST['direction']) ? $_POST['direction'] : "default";
$images = array();
switch($direction) {
case "n":
if(checkCentral($w,$h-1)) {
$images[] = getName($w-1,$h-2);
$images[] = getName($w,$h-2);
$images[] = getName($w+1,$h-2);
$images[] = getName($w-1,$h-1);
$images[] = getName($w,$h-1);
$images[] = getName($w+1,$h-1);
$images[] = getName($w-1,$h);
$images[] = getName($w,$h);
$images[] = getName($w+1,$h);
}
break;
case "e":
...
case "w":
...
case "s":
...
case "nw":
...
case "ne":
...
case "se":
...
case "sw":
...
case "default":
for($x=0;$x<3;$x++)
for($y=0;$y<3;$y++)
$images[] = sprintf($filename,$x,$y);;
break;
}
Ho ridotto la parte di codice in per mostrare solo le parti essenziali e importanti. Una volta ottenuta la direzione (dal parametro in GET o eventualmente impostata a 'default') e creato l'array vuoto, in base alla direzione possiamo riempire l'array.
Per prima cosa viene invocata la funzione checkCentral
per controllare se il "futuro" frammento master è valido e in caso di risposta positiva si assegnano i vari URL delle immagini, appositamente generati dalla funzione getName
. La logica è simile per ciascuna direzione, una volta compreso il meccanismo si tratta solamente di cambiare pochi caratteri.
Nel caso invece la direzione non venga inviata dal client (comportamento iniziale dell'applicazione) vengono prese in considerazione i primi 9 frammenti a partire dall'angolo superiore sinistro.
Una volta costruito l'array $images
, che può essere vuoto in caso di spostamento impossibile, può contenere 9 stringhe identificative dei 9 frammenti. Grazie alla libreria JSON, è possibile stampare il contenuto dell'array codificato secondo la notazione JavaScript.
Listato 9. Codifica JSON
$json = new json;
echo $json->encode($images);
Conclusioni
La gestione delle immagini di grandi dimensioni sul web può essere effettuata tramite tante soluzioni. Questo articolo ha un taglio più didattico che funzionale, tralasciando aspetti che avrebbero migliorato notevolmente l'usabilità dell'applicazione (drag & drop per esempio). Abbiamo concentrato l'attenzione soprattutto sulla logica, talvolta anche a scapito dell'eleganza nel codice, che può risultare un po' troppo macchinoso una volta comprese le logiche di funzionamento, ma che svolge il suo compito.
Alla prossima!