/*
* Copyright 2011 by Mark Coletti, Keith Sullivan, Sean Luke, and
* George Mason University Mason University Licensed under the Academic
* Free License version 3.0
*
* See the file "LICENSE" for more information
*
* $Id$
*/
package sim.field.geo;
import com.vividsolutions.jts.algorithm.ConvexHull;
import com.vividsolutions.jts.geom.*;
import com.vividsolutions.jts.geom.prep.PreparedGeometryFactory;
import com.vividsolutions.jts.geom.prep.PreparedPoint;
import com.vividsolutions.jts.geom.prep.PreparedPolygon;
import com.vividsolutions.jts.index.quadtree.Quadtree;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import sim.engine.SimState;
import sim.engine.Steppable;
import sim.portrayal.DrawInfo2D;
import sim.util.Bag;
import sim.util.geo.AttributeValue;
import sim.util.geo.GeometryUtilities;
import sim.util.geo.MasonGeometry;
/**
* A GeomVectorField contains one or more MasonGeometry objects. The field
* stores the geometries as a quadtree and used the quadtree during various
* queries. As objects are inserted into the field, the minimum bounding
* rectangle (MBR) is expanded to include the new object. This allows a
* determination of the area of the field.
* <p/>
* <p>Note that the field assumes the geometries use the same coordinate system.
*/
public class GeomVectorField extends GeomField
{
private static final long serialVersionUID = -754748817928825708L;
/**
* A spatial index of all the geometries in the field.
*/
private Quadtree spatialIndex = new Quadtree();
/**
* Redundant container of MasonGeometry used to quickly rebuild spatial
* index and save some overhead with regards to returning all MasonGeometry.
*
* @see updateSpatialIndex()
* @see getGeometries()
*/
private Bag geometries = new Bag();
/**
* The convex hull of all the geometries in this field
*/
private PreparedPolygon convexHull;
/**
* Helper factory for computing the union or convex hull
*/
private GeometryFactory geomFactory;
/**
* Defines the outer shell of all the geometries within this field
*/
private PreparedPolygon globalUnion;
/**
* Is true if the spatial index needs to be rebuilt
* <p/>
* @see removeGeometry()
* @see clear()
* @see updateSpatialIndex()
* @see setGeometryLocation()
*/
private boolean needToRebuildIndex = false;
public GeomVectorField()
{
this(0, 0);
}
/**
* @param w, the field width in display units for managing scale changes
* @param h, the field height in display units for managing scale changes
*/
public GeomVectorField(int w, int h)
{
super(w, h);
geomFactory = new GeometryFactory();
}
/**
* Adds the MasonGeometry to the field and also expands the MBR
*/
public void addGeometry(final MasonGeometry g)
{
Envelope e = g.getGeometry().getEnvelopeInternal();
MBR.expandToInclude(e);
spatialIndex.insert(e, g);
geometries.add(g);
}
/**
* Removes the given geometry
* <p>
* <em>NOTE:</em> removing geometry can be computationally expensive as the
* significant chunks of the spatial index may have to be rebuilt depending
* on the removed geometry. Therefore the spatial index is not updated to
* reflect that the geometry no longer exists. You must explicitly invoke
* updateSpatialIndex() to get the spatial index in sync.
*/
public void removeGeometry(final MasonGeometry g)
{
// spatialIndex.remove(g.getGeometry().getEnvelopeInternal(), g);
geometries.remove(g); // TODO: O(n); maybe store Bag index w/ g?
needToRebuildIndex = true;
}
/**
* Removes all geometry objects and resets the MBR.
*/
@Override
public void clear()
{
super.clear();
spatialIndex = new Quadtree();
geometries.clear();
needToRebuildIndex = false;
}
/**
* Computes the convex hull of all the geometries in this field. Call this
* method once.
*/
public void computeConvexHull()
{
ArrayList<Coordinate> pts = new ArrayList<Coordinate>();
// List<?> gList = spatialIndex.queryAll();
if (geometries.isEmpty())
{
return;
}
// Accumulate all the Coordinates in all the geometry into 'pts'
for (int i = 0; i < geometries.size(); i++)
{
Geometry g = ((MasonGeometry) geometries.get(i)).getGeometry();
Coordinate c[] = g.getCoordinates();
pts.addAll(Arrays.asList(c));
}
// ConvexHull expects a vector of Coordinates, so now convert
// the array list of Coordinates into a vector
Coordinate[] coords = pts.toArray(new Coordinate[pts.size()]);
ConvexHull hull = new ConvexHull(coords, geomFactory);
this.convexHull = new PreparedPolygon((Polygon) hull.getConvexHull());
}
/**
* Determine if the Coordinate is within the convex hull of the field's
* geometries. Call computeConvexHull first.
*/
public boolean isInsideConvexHull(final Coordinate coord)
{
Point p = geomFactory.createPoint(coord);
// XXX is intersects() correct? Would covers be appropriate?
if (convexHull.intersects(p))
{
return true;
}
return false;
}
/**
* Compute the union of the field's geometries. The resulting Geometry is
* the outside points of the field's geometries. Call this method only once.
*/
public void computeUnion()
{
Geometry p = new Polygon(null, null, geomFactory);
// List<?> gList = spatialIndex.queryAll();
if (geometries.isEmpty())
{
return;
}
for (int i = 0; i < geometries.size(); i++)
{
Geometry g = ((MasonGeometry) geometries.get(i)).getGeometry();
p = p.union(g);
}
p = p.union();
globalUnion = new PreparedPolygon((Polygon) p);
}
/**
* Determine if the Coordinate is within the bounding Geometry of the
* field's geometries.
* <p>
* Call computeUnion() first.
*/
public boolean isInsideUnion(final Coordinate point)
{
Point p = geomFactory.createPoint(point);
if (globalUnion.intersects(p))
{
return true;
}
return false;
}
/**
* @return all the geometries that intersect the provided envelope; will be
* empty if none intersect
*/
public synchronized Bag queryField(Envelope e)
{
List<?> gList = spatialIndex.query(e);
Bag geometries = new Bag(gList.size());
// However, the JTS QuadTree query is a little sloppy, which means it
// may return objects that are still outside the range. We need to do
// a second pass to trim out the objects that are further than distance.
for (int i = 0; i < gList.size(); i++)
{
MasonGeometry tempGeometry = (MasonGeometry) gList.get(i);
if (e.intersects(tempGeometry.getGeometry().getEnvelopeInternal()))
{
geometries.add(tempGeometry);
}
}
return geometries;
}
/**
* Returns Bag of all the field's geometry objects.
* <p>
* Do not modify the Bag, nor the Geometries inside the bag, as this will
* have undefined consequences for drawing and inspecting.
*/
public Bag getGeometries()
{
return geometries;
}
/**
* Returns a bag containing all those objects within distance of the given
* geometry. The distance calculation follows the JTS convention, which
* determines the distance between the closest points of two geometries. Do
* not modify the returned Bag.
*/
public Bag getObjectsWithinDistance(final Geometry g, final double dist)
{
Bag nearbyObjects = new Bag();
Envelope e = g.getEnvelopeInternal();
e.expandBy(dist);
List<?> gList = spatialIndex.query(e);
// However, the JTS QuadTree query is a little sloppy, which means it
// may return objects that are still outside the range. We need to do
// a second pass to trim out the objects that are further than distance.
for (int i = 0; i < gList.size(); i++)
{
MasonGeometry tempGeometry = (MasonGeometry) gList.get(i);
if (g.isWithinDistance(tempGeometry.getGeometry(), dist))
{
nearbyObjects.add(tempGeometry);
}
}
return nearbyObjects;
}
public Bag getObjectsWithinDistance(final MasonGeometry mg, final double dist)
{
return getObjectsWithinDistance(mg.getGeometry(), dist);
}
/**
* Returns geometries that cover the given object. Cover here means
* completely cover, including points on the boundaries. Do not modify the
* returned Bag.
*/
public final Bag getCoveringObjects(final Geometry g)
{
Bag coveringObjects = new Bag();
Envelope e = g.getEnvelopeInternal();
List<?> gList = spatialIndex.query(e);
for (int i = 0; i < gList.size(); i++)
{
MasonGeometry gm = (MasonGeometry) gList.get(i);
Geometry g1 = gm.getGeometry();
if (!g.equals(g1) && g1.covers(g))
{
coveringObjects.add(gm);
}
}
return coveringObjects;
}
public final Bag getCoveringObjects(final MasonGeometry mg)
{
return getCoveringObjects(mg.getGeometry());
}
/**
* Return geometries that are covered by the given geometry.
* <p>
* Do not modify the returned Bag.
* <p>
* XXX Could be made more efficient by using spatial index to narrow
* candidates.
*/
public final Bag getCoveredObjects(MasonGeometry g)
{
Bag coveringObjects = new Bag();
if (g.preparedGeometry == null)
{
g.preparedGeometry = PreparedGeometryFactory.prepare(g.getGeometry());
}
for (int i = 0; i < geometries.size(); i++)
{
MasonGeometry gm = (MasonGeometry) geometries.get(i);
Geometry g1 = gm.getGeometry();
if (g.preparedGeometry.covers(g1))
{
coveringObjects.add(gm);
}
}
return coveringObjects;
}
/**
* Returns geometries that contain the given object.
* <p>
* Contain is more exclusive than cover and doesn't include things on the
* boundary. Do not modify the returned Bag.
*/
public final Bag getContainingObjects(final Geometry g)
{
Bag containingObjects = new Bag();
Envelope e = g.getEnvelopeInternal();
List<?> gList = spatialIndex.query(e);
for (int i = 0; i < gList.size(); i++)
{
MasonGeometry gm = (MasonGeometry) gList.get(i);
Geometry g1 = gm.getGeometry();
if (!g.equals(g1) && g1.contains(g))
{
containingObjects.add(gm);
}
}
return containingObjects;
}
public final Bag getContainingObjects(final MasonGeometry mg)
{
return getContainingObjects(mg.getGeometry());
}
/**
* Returns geometries that touch the given geometry.
* <p>
* Do not modify the returned Bag.
*/
public final Bag getTouchingObjects(MasonGeometry mg)
{
Bag touchingObjects = new Bag();
Envelope e = mg.getGeometry().getEnvelopeInternal();
e.expandBy(java.lang.Math.max(e.getHeight(), e.getWidth()) * 0.01);
List<?> gList = spatialIndex.query(e);
if (mg.preparedGeometry == null)
{
mg.preparedGeometry = PreparedGeometryFactory.prepare(mg.getGeometry());
}
for (int i = 0; i < gList.size(); i++)
{
MasonGeometry gm = (MasonGeometry) gList.get(i);
Geometry g1 = gm.getGeometry();
if (!mg.equals(gm) && mg.getGeometry().touches(g1))
{
touchingObjects.add(gm);
}
}
return touchingObjects;
}
/**
* Returns true if the given Geometry is covered by any geometry in the
* field.
* <p>
* Cover here includes points in the boundaries.
*/
public boolean isCovered(MasonGeometry g)
{
Envelope e = g.getGeometry().getEnvelopeInternal();
List<?> gList = spatialIndex.query(e);
if (g.preparedGeometry == null)
{
g.preparedGeometry = PreparedGeometryFactory.prepare(g.getGeometry());
}
for (int i = 0; i < gList.size(); i++)
{
Geometry g1 = ((MasonGeometry) gList.get(i)).getGeometry();
if (!g.equals(g1) && g.preparedGeometry.covers(g1))
{
return true;
}
}
return false;
}
/**
* Returns true if the coordinate is within any geometry in the field.
* <p>
* However, it offers no guarantee for points on the boundaries. Use this
* version if you want to check if an agent is within a geometry; its
* roughly an order of magnitude faster than using the Geometry version.
*/
public boolean isCovered(final Coordinate point)
{
Envelope e = new Envelope(point);
List<?> gList = spatialIndex.query(e);
PreparedPoint p = new PreparedPoint(geomFactory.createPoint(point));
for (int i = 0; i < gList.size(); i++)
{
Geometry g1 = ((MasonGeometry) gList.get(i)).getGeometry();
if (p.intersects(g1))
{
return true;
}
}
return false;
}
/**
* Get the centroid of the given Geometry.
* <p>
* Note that the returned location uses the coordinate system of the
* underlying GIS data.
*/
public Point getGeometryLocation(MasonGeometry g)
{
MasonGeometry g1 = findGeometry(g);
if (g1.equals(g))
{
return g1.geometry.getCentroid();
}
return null;
}
/**
* Moves the centroid of the given geometry to the provided point.
* <p>
* <em>Note</em> that the spatial index is not notified of the geometry
* changes. It is strongly recommended that updateSpatialIndex() be invoked
* after all geometry position changes.
* <p/>
* @see GeomVectorField#updateSpatialIndex()
*/
public void setGeometryLocation(MasonGeometry g, CoordinateSequenceFilter p)
{
MasonGeometry g1 = findGeometry(g);
if (g1 != null)
{
// 1/8/2013, spatial index no longer updated; use updateSpatialIndex()
// spatialIndex.remove(g1.getGeometry().getEnvelopeInternal(), g1);
g1.geometry.apply(p);
g1.geometry.geometryChanged();
// spatialIndex.insert(g1.geometry.getEnvelopeInternal(), g1);
}
needToRebuildIndex = true;
}
/**
* Rebuild the spatial index from the current set of geometry
* <p>
* If the objects contained in this field have moved, then the spatial index
* will have to be updated. This is done by replacing the current spatial
* index with an entirely new one built from the same stored geometry.
*/
public void updateSpatialIndex()
{
if (needToRebuildIndex)
{
spatialIndex = new Quadtree();
for (int i = 0; i < geometries.size(); i++)
{
spatialIndex.insert(((MasonGeometry) geometries.get(i)).geometry.getEnvelopeInternal(), geometries.get(i));
}
needToRebuildIndex = false;
}
}
/**
* Schedules a repeating Steppable that updates spatial index
* <p>
* The spatial index for a GeomVectorField containing moving objects will
* need to be updated after all such objects have moved. This method returns
* a Steppable that invokes updateSpatialIndex() that does this.
* <p/>
* @return a Steppable that can be used to remove this Steppable from the
* schedule
*/
public Steppable scheduleSpatialIndexUpdater()
{
return new Steppable()
{
public void step(SimState state)
{
updateSpatialIndex();
}
};
}
/**
* Locate a specific geometry inside the quadtree
* <p>
* XXX Is returning what we're looking for when the target geometry is not
* found the desired behavior?
* <p/>
* @param g is geometry for which we're looking
* <p/>
* @return located geometry; will return g if not found.
*/
public synchronized MasonGeometry findGeometry(MasonGeometry g)
{
List<?> gList = spatialIndex.query(g.getGeometry().getEnvelopeInternal());
for (int i = 0; i < gList.size(); i++)
{
MasonGeometry g1 = ((MasonGeometry) gList.get(i));
if (g1.equals(g))
{
return g1;
}
}
return g;
}
// Deprecated. Removed 4/12/2013.
// public void updateTree(Geometry g, com.vividsolutions.jts.geom.util.AffineTransformation at)
// {
// MasonGeometry mg = new MasonGeometry(g);
// if (spatialIndex.remove(g.getEnvelopeInternal(), mg))
// {
// mg.geometry.apply(at);
// addGeometry(mg);
// }
//
// dirty = true;
// }
/**
* Searches the field for the first object with attribute <i>name</i> that
* has value <i>value</i>.
* <p/>
* @param name of attribute
* @param value of attribute
* <p/>
* TODO What if there is more than one such object?
* <p/>
* @return MasonGeometry with specified attribute otherwise null
*/
public MasonGeometry getGeometry(String name, Object value)
{
AttributeValue key = new AttributeValue(name);
for (int i = 0; i < geometries.size(); i++)
{
MasonGeometry mg = (MasonGeometry) geometries.get(i);
if (mg.hasAttribute(name) && mg.getAttribute(name).equals(value))
{
return mg;
}
}
return null;
}
public Envelope clipEnvelope;
DrawInfo2D myInfo;
public AffineTransform worldToScreen;
public com.vividsolutions.jts.geom.util.AffineTransformation jtsTransform;
public void updateTransform(DrawInfo2D info)
{
//if (info.draw.width == 4800)
// System.out.println("here");
// need to update the transform
if (!info.equals(myInfo))
{
myInfo = info;
// compute the transform between world and screen coordinates, and
// also construct a geom.util.AffineTransform for use in hit-testing
// later
Envelope myMBR = getMBR();
worldToScreen = GeometryUtilities.worldToScreenTransform(myMBR, info);
jtsTransform = GeometryUtilities.getPortrayalTransform(worldToScreen, this, info.draw);
Point2D p1 = GeometryUtilities.screenToWorldPointTransform(worldToScreen, info.clip.x, info.clip.y);
Point2D p2 = GeometryUtilities.screenToWorldPointTransform(worldToScreen, info.clip.x + info.clip.width,
info.clip.y + info.clip.height);
clipEnvelope = new Envelope(p1.getX(), p2.getX(), p1.getY(), p2.getY());
}
}
}