package ika.geo;
import java.awt.geom.*;
import java.awt.*;
import java.io.Serializable;
/**
* GeoObject - an abstract base class for GeoObjects. A GeoObject has a spatial
* extension and can be selected and drawn in a map.
* @author Bernhard Jenny, Institute of Cartography, ETH Zurich.
*/
public abstract class GeoObject implements Serializable, Cloneable {
private static final long serialVersionUID = 3361920857810179938L;
public static final double UNDEFINED_SCALE = -1;
private GeoSet parent;
/**
* Flag that indicates whether this GeoObject has been selected by some user action.
*/
private boolean selected;
/**
* selectable determines whether this GeoObject can be selected.
*/
private boolean selectable;
/**
* visible determines whether this GeoObject should be drawn in a map.
*/
private boolean visible;
/**
* The name of this GeoObject
*/
private String name;
/**
* An ID that is not guaranteed to be unique. Default value is 0.
*/
private long id;
/**
* Protected consructor.
*/
protected GeoObject() {
this.parent = null;
this.selected = false;
this.selectable = true;
this.visible = true;
this.name = null;
this.id = 0;
}
/**
* Returns a copy of this GeoObject. The parent of the copy is null. The ID
* of the copy is the same as the ID of this GeoObject.
* @return A copy.
*/
@Override
public GeoObject clone() {
try {
GeoObject copy = (GeoObject)super.clone();
copy.parent = null;
return copy;
} catch (CloneNotSupportedException exc) {
return null;
}
}
public void cloneIfSelected(GeoSet geoSet) {
if (this.isSelected())
geoSet.add((GeoObject)this.clone());
}
/**
* Returns whether this GeoObject can be selected. An attempt to select an object
* that cannotbe selected using setSelected (true) will have no effect.
* @return True if the object can be selected.
*/
public final boolean isSelectable() {
return this.selectable;
}
/**
* Set the selectable state of this GeoObject.
* @param selectable The new selection state.
*/
public void setSelectable(boolean selectable) {
final boolean change = this.selectable != selectable;
this.selectable = selectable;
if (!selectable && this.selected) {
this.selected = false;
MapEventTrigger.inform(MapEvent.selectionChange(), this);
} else if (change) {
MapEventTrigger.inform(this);
}
}
/**
* Return whether this GeoObject is selected.
* @return The selection state.
*/
public final boolean isSelected() {
return this.selected;
}
/**
* Set the selection state of this GeoObject. A call to select an object
* that cannot be selected (selectable is false) will be ignored.
* @param selected The new selection state.
*/
public void setSelected(boolean selected) {
final boolean newSelection = selected ? this.selectable : false;
if (this.selected != newSelection) {
this.selected = newSelection;
if (this.parent != null)
MapEventTrigger.inform(MapEvent.selectionChange(), this);
}
}
/**
* Returns a bounding box in world coordinates.
*/
abstract public Rectangle2D getBounds2D(double scale);
public Rectangle2D getBounds2D(double scale, boolean onlyVisible,
boolean onlySelected) {
if (onlySelected && !this.isSelected())
return null;
if (onlyVisible && !this.isVisible())
return null;
return this.getBounds2D(scale);
}
/**
* Returns this object if its symbol is under the passed point, null otherwise.
* @param point The point for hit detection.
* @param tolDist The tolerance to use for hit detection in world coordinates.
* @param scale The current scale of the map.
* @return Returns the GeoObject if its symbol is hit by the passed point,
* null otherwise.
*/
public GeoObject getObjectAtPosition(Point2D point,
double tolDist,
double scale,
boolean onlySelectable,
boolean onlyVisible) {
// filter invisible GeoObjects
if (onlyVisible && !this.isVisible())
return null;
// filter GeoObjects that are not selectable
if (onlySelectable && !this.isSelectable())
return null;
// return this GeoObject if the point is on its symbol
return this.isPointOnSymbol(point, tolDist, scale) ? this : null;
}
public double getCenterX(double scale) {
final Rectangle2D bbox = this.getBounds2D(scale);
return bbox.getCenterX();
}
public double getCenterY(double scale) {
final Rectangle2D bbox = this.getBounds2D(scale);
return bbox.getCenterY();
}
/**
* Draw this GeoObject into a Graphics2D object. This method is only called
* if the object is visible.
* @param rp Parameters for rendering.
*/
abstract public void drawNormalState(RenderParams rp);
/**
* Draw this GeoObject as it appears when it is selected.
* This method is only called if the object is visible. Overwriting methods
* must make sure the object is selected before drawing the selected state.
* The foreground color in rp.g2d is set to a highlighting color; the stroke
* width is set to a scale-independent thin width; the affine transformation
* is set to transform selected objects if required; rendering hints are
* set for fast not anti-aliased rendering.
* Important: THESE SETTINGS MUST NOT BE CHANGED!
* @param rp Parameters for rendering.
*/
abstract public void drawSelectedState(RenderParams rp);
/**
* Tests whether a point hits the graphical representation of this GeoObject.
* @param point The point to test.
* @param tolDist The tolerance distance for hit detection.
* @param scale The current scale of the map.
* @return Returns true if the passed point hits this GeoObject.
*/
abstract public boolean isPointOnSymbol(Point2D point, double tolDist,
double scale);
abstract public boolean isIntersectedByRectangle(Rectangle2D rect, double scale);
/**
* Select this GeoObject if it intersects with a rectangle.
* @param rect The rectangle to test.
* @param scale The current scale of the map.
* @param extendSelection If true, this GeoObject is not deselected if the
* rectangle does not interesect with it. Otherwise, the GeoObject is deselected.
*/
public boolean selectByRectangle(Rectangle2D rect, double scale,
boolean extendSelection) {
// don't do anything if this object cannot be selected.
if (!this.selectable)
return false;
// remember the inital selection state
final boolean initialSelection = this.isSelected();
// default is that the rectangle does not intersect with the object.
boolean newSelection = extendSelection && initialSelection;
// test if this object is hit by the passed point.
if (this.isIntersectedByRectangle(rect, scale) == true) {
// The object is hit and already selected.
// Toggle selection state if needed.
if (this.isSelected())
newSelection = !extendSelection;
else // object is hit but not selected yet.
newSelection = true;
}
this.setSelected(newSelection);
return initialSelection != newSelection;
// trigger MapEventTrigger.inform(this); ? !!! ???
}
/**
* Select this GeoObject if it is hit by a passed point.
* @param point The point to test.
* @param scale The current scale of the map.
* @param extendSelection If true, this GeoObject is not deselected if the
* point does not hit this object. Otherwise, it is deselected.
* @return Returns true if the selection state has been changed
*/
public boolean selectByPoint(Point2D point, double scale,
boolean extendSelection, double tolDist) {
// don't do anything if this object cannot be selected.
if (!this.selectable || !this.visible)
return false;
// remember the inital selection state
final boolean initialSelection = this.isSelected();
// default is that the point does not hit the object.
boolean newSelection = extendSelection && initialSelection;
// test if this object is hit by the passed point.
if (this.isPointOnSymbol(point, tolDist, scale) == true) {
// Toggle selection state if needed.
if (this.isSelected()) {
// The object is hit and already selected.
newSelection = !extendSelection;
} else {
// object is hit but not selected yet.
newSelection = true;
}
}
this.setSelected(newSelection);
return initialSelection != newSelection;
// trigger MapEventTrigger.inform(this); ? !!! ???
}
/**
* Move this object by a specified amount horizontally and vertically.
* This default implementation uses transform(AffineTransform). Derived
* classes that can provide a better solution can overwrite this method.
* @param dx The distance by which this object should be moved in horizontal
* direction.
* @param dy The distance by which this object should be moved in vertical
* direction.
*/
public void move(double dx, double dy) {
this.transform(AffineTransform.getTranslateInstance(dx, dy));
}
/**
* Move this object if it is selected by a specified amount horizontally
* and vertically.
* @param dx The distance by which this object should be moved in horizontal
* direction.
* @param dy The distance by which this object should be moved in vertical
* direction.
* @return True if the object has been moved.
*/
public boolean moveSelected(double dx, double dy) {
if (this.isSelected()) {
this.move(dx, dy);
return dx != 0 || dy != 0;
}
return false;
}
/**
* Clone this object if it is selected and move it by a specified amount
* horizontally and vertically. This object is deselected, the new clone
* is selected.
* @param dx The distance by which this object should be moved in horizontal
* direction.
* @param dy The distance by which this object should be moved in vertical
* direction.
* @return True if the object has been moved.
*/
public boolean cloneAndMoveSelected(double dx, double dy) {
if (this.parent == null)
return false;
MapEventTrigger trigger = new MapEventTrigger(this);
try {
if (this.isSelected() && (dx != 0 || dy != 0)) {
GeoObject clone = (GeoObject)this.clone();
clone.move(dx, dy);
this.setSelected(false);
this.parent.add(clone);
return true;
}
return false;
} finally {
trigger.inform();
}
}
/**
* Scale this object by a factor relative to the origin of the coordinate
* system. This default implementation uses transform(AffineTransform).
* Derived classes that can provide a better solution can overwrite this method.
* @param scale Scale factor.
*/
public void scale(double scale) {
this.transform(AffineTransform.getScaleInstance(scale, scale));
}
/**
* Scale this GeoObject horizontally and vertically.
* This default implementation uses transform(AffineTransform). Derived
* classes that can provide a better solution can overwrite this method.
* Attention: not all derived classes support scaling, or uneven scaling.
* @param hScale The horizontal scale factor.
* @param vScale The vertical scale factor.
*/
public void scale(double hScale, double vScale) {
this.transform(AffineTransform.getScaleInstance(hScale, vScale));
}
/**
* Scale this object by a factor relative to a passed origin.
* Derived classes that can provide a more efficient solution can overwrite
* this method.
* @param scale Scale factor.
* @param cx The x coordinate of the point relativ to which the object is scaled.
* @param cy The y coordinate of the point relativ to which the object is scaled.
*/
public void scale(double scale, double cx, double cy) {
this.move(-cx, -cy);
this.scale(scale);
this.move(cx, cy);
}
/**
* Scale this object if it is selected.
* @param hScale The horizontal scale factor.
* @param vScale The vertical scale factor.
* @return True if the object has been scaled.
*/
public boolean scaleSelected(double hScale, double vScale) {
if (this.isSelected()) {
this.scale(hScale, vScale);
return hScale != 1 || vScale != 1;
}
return false;
}
/**
* Rotate this object around the origin of the coordinate system in
* counter-clockwise direction.
* This default implementation uses transform(AffineTransform). Derived
* classes that can provide a better solution can overwrite this method.
* Attention: not all derived classes support rotating.
*/
public void rotate(double rotRad) {
this.transform(AffineTransform.getRotateInstance(rotRad));
}
abstract public void transform(AffineTransform affineTransform);
/**
* Transform this object if it is selected.
* @param affineTransform The affine transformation to apply.
* @return True if the object has been transformed.
*/
public boolean transformSelected(AffineTransform affineTransform) {
if (this.isSelected() && affineTransform.isIdentity() == false) {
this.transform(affineTransform);
return true;
}
return false;
}
/**
* Rotate this object around a point in counter-clockwise direction.
* @param rotRad The rotation angle.
*/
public void rotateAroundPoint(double rotRad, double x, double y) {
MapEventTrigger trigger = new MapEventTrigger(this);
try {
this.move(-x, -y);
this.rotate(rotRad);
this.move(x, y);
} finally {
trigger.inform();
}
}
/**
* Returns whether this GeoObject is currently visible.
* @return True if visible.
*/
public boolean isVisible() {
return visible;
}
/**
* Show or hide an object.
* @param visible True: the object should be made visible.
*/
public void setVisible(boolean visible) {
final boolean change = this.visible != visible;
this.visible = visible;
if (change)
MapEventTrigger.inform(MapEvent.visibilityChange(), this);
}
/**
* Returns the name of this GeoObject.
* @return The name of this GeoObject.
*/
public String getName() {
return name;
}
/**
* Changes the name of this GeoObject.
* @param name The new name of this object. Can be null.
*/
public void setName(String name) {
final boolean change;
if (name == null)
change = this.name != null;
else
change = !name.equals(this.name);
this.name = name;
if (change)
MapEventTrigger.inform(this);
}
/**
* Returns this GeoObject if its name is equal to the passed name.
* Returns null otherwise.
* @param name The name of the object that is searched.
* @return A reference to this GeoObject if the passed name equals its name.
*/
public synchronized GeoObject getGeoObject(String name) {
return name.equals(this.getName()) ? this : null;
}
/**
* Returns a string representation of this GeoObject, which is simply the
* name.
* @return The name of this GeoObject.
*/
@Override
public String toString() {
String n = this.getName();
return n != null ? n : "<unnamed>";
}
/**
* Returns the ID of this GeoObject.
* @param The ID.
*/
public long getID() {
return id;
}
/**
* Change the ID of this GeoObject.
* Important: It is not checked whether the new ID is unique!
* @param id The new ID.
*/
public void setID(long id) {
final boolean change = this.id != id;
this.id = id;
if (change)
MapEventTrigger.inform(this);
}
/**
* Returns the parent GeoSet, i.e. the GeoSet that contains this GeoObject.
* @return The parent GeoSet or null if this object is not contained by
* any GeoSet.
*/
public GeoSet getParent() {
return parent;
}
/**
* Set the parent of this GeoObject. The parent is the GeoSet that contains
* this GeoObject.
* This method does not trigger an event to inform MapEventListeners.
* @param parent The parent GeoSet.
*/
protected void setParent(GeoSet parent) {
this.parent = parent;
// don't inform MapEventListeners. setParent is only called from
// methods that change the structure of the tree of GeoObjects and
// inform MapEventListeners themselves.
}
/**
* Returns the GeoTreeRoot of the tree of GeoObjects. Returns null if the
* tree has not a GeoTreeRoot as root. Returns null if this object has no
* parent.
*
* @return The GeoSetBroadcaster of the tree, which is the topmost GeoSet of the tree.
*/
protected GeoTreeRoot getRoot() {
return this.parent == null ? null : this.parent.getRoot();
}
/**
* Returns an array with all parent GeoSets of this GeoObject and the object
* itself.
* @return An array of all parent GeoSets and this object. The first object
* in the array is this object, the parents follow. The last element is the root.
*/
public Object[] getTreePath() {
if (this.parent == null)
return new GeoObject[] { this };
java.util.ArrayList path = new java.util.ArrayList(4);
this.constructTreePath(path);
return path.toArray();
}
/**
* Constructs a chain of all parent GeoSets and this object itself.
* Recursevly calls the parent GeoSets until it reaches the root of the tree.
* @param path The ArrayList that will contain the complete path.
*/
protected void constructTreePath(java.util.ArrayList path) {
path.add(this);
if (this.parent != null)
this.parent.constructTreePath(path);
}
}