/*
* (C) 2004 - Geotechnical Software Services
*
* This code is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This code is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this program; if not, write to the Free
* Software Foundation, Inc., 59 Temple Place - Suite 330, Boston,
* MA 02111-1307, USA.
*/
package no.geosoft.cc.graphics;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import no.geosoft.cc.geometry.Box;
import no.geosoft.cc.geometry.Geometry;
import no.geosoft.cc.geometry.Rect;
import no.geosoft.cc.geometry.Region;
/**
* Class for holding a polyline. <tt>GSegment</tt>s are contained by
* <tt>GObjects</tt>. They can have its own rendering style (<tt>GStyle</tt>)
* or inherit style from its parent <tt>GObject</tt> if not specified.
* <p>
* Example usage:
*
* <pre>
* public class Box extends GObject
* {
* private double x0_, y0_, width_, height_;
* private GSegment border_;
*
* public Box (double x0, double y0, double width, double height)
* {
* // Store the abstract representation of the box
* x0_ = x0;
* y0_ = y0;
* width_ = width;
* height_ = height;
*
* // Prepare the graphics representation of the box
* border_ = new GSegment();
* addSegment (border_);
* }
*
* public void draw()
* {
* // Complete the graphics representation of the box
* double[] xy = new double {x0_, y0_,
* x0_ + width_, y0_,
* x0_ + width_, y0_ + height_,
* x0_, y0_ + height_,
* x0_, y0}
* border_.setGeometry (xy);
* }
* }
* </pre>
*
* A typical <tt>GObject</tt> will have many <tt>GSegment</tt>s and
* sub-<tt>GObject</tt>s. Some of these can be created in the constructor
* while others may need to be created within the draw method as they depend
* ont external factors such as zoom etc.
* <p>
* For efficiency, <b>G</b> does not store world coordinates internally but
* converts these to device coordinates in the rendering step. It is
* therfore essential that geometry is provided in the <tt>draw()</tt>
* method which is called by <b>G</b> on retransformations
* (zoom/resize etc.).
*
* @author <a href="mailto:info@geosoft.no">GeoSoft</a>
*/
public class GSegment
implements GStyleListener
{
private GObject owner_; // Owner
private int x_[], y_[];
private GImage vertexImage_;
private Rect rectangle_; // Bounding box
private Object userData_; // Whatever app assoc with graphics
private GStyle style_; // As applied to this object
private GStyle actualStyle_; // Adjusted for owner inherits
private boolean isVisible_; // Due to position not vis. setting
private List texts_; // of GText
private Collection components_; // of GComponent
private Collection images_; // of GImage
/**
* Create a GSegment.
*/
public GSegment()
{
owner_ = null;
x_ = null;
y_ = null;
rectangle_ = null;
texts_ = null;
images_ = null;
vertexImage_ = null;
components_ = null;
isVisible_ = false;
style_ = null;
actualStyle_ = new GStyle();
}
/**
* Return the owner GObject of this GSegment. If the GSegment has not been
* added to a GObject, owner is null.
*
* @return GObject owner of this GSegment, or null if not attacted to one.
*/
public GObject getOwner()
{
return owner_;
}
/**
* Set the owner of this GSegment.
*
* @param owner New owner of this GSegment.
*/
void setOwner (GObject owner)
{
owner_ = owner;
updateContext();
}
/**
* Convenience method to get the scene of the graphics hierarchy of this
* GSegment.
*
* @return Scene of the graphics hierarchy of this GSegment (or null if
* it is somehow not attached to a scene).
*/
GScene getScene()
{
return owner_ == null ? null : owner_.getScene();
}
/**
* Set user data of this GSegment.
*
* @param userData User data of this GSegment.
*/
public void setUserData (Object userData)
{
userData_ = userData;
}
/**
* Return user data of this GSegment.
*
* @return User data of this GSegment.
*/
public Object getUserData()
{
return userData_;
}
/**
* Return device X coordinates of this segment.
*
* @return Device X coordinates of this segment.
*/
int[] getX()
{
return x_;
}
/**
* Return device X coordinates of this GSegment.
*
* @return Device X coordinates of this segment.
*/
int[] getY()
{
return y_;
}
/**
* Return number of points in the polyline of this GSegment.
*
* @return Number of points in this GSegment. May be 0 if the GSegment
* is empty.
*/
int getNPoints()
{
return x_ == null ? 0 : x_.length;
}
/**
* Return rectangle bounding box of this GSegment. Covers GSegment geometry
* only, not associated annotations, images or AWT components.
*
* @return Rectangle bounding box of the geometry of this GSegment.
*/
Rect getRectangle()
{
return rectangle_;
}
/**
* Return region of this GSegment including associated annotations,
* images and AWT components.
*
* @return Region of this GSegment includings its sub components.
*/
Region getRegion()
{
Region region = new Region();
// First add geometry part
if (rectangle_ != null)
region.union (rectangle_);
// Add extent of all texts
if (texts_ != null) {
for (Iterator i = texts_.iterator(); i.hasNext(); ) {
GText text = (GText) i.next();
region.union (text.getRectangle());
}
}
// Add extent of all images
if (images_ != null) {
for (Iterator i = images_.iterator(); i.hasNext(); ) {
GImage image = (GImage) i.next();
region.union (image.getRectangle());
}
}
// Add extent of all AWT components
if (images_ != null) {
for (Iterator i = images_.iterator(); i.hasNext(); ) {
GComponent component = (GComponent) i.next();
region.union (component.getRectangle());
}
}
return region;
}
/**
* Return the X center of the geometry of this GSegment.
*
* @return X center of the geometry of this GSegment.
*/
int getCenterX()
{
return rectangle_ == null ? 0 : rectangle_.getCenterX();
}
/**
* Return the Y center of the geometry of this GSegment.
*
* @return Y center of the geometry of this GSegment.
*/
int getCenterY()
{
return rectangle_ == null ? 0 : rectangle_.getCenterY();
}
/**
* This method is called in a response to updated geometry, new
* parent or new style settings.
*/
private void updateContext()
{
isVisible_ = false; // Until otherwise proved
// Nothing to update if we are not in the tree
if (owner_ == null)
return;
// Flag owner region as invalid
owner_.flagRegionValid (false);
// Not more we can do if we're not in the scene
if (owner_.getScene() == null)
return;
// Check if we're outside the scene
if (rectangle_ != null)
isVisible_ = owner_.getScene().getRegion().isIntersecting (rectangle_);
// Update GWindow damage area
updateDamage();
// If segment has text, annotation must be updated
if (texts_ != null)
owner_.getScene().setAnnotationValid (false);
// If segment has images, their positions must be updated
if (images_ != null)
owner_.getScene().computePositions (images_);
}
/**
* Flag the geometry part of this GSegment as damaged.
*/
void updateDamage()
{
if (isVisible_ &&
rectangle_ != null &&
!rectangle_.isEmpty() &&
owner_ != null &&
owner_.getWindow() != null)
owner_.getWindow().updateDamageArea (rectangle_);
}
/**
* Compute the rectangle bounding box of this GSegment.
*/
private void computeRectangle()
{
// Actual rectangle depends on line width
int lineWidth = actualStyle_.getLineWidth() - 1;
if (x_ != null) {
if (rectangle_ == null) rectangle_ = new Rect();
rectangle_.set (x_, y_);
rectangle_.expand (lineWidth + 1, lineWidth + 1);
}
}
/**
* Set single point device coordinate geometry.
*
* @param x X coordinate.
* @param y Y coordinate.
*/
public void setGeometry (int x, int y)
{
setGeometry (new int[] {x}, new int[] {y});
}
/**
* Set two point (line) device coordinate geometry.
*
* @param x0 X coordinate of first end point.
* @param y0 Y coordinate of first end point.
* @param x1 X coordinate of second end point.
* @param y1 Y coordinate of second end point.
*/
public void setGeometry (int x0, int y0, int x1, int y1)
{
setGeometry (new int[] {x0, x1}, new int[] {y0, y1});
}
/**
* Set polyline device coordinate geometry.
*
* @param x X coordinates.
* @param y Y coordinates.
*/
public void setGeometry (int[] x, int[] y)
{
// Mark old area as damaged
updateDamage();
// Update geometry
if (x == null) {
x_ = null;
y_ = null;
rectangle_ = null;
}
else {
int nPoints = x.length;
// Reallocate
if (x_ == null || x_.length != nPoints) {
x_ = new int[nPoints];
y_ = new int[nPoints];
}
for (int i = 0; i < nPoints; i++) {
x_[i] = x[i];
y_[i] = y[i];
}
// Update bounding box
computeRectangle();
}
updateContext();
}
/**
* Set polyline device coordinate geometry.
*
* @param xy Polyline geometry [x,y,x,y,...]. null can be specified
* to indicate that the present geometry should be removed.
*/
public void setGeometry (int[] xy)
{
// Mark old area as damaged
updateDamage();
// Update geometry
if (xy == null) {
x_ = null;
y_ = null;
rectangle_ = null;
}
else {
int nPoints = xy.length / 2;
// Reallocate
if (x_ == null || x_.length != nPoints) {
x_ = new int[nPoints];
y_ = new int[nPoints];
}
for (int i = 0; i < nPoints; i++) {
x_[i] = xy[i*2 + 0];
y_[i] = xy[i*2 + 1];
}
// Update bounding box
computeRectangle();
}
updateContext();
}
/**
* Set single point world coordinate geometry.
*
* @param x X coordinate.
* @param y Y coordinate.
* @param z Z coordinate.
*/
public void setGeometry (double x, double y, double z)
{
setGeometry (new double[] {x}, new double[] {y}, new double[] {z});
}
/**
* Set single point world coordinate geometry. Ignore Z coordinate
* (set to 0.0).
*
* @param x X coordinate.
* @param y Y coordinate.
*/
public void setGeometry (double x, double y)
{
setGeometry (x, y, 0.0);
}
/**
* Set two point (line) world coordinate geometry.
*
* @param x0 X coordinate of first end point.
* @param y0 Y coordinate of first end point.
* @param z0 Z coordinate of first end point.
* @param x1 X coordinate of second end point.
* @param y1 Y coordinate of second end point.
* @param z1 Z coordinate of second end point.
*/
public void setGeometry (double x0, double y0, double z0,
double x1, double y1, double z1)
{
setGeometry (new double[] {x0, x1},
new double[] {y0, y1},
new double[] {z0, z1});
}
/**
* Set two point (line) world coordinate geometry. Ignore Z coordinate
* (set to 0.0).
*
* @param x0 X coordinate of first end point.
* @param y0 Y coordinate of first end point.
* @param x1 X coordinate of second end point.
* @param y1 Y coordinate of second end point.
*/
public void setGeometry (double x0, double y0, double x1, double y1)
{
setGeometry (new double[] {x0, x1},
new double[] {y0, y1},
new double[] {0.0, 0.0});
}
/**
* Set polyline world coordinate geometry.
* TODO: Look at the implementation
*
* @param wx X coordinates.
* @param wy Y coordinates.
* @param wz Z coordinates. May be null if Z is to be ignored.
*/
public void setGeometry (double x[], double y[], double z[])
{
GTransformer transformer = owner_.getScene().getTransformer();
int nPoints = x.length;
double[] world = new double[3];
int[] device = new int[2];
int[] devx = new int[nPoints];
int[] devy = new int[nPoints];
for (int i = 0; i < x.length; i++) {
world[0] = x[i];
world[1] = y[i];
world[2] = z == null ? 0.0 : z[i];
device = transformer.worldToDevice (world);
devx[i] = device[0];
devy[i] = device[1];
}
setGeometry (devx, devy);
}
/**
* Set polyline world coordinate geometry. Ignore Z coordinate
* (interpret all as 0.0).
*
* @param x X coordinates.
* @param y Y coordinates.
*/
public void setGeometry (double[] x, double[] y)
{
setGeometry (x, y, null);
}
/**
* Set polyline world coordinate geometry.
*
* @param xyz Polyline geometry [x,y,z,x,y,z,...].
*/
public void setGeometry (double[] xyz)
{
GTransformer transformer = owner_.getScene().getTransformer();
int nPoints = xyz.length / 3;
int[] devxy = new int[nPoints * 2];
double[] world = new double[3];
int[] device = new int[2];
int wIndex = 0;
int dIndex = 0;
for (int i = 0; i < nPoints; i++) {
world[0] = xyz[wIndex + 0];
world[1] = xyz[wIndex + 1];
world[2] = xyz[wIndex + 2];
device = transformer.worldToDevice (world);
devxy[dIndex + 0] = device[0];
devxy[dIndex + 1] = device[1];
wIndex += 3;
dIndex += 2;
}
setGeometry (devxy);
}
/**
* Set polyline world coordinate geometry. Ignore Z coordinate
* (set to 0.0).
*
* @param xy Polyline geometry [x,y,x,y,...].
*/
public void setGeometryXy (double[] xy)
{
GTransformer transformer = owner_.getScene().getTransformer();
int nPoints = xy.length / 2;
int[] devxy = new int[nPoints * 2];
double[] world = new double[3];
int[] device = new int[2];
int wIndex = 0;
int dIndex = 0;
for (int i = 0; i < nPoints; i++) {
world[0] = xy[wIndex + 0];
world[1] = xy[wIndex + 1];
world[2] = 0.0;
device = transformer.worldToDevice (world);
devxy[dIndex + 0] = device[0];
devxy[dIndex + 1] = device[1];
wIndex += 2;
dIndex += 2;
}
setGeometry (devxy);
}
/**
* Translate this segment in device.
*
* @param dx Translation in x direction.
* @param dy Translation in Y direction.
*/
public void translate (int dx, int dy)
{
if (x_ == null) return;
int[] newX = new int[x_.length];
int[] newY = new int[x_.length];
for (int i = 0; i < x_.length; i++) {
newX[i] = x_[i] + dx;
newY[i] = y_[i] + dy;
}
setGeometry (newX, newY);
}
/**
* Set new style for this segment. Style elements not explicitly
* set within this GStyle object are inherited from parent
* objects. Default style is null, i.e. all style elements are
* inherited from parent.
*
* @param style Style for this segment (or null if the intent is to
* unset the current style).
*/
public void setStyle (GStyle style)
{
if (style_ != null)
style_.removeListener (this);
style_ = style;
if (style_ != null)
style_.addListener (this);
updateStyle();
}
/**
* Return style for this segment. This is the style set by setStyle()
* and not necesserily the style as it appears on screen as unset
* style elements are inherited from parents.
*
* @return Style of this GSegment as specified with setStyle(), (or
* null if no style has been provided).
*/
public GStyle getStyle()
{
return style_;
}
/**
* These are the actual style used for this GSegment when
* inheritance for unset values are resolved.
* TODO: Make this public?
*
* @return Actual style for this segment.
*/
GStyle getActualStyle()
{
return actualStyle_;
}
/**
* Resolve unset values in segment style.
*/
void updateStyle()
{
// Invalidate all style
actualStyle_ = new GStyle();
// Update with owner style
if (owner_ != null)
actualStyle_.update (owner_.getActualStyle());
// Update (and possibly override) with present style
if (style_ != null)
actualStyle_.update (style_);
// Update children object style
if (texts_ != null) {
for (Iterator i = texts_.iterator(); i.hasNext(); ) {
GText text = (GText) i.next();
text.updateStyle();
}
}
// TODO: This might not be necessary for all style changes
computeRectangle();
updateContext();
}
/**
* Find region of a set of positionals.
*
* @param positionals Positionals to find region of.
* @return Region of specified positionals.
*/
private Region findRegion (Collection positionals)
{
Region region = new Region();
for (Iterator i = positionals.iterator(); i.hasNext(); ) {
GPositional positional = (GPositional) i.next();
if (positional.isVisible())
region.union (positional.getRectangle());
}
return region;
}
/**
* Add a text element to this segment.
* <p>
* Text elements without line position hint will be associated with
* the n'th segment coordinate according to the number of texts added.
*
* @param text Text element to add.
*/
public void addText (GText text)
{
// Create if first text
if (texts_ == null)
texts_ = new ArrayList();
// Add to list
texts_.add (text);
text.setSegment (this);
// Flag owner region as invalid and annotation too
if (owner_ != null) {
owner_.flagRegionValid (false);
if (owner_.getScene() != null)
owner_.getScene().setAnnotationValid (false);
}
}
/**
* Set text element of this segment. Replaces all current
* text elements of this segment.
*
* @param text Text element to set.
*/
public void setText (GText text)
{
removeText();
addText (text);
}
/**
* Return all text elements of this segment.
*
* @return All text elements of this segment (or null if none).
*/
public List getTexts()
{
return texts_;
}
/**
* Return the first text element of this segment. Convenient when
* caller knows that there are exactly one text element.
*
* @return First text elements of this segment (or null if none).
*/
public GText getText()
{
return texts_ != null ? (GText) texts_.iterator().next() : null;
}
/**
* Remove all text elements set on this segment.
*/
public void removeText()
{
// Update damage area
if (owner_ != null && owner_.getWindow() != null && texts_ != null) {
Region damage = findRegion (texts_);
owner_.getWindow().updateDamageArea (damage);
}
// Nullify texts
texts_ = null;
}
/**
* Add an image to this segment.
*
* @param image Image to add.
*/
public void addImage (GImage image)
{
// Create if first time
if (images_ == null)
images_ = new ArrayList();
// Add to list
images_.add (image);
image.setSegment (this);
// Flag owner region as invalid
if (owner_ != null)
owner_.flagRegionValid (false);
}
/**
* Set image of this segment. All current images are removed.
*
* @param image Image to set.
*/
public void setImage (GImage image)
{
removeImages();
addImage (image);
}
/**
* Return all images associated with this segment.
*
* @return All images associated with this segment.
*/
public Collection getImages()
{
return images_;
}
/**
* Remove all images from this GSegment.
*/
public void removeImages()
{
// Update damage area
if (owner_ != null && owner_.getWindow() != null && images_ != null) {
Region damage = findRegion (images_);
owner_.getWindow().updateDamageArea (damage);
}
// Nullify images
images_ = null;
}
/**
* Set image to associate with every vertex of this GSegment.
*
* @param image Image to decorate every vertex of this
* polyline (or null to turn off this feature).
*/
public void setVertexImage (GImage image)
{
vertexImage_ = image;
}
/**
* Return the image that is to be associated with all vertices
* of this GSegment. Return null if none is specified.
*
* @return Image that decorates every vertex of this
* GSegment (or null if not specified).
*/
public GImage getVertexImage()
{
return vertexImage_;
}
/**
* Add a AWT component to this segment.
*
* @param component Component to add.
*/
public void addComponent (GComponent component)
{
// Create if first time
if (components_ == null)
components_ = new ArrayList();
component.setSegment (this);
// Add to list
components_.add (component);
}
/**
* Set component of this segment. All current components are removed.
*
* @param component Component to set.
*/
public void setComponent (GComponent component)
{
removeComponents();
addComponent (component);
}
/**
* Return all AWT components of this segment.
*
* @return All components of this segment.
*/
public Collection getComponents()
{
return components_;
}
/**
* Remove all AWT components from this GSegment.
*
* @param component
*/
public void removeComponents()
{
// Update damage area
if (owner_ != null && owner_.getWindow() != null && components_ != null) {
Region damage = findRegion (components_);
owner_.getWindow().updateDamageArea (damage);
}
// Nullify images
components_ = null;
}
/**
* Check if this segment is visible. This is visibility due to
* geometry position relative to viewport, <em>not</em> due to
* GObject visibility settings.
*
* @return True if segment geometry is visible, false otherwise.
*/
boolean isVisible()
{
return isVisible_;
}
/**
* Check if this segment is filled. The <em>fill</em> property depends
* on the style settings of the segment and it is used to determine
* segment intersections.
*
* @return True of the segment is filled, false otherwise.
*/
boolean isFilled()
{
return actualStyle_.isDefiningFill();
}
/**
* Check if the geometry of this GSegment is inside the specified
* rectangle.
*
* @param x0 X coordinate of upper left corner of rectangle.
* @param y0 Y coordinate of upper left corner of rectangle.
* @param x1 X coordinate of lower right corner of rectangle.
* @param y1 Y coordinate of lower right corner of rectangle.
* @return True if the geometry of this GSegment is completely
* inside the specified rectangle, false otherwise. If
* this GSegment has no geometry, false is returned.
*/
boolean isInsideRectangle (int x0, int y0, int x1, int y1)
{
if (rectangle_ == null) return false;
Box box = new Box (rectangle_);
return box.isInsideOf (new Box (x0, y0, x1, y1));
}
/**
* Check if the geometry of this GSegment intersects the specified
* rectangle.
*
* @param x0 X coordinate of upper left corner of rectangle.
* @param y0 Y coordinate of upper left corner of rectangle.
* @param x1 X coordinate of lower right corner of rectangle.
* @param y1 Y coordinate of lower right corner of rectangle.
* @return True if the geometry of this GSegment intersects
* the specified rectangle, false otherwise. If
* the GSegment has no geometry, false is returned.
*/
boolean isIntersectingRectangle (int x0, int y0, int x1, int y1)
{
if (x_ == null) return false;
return (isFilled() &&
Geometry.isPolygonIntersectingRectangle (x_, y_, x0, y0, x1, y1)) ||
(!isFilled() &&
Geometry.isPolylineIntersectingRectangle (x_, y_, x0, y0, x1, y1));
}
/**
* Check if this GSegment intersects the specified point.
*
* @param x X coordinate of point.
* @param y Y coordinate of point.
* @return True if this GSegment intersects the specified point,
* false otherwise. If the GSegment has no geometry,
* false is returned.
*/
boolean isIntersectingPoint (int x, int y)
{
if (x_ == null) return false;
return (isFilled() &&
Geometry.isPointInsidePolygon (x_, y_, x, y)) ||
(!isFilled() &&
Geometry.isPolylineIntersectingRectangle (x_, y_,
x-1, y-1, x+1, y+1));
}
/**
* Called when the style of this object is changed.
*
* @param style Style that has changed.
*/
public void styleChanged (GStyle style)
{
updateStyle();
}
}