NASA World Wind for Java

Integrare un "virtual globe" nelle applicazioni desktop Javadi

Quanti di noi da bambini giocavano con il mappamondo e l‘atlante immaginando viaggi in paesi lontani, in mezzo a deserti o isole sperdute nel Pacifico? Oggi con NASA World Wind for Java possiamo avere un mappamondo (e che mappamondo!) integrato nelle nostre applicazioni Java desktop.

Introduzione

Se parliamo di applicazioni desktop tradizionali, Google Maps non è un‘opzione dal momento che il suo contesto è quello della pagine web dinamiche. Google Earth, invece, non è open source e non possiamo metterci le mani dentro. NASA World Wind for Java, o WWJ in breve, è un prodotto alternativo (in .NET esiste un reale competiore di Google Earth, mentre in Java per ora c‘è solo un SDK corredato da alcuni esempi) e open source, che possiamo usare liberamente. In questo articolo ne introduciamo gli aspetti principali, effettuando una carrellata su alcune demo distribuite con i sorgenti.

Figura 1 - World Wind for Java come ci appare appena lanciato da Java Web Start

Alcune note preliminari sul deployment

Per prendere velocemente confidenza con WWJ è opportuno dare un‘occhiata alla decina di piccole applicazioni d‘esempio contenute nella distribuzione. Partiamo quindi con lo scaricare lo .zip con i sorgenti da http://worldwind.arc.nasa.gov/java (nel momento in cui scrivo è disponibile la versione 0.3.0). Chi ha particolarmente fretta può anche lanciare la versione in Java WebStart, ma in questo caso vedrà  solo una demo generica e non quelle che sto per descrivere.

Mentre scaricate, ne approfitto per ricordare che WWJ è attualmente in versione preliminare (come suggerisce quello "zero" nel numero di versione principale). Da un punto di vista qualitativo è robusto, ma l‘ingegnerizzazione non è completata e può capitare che, da una versione all‘altra, ci siano un po‘ di cambiamenti di nomi di classi e di package; pertanto, ogni tanto ci toccherà  eseguire un refactoring sui sorgenti. Ma il fatto che sia l‘US Navy che la Marina Italiana stiano usando WWJ per alcune applicazioni di navigazione (come pure alcune compagnie private che distribuiscono prodotti per la navigazione civile) è comunque una buona testimonianza della qualità  del prodotto, del commitment di NASA nel portarlo avanti e della buona reputazione che si è già  creato.

L‘unica precauzione da valutare, prima di utilizzarlo seriamente in un progetto reale, è la compatibilità  con l‘hardware. WWJ usa JOGL, le API di Java per usare OpenGL, che sono portabili su diverse piattaforme (io non ho avuto grossi problemi su tutti i Mac OS X e Windows che girano su macchine non troppo vecchie e gran parte dei Linux), ma richiedono una scheda grafica con accelerazione 3D e dotata del driver opportuno (nel qual caso, le prestazioni sono eccellenti). Questo è un problema su certe installazioni di Linux anche su macchine moderne, anche se generalmente è risolvibile scaricando un driver alternativo. Su macchine vecchie non dotate di schede con accelerazione 3D, o se non avete un driver aggiornato, le performance sono scadenti (si riescono a visualizzare pochi fotogrammi al secondo).

Le classi principali

Una volta scompattato lo .zip dei sorgenti, è già  possibile eseguire le applicazioni di esempio usando lo script run-demo.bash e passando come argomento gov.nasa.worldwind.examples.NOME_ESEMPIO. Gli esempi che stiamo per prendere in considerazione sono i seguenti:

  • ViewIteration
  • GlobalGridAboveSurface
  • Shapes
  • DraggingShapes
  • SurfaceImages
  • TexturedSurfaceShape
  • Tracks
  • AnaglyphStereo

Gli utenti Windows non troveranno un .bat pronto per l‘uso, ma potranno costruirlo a partire dalle informazioni in run-demo.bash.

Prima di partire con il primo esempio conviene dare un‘occhiata alla classe

 gov.nava.worldwind.examples.ApplicationTemplate

che è usata come contenitore da tutte le demo ed effettua il setup di base. Questo ci permette di prendere conoscenza delle poche classi fondamentali necessarie per mettere in piedi WWJ

Configuration

Ã? un contenitore di proprietà  che permette di prendere il controllo di alcune specifiche modalità  di esecuzione, come il settaggio delle cache su disco, il threading, il controllore di rendering, eccetera. In generale, questa classe si usa settando solo le proprietà  che intendiamo modificare rispetto al default:

Configuration.getInstance().setValue(key, value);

ovviamente come prima operazione prima di toccare altre componenti di WWJ. Nel 90% dei casi i settaggi di default vanno bene cosଠcome sono (vedremo un esempio di modifica nella demo AnaglyphStereo).

WorldWindowGLCanvas

Ã? il componente grafico dove avviene il rendering dello scenario tridimensionale. Si tratta di un componente heavyweight di AWT, che può essere integrato in un‘applicazione Swing con qualche precauzione (attualmente non è possibile usare una versione lightweight a causa di alcuni bug). Ã? sufficiente istanziarlo e inserirlo in un layout come si fa per un qualsiasi componente grafico di Java.

Model

Rappresenta il modello dell‘oggetto che vogliamo renderizzare, che è sempre una sfera (o meglio un‘ellissoide tenendo presente dello schiacciamento polare) modificata per tenere conto dell‘elevazione del terreno. In tutto il resto dell‘articolo si farà  riferimento a un modello della Terra, ma è possibile utilizzare al suo posto la Luna od alcuni satelliti di Giove e Saturno per i quali la NASA mette a disposizione i dati raccolti dalle sonde spaziali inviate negli ultimi trent‘anni.

Layers

I layer sono uno dei concetti più importanti di WWJ. In pratica si tratta di diversi strati che vengono "avvolti" intorno al Model (ad esempio la Terra) e permettono di disegnare varie informazioni. I layer principali sono quelli che disegnano mappe o fotorilevamenti satellitari, ma noi possiamo aggiungere i nostri layer customizzati. Più avanti nell‘articolo vengono discussi i tipi di layer principali.

View, OrbitView

Questa classe rappresenta la posizione corrente della "telecamera" e i suoi settaggi, come ad esempio l‘ampiezza dell‘angolo di vista. OrbitView è una particolare View che può essere controllata mediante latitudine, longitudine ed altezza dal suolo e, pertanto, si presta più facilmente ad essere usata per la visualizzazione di dati geospaziali alla Google Earth (alternativamente, View potrebbe essere invece usata per implementare un simulatore di volo dove la telecamera è solidale con un oggetto che si muove con leggi proprie, ad esempio un aereo).

SceneController

Ã? la parte "C" del pattern MVC usato da WWJ (ovviamente la "M" è il Model e la "V" la View appena descritti) e, come tale, si occupa di controllare vari aspetti del rendering e dei movimenti della View attraverso i comandi dell‘utente, per mezzo di mouse e tastiera.

Detto questo, l‘inizializzazione di WWJ è facilmente eseguibile con le seguenti righe di codice:

WorldWindowGLCanvas wwd = new WorldWindowGLCanvas();
// inserisci wwd in un Layout
Model m
= (Model)WorldWind.createConfigurationComponent(AVKey.MODEL_CLASS_NAME);
wwd.setModel(m);

I Layer predefiniti

L‘ApplicationTemplate istanzia e configura per default una collezione di layer per eseguire il "rendering standard" della Terra (noi ovviamente possiamo scegliere di usarli tutti od una parte, o aggiungere i nostri a piacimento). Questi layer standard sono:

Stars

Renderizza un po‘ di stelle visibili per mettere la Terra nel suo contesto naturale.

Fog

Implementa un effetto "foschia" sulle distanze per migliorare l‘effetto tridimensionale del rendering.

SkyGradient

Disegna la sottile fascia d‘atmosfera che si assottiglia verso lo spazio esterno.

BMNGSurface

Disegna il modello grafico noto come "BlueMarble", un insieme di foto che disegnano tutto il pianeta, armonizzate tra di loro (i mosaici di foto satellitari "grezzi" hanno dei salti di colore qua e là  dovuti al fatto che non sono stati ripresi tutti nello stesso momento), ma con risoluzione piuttosto bassa (quindi non sono adatti a zoom oltre ad un certo livello). Tuttavia, il loro ingombro ridotto fa sଠche le bitmap siano contenute dentro la stessa distribuzione di WWJ, quindi non è necessario essere collegati in rete per visualizzarli.

LandsatI3

Una serie di bitmap derivate dai dati Landsat (oggi chiamati iCube) a più alta risoluzione. Questo dato dipende grandemente dall‘area geografica; in generale, in Europa non si va sotto i 30m, quindi una qualità  decisamente più bassa di quanto non si ottenga da Google Earth. Il vantaggio, però, è che i dati di iCube possono essere utilizzati liberamente (a patto di citarne la fonte), mentre Google Earth è usabile solo per scopi personali (a meno che non si stipuli un contratto commerciale con Google). Ovviamente è necessario un collegamento in rete per visualizzare questi dati.

USGSDigitalOrtho e USGSUrbanAreaOrtho

Sono due livelli di dati ancora a maggior dettaglio che coprono l‘area degli USA e in particolare quella delle grandi città .

CountryBoundaries

Questo layer disegna i confini degli stati.

EarthNASAPlaceName

Questo layer disegna i nomi degli stati, dei mari e di altri oggetti geografici di rilievo.

ScaleBar

Questo layer disegna la barra di scala.

WorldMap

Un layer che disegna una piccola proiezione rettificata della terra con una crocetta che rappresenta la posizione corrente della telecamera, utile per capire dove siamo quando abbiamo zoomato notevolmente.

Compass

Una bussola che indica l‘orientamento attuale della zona renderizzata.

Per default, lo SceneController permette di zoomare e ruotare il modello attraverso il mouse, la rotellina del mouse, i tasti freccia e varie combinazione di alt, shift o ctrl. Per visualizzare un layer, bisogna aggiungerlo ad una LayerList presente nel Model, ad esempio:

model.getLayerList().add(prevLayer, new BMNGLayer());

specificando la posizione di inserimento (prevLayer). Infatti, la maggior parte dei layer non è trasparente e quindi l‘ordine con cui vengono disegnati è importante.

Siamo ora in grado di analizzare il codice significativo dei vari programmi di esempio

ViewIteration

Questa semplice demo ci mostra come muovere la View dalla posizione corrente ad una nuova con un‘animazione morbida. Ã? sufficiente estendere una AbstractAction standard (che quindi può essere collegata ad un menù o ad un bottone) ed usare uno ScheduledOrbitViewStateIterator specificando le nuove coordinate su cui posizionare la View.

class GoToLatLonFromCurrent extends AbstractAction {
private final LatLon latlon;

GoToLatLonFromCurrent(String name, LatLon latlon) {
super(name);
this.latlon = latlon;
}

public void actionPerformed(ActionEvent actionEvent) {
OrbitView view = (OrbitView) wwjPanel.getWwd().getView();
ScheduledOrbitViewStateIterator vsi
= ScheduledOrbitViewStateIterator.createLatLonIterator(view, this.latlon);
wwjPanel.getWwd().getView().applyStateIterator(vsi);
}
}

In generale, la famiglia degli ViewStateIterator contiene varie implementazioni che permettono di modificare in modo "morbido", cioè con una transizione, le proprietà  della View da uno stato iniziale ad uno finale. Ovviamente, anzichè usare le classi predefinite, possiamo specificare le nostre implementazioni qualora necessitassimo di comportamenti particolari. Degna di nota, nel codice illustrato, la classe LatLon, che rappresenta una coppia di coordinate latitudine/longitudine.

GlobalGridAboveSurface

Questa seconda demo, disegnando la griglia di paralleli e meridiani sulla superficie terrestre, ci spiega come possiamo disegnare i nostri contenuti specifici sopra i layer standard.

Figura 2 - Il reticolo di meridiani e paralleli su WWJ

A questo scopo esiste uno speciale layer, chiamato RenderableLayer, dentro il quale è possibile configurare una lista di oggetti georeferenziati. Come vedremo tra poco, questi oggetti possono essere varie forme geometriche ed anche icone, ma per ora ci focalizziamo sulla PolyLine, che - come dice il nome - è una semplice linea composta da vari segmenti. Il codice rilevante è il seguente:

RenderableLayer shapeLayer = new RenderableLayer();

// Generate meridians
ArrayList positions = new ArrayList(3);
for (double lon = -180; lon < 180; lon += 10)
{
Angle longitude = Angle.fromDegrees(lon);
positions.clear();
positions.add(new Position(Angle.NEG90, longitude, 10e3));
positions.add(new Position(Angle.ZERO, longitude, 10e3));
positions.add(new Position(Angle.POS90, longitude, 10e3));
Polyline polyline = new Polyline(positions);
polyline.setFollowTerrain(false);
polyline.setNumSubsegments(30);
polyline.setColor(new Color(1f, 1f, 1f, 0.5f));
shapeLayer.addRenderable(polyline);
}

// Generate parallels
...

Come si vede, una PolyLine si costruisce a partire da una lista di Positions (una classe che contiene non solo latitudine e longitudine, ma anche l‘altezza sul livello del mare) attraverso le quali verrà  interpolata la linea da disegnare, scomposta in un numero di segmenti definito da setNumSubSegments(). Per disegnare un meridiano è sufficiente specificare i due poli ed il punto di passaggio all‘incrocio con l‘equatore. Ã? possibile definire un colore (compresa la trasparenza); degna di nota è l‘opzione setFollowTerrain() che specifica se la PolyLine deve usare dati espliciti per l‘altitudine oppure se deve essere "adagiata" sul terreno. In quest‘ultimo caso le prestazioni sono inferiori, perchà© è necessario scaricare i relativi dati di altitudine del terreno (e questo accade anche quando fate uno zoom, ovviamente). Nella demo si è scelto un approccio più semplice, che consiste nel piazzare la griglia ad un‘altitudine fissa di 10.000 metri (per evitare che "sparisca" sotto le montagne). Questo va bene finchà© non zoomate troppo; ad una certa altitudine si inizia a notare l‘errore di parallasse, e ovviamente se andate sotto i 10.000 metri la griglia sparisce (o la vedete "sopra" di voi se orientate la telecamera verso l‘alto).
Le PolyLine hanno anche altri parametri, non dimostrati nella demo corrente, come la possibilità  di definire un tratteggio e varie modalità  di interpolazione.
Infine, degna di nota nel codice illustrato è la classe Angle, un contenitore opaco che rappresenta un angolo generico e si assicura che stiamo esplicitamente scegliendo se il valore è in gradi o in radianti.

Shapes

La demo successiva completa la visuale relativa a cosa è possibile disegnare sul modello.

Figura 3 - Alcuni oggetti tridimensionali custom su NASA World Wind

Infatti, oltre alla PolyLine, il codice ci mostra tutta una serie di nuove forme geometriche che possono essere collocate in un RenderableLayer:

  • SurfaceSector: un settore racchiuso tra una coppia di latitudini e longitudini minime e massime (si faccia attenzione al fatto che la figura geometrica risultante NON è un rettangolo, visto che i meridiani si avvicinano tra loro ai poli).
  • SurfaceEllipse: una ellisse
  • SurfaceSquare: un quadrato
  • SurfaceCircle: un cerchio
  • SurfaceQuadrilateral: un quadrilatero
  • SurfacePolygon: un poligono con un numero arbitrario di vertici

Non ci pare necessario scendere nel dettaglio del codice (che in questo caso è complicato esclusivamente perchà© la demo presenta all‘utente un paio di pannelli attraverso ii quali è possibile modificare le proprietà  delle forme visualizzate): in definitiva, ogni forma geometrica richiede nel costruttore i parametri necessari per descriverla; si tratta di ricordare la geometria imparata alle elementari.
La demo Shapes in realtà  fa una cosa più interessante, cioè dimostra come sia possibile implementare un meccanismo di selezione e di trascinamento delle forme con il mouse, ma questo è molto più chiaro da analizzare nella demo seguente.

DraggingShapes

DraggingShapes ci insegna a costruire della interattività  con WWJ. Il codice chiave è tutto contenuto in una sottoclasse di SelectListener, che, come dice il nome, ci permette di gestire la selezione e il trascinamento di un oggetto in un layer. Prima di tutto, però, introduciamo due nuovi oggetti che completano la famiglia di oggetti disegnabili in WWJ:

  • UserFacingIcon rappresenta un‘icona, che contiene una BufferedImage, posizionabile ad una precisa terna di coordinate. A differenza degli altri oggetti sinora descritti, che vengono sempre renderizzati in modo conforme alla prospettiva, una UserFacingIcon è sempre perpendicolare all‘asse della visuale, in modo che l‘utente la veda sempre interamente e senza distorsioni prospettiche.
  • IconLayer è un layer appositamente creato per le icone.

Ora, per meglio comprendere il codice di selezione e dragging, ricordiamo che getWwd() nelle demo illustrate restituisce un‘istanza di WorldWindowGLCanvas.

this.getWwd().addSelectListener(new SelectListener(){
private BasicDragger dragger = new BasicDragger(getWwd());

public void selected(SelectEvent event){
if (event.getEventAction().equals(SelectEvent.HOVER))
{
// manage tool tips
}
else if (event.getEventAction().equals(SelectEvent.ROLLOVER) && !this.dragger.isDragging())
{
AppFrame.this.highlight(event.getTopObject());
}
// Have drag events drag the selected object.
else if (event.getEventAction().equals(SelectEvent.DRAG_END)
|| event.getEventAction().equals(SelectEvent.DRAG))
{
// Delegate dragging computations to a dragger.
this.dragger.selected(event);

if (event.getEventAction().equals(SelectEvent.DRAG_END))
{
PickedObjectList pol = getWwd().getObjectsAtCurrentPosition();
if (pol != null)
{
AppFrame.this.highlight(pol.getTopObject());
AppFrame.this.getWwd().repaint();
}
}
}
}
});

Come si vede, il SelectListener definisce un singolo metodo di callback:

public void selected (SelectEvent event);

il cui evento descrive cosa sta succedendo con quattro valori specifici:

  • SelectEvent.HOVER: indica che il mouse si è fermato da qualche decimo di secondo sopra un certo oggetto (nell‘esempio, si usa questa informazione per mostrare un tooltip se l‘oggetto in questione è un‘icona).
  • SelectEvent.ROLLOVER: indica che il mouse è sopra un certo oggetto (e quindi, ad esempio, si può evidenziarlo cambiandone colore o forma).
  • SelectEvent.DRAG: indica che è iniziato un trascinamento.
  • SelectEvent.DRAG_END: indica che è terminato un trascinamento.

Dall‘oggetto SelectEvent si possono recuperare le reference relative agli oggetti coinvolti nell‘evento (che in generale possono essere più di uno, se sono sovrapposti):

  • event.getTopObject() restituisce l‘oggetto selezionato che sta sopra gli altri
  • event.getObjectsAtCurrentPosition() li restituisce tutti, incapsulandoli in una speciale PickedObjectList.

Tutto il "lavoro sporco" per il trascinamento viene delegato ad una classe chiamata BasicDragger:

  • private BasicDragger dragger = new BasicDragger(getWwd());
  • ...
  • this.dragger.selected(event);

Alla fine dell‘evento di trascinamento, gli oggetti coinvolti sono stati spostati in modo definitivo (cioà© le loro coordinate sono state aggiornate).

Tracks

Una delle cose più tipiche che si può visualizzare su una mappa è un percorso. La demo Tracks ci mostra come sia possibile, per esempio, leggere una traccia registrata da un navigatore GPS (nel caso, in formato .GPX) e visualizzarla su un layer specifico.

Nota: la versione 0.3.0 ha in questo caso un file mancante, tuolumne.gpx, che dovrebbe contenere i dati GPS da visualizzare. Per lanciare la demo, dovrete quindi modificare il nome del file da usare:

private static final String TRACK_FILE = "/tmp/PipeTracks2.gpx";

sostituendolo ad esempio con PipeTracks2.gpx (usato in un‘altra demo). Nonostante il percorso di un oleodotto sia meno poetico di un sentiero di trekking nella contea di Tuolumne (uno dei posti più pittoreschi nel parco di Yosemite), avrete comunque un‘idea della funzionalità .

Figura 4 - La visualizzazione di un percorso mediante marcatori

Le tracks si gestiscono con un layer apposito, chiamato TrackMarkerLayer:


GpxReader reader = new GpxReader();
reader.readFile(TRACK_FILE);
List tracks = reader.getTracks();
TrackMarkerLayer layer = new TrackMarkerLayer(tracks);
layer.setMaterial(Material.WHITE);
layer.setMarkerShape("Cylinder");

al quale va passata una lista di Tracks (che a sua volta, altro non è che un contenitore di una sequenza di coordinate organizzate in segmenti). La differenza fondamentale con una PolyLine è che il percorso viene visualizzato non mediante una linea continua, ma attraverso una serie di punti (disegnati con una forma ed un colore specificati da apposite proprietà ). Il numero di punti visualizzati viene calcolato automaticamente in funzione del livello di zoom, e questo è un punto piuttosto interessante per le performance (una traccia GPS può contenere parecchie migliaia di campioni e creare qualche problema alle PolyLine).

Personalmente questa modalità  di visualizzazione non mi piace un granchà© e preferisco la PolyLine. Tuttavia, al momento ho risolto solo parzialmente il problema di performance di una PolyLine con tantissimi campioni, e quindi per ora non approfondisco l‘argomento.

AnaglyphStereo

E per concludere, una demo avanzata. AnaglyphStereo rimpiazza il controllore standard con uno specializzato nella produzione di viste stereoscopiche, dove i canali del blu e del rosso vengono disassati per produrre i due punti di vista per l‘occhio destro e sinistro. Per apprezzare l‘effetto è necessario dotarsi di un paio di occhialini con lenti colorate come quelli che una volta usavano al cinema 3D. E, ovviamente, dovete muovere la View in modo che sia visualizzata una prospettiva con oggetti situati a diverse distanze: non va bene la visuale dall‘alto di default, provate invece a zoomare sulle Alpi e poi orientare la telecamera verso l‘orizzonte.

Figura 5 - Panoramica su Genova e gli Appennini in stereogramma

Il punto chiave della demo è la configurazione del controller specializzato che avviene, come descritto nell‘introduzione, manipolando l‘oggetto Configuration:

Configuration.setValue(AVKey.SCENE_CONTROLLER_CLASS_NAME,
AnaglyphSceneController.class.getName());

Una volta installato, è possibile attivare la modalità  stereoscopica con:

AnaglyphSceneController asc 
= (AnaglyphSceneController)this.getWwd().getSceneController();
asc.setDisplayMode(AnaglyphSceneController.DISPLAY_MODE_STEREO));
asc.setFocusAngle(this.focusAngle);

e controllare la profondità  dell‘effetto stereografico con il parametro focusAngle.

Conclusione

Con quest‘ultima demo abbiamo terminato la nostra carrellata introduttiva su NASA World Wind for Java. Si tratta sicuramente di un prodotto interessante e che consente di introdurre con relativa facilità  una funzionalità  estremamente sofisticata e "cool" nelle nostre applicazioni desktop (ma non solo: WWJ può girare anche in un Applet e essere deployato su web). Insomma, con uno slogan si potrebbe dire che fa uscire le tecnologie geospaziali dai laboratori dei professionisti GIS e ci permette di usarle come un qualsiasi componente grafico.

Il prodotto è facilmente estendibile (anche se per ora c‘è poca documentazione, compensata però da una buona comunità  che risponde a tutte le vostre domande sui forum). Non lo abbiamo menzionato, ma è ovviamente possibile provvedere dei layer che, invece di disegnare oggetti geometrici, forniscono immagini o mappe scaricate da sorgenti diverse da quelle predefinite (supponendo che ciò sia legale! Ad esempio, sarebbe tecnicamente possibile usare i dati di Google Earth e Google Maps, dal momento che vengono resi disponibili su Internet, ma questo andrebbe contro la licenza d‘uso che Google ci fornisce).

A conclusione dell‘articolo, includo uno screenshot di blueMarine, un‘applicazione che sto sviluppando e che usa WWJ per il geotagging delle fotografie.

Figura 6 - NASA World Wind integrato in blueMarine per il geotagging delle fotografie

Riferimenti

[1] La home page del prodotto
http://worldwind.arc.nasa.gov/java

[2] La home page dei forum dedicati alla versione Java
http://forum.worldwindcentral.com/forumdisplay.php?f=37

[3] L‘applicazione blueMarine
http://bluemarine.tidalwave.it

Condividi

Pubblicato nel numero
123 novembre 2007
Fabrizio Giudici ha iniziato a occuparsi di Java durante il suo dottorato di ricerca, concluso nel 1998 presso l‘Università di Genova e focalizzato sulle applicazioni industriali della tecnologia di Sun. In quegli anni ha iniziato la collaborazione con MokaByte, scrivendo articoli tecnici e partecipando al gruppo di consulenti che iniziavano…
Ti potrebbe interessare anche