Capítulo 3: Figuras geométricas en Java2D
3.4 La interfaz Shape
3.4.2 Figuras geométricas a la medida: la bañera
En el apartado 3.3 se estudió un ejemplo en el que se creaba una bañera dibujando sucesivamente un rectángulo que delimitaba sus dimensiones, un rectángulo de esquinas redondeadas para representar el hueco de la bañera y un círculo para representar el sumidero. Supongamos por un momento que, por motivos de nuestro desarrollo, nos viéramos obligados a dibujar multitud de estas bañeras (por ejemplo en un plano de viviendas residenciales). Con lo visto hasta ahora nos veríamos obligados a crear las tres figuras integrantes de una bañera una y otra vez para reubicarlas en los diferentes puntos del plano. Sin embargo, parece bastante claro que tal solución es impropia de la reutilización de la que se vanagloria la programación orientada a objetos.
Para evitar esta repetición de código, lo más interesante resulta crear nuestra propia figura geométrica, por ejemplo la clase Bañera (sí, con eñe y todo, ya que Java permite usar la eñe y los acentos en los identificadores de usuario) que hereda de Shape. Claro está, Shape es una interfaz y es necesario implementar en nuestra clase Bañera todos sus métodos, que aparecen en la tabla del epígrafe 3.4. Vayamos por pasos.
Antes de nada, nuestra clase Bañera va a contener las tres figuras que van a soportar su dibujo (estará compuesta por tres campos) :
• Un rectángulo exterior (campo exterior) de tipo Rectangle2D que delimita sus dimensiones.
• Un hueco formado por un objeto (interior) de la clase RoundRectangle2D. • Un sumidero representado por una Ellipse2D guardada en el campo
sumidero.
La mayoría de las funciones que debe implementar una Shape y que aparecen en la tabla del epígrafe 3.4 están referidas a los límites de la figura que, en
nuestro caso, coinciden con los del campo exterior. Por ejemplo, por ser una Shape nuestra Bañera debe implementar la función getBounds() que devuelve sus límites; dado que los límites de la bañera coinciden con los de su campo exterior, nuestra implementación de getBounds() será:
public Rectangle getBounds() {
returnexterior.getBounds(); }
de tal manera que la mayoría de funciones que debe implementar nuestra Bañera quedan relegadas a sus homónimas del campo exterior. Así el primer bloque de código que conforma nuestra clase Bañera es:
import java.awt.*;
import java.awt.geom.*;
publicclass Bañera implements Shape { private Rectangle2D exterior; private RoundRectangle2D interior; private Ellipse2D sumidero;
public Bañera(double x, double y, double w, double h){ exterior = new Rectangle2D.Double(x, y, w, h);
interior = new RoundRectangle2D.Double(x+10, y+10, w-20, h-20, 25.0, 25.0); sumidero = new Ellipse2D.Double(x+w/2-5, y+15, 10.0, 10.0);
}
publicboolean contains(Point2D p) { returnexterior.contains(p); }
publicboolean contains(Rectangle2D r) { returnexterior.contains(r);
}
publicboolean contains(double x, double y) { returnexterior.contains(x, y);
}
publicboolean contains(double x, double y, double w, double h) { returnexterior.contains(x, y, w, h);
}
public Rectangle getBounds() { returnexterior.getBounds(); }
public Rectangle2D getBounds2D() { returnexterior.getBounds2D(); }
publicboolean intersects(Rectangle2D r) { returnexterior.intersects(r);
}
publicboolean intersects(double x, double y, double w, double h) { returnexterior.intersects(x, y, w, h);
} ...
Ahora quedan por implementar las dos funciones más peliagudas, que son getPathIterator() en sus dos versiones (una con un parámetro y otra con dos). El objetivo de esta función es devolver un objeto de tipo PathIterator; un objeto tal es, sencillamente, un iterador que el entorno de dibujo Java va invocando para ir recuperando en secuencia los segmentos que van formando el dibujo final. Conceptualmente, esto tiene mucha relación con la clase GeneralPath; en esta clase nosotros, como programadores, añadíamos uno a uno los segmentos que conformaban el dibujo (operaciones moveTo(), curveTo(), lineTo(), quadTo() y closePath()) y son precisamente tales segmentos los que el PathIterator retorna al sistema cuando éste va realizando progresivamente el dibujo de la figura.
En resumen, nuestra Bañera tiene dentro tres objetos Shape que, por tanto, también incorporan sus correspondientes operaciones getPathIterator(). Así pues, para poder implementar las operaciones getPathIterator() en nuestra clase Bañera lo que vamos a hacer es:
• Iterar primero sobre los segmentos del campo exterior • Iterar después sobre los segmentos del campo interior • Por último iterar sobre los del campo sumidero
Para poder hacer esto nos crearemos una clase interna BañeraPathIterator que hereda de PathIterator. Esta BañeraPathIterator irá delegando funciones en los PathIterators de los campos interno, externo y sumidero, en ese orden, para lo cual hará uso de un campo interno actualIterator que apuntará en cada momento al PathIterator de objeto Shape que se esté dibujando en ese momento. Todas las funciones de BañeraPathIterator se delegan a las homónimas del campo actualIterator: cuando se acaba de iterar sobre la figura externa, comienza a iterarse sobre interna y, después, sobre sumidero. Siguiendo este criterio, el resto del código que nos queda por definir es (teniendo en cuenta las dos modalidades con que el sistema de dibujo Java puede querer obtener un PathIterator, esto es, con un AffineTransform sólo, o con un AffineTransform y un double):
...
public PathIterator getPathIterator(AffineTransform at) { returnnew BañeraPathIterator(at);
}
public PathIterator getPathIterator(AffineTransform at, double flatness) { returnnew BañeraPathIterator(at, flatness);
}
privateenum Parte {EXTERIOR, INTERIOR, SUMIDERO} privateclass BañeraPathIterator implements PathIterator { private PathIterator actualIterator;
privatebooleanhayFlatness; private AffineTransform at; privatedoubleflatness; private BañeraPathIterator(){ parteActual = Parte.EXTERIOR; }
public BañeraPathIterator(AffineTransform at){ this();
hayFlatness = false; this.at = at;
actualIterator=exterior.getPathIterator(at); }
public BañeraPathIterator(AffineTransform at, double flatness){ this();
hayFlatness = true; this.flatness = flatness; this.at = at;
actualIterator=exterior.getPathIterator(at, flatness); }
publicint currentSegment(float[] coords) { returnactualIterator.currentSegment(coords); }
publicint currentSegment(double[] coords) { returnactualIterator.currentSegment(coords); }
publicint getWindingRule() {
returnactualIterator.getWindingRule(); }
publicboolean isDone() { if (actualIterator.isDone()){ switch(parteActual){
case EXTERIOR : parteActual = Parte.INTERIOR; if (hayFlatness)
actualIterator = interior.getPathIterator(at, flatness); else
actualIterator = interior.getPathIterator(at); break;
case INTERIOR : parteActual = Parte.SUMIDERO; if (hayFlatness)
actualIterator = sumidero.getPathIterator(at, flatness); else
actualIterator = sumidero.getPathIterator(at); break;
} }
returnactualIterator.isDone(); }
actualIterator.next(); }
} }
Para obtener el fichero Bañera.java basta con yuxtaponer estos dos grandes trozos de código.
La figura 3.13 muestra el resultado de dibujar diferentes bañeras mediante el código:
publicvoid paint (Graphics g) { super.paint(g);
Graphics2D g2 = (Graphics2D)g; g2.setColor(Color.blue);
g2.setStroke(new BasicStroke(3.0f)); Bañera b1 = new Bañera(50, 50, 120, 180); g2.fill(b1);
Bañera b2 = new Bañera(200, 50, 120, 180); g2.draw(b2);
g2.setColor(Color.black);
g2.setStroke(new BasicStroke(1.0f)); Bañera b3 = new Bañera(350, 50, 50, 120); g2.draw(b3);
}