/* * GeoTools java GIS tookit (c) The Centre for Computational Geography 2002 * * This library 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 version 2.1 */ package uk.ac.leeds.ccg.geotools; import java.lang.*; import java.awt.*; import java.util.*; /** * This class is intended for storing georaphical polygons such as boundary data * for wards, coastlines etc.

* * $Log: GeoPolygon.java,v $ * Revision 1.22 2002/03/15 15:04:15 jmacgill * removed debug statements from calculateArea method * * Revision 1.21 2002/03/15 14:36:44 jmacgill * area calculations now itterate though all parts * area caluclation call removed from one of the constructors * * Revision 1.20 2002/01/19 17:40:30 loxnard * Fixed JavaDoc comments. * * * @author James Macgill * @author Mathieu Van Loon - getPointsAsArrayX, getPointsAsArrayY, dropVector * @version $Revision: 1.22 $ $Date: 2002/03/15 15:04:15 $ * @since 0.5.0 */ public class GeoPolygon extends GeoShape { /** * Control debugging. For now, either turn it off or on. */ private final static boolean DEBUG=false; /** * Performance switch, to turn off use of Vector. Use with CARE */ private boolean useVector = true; /** * Polygon centroid coordinates. */ private double xcent,ycent; /** * Number of points in polygon. */ public int npoints; /** * Area of polygon. */ private double area; /** * Array of points. */ public double xpoints[]; /** * Array of points. */ public double[] ypoints; private Vector storedPoints = null; private boolean pointsStored = false; private boolean areaDone,centroidDone; /** * Probably redundant constant. */ public static final int OUTSIDE = -1; /** * Probably redundant constant. */ public static final int NA = Integer.MAX_VALUE; /** * Create an empty polygon. */ public GeoPolygon(){ if ( useVector ) storedPoints = new Vector(); setBounds(new GeoRectangle()); } //Almost Empty /** * Construct a polygon with full details. * * @param id Polygon ID. * @param xcent X-coordinate of centroid. * @param ycent Y-coordinate of centroid. * @param xpoints Vector of x values in Doubles (npoints in size). * @param ypoints Vector of y values in Doubles (npoints in size) where npoints = number of points. */ public GeoPolygon(int id,double xcent,double ycent,Vector xpoints,Vector ypoints) { if ( useVector ) storedPoints = new Vector(); this.id = id; this.npoints = xpoints.size(); //System.out.println("Building with "+npoints); this.xpoints = new double[npoints]; this.ypoints = new double[npoints]; for(int i=0;i 0) { double xtemp[]; double ytemp[]; xtemp = xpoints; ytemp = ypoints; xpoints = new double[npoints+1]; System.arraycopy(xtemp,0,xpoints,0,npoints); xtemp=null; ypoints = new double[npoints+1]; System.arraycopy(ytemp,0,ypoints,0,npoints); ytemp=null; } else { xpoints = new double[1]; ypoints = new double[1]; } npoints++; xpoints[npoints-1] = x;//-1 to account for 0 array indexing ypoints[npoints-1] = y; if(useVector)storedPoints.addElement(new GeoPoint(x,y));//is this risky? extendBounds(x,y); //calculateArea();//TODO JM:v.wastefull, save until a area request is made? //calculateCentroidLocation();//TODO JM:v.wasteful, save until an area request is made? areaDone=centroidDone=false; } /** * Add a vertex to the polygon. * * @param p A GeoPoint to add. */ public void addPoint(GeoPoint p){ addPoint(p.x,p.y); } /** * Returns a standard AWT.Polygon from the GeoPolygon. * Note The GeoPolygon uses doubles, the Polygon uses ints, * so there will be precision loss. * @return Standard AWT.Polygon. */ public Polygon toAWTPolygon() { int x[] = new int[npoints]; int y[] = new int[npoints]; // This next bit might throw a number exception put in try{} ? for(int i=0;i * Value is currently generated per request. * @return double The perimeter of this polygon. */ public double getPerimeter(){ double dist = 0; int i; for(i=0;i * Automatically calculated centroids are not guaranteed to be inside the * polygon. Instead, they represent the polygon's centre of mass. * @return GeoPoint The centroid of this polygon. */ public GeoPoint getCentroidLocation(){ if(!centroidDone){ calculateCentroidLocation(); } return new GeoPoint(xcent,ycent); } /** * Return the number of points that make the polygon. * @return Number of points. */ public int getNPoints(){ return npoints; } /** * Get the geopoints as an array of x. * @return Geopoints as array of x. */ public double[] getPointsAsArrayX() { return xpoints; } /** * Get the geopoints as an array of y. * @return Geopoints as an array of y. */ public double[] getPointsAsArrayY() { return ypoints; } /** * Drop the use of a Vector to store the points. This method * is available for performance reasons. You will only want to use * this method if you store a lot of GeoPolygons and you wish * to decrease the memory load. */ public void dropVector() { useVector = false; storedPoints = null; } /** * Points which make up this polygon stored in a Vector. * @return A Vector of points which make up the polygon. */ public final Vector getPoints(){ if(useVector) { return storedPoints; } else { Vector reply = new Vector(npoints); for(int i=0;i * Anti-clockwise polygons often represent holes.
* This is computed on the fly, so might be expensive.
* Adapted from C function written by Paul Bourke. * @return boolean True if polygon is clockwise. */ public boolean isClockwise(){ if (npoints < 3) return false; int j,k,count=0; double z; for (int i=0;i 0) count++; } if (count > 0) return false; else if (count < 0) return true; else return false;//coliniar points? } /** * Calculates the area of the polygon. * The result is stored in the private member variable area.
* Must be called during construction and after any changes to the polygon's pointset. * N.B. Polygons with anti-clockwise point storage will result in a negative area. */ private void calculateArea(){ //move polygon to origin to remove negative y values and reduce coordinate sizes GeoRectangle box = getBounds(); double xShift = 0-box.x; double yShift = 0-box.y; double area; double total =0; //Calculate area of each trapezoid formed by droping lines from each pair of point to the x-axis. //x[i] * The result is stored in the private member variables, xcent and ycent, * overwriting any values that may already be stored there.
* Area must be set before calling this method, e.g. a call to calculateArea should have been made * during construction.
* Polygons must have clockwise point encoding in order to have a centroid. Holes (encoded * with anti-clockwise points) have undefined behaviour at this point. * N.B. The centroid will be the 'centre of mass' but this is not guaranteed to be inside the * polygon. * @see uk.ac.leeds.ccg.geotools.GeoPolygon#calculateArea. */ protected void calculateCentroidLocation(){ GeoRectangle box = getBounds(); double xShift = 0-box.x; double yShift = 0-box.y; if(!areaDone)calculateArea(); if(area == 0){ //test and approx fix for polygons that are effectively lines or points. xcent = (box.x+(box.width/2d)); ycent = (box.y+(box.height/2d)); } else { double total =0; double x=0,y=0,x1,x2,y1,y2; for(int i=0;iThis code is somewhat experimental and has not been fully tested. * @param gp The GeoPolygon to test for contiguity. * @return boolean True if this polygon and gp share two or more vertices that are consecutive in both polygons. */ public boolean isContiguous(GeoPolygon gp){ GeoRectangle bBox = getBounds(); GeoRectangle safe = new GeoRectangle(bBox.x-1,bBox.y-1,bBox.width+2,bBox.height+2); if(!gp.getBounds().intersects(safe))return false; Vector a = getPoints(); Vector b = gp.getPoints(); int aLast = a.size()-2; // one for 0 counting one for duplicate final point. int bLast = b.size()-2; for(int i=0;i<=aLast;i++){ if(b.contains(a.elementAt(i))){ int indexB = b.indexOf(a.elementAt(i)); int nextA = (i+1)%(a.size()-1); if(b.contains(a.elementAt(nextA))){ int nextB = b.indexOf(a.elementAt(nextA)); if((indexB == 0 && nextB == bLast) || (indexB == bLast && nextB==0)) return true;//wrap round end. if(Math.abs(indexB-nextB)==1)return true;//no break in chain //keep trying. } } } if(subParts!=null){ for(int i=0;iThis code is somewhat experimental and has not been fully tested. * @return double The total length of all shared arcs between the two polygons. Returns as 0 if there are no shared arcs. */ public double getContiguousLength(GeoPolygon gp){ double dist = 0; //int start = 0; Vector a = getPoints(); Vector b = gp.getPoints(); int last = a.size()-1;//0 count GeoPoint start = (GeoPoint)a.elementAt(0); GeoPoint end; for(int i=0;iThis code is somewhat experimental and has not been fully tested. * @param p1 The first point to test. * @param p2 The second point to test. * @return boolean True if p1 follows p2 or p2 follows p1. False in any other case, including p1 and/or p2 being absent from the polygon. * */ public final boolean areConsecutive(GeoPoint p1,GeoPoint p2){ int i1 = storedPoints.indexOf(p1); if(i1== -1) return false; int i2 = storedPoints.indexOf(p2); if( i2 == -1)return false; if(Math.abs(i2-i1)==1){return true;} if(i1 == storedPoints.size()-2 && i2 ==0) return true; if(i2 == storedPoints.size()-2 && i1 ==0) return true; if(subParts!=null){ for(int i=0;iThis code is somewhat experimental and has not been fully tested. * @param polys A Vector of GeoPolgyons to test against for contiguity. * @return Vector A sub-set of polys that contains only the polygons that are adjacent to this one. */ public Vector getContiguityList(Vector polys){ //build contiguity list Vector list = new Vector(); for(int i=0;ip1 and p2 SHOULD be points that are in this polygon. *
This code is somewhat experimental and has not been fully tested. * @param p1 GeoPoint The first endpoint of the line to test. * @param p2 GeoPoint The second endpoint of the line to test. * @param polys A Vector of polygons to test the line against for inside/outside ness. * @return boolean True if external. */ public boolean isOnOutside(GeoPoint p1,GeoPoint p2,Vector polys){ boolean outside = true; for(int j=0;jThis code is somewhat experimental and has not been fully tested. * @param polys A Vector of polygons to test against. * @return boolean True if external. */ public boolean isOnOutside(Vector polys){ Vector points = getPoints(); boolean done = false; for(int i=0;iThis code is somewhat experimental and has not been fully tested. * In particular, if the polygons do not define a single clump then it will not work as expected. * @param polys A Vector of GeoPolgyons to dissolve. * @return GeoPolygon The dissolved version of the polys set. */ public static GeoPolygon dissolve(Vector polys){ return dissolve(0,polys); } /** * A static method that will dissolve all internal borders within the provided set to create a single polygon * which describes the outline of all the given polygons combined. * *
This code is somewhat experimental and has not been fully tested. * In particular, if the polygons do not define a single clump then it will not work as expected. * @param polys A Vector of GeoPolgyons to dissolve. * @param id ID for this new polygon. * @return GeoPolygon The dissolved version of the polys set. */ public static GeoPolygon dissolve(int id,Vector polys){ Vector outList = (Vector)polys.clone(); Vector holes = new Vector();Vector fills = new Vector(); int count = outList.size(); GeoPolygon multiPartTest; for(int i=0;i=0){ if(multiPartTest.getPart(j).getPoints().size()<3) { if(DEBUG)System.err.println("Multipart polygon only had 2 points."); } outList.addElement(multiPartTest.getPart(j)); } else{ if(DEBUG)System.out.println("Hole"); /* TODO JM: sort out holes properly by implementing cases 2 and 3, although a change in the represenation of * polygon toplogy may come first. * This part represents a hole. * There are three states that this hole could be in for this zone. * 1) Empty, with no polygons at all inside it. * 2) Filled, with polygons that belong to this zone. * 3) Semi-filled, with some or all of the polygons inside it not belonging to this zone. * 3 is a hard case that will need to be planned out carefully and probably requires * more constructive geometry methods than exist at the moment */ //Is the hole empty? GeoPolygon hole = (GeoPolygon)multiPartTest.getPart(j); Vector fillers = hole.getContiguityList(polys); if(fillers.size()==0){ //case 1 from above. //hole is only contiguous with the polygon to which it is a hole //therefore keep it! holes.addElement(hole); } else{ //case 2 or 3 from above. //There is something attached to this hole, other than the polygon to which it is attached. //For now we will assume a simple case 2 by removing all polygons attached to this hole //this is the danger that a polygon will be attached to a polygon that is attached to this hole. //TODO JM: implement case 2 properly and sort out case 3 holes.addElement(hole); for(int k =0;k0){ if(DEBUG)System.out.println("Finished with "+toDo.size()); d.addSubPart(dissolve(id,toDo)); } d.calculateCentroidLocation(); return d; } /** * Combines this polygon with the provided polygon to create a new single polygon with the combined outline of both. *
Both polygons must be contiguous. * @param gp The GeoPolygon to dissolve into this one. * @return GeoPolygon The resulting dissolved polygon. */ public GeoPolygon dissolve(GeoPolygon gp){ if(!isContiguous(gp)){throw new IllegalArgumentException("Polygon is not Contiguous, dissolve imposible");} Vector a = getPoints(); Vector b = gp.getPoints(); int lastA = a.size()-2; // one for 0 counting one for duplicate final point. int lastB = b.size()-2; GeoPolygon d = new GeoPolygon();// construct with same id as this polygon? //find first non shared point int indexA = 0,indexB=0,start = 0; while(b.contains(a.elementAt(indexA))){indexA++;} start = indexA; //System.out.println("Starting at "+indexA); boolean done = false,first = true; final int A=0,B=1,A2B=2,B2A=3; int direction =1; int state = A; while(!done){ // System.out.println(""+indexA+" "+indexB+" "+state); switch(state){ case A: if(indexA==start && !first){done=true;continue;} first = false; d.addPoint((GeoPoint)a.elementAt(indexA)); indexA=(indexA+1)%(lastA+1); if(b.contains(a.elementAt(indexA))){ state=A2B; } break; case B: d.addPoint((GeoPoint)b.elementAt(indexB)); indexB+=1*direction; indexB%=(lastB+1); if(indexB<0)indexB=lastB; if(a.contains(b.elementAt(indexB))){ state = B2A; } break; case A2B: //add the last point of a d.addPoint((GeoPoint)a.elementAt(indexA)); //which way round B do we go? int join = b.indexOf(a.elementAt(indexA)); if(a.contains(b.elementAt((join+1)%lastB))){ direction = -1; } else{ direction = 1; } indexB = join+direction; state = B; break; case B2A: indexA = a.indexOf(b.elementAt(indexB)); state = A; break; } } d.addPoint((GeoPoint)a.elementAt(start)); d.calculateCentroidLocation(); return d; } /** * Tests to see if a point is contained by this polygon. * @param p The GeoPoint to test. * @return boolean True if point is contained by this polygon. */ public boolean contains(GeoPoint p){ /*if(!getMultiPartBounds().contains(p)){ return false; }*/ if(getBounds().contains(p)){ /* Start andys code */ int number_of_lines_crossed = 0; // holds lines intersecting with number_of_lines_crossed = 0; // set lines crossed = 0 for each polygon // lets us know which part start we're looking for for(int i=0;i= Math.min(ypoints[i],ypoints[i+1])) && ((xpoints[i] >= p.x) || (xpoints[i+1] >= p.x))) { // if polygon contains a suitable value if(((xpoints[i] >= p.x) && (xpoints[i+1] >= p.x))){ number_of_lines_crossed++;//simple case } else{ double gradient; // calc. gradient if (xpoints[i] > xpoints[i+1]) gradient = ((xpoints[i] - xpoints[i+1])/(ypoints[i] - ypoints[i+1])); else gradient = ((xpoints[i+1] - xpoints[i])/(ypoints[i+1] - ypoints[i])); double x_intersection_axis = (xpoints[i] - (gradient * ypoints[i])); // calc. intersect with x-axis double x_intersection_line = (gradient*p.y) + x_intersection_axis; // calc. intersect with y=const // line extending from location if ((x_intersection_line <= Math.max(xpoints[i],xpoints[i+1])) && // check intersect inside polygon (x_intersection_line >= Math.min(xpoints[i],xpoints[i+1])) && (x_intersection_line >= p.x)) { number_of_lines_crossed++; // increment line counter } // end check for inside polygon } } // end of if polygon points suitable } // end of run through points in polygon if ((number_of_lines_crossed != 0) && // if number of polygon lines crossed (((number_of_lines_crossed % 2) == 1))) { // by a line in one direction from the return true; // initial location is odd, the location // lies in that polygon } // end of run through polygons } if(this.isMultiPart()){ for(int i=0;iid] {x1,y1,...,xn,yn}".*/ public String toString(){ StringBuffer sb = new StringBuffer(); sb.append("GeoPolygon : [id "); sb.append(id); sb.append("] "); for(int i=0;i to reduce * memory usage. * * * */