L'interfaccia java.awt.Shape
Come accennato, nelle Java2D API è stata introdotta la nuova classe java.awt.Graphics2D che, estendendo la classe java.awt.Graphics (per questioni di compatibilità con le precedenti versioni), aggiunge nuove funzionalità ed un nuovo modo di visualizzare figure geometriche, testi ed immagini. Per visualizzare e rappresentare una figura geometrica si potevano seguire molte strade. Quella utilizzata fino al JDK 1.1 consiste nel dotare l'oggetto Graphics di un insieme di metodi per la visualizzazione di rettangoli, ellissi, linee e punti. Questo approccio non ha molti vantaggi in quanto è un po' limitativo in termini di figure geometriche rappresentabili. Nel caso in cui si volessero aggiungere nuove forme geometriche, come ad esempio particolari curve, bisognerebbe creare nuovi metodi per la loro visualizzazione.
Inoltre, figure geometriche e testo sono considerati oggetti diversi sebbene entrambi debbano essere visualizzati graficamente. Un approccio migliore è quello utilizzato nel JDK 1.2 con l'oggetto Graphics2D. Il compito principale di questo oggetto è quello di visualizzare figure geometriche, testo ed immagini. Sarebbe molto comodo se Graphics2D vedesse tutti gli elementi da rappresentare come oggetti dello stesso tipo. Se ci pensiamo bene, ogni oggetto da visualizzare è composto da un contorno e da una area. Un rettangolo vuoto (di quelli che si creano con drawRect()) è costituito da un contorno rettangolare e da un'area dello stesso colore dello sfondo. Lo stesso testo si può considerare composto da un contorno e da una regione "piena".
Se il modo con cui ciascun oggetto descrive il suo contorno e la sua area è lo stesso, è chiaro che l'oggetto responsabile delle effettiva visualizzazione vedrà ogni oggetto da visualizzare allo stesso modo. Ecco che nelle Java2D API è stata creata l'interfaccia java.awt.Shape che descrive i metodi che devono essere implementati da ciascun elemento da visualizzare e che descrivono l'elemento stesso in termini di contorno, area e non solo. I metodi dell'interfaccia si possono dividere in tre gruppi principali:
- Metodi per determinare l'area di una Shape
- Metodi per determinare il contorno della Shape
- Metodi per gestire l'intersezione di Shape diverse.
Vediamole una ad una.
Metodi per determinare l'area della Shape
| Metodo | Descrizione | | --- | --- | | public boolean contains(int x, int y); | Indica se il punto di coordinate (x,y) è interno alla Shape oppure no. | | public boolean contains(int x, int y, int w, int h); | Indica se il rettangolo di dimensioni (w,h) nella posizione (x,y) è completamente interno alla Shape oppure no. | | public void contains(Point2D p); | Indica se il punto rappresentato dall'oggetto java.awt.geom.Point2D è interno alla Shape oppure no. | | public void contains(Rectangle2D r); | Indica se il rettangolo rappresentato dall'oggetto java.awt.geom.Rectangle2D è contenuto nella Shape oppure no. | | public Rectangle getBounds(); | Indica la regione rettangolare che contiene completamente la Shape. Il valore di ritorno è un oggetto della classe java.awt.Rectangle. | | public Rectangle2D getBounds2D(); | Indica la regione rettangolare che contiene completamente la Shape. Il valore di ritorno è un oggetto della classe java.awt.geom.Rectangle2D. |
I metodi elencati permettono di determinare i punti interni alla Shape che l'oggetto Graphics2D deve poter visualizzare. Il procedimento da seguire sarà quello di ciclare tra tutti i punti del rettangolo ottenuto dai metodi getBounds() o getBounds2D() e verificare se esso è interno all'area oppure no con i metodi contains(). In caso affermativo, l'oggetto Graphics2D provvederà alla visualizzazione del punto.
Metodi per determinare il contorno della Shape: l'interfaccia java.awt.geom.PathIterator
| Metodo | Descrizione | | --- | --- | | public PathIterator getPathIterator(AffineTransform at); | Fornisce un oggetto che implementa l'interfaccia java.awt.geom.PathIterator e descrive il contorno della Shape. Al contorno restituito è stata applicata la trasformazione bilineare passata come parametro. | | public PathIterator getPathIterator(AffineTransform at, double flatness); | A differenza del precedente, permette di specificare anche la piattezza (flatness) della curva. |
I metodi qui elencati permettono di ottenere una descrizione del contorno della Shape attraverso un oggetto di tipo java.awt.geom.PathIterator, ovvero che implementa questa interfaccia. La classe AffineTransform sarà argomento di un prossimo capitolo; per ora consideriamola come rappresentativa di una trasformazione bilineare. L'interfaccia PathIterator raccoglie i metodi che permettono di descrivere ogni contorno in termini di successione di segmenti. I metodi descritti sono i seguenti:
| Metodo | Descrizione | | --- | --- | | public int currentSegment(double[] coords); | Permette di ottenere le coordinate relative ai punti, in versione double, del segmento correntemente descritto dal PathIterator. | | public int currentSegment(float[] coords); | Come il precedente, solamente che le coordinate sono espresse in termini di float. | | public int getWindingRule(); | Ritorna un valore intero che identifica la regola di curvatura (winding rule) utilizzata. | | public boolean isDone(); | Indica se il path è terminato oppure no. | | public void next(); | Se il path non è terminato, permette di spostarsi al segmento seguente. |
Per tracciare un determinato contorno, l'oggetto Graphics2D chiamerà il metodo currentSegment() che ritornerà le coordinate relative al nuovo segmento ed al tipo di curva. Per andare al segmento successivo basterà chiamare next(), e questo fino a che il metodo isDone() non ritornerà true, nel qual caso il contorno sarà terminato. Qualche perplessità potrebbe dare il modo con cui le coordinate vengono ottenute: attraverso un parametro e non un valore di ritorno. Sebbene le coordinate siano rappresentate con float o double, esse non vengono scambiate per valore. Quello che si passa al metodo è, infatti, un array che in Java è un Object a tutti gli effetti. Modificando, all'interno del metodo, i valori degli elementi di coords (e non coords!), essi verranno comunicati all'esterno.
Per rappresentare un path non basta fornire le coordinate (x,y) del punto iniziale e quelle del punto finale in quanto non è detto che il tragitto sia rettilineo. Potremmo andare da un punto ad un altro attraverso una linea diretta, oppure attraverso una curva più o meno pronunciata. Sarà quindi il metodo currentSegment() a fornire, attraverso il valore di ritorno di tipo int, il tipo di curva. A seconda di questo tipo varierà anche il numero di punti necessari alla rappresentazione del segmento. L'interfaccia PathIterator prevede, infatti, i seguenti tipi di curve rappresentati da opportune costanti statiche.
| Costante | Descrizione | | --- | --- | | PathIterator.SEGMOVETO | Le coordinate contenute in coords sono relative al punto iniziale di un subpath. È come se si staccasse la matita e si riprendesse a scrivere dalla nuova posizione. Le coordinate sono quindi relative ad un unico punto, quello iniziale. | | PathIterator.SEGLINETO | L'unico punto le cui coordinate sono contenute in coords è il punto finale di un path. Il segmento corrispondente sarà una linea che unisce l'ultimo punto considerato a quello qui specificato. | | PathIterator.SEGQUADTO | Fornisce le coordinate relative a due punti che specificano una curva parametrica quadratica. | | PathIterator.SEGCUBICTO | Fornisce le coordinate relative a tre punti che specificano una curva parametrica cubica. | | PathIterator.SEGCLOSE | Non è rappresentata da alcuna coppia di coordinate ma indica che il path deve essere chiuso, ovvero che il punto corrente deve essere unito all'ultimo punto specificato attraverso un segmento di tipo SEGMOVETO. |
Come abbiamo visto esiste anche il metodo getWindingRule() che fornisce informazioni relative alle regole di curvatura. L'interfaccia PathIterator permette di specificare, sempre attraverso due costanti statiche, i seguenti due tipi di curvatura:
| Costante | Descrizione | | --- | --- | | PathIterator.WINDEVENODD | Un punto è interno al path se il raggio che parte dal punto stesso verso l'infinito attraversa il path un numero dispari di volte. | | PathIterator.WINDNONZERO | Un punto è interno al path se il raggio che parte dal punto stesso verso l'infinito attraversa il path in senso orario un numero diverso di volte rispetto al senso antiorario. |
Riassumendo, attraverso una particolare implementazione dell'interfaccia PathIterator, si riesce a descrivere il contorno di una figura geometrica.
Metodi per gestire le intersezioni tra Shape
L'interfaccia Shape prevede la definizione dei seguenti metodi che permettono di gestire le intersezioni tra Shape:
| Metodo | Descrizione | | --- | --- | | public boolean intersect(double x, double y, double w, double h); | Indica se il rettangolo posizionato in (x,y) di dimensioni w e h è completamente contenuto nella Shape. | | public boolean intersect(Rectangle2D r); | Come il metodo precedente, solamente che il rettangolo è rappresentato da un oggetto java.awt.geom.Rectangle2D. |
Talvolta questi metodi ritornano true anche se il rettangolo specificato non è completamente interno alla Shape. Questo nel caso in cui verificare esattamente l'intersezione fosse troppo dispendioso relativamente all'importanza dell'operazione stessa.
Come si visualizza una Shape
Abbiamo quindi visto che ogni figura geometrica si può considerare un oggetto Shape. Abbiamo detto che il passaggio attraverso una interfaccia era il prezzo da pagare affinché Graphics2D si comportasse sempre allo stesso modo. Per visualizzare una qualunque Shape basterà, infatti, semplicemente eseguire la seguente istruzione su un oggetto g2d di tipo Graphics2D:
g2d.draw(shape);
Un esempio: la Shape Trapezio
Prima di esaminare le varie Shape disponibili nel JDK 1.2, esercitiamoci creando la classe TrapezioIsoscele da visualizzare con il metodo draw() della classe Graphics2D. Per creare la classe Trapezio seguiremo il seguente procedimento:
1. Creiamo una TrapezioIsoscelePathIterator che ci permette di ottenere il contorno dello stesso a partire dalle informazioni relative alle basi (maggiore e minore), all'altezza ed alle coordinate del vertice in basso a sinistra. 2. Creiamo la classe TrapezioIsoscele implementando l'interfaccia java.awt.Shape ed utilizzando il precedente PathIterator.
Creazione del PathIterator
Il primo passo nella creazione di una Shape è quello di creare un PathIterator che ne descriva il contorno. Dobbiamo creare, quindi, una classe che implementa tale interfaccia. Per il trapezio consideriamo il Listato 2_1 qui di seguito, le cui righe sono state numerate per semplicità di esposizione.
1: package corso.java2d.capitolo2;
3: import java.awt.geom.PathIterator;
4: import java.awt.geom.AffineTransform;
5: import java.awt.geom.NoninvertibleTransformException;
7: /**
8: * Questa classe permette di ottenere i segmenti relativi ai
9: * lati di un trapezio isoscele conoscendo le lunghezze delle basi
10: * (maggiore e minore), dell'altezza, e la posizione (x,y) del
11: * vertice in basso a sinistra.
12: *
13: * @author Massimo Carli
14: * @version v0.01 (22/01/1999)
15: */
17: public class TrapezioIsoscelePathIterator implements PathIterator {
19: /*
20: * Ascissa del vertice in basso a sinistra
21: */
22: private int x;
24: /*
25: * Ordinata del vertice in basso a sinistra
26: */
27: private int y;
29: /*
30: * Ampiezza della base maggiore
31: */
32: private int maj_base;
34: /*
35: * Ampiezza della base minore
36: */
37: private int min_base;
39: /*
40: * Altezza del trapezio
41: */
42: private int altezza;
44: /*
45: * Trasformazione bilineare delle coordinate
46: */
47: private AffineTransform transform;
49: /*
50: * Utilizziamo delle costanti per indicare lo stato di tracciatura
51: */
52: private final int TRACCIATURA_VERTICE_INIZIALE = 0;
53: private final int TRACCIATURA_DIAGONALE_SINISTRA = 1;
54: private final int TRACCIATURA_BASE_MINORE = 2;
55: private final int TRACCIATURA_DIAGONALE_DESTRA = 3;
56: private final int TRACCIATURA_BASE_MAGGIORE = 4;
57: private final int TRACCIATURA_TERMINATA = 5;
59: /*
60: * Indica lo stato attuale di tracciatura
61: */
62: private int stato_di_tracciatura;
64: /**
65: * Crea un PathIterator relativo ad un TrapezioIsoscele conoscendo
66: * le coordinate del vertice in basso a sinistra, le dimensioni delle
67: * due basi (maggiore e minore) e dell'altezza.
68: *
69: * @param x Ascissa del vertice in basso a sinistra
70: * @param y Ordinata del vertice in basso a sinistra
71: * @param maj_base Ampiezza della base maggiore
72: * @param min_base Ampiezza della base minore
73: * @param altezza Altezza del trapezio
74: */
75: public TrapezioIsoscelePathIterator(int x, int y,
int maj_base, int min_base, int altezza,
AffineTransform transform) {
76: this.x = x;
77: this.y = y;
78: this.maj_base = maj_base;
79: this.min_base = min_base;
80: this.altezza = altezza;
81: this.transform = transform;
82: /*
83: * Quando creiamo l'oggetto siamo all'inizio
84: */
85: stato_di_tracciatura = TRACCIATURA_VERTICE_INIZIALE;
86: } // fine costruttore
89: /**
90: * Fornisce le coordinate relative al segmento corrente
91: * @param coords Contiene le informazioni relative ai punti
92: */
93: public int currentSegment(float[] coords) {
94: /*
95: * Dobbiamo fornire le coordinate dei punti relativamente allo stato
96: * di tracciatura.
97: */
98: float[] new_coords = new float[6];
99: switch(stato_di_tracciatura) {
100: case TRACCIATURA_VERTICE_INIZIALE:
101: /*
102: * Ritorniamo le informazioni relative al punto iniziale. Il tipo
103: * di segmento sarà SEG_MOVETO ed il punto passato solamente (x,y).
104: */
105: new_coords[0] = (float)x;
106: new_coords[1] = (float)y;
107: if(transform != null)
108: transform.transform(new_coords, 0, new_coords, 0, 1);
109: coords[0] = new_coords[0];
110: coords[1] = new_coords[1];
111: return PathIterator.SEG_MOVETO;
112: case TRACCIATURA_DIAGONALE_SINISTRA:
113: /*
114: * Dobbiamo ritornare un segmento che rappresenta il lato obliquo
115: * sinistro. Il tipo del segmento sarà LINE_TO ed il punto specificato
116: * sarà quello di arrivo. Bisogna poi aggiornare lo stato di tracciatura
117: */
118: new_coords[0] = (float)(x + (maj_base - min_base) / 2f);
119: new_coords[1] = (float)(y - altezza);
120: if(transform != null)
121: transform.transform(new_coords, 0, new_coords, 0, 1);
122: coords[0] = new_coords[0];
123: coords[1] = new_coords[1];
124: return PathIterator.SEG_LINETO;
125: case TRACCIATURA_BASE_MINORE:
126: /*
127: * Dobbiamo ritornare il segmento relativo alla base minore. Anche in
128: * questo caso il tipo di linea è SEG_LINETO. Bisognerà, poi, aggiornare
129: * lo stato di tracciatura.
130: */
131: new_coords[0] = (float)(x + (maj_base - min_base) / 2f + min_base);
132: new_coords[1] = (float)(y - altezza);
133: if(transform != null)
134: transform.transform(new_coords, 0, new_coords, 0, 1);
135: coords[0] = new_coords[0];
136: coords[1] = new_coords[1];
137: return PathIterator.SEG_LINETO;
138: case TRACCIATURA_DIAGONALE_DESTRA:
139: /*
140: * Questo segmento è relativo alla diagonale destra. Il tipo è
141: * nuovamente SEG_LINETO
142: */
143: new_coords[0] = (float)(x + maj_base);
144: new_coords[1] = (float)y;
145: if(transform != null)
146: transform.transform(new_coords, 0, new_coords, 0, 1);
147: coords[0] = new_coords[0];
148: coords[1] = new_coords[1];
149: return PathIterator.SEG_LINETO;
150: case TRACCIATURA_BASE_MAGGIORE:
151: /*
152: * A questo punto basterà semplicemente chiudere il percorso per
153: * cui il tipo di linea sarà SEG_CLOSE e non verrà specificato alcun
154: * punto. Aggiorniamo poi lo stato di tracciatura
155: */
156: return PathIterator.SEG_CLOSE;
157: } // fine switch/case
158: /*
159: * Non dovremmo mai arrivare a questo punto.
160: */
161: return PathIterator.SEG_CLOSE;
162: } // fine
164: /**
165: * Fornisce le coordinate relative al segmento corrente
166: * @param coords Contiene le informazioni relative ai punti
167: */
168: public int currentSegment(double[] coords) {
169: float[] float_coords = new float[6];
170: int ret_val = this.currentSegment(float_coords);
171: for(int i = 0; i < 6; i++)
172: coords[i] = (double)float_coords[i];
173: return ret_val;
174: } // fine
176: /**
177: * Indica la regola di curvatura
178: */
179: public int getWindingRule() {
180: /*
181: * Nel caso del trapezio utilizziamo la regola WIND_EVEN_ODD
182: */
183: return PathIterator.WIND_EVEN_ODD;
184: } // fine
186: /**
187: * Indica se abbiamo finito o no
188: */
189: public boolean isDone() {
190: /*
191: * Abbiamo finito se lo stato di tracciatura è TRACCIATURA_TERMINATA
192: */
193: return (stato_di_tracciatura == TRACCIATURA_TERMINATA);
194: } // fine
196: /*
197: * Permette di posizionarsi sul segmento seguente
198: */
199: public void next() {
200: /*
201: * Basterà testare lo stato di tracciatura ed aggiornarlo di
202: * conseguenza:
203: */
204: switch(stato_di_tracciatura) {
205: case TRACCIATURA_VERTICE_INIZIALE:
206: stato_di_tracciatura = TRACCIATURA_DIAGONALE_SINISTRA;
207: break;
208: case TRACCIATURA_DIAGONALE_SINISTRA:
209: stato_di_tracciatura = TRACCIATURA_BASE_MINORE;
210: break;
211: case TRACCIATURA_BASE_MINORE:
212: stato_di_tracciatura = TRACCIATURA_DIAGONALE_DESTRA;
213: break;
214: case TRACCIATURA_DIAGONALE_DESTRA:
215: stato_di_tracciatura = TRACCIATURA_BASE_MAGGIORE;
216: break;
217: case TRACCIATURA_BASE_MAGGIORE:
218: stato_di_tracciatura = TRACCIATURA_TERMINATA;
219: break;
220: } // fine
221: } // fine
223:} // fine classe
Il compito del nostro TrapezioIsoscelePathIterator è quello di fornirci i segmenti che compongono il trapezio a partire dalle informazioni relative al vertice in basso a sinistra, alle dimensioni delle due basi (minore e maggiore) e dell'altezza. L'intestazione della classe (riga 17) indica che essa implementa l'interfaccia java.awt.geom.PathIterator e che quindi ne deve implementare i metodi. Alla riga 47 viene definita una variabile di tipo java.awt.geom.AffineTransform. Essa descrive una particolare trasformazione applicata alla nostra Shape. Gli oggetti di questo tipo saranno argomento di un intero capitolo per cui consideriamoli, per ora, solo come descrittori di deformazioni della nostra Shape. Nelle righe 52-57 elenchiamo un insieme di costanti intere che ci permettono di indicare quello che è lo stato di tracciatura, ovvero il segmento che si sta considerando in un determinato istante.
Il nostro PathIterator deve sapere in ogni momento, infatti, di quale segmento rendere disponibili le informazioni attraverso i metodi currentSegment(). Il costruttore (riga 75) è abbastanza semplice e permette di memorizzare nelle nostre variabili locali le informazioni relative al trapezio che andremo a costruire ed alla sua eventuale deformazione. Notiamo come (riga 85) lo stato di tracciatura iniziale sia TRACCIATURAVERTICEINIZIALE. Come detto in precedenza, l'oggetto responsabile della visualizzazione della Shape (l'oggetto Graphics2D) eseguirà ciclicamente i metodi currentSegment() e next() del PathIterator fino a che isDone() non ritorna true.
Come è semplice intuire, la nostra implementazione del metodo next() (riga 199) non fa altro che passare dallo stato di tracciatura corrente al successivo. Il metodo isDone() (riga 189) sarà ancora più semplice in quanto ritornerà true solamente se lo stato di tracciatura è TRACCIATURA_TERMINATA, ovvero quando saranno stati resi disponibili tutti i segmenti del trapezio. Il corpo della classe è, quindi, l'implementazione del metodo currentSegment(). Esso, in base allo stato di tracciatura corrente e utilizzando le informazioni relative al trapezio da costruire, dovrà fornire le coordinate dei punti ed il tipo di segmento.
Alla riga 98 creiamo un array di supporto che conterrà le informazioni relative ai vari punti prima dell'eventuale deformazione. Quando il metodo currentSegment() viene invocato per la prima volta, deve ritornare le coordinate del punto iniziale, ovvero di quel vertice passato nel costruttore. Dopo averle deformate, in base alla eventuale trasformazione applicata (righe 107-108), le coordinate del punto iniziale vengono assegnate all'array coords condiviso. Notiamo che il valore di ritorno è PathIterator.SEGMOVETO, indicativo di un segmento iniziale. Per altri stati di tracciatura, il funzionamento è simile, solamente che il tipo di curva è ora PathIterator.SEGLINETO. Per finire, l'ultimo segmento viene tracciato senza ritornare informazioni relativamente alle sue coordinate, ma semplicemente ritornando il valore relativo a PathIterator.SEGCLOSE. L'altra versione del metodo currentSegment() è stata dedotta dalla precedente, mentre il metodo getWindingRule() ha ritornato il valore PathIterator.WINDEVEN_ODD anche se per il trapezio la sua importanza è molto relativa.
La realizzazione di un PathIterator è quindi molto semplice anche se forse un po' macchinosa. Il lavoro fatto adesso ci sarà, comunque, utile in futuro quando andremo ad utilizzare la nostra classe.
Creazione della classe TrapezioIsoscele
Il secondo passo è quello di creare, effettivamente, la nostra Shape, ovvero la classe TrapezioIsoscele che implementa l'interfaccia java.awt.Shape. Esaminiamo il Listato 2_2.
1: package corso.java2d.capitolo2;
3: import java.awt.geom.*;
4: import java.awt.*;
6: /**
7: * Questa classe rappresenta un TrapezioIsoscele ed implementa
8: * l'interfaccia java.awt.Shape.
9: *
10: * @author Massimo Carli
11: * @version v0.01 (22/01/1999)
12: */
14: public class TrapezioIsoscele implements Shape {
16: /*
17: * Ascissa del vertice in basso a sinistra
18: */
19: private int x;
21: /*
22: * Ordinata del vertice in basso a sinistra
23: */
24: private int y;
26: /*
27: * Ampiezza della base maggiore
28: */
29: private int maj_base;
31: /*
32: * Ampiezza della base minore
33: */
34: private int min_base;
36: /*
37: * Altezza del trapezio
38: */
39: private int altezza;
41: /**
42: * Crea un TrapezioIsoscele specificando il punto relativo
43: * al vertice in basso a sinistra, le lunghezze delle due
44: * basi (maggiore e minore) e l'altezza.
45: *
46: * @param x Ascissa del vertice in basso a sinistra
47: * @param y Ordinata del vertice in basso a sinistra
48: * @param maj_base Ampiezza della base maggiore
49: * @param min_base Ampiezza della base minore
50: * @param altezza Altezza del trapezio
51: */
52: public TrapezioIsoscele(int x, int y, int maj_base, int min_base, int altezza) {
53: this.x = x;
54: this.y = y;
55: this.maj_base = maj_base;
56: this.min_base = min_base;
57: this.altezza = altezza;
58: } // fine costruttore
60: /**
61: * Specifica la regione rettangolare che contiene la figura
62: */
63: public Rectangle getBounds() {
64: return new Rectangle(x, y - altezza, maj_base, altezza);
65: } // fine
67: /**
68: * Specifica la regione rettangolare che contiene la figura
69: */
70: public Rectangle2D getBounds2D() {
71: return new Rectangle2D.Double(x, y - altezza, maj_base, altezza);
72: } // fine
74: /**
75: * Indica se il punto (x,y) è interno o no al trapezio.
76: * @param x Ascissa del punto da testare
77: * @param y Ordinata del punto da testare
78: */
79: public boolean contains(double x, double y) {
80: /*
81: * Calcoliamo alcuni valori utili alla verifica.
82: */
83: double x0 = (double)this.x;
84: double y0 = (double)this.y;
85: double B = (double)maj_base;
86: double b = (double)min_base;
87: double h = (double)altezza;
88: double lim_sin = x0 - ((B - b) / (2d * h)) * (y - y0);
89: double lim_des = x0 + ((B + b) / 2d) + ((B - b) / (2d * h)) * (y - (y0 - h));
90: return (y <= y0) && (y >= (y0 - h)) && (x >= lim_sin) && (x <= lim_des);
91: } // fine
93: /**
94: * Indica se il punto p è interno alla figura
95: * @param p Punto da testare
96: */
97: public boolean contains(Point2D p) {
98: /*
99: * Sfruttiamo il precedente metodo.
100: */
101: return this.contains(p.getX(), p.getY());
102: } // fine
104: /*
105: * Indica se il rettangolo specificato è completamente interno alla
106: * figura
107: * @param x Ascissa del vertice in alto a sinistra del rettangolo da testare
108: * @param y Ordinata del vertice in alto a sinistra del rettangolo da testare
109: * @param w Larghezza del rettangolo da testare
110: * @param h Altezza del rettangolo da testare
111: */
112: public boolean contains(double x, double y, double w, double h) {
113: /*
114: * Il rettangolo specificato è contenuto all'interno del trapezio
115: * se e solo se lo sono tutti i quattro vertici.
116: */
117: return this.contains(x, y) && this.contains(x + w, y)
&& this.contains(x + w, y + h) && this.contains(x, y + h);
118: } // fine
120: /*
121: * Indica se il rettangolo specificato è completamente interno alla
122: * figura
123: *
124: * @param r Rettangolo da testare
125: */
126: public boolean contains(Rectangle2D r) {
127: /*
128: * Sfruttiamo il precedente metodo
129: */
130: return this.contains(r.getX(), r.getY(), r.getWidth(), r.getHeight());
131: } // fine
133: /**
134: * Indica se il rettangolo specificato interseca la figura
135: * @param x Ascissa del vertice in alto a sinistra del rettangolo da testare
136: * @param y Ordinata del vertice in alto a sinistra del rettangolo da testare
137: * @param w Larghezza del rettangolo da testare
138: * @param h Altezza del rettangolo da testare
139: */
140: public boolean intersects(double x, double y, double w, double h) {
141: /*
142: * Nel caso del trapezio possiamo dire che esso interseca un rettangolo
143: * se una delle due figure comprende un vertice dell'altra.
144: */
145: Rectangle bounds = getBounds();
146: return this.contains(x, y)
|| this.contains(x + w, y)
|| this.contains(x + w, y + h)
|| this.contains(x, y + h)
147: || bounds.contains(x, y)
|| bounds.contains(x + w, y)
|| bounds.contains(x + w, y + h)
|| bounds.contains(x, y + h);
148: } // fine
150: /**
151: * Indica se il rettangolo specificato interseca la figura
152: * @param r Rettangolo da testare
153: */
154: public boolean intersects(Rectangle2D r) {
155: /*
156: * Sfruttiamo il precedente metodo
157: */
158: return this.intersects(r.getX(), r.getY(), r.getWidth(), r.getHeight());
159: } // fine
161: /**
162: * Ritorna il PathIterator utilizzato per delimitare il contorno della figura.
163: * @param at AffineTransform da sottoporre
164: */
165: public PathIterator getPathIterator(AffineTransform at) {
166: return new TrapezioIsoscelePathIterator(x, y, maj_base, min_base, altezza, at);
167: } // fine
169: /**
170: * Ritorna il PathIterator utilizzato per delimitare il contorno della figura
171: * @param at AffineTransform da sottoporre
172: * @param flatness Piattezza della curva
173: */
174: public PathIterator getPathIterator(AffineTransform at, double flatness) {
175: return new FlatteningPathIterator(getPathIterator(at), flatness);
176: } // fine
178:} // fine classe
Alla riga 14 vi è l'intestazione della classe in cui si dichiara l'implementazione dell'interfaccia java.awt.Shape. Di seguito (righe 16-39) sono definite le variabili locali che memorizzano le informazioni relative al trapezio da rappresentare. Alla riga 52 vi è il costruttore che ci permette di memorizzare le informazioni relative al trapezio nelle nostre variabili locali.
Di seguito si implementano i metodi dell'interfaccia Shape. I metodi getBounds() e getBounds2D() permettono di conoscere, in due modi diversi, il rettangolo che contiene completamente la figura geometrica. Nel nostro caso (righe 64 e 71) non facciamo altro che ritornare il rettangolo che ha come base la base maggiore del trapezio e come altezza l'altezza del trapezio. I metodi contains() devono ritornare un booleano che indica se un punto è interno alla nostra figura. Fatto qualche piccolo conto (righe 83-89) abbiamo raggiunto l'obiettivo. Per quello che riguarda l'ordinata ci è bastato verificare se era compreso tra le due basi, mentre per l'ascissa abbiamo verificato se al di sotto delle rette relative ai lati obliqui.
La versione del metodo contains() con parametro di tipo java.awt.geom.Point2D ha utilizzato, poi, la precedente implementazione. Per verificare se un rettangolo è completamente interno al trapezio ci è bastato verificare che tutti i vertici del rettangolo fossero interni al trapezio stesso (righe 106-132). Per verificare se il trapezio intersecava o meno un rettangolo passato come parametro (righe 134-160) ci è bastato verificare se uno dei vertici del rettangolo fosse o meno interno al trapezio o, viceversa, un vertice del trapezio fosse interno al rettangolo. Gli ultimi metodi da implementare sono quelli relativi al PathIterator. Nella prima implementazione ci è bastato ritornare una istanza della precedente classe TrapezioIsoscelePathIterator a cui è stata passata anche la trasformazione passata come parametro al metodo getPathIterator().
Nella versione del metodo che prevede la specifica della flatness (piattezza) del PathIterator, abbiamo utilizzato una classe di utilità che permette di modificare, in modo automatico, un PathIterator in base ad un determinato valore di flatness. Abbiamo quindi ritornato un oggetto di tipo java.awt.geom.FlatteningPathIterator.
Visualizziamo il Trapezio
Per testare il funzionamento della nostra classe abbiamo creato l'applicazione del Listato 2_3.
1: package corso.java2d.capitolo2;
3: import java.awt.geom.*;
4: import java.awt.*;
6: /**
7: * Questa classe permette di provare la classe TrapezioIsoscele.
8: *
9: * @author Massimo Carli
10: * @version v0.01 (22/01/1999)
11: */
13: public class ProvaTrapezioIsoscele extends Panel {
15: /*
16: * Trapezio da visualizzare
17: */
18: private TrapezioIsoscele trapezio;
20: /**
21: * Crea un pannello con all'interno un Trapezio isoscele
22: */
23: public ProvaTrapezioIsoscele() {
24: trapezio = new TrapezioIsoscele(50, 150, 100, 50, 40);
25: setSize(400, 300);
26: } // fine
28: /**
29: * Per visualizzare il trapezio utilizziamo il metodo draw() della classe
30: * Graphics2D, la cui istanza è stata ottenuta attraverso casting dall'oggetto
31: * di tipo Graphics.
32: */
33: public void paint(Graphics g) {
34: Graphics2D g2d = (Graphics2D)g;
35: g2d.draw(trapezio);
36: } // fine
39: public static void main(String str[]) {
40: Frame frame = new Frame("Prova TrapezioIsoscele");
41: ProvaTrapezioIsoscele prova = new ProvaTrapezioIsoscele();
42: frame.add(prova);
43: frame.setBounds(400, 300, 300, 300);
44: frame.setVisible(true);
45: } // fine
47:} // fine classe
Esso rappresenta un semplice Frame all'interno del quale abbiamo inserito un Panel di cui abbiamo ridefinito il metodo paint(). Alla riga 34 otteniamo un riferimento ad un oggetto Graphics2D attraverso una operazione di casting dall'oggetto di tipo Graphics passato come parametro del metodo paint(). Questo è il procedimento standard per ottenere tale oggetto. Alla riga seguente non facciamo altro che disegnare il trapezio, creato nel costruttore (riga 24), attraverso un metodo draw().
Otteniamo quindi quello della figura seguente.
Abbiamo quindi visto che, implementando l'interfaccia Shape e creando un opportuno PathIterator, è possibile creare una qualunque figura geometrica. Abbiamo poi visualizzato il nostro trapezio attraverso il metodo draw(), metodo standard per la visualizzazione di una qualunque Shape. In seguito vedremo altri metodi che ci faranno apprezzare maggiormente il lavoro svolto.
Conclusioni
Il prossimo mese vedremo le forme geometriche presenti nel JDK 1.2 e vedremo come modificare il tipo di linea.
