tilt.image.page.Line.java Source code

Java tutorial

Introduction

Here is the source code for tilt.image.page.Line.java

Source

/*
 * This file is part of TILT.
 *
 *  TILT is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  TILT 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 General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with TILT.  If not, see <http://www.gnu.org/licenses/>.
 *  (c) copyright Desmond Schmidt 2014
 */

package tilt.image.page;

import tilt.image.geometry.Polygon;
import tilt.image.geometry.Point;
import tilt.image.geometry.Segment;
import java.util.ArrayList;
import java.util.HashMap;
import java.awt.Graphics;
import java.awt.Shape;
import java.awt.Rectangle;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.image.WritableRaster;
import java.util.Arrays;
import org.json.simple.*;
import java.awt.geom.Area;
import java.awt.Graphics2D;
import java.awt.geom.Line2D;
import tilt.exception.AlignException;

/**
 * Represent a discovered line in an image
 * @author desmond
 */
public class Line implements Comparable<Line> {
    boolean open;
    /** array of actual image coordinates to draw the line between */
    ArrayList<Point> points;
    /** array of shapes representing words */
    ArrayList<Polygon> shapes;
    /** scaled vertical endpoint */
    int end;
    /** shapes shared with some other lines */
    HashMap<Line, ArrayList<Polygon>> shared;
    /** Words in text alignment, one per shape */
    ArrayList<Word> words;
    /** total number of shapes, shared and non-shared */
    int total;
    /** median y-value */
    int medianY;
    int averageY;
    static float MIN_X_DIFF = 2.0f;
    static float SHARED_RATIO = 0.75f;
    public static final int NO_OFFSET = -1;
    /** true if this line is short and between-lines */
    boolean rogue;

    public Line() {
        points = new ArrayList<>();
        shapes = new ArrayList<>();
        words = new ArrayList<>();
        shared = new HashMap<>();
        open = true;
    }

    /**
     * Add a point to a line
     * @param loc the absolute position of the new point
     * @param end the new end in scaled pixels
     */
    void addPoint(Point loc, int end) {
        points.add(loc);
        this.end = end;
        this.open = true;
    }

    /**
     * Get the scaled endpoint of the line
     * @return an int
     */
    int getEnd() {
        return end;
    }

    public void setRogue(boolean status) {
        this.rogue = status;
    }

    public boolean isRogue() {
        return rogue;
    }

    public int getWidth() {
        if (points.size() > 0) {
            Point last = points.get(points.size() - 1);
            Point first = points.get(0);
            return Math.round(last.x - first.x);
        } else
            return 0;
    }

    /**
     * Update the line by adding anew point, update end
     * @param loc the new location of the line-end in real coordinates
     * @param end the unscaled vertical position
     */
    void update(Point loc, int end) {
        points.add(loc);
        this.end = end;
    }

    /**
     * Get the points of the line
     * @return an ordinary array of Points
     */
    public Point[] getPoints() {
        Point[] line = new Point[points.size()];
        points.toArray(line);
        return line;
    }

    public Polygon[] getShapes() {
        Polygon[] polys = new Polygon[shapes.size()];
        shapes.toArray(polys);
        return polys;
    }

    /**
     * Add gaps between shapes to the map
     * @param map a map of shape gaps to frequency of that gap
     */
    void addGaps(HashMap<Integer, Integer> map) {
        Polygon prev = null;
        for (int i = 0; i < shapes.size(); i++) {
            Polygon pg = shapes.get(i);
            if (prev != null) {
                double gap = prev.distanceBetween(pg);
                Integer key = new Integer((int) Math.round(gap));
                int current = 0;
                if (map.containsKey(key))
                    current = map.get(key).intValue();
                current++;
                map.put(key, new Integer(current));
                prev = pg;
            } else
                prev = pg;
        }
    }

    /**
     * Draw the line on the image for debugging
     * @param g the graphics environment to draw in
     * @param n the number of the line
     */
    public void draw(Graphics g, int n) {
        Point p1, p2 = null;
        for (int i = 0; i < points.size(); i++) {
            p1 = p2;
            p2 = points.get(i);
            if (p1 != null) {
                int x1 = Math.round(p1.x);
                int y1 = Math.round(p1.y);
                int x2 = Math.round(p2.x);
                int y2 = Math.round(p2.y);
                if (!rogue) {
                    g.drawLine(x1, y1, x2, y2);
                } else {
                    Graphics2D g2 = (Graphics2D) g;
                    g2.setStroke(new BasicStroke(3));
                    g2.draw(new Line2D.Float(x1, y1, x2, y2));
                    g2.setStroke(new BasicStroke(1));
                }
            }
        }
        Rectangle bounds = getPointsBounds();
        String num = Integer.toString(n);
        g.drawString(num, bounds.x - 30, (bounds.y * 2 + bounds.height) / 2);
        //System.out.println("width of line"+n+": "+bounds.width);
    }

    /**
     * This line is not matched in the next column. Terminate it!
     * @param hScale width of a rectangle
     */
    public void close(int hScale) {
        if (points.size() > 0) {
            Point p = points.get(points.size() - 1);
            points.add(new Point(p.x + hScale, p.y));
        }
        this.open = false;
    }

    /**
     * Move the leftmost x position up to the first pixel that is black
     * @param wr the raster of the image
     * @param hScale the width of a rectangle or slice
     * @param vScale the height of a rectangle or slice
     * @param black value of a black pixel
     */
    public void refineLeft(WritableRaster wr, int hScale, int vScale, int black) {
        if (points.size() > 0) {
            int[] iArray = new int[1];
            java.awt.Point leftMost = points.get(0).toAwt();
            int top = Math.round(leftMost.y);
            int bottom = top + vScale;
            int right = leftMost.x + hScale;
            int left = (leftMost.x - hScale > 0) ? leftMost.x - hScale : 0;
            int nPixels = 0;
            for (int x = left; x < right; x++) {
                for (int y = top; y <= bottom; y++) {
                    wr.getPixel(x, y, iArray);
                    if (iArray[0] <= black) {
                        nPixels++;
                        if (nPixels >= 2) {
                            leftMost.x = x;
                            break;
                        }
                    }
                }
                if (nPixels >= 2)
                    break;
            }
            points.get(0).x = leftMost.x;
            points.get(0).y = leftMost.y;
        }
    }

    /**
     * Check that the given point is at least a bit different 
     * @param pos the position to check at the end or the start
     */
    private void checkPoint(int pos) {
        Point p = points.get(pos);
        if (pos > 0 && points.size() > pos - 1) {
            Point q = points.get(pos - 1);
            if (Math.abs(p.x - q.x) < MIN_X_DIFF)
                points.remove(pos);
        } else if (pos == 0 && points.size() > 1) {
            Point q = points.get(pos + 1);
            if (Math.abs(p.x - q.x) < MIN_X_DIFF)
                points.remove(pos);
        }
    }

    /**
     * Move the rightmost x position inwards to the first pixel that is black
     * @param wr the raster of the image
     * @param hScale the width of a rectangle or slice
     * @param vScale the height of a rectangle or slice
     * @param black value of a black pixel
     */
    public void refineRight(WritableRaster wr, int hScale, int vScale, int black) {
        boolean boundary = false;
        int[] iArray = new int[1];
        while (!boundary && points.size() > 0) {
            java.awt.Point rightMost = points.get(points.size() - 1).toAwt();
            int top = rightMost.y - (vScale / 2);
            int bottom = top + (vScale / 2);
            int right = rightMost.x;
            int left = (rightMost.x - hScale > 0) ? rightMost.x - hScale : 0;
            int nPixels = 0;
            for (int x = right; x >= left; x--) {
                int y;
                for (y = top; y <= bottom; y++) {
                    wr.getPixel(x, y, iArray);
                    if (iArray[0] <= black) {
                        nPixels++;
                        if (nPixels >= 2) {
                            rightMost.x = x;
                            boundary = true;
                            break;
                        }
                    }
                }
                if (nPixels >= 2)
                    break;
            }
            if (boundary) {
                Point endP = points.get(points.size() - 1);
                endP.x = rightMost.x;
                checkPoint(points.size() - 1);
            } else {
                points.remove(points.size() - 1);
            }
        }
    }

    /**
     * Add a shape (polygon or rectangle) to the line. Keep it sorted.
     * @param s the shape to add
     */
    public void add(Polygon s) {
        int i;
        Rectangle sBounds = s.getBounds();
        for (i = 0; i < shapes.size(); i++) {
            Shape t = shapes.get(i);
            Rectangle bounds = t.getBounds();
            if (bounds.x >= sBounds.x) {
                shapes.add(i, s);
                break;
            }
        }
        if (i == shapes.size())
            shapes.add(s);
        total++;
    }

    /**
     * Print the shapes onto the original image
     * @param g the graphics environment
     * @param wr the raster to write on
     * @param n the number of the line
     */
    public void print(Graphics g, WritableRaster wr, int n) {
        Color tColour;
        if (n % 3 == 0)
            tColour = new Color(255, 0, 0, 128);
        else if (n % 3 == 1)
            tColour = new Color(0, 255, 0, 128);
        else
            tColour = new Color(0, 0, 255, 128);
        Color old = g.getColor();
        if (shapes.size() > 0) {
            Rectangle bounds = shapes.get(0).getBounds();
            g.setColor(Color.black);
            g.drawString(Integer.toString(n), bounds.x - 20, (bounds.y * 2 + bounds.height) / 2);
        }
        for (int i = 0; i < shapes.size(); i++) {
            Shape s = shapes.get(i);
            g.setColor(tColour);
            if (s instanceof Polygon)
                g.fillPolygon((Polygon) s);
            else if (s instanceof Rectangle) {
                Rectangle r = (Rectangle) s;
                g.fillRect(r.x, r.y, r.width, r.height);
            }
        }
        g.setColor(old);
    }

    Point closestTo(Point other) {
        float minDist = Float.MAX_VALUE;
        Point minP = null;
        for (Point p : points) {
            float dist = p.distance(other);
            if (dist < minDist) {
                minDist = dist;
                minP = p;
            }
        }
        return minP;
    }

    /**
     * Get the median Y value of the line relative to another line
     * @return an int,being the median yValue of the points in the line
     */
    public int getMedianY(Line other) {
        // we only consider points that are adjacent
        if (this.points.size() > 0 && other.points.size() > 0) {
            Point leastFirst, leastLast;
            Point matchLast, matchFirst;
            Point thisFirst = this.points.get(0);
            Point thisLast = this.points.get(this.points.size() - 1);
            Point otherFirst = other.points.get(0);
            Point otherLast = other.points.get(other.points.size() - 1);
            if (thisFirst.x > otherFirst.x) {
                leastFirst = thisFirst;
                matchFirst = other.closestTo(leastFirst);
            } else {
                leastFirst = otherFirst;
                matchFirst = this.closestTo(leastFirst);
            }
            if (thisLast.x < otherLast.x) {
                leastLast = thisLast;
                matchLast = other.closestTo(leastLast);
            } else {
                leastLast = otherLast;
                matchLast = this.closestTo(leastLast);
            }
            return Math.round(leastFirst.y + matchFirst.y + leastLast.y + matchLast.y) / 4;
        } else {
            Rectangle r = getPointsBounds();
            return r.y + r.height / 2;
        }
    }

    /**
     * Get the median Y value of the line
     * @return an int,being the median yValue of the points in the line
     */
    public int getMedianY() {
        if (medianY == 0) {
            ArrayList<Integer> yValues = new ArrayList<>();
            for (int i = 0; i < points.size(); i++) {
                yValues.add(new Integer(Math.round(points.get(i).y)));
            }
            if (yValues.size() > 0) {
                Integer[] array = new Integer[yValues.size()];
                yValues.toArray(array);
                Arrays.sort(array);
                medianY = array[array.length / 2];
            }
        }
        return medianY;
    }

    public int getAverageXStep() {
        int totalSteps = 0;
        int nSteps = 0;
        Point last = null;
        for (Point p : points) {
            if (last != null) {
                nSteps++;
                totalSteps += Math.abs(p.x - last.x);
            }
            last = p;
        }
        return totalSteps / nSteps;
    }

    /**
     * Get the average Y value of the line
     * @return an int,being the average yValue of the points in the line
     */
    public int getAverageY() {
        int ltotal = 0;
        for (int i = 0; i < points.size(); i++)
            ltotal += points.get(i).y;
        averageY = ltotal / points.size();
        return averageY;
    }

    /**
     * Merge polygons that are close together
     * @param minWordGap average word gap on the page
     */
    public void mergeWords(int minWordGap) {
        ArrayList<Integer> positions = new ArrayList<>();
        Polygon prev = null;
        ArrayList<Polygon> newShapes = new ArrayList<>();
        for (int i = 0; i < shapes.size(); i++) {
            Polygon pg = shapes.get(i);
            if (prev != null) {
                // gaps will change due to merging
                int current = (int) Math.round(prev.distanceBetween(pg));
                if (current >= minWordGap)
                    positions.add(new Integer(i));
            }
            prev = pg;
        }
        positions.add(shapes.size());
        // now actually merge the words
        for (int last = 0, i = 0; i < positions.size(); i++) {
            int pos = positions.get(i).intValue();
            prev = null;
            for (int j = last; j < pos; j++) {
                Polygon pg = shapes.get(j);
                if (prev != null)
                    prev = prev.merge(pg);
                else
                    prev = pg;
            }
            if (prev != null)
                newShapes.add(prev);
            last = pos;
        }
        this.shapes = newShapes;
    }

    /**
     * Compute the horizontal overlap between two lines
     * @param other the other line
     * @return the fraction of the two lines together which overlaps
     */
    float overlap(Line other) {
        if (this.points.isEmpty() || other.points.isEmpty())
            return 0.0f;
        else {
            int left1 = this.points.get(0).toAwt().x;
            int left2 = other.points.get(0).toAwt().x;
            int right1 = this.points.get(points.size() - 1).toAwt().x;
            int right2 = other.points.get(other.points.size() - 1).toAwt().x;
            if (right1 < left2 || right2 < left1)
                return 0.0f;
            // we completely enclose other
            else if (left2 > left1 && right2 < right1)
                return (float) (right2 - left2) / (float) (right1 - left1);
            // other completely encloses us
            else if (left1 > left2 && right1 < right2)
                return (float) (right1 - left1) / (float) (right2 - left2);
            // we precede other but overlap
            else if (right1 > left2) {
                int overlap = right1 - left2;
                int total = right2 - left1;
                return (float) overlap / (float) total;
            }
            // other precedes us but overlaps
            else if (right2 > left1) {
                int overlap = right2 - left1;
                int total = right1 - left2;
                return (float) overlap / (float) total;
            }
            // shouldn't happen
            else
                return 0.0f;
        }
    }

    /**
     * A line is before another based on first point position in x direction.
     * @param other the other line
     * @return 1 if we are after other, 0 if at the same position else -1
     */
    @Override
    public int compareTo(Line other) {
        if (this.points.isEmpty()) {
            if (other.points.isEmpty())
                return 0;
            else // we are less than other
                return -1;
        } else if (other.points.isEmpty()) {
            if (this.points.isEmpty())
                return 0;
            else // other is less than us
                return 1;
        } else {
            return Math.round(this.points.get(0).x - other.points.get(0).x);
        }
    }

    /**
     * Insert sort the points on the x-coordinate
     */
    void sortPoints() {
        for (int i = 0; i < points.size(); i++) {
            Point p = points.get(i);
            int j = i;
            while (j > 0 && points.get(j - 1).x > p.x) {
                points.set(j, points.get(j - 1));
                j--;
            }
            points.set(j, p);
        }
    }

    /**
     * Get the overall bounding box of this line based on its shapes
     * @return a rectangle
     */
    public Rectangle getShapesBounds() {
        Area region = new Area();
        for (int i = 0; i < shapes.size(); i++) {
            Polygon shape = shapes.get(i);
            region.add(new Area(shape));
        }
        return region.getBounds();
    }

    /**
     * Get the overall bounding box of this line based on its points
     * @return a rectangle
     */
    public Rectangle getPointsBounds() {
        int minX = Integer.MAX_VALUE;
        int minY = Integer.MAX_VALUE;
        int maxX = 0;
        int maxY = 0;
        for (int i = 0; i < points.size(); i++) {
            Point pt = points.get(i);
            if (pt.x < minX)
                minX = Math.round(pt.x);
            if (pt.y < minY)
                minY = Math.round(pt.y);
            if (pt.y > maxY)
                maxY = Math.round(pt.y);
            if (pt.x > maxX)
                maxX = Math.round(pt.x);
        }
        return new Rectangle(minX, minY, maxX - minX, maxY - minY);
    }

    /**
     * Convert the line to a GeoJSON object for export
     * @param pageWidth the width of the image in pixels
     * @param pageHeight the height of the image in pixels
     * @return a GeoJSON object for this line
     */
    public JSONObject toGeoJSON(int pageWidth, int pageHeight) {
        JSONObject collection = new JSONObject();
        collection.put("type", "FeatureCollection");
        collection.put("name", "Line");
        Rectangle bounds = getShapesBounds();
        JSONArray bbox = new JSONArray();
        bbox.add((float) bounds.x / (float) pageWidth);
        bbox.add((float) bounds.y / (float) pageHeight);
        bbox.add((float) (bounds.x + bounds.width) / (float) pageWidth);
        bbox.add((float) (bounds.y + bounds.height) / (float) pageHeight);
        collection.put("bbox", bbox);
        JSONArray features = new JSONArray();
        for (int i = 0; i < shapes.size(); i++) {
            JSONObject feature = new JSONObject();
            feature.put("type", "Feature");
            ArrayList<Point> pts = shapes.get(i).toPoints();
            JSONObject geometry = new JSONObject();
            JSONArray coordinates = new JSONArray();
            for (int j = 0; j < pts.size(); j++) {
                geometry.put("type", "Polygon");
                JSONArray point = new JSONArray();
                Point pt = pts.get(j);
                point.add(pt.x / (float) pageWidth);
                point.add(pt.y / (float) pageHeight);
                coordinates.add(point);
            }
            geometry.put("coordinates", coordinates);
            feature.put("geometry", geometry);
            // add feature properties like text-offset here
            if (words.size() > i) {
                Word val = words.get(i);
                if (val != null) {
                    JSONObject properties = new JSONObject();
                    properties.put("offset", val.offset());
                    if (val.hyphenPos() > 0)
                        properties.put("hyphen", val.hyphenPos());
                    feature.put("properties", properties);
                }
            }
            features.add(feature);
        }
        collection.put("features", features);
        return collection;
    }

    /**
     * Get the widths of the polygons on the line
     * @return an array of word-widths on the line
     */
    int[] getWidths() {
        int[] widths = new int[shapes.size()];
        for (int i = 0; i < shapes.size(); i++) {
            Polygon pg = shapes.get(i);
            Rectangle bounds = pg.getBounds();
            widths[i] = bounds.width;
        }
        return widths;
    }

    /**
     * Get the number of shapes on a line
     * @return an int
     */
    public int countShapes() {
        return shapes.size();
    }

    public int countPoints() {
        return this.points.size();
    }

    /**
     * Fill in the pixel widths of each shape in the line
     * @param widths the array to partly fill in
     * @param start the start index into widths
     * @return the number of entries filled in
     */
    int getShapeWidths(int[] widths, int start) {
        for (int i = 0; i < shapes.size(); i++) {
            Polygon pg = shapes.get(i);
            widths[start + i] = pg.getBounds().width;
        }
        return shapes.size();
    }

    /**
     * Does the index for this shape exist?
     * @param index the index to query
     * @return true if a shape at that index in shapes exists else false
     */
    boolean hasShape(int index) {
        return index < shapes.size();
    }

    public int[] getShapeIDs() {
        int[] ids = new int[shapes.size()];
        int i = 0;
        for (Polygon pg : this.shapes)
            ids[i++] = pg.ID;
        return ids;
    }

    public boolean hasShapeByID(int ID) {
        for (int i = this.shapes.size() - 1; i >= 0; i--) {
            Polygon pg = this.shapes.get(i);
            if (pg.ID == ID)
                return true;
        }
        return false;
    }

    /**
     * Set the word offset of the indexed shape
     * @param index the index of the shape in shapes
     * @param word the word to associate with the shape at this index
     */
    public void setShapeWord(int index, Word word) throws AlignException {
        if (index < words.size())
            throw new AlignException("Add words in correct order");
        while (index > words.size())
            words.add(null);
        if (index >= shapes.size())
            throw new AlignException("More offsets than shapes");
        words.add(word);
    }

    /**
     * Split one shape into several at the correct places
     * @param index the index into shapes
     * @param offsets the word-offsets to set for each new shape
     * @param wr the raster of the cleaned image
     * @return the number of split shapes 
     */
    public int splitShape(int index, int[] offsets, WritableRaster wr) {
        return 0;
    }

    /**
     * Get a shape by its index
     * @param index the index
     * @return a Polygon
     */
    Polygon getShape(int index) {
        return shapes.get(index);
    }

    /**
     * Get the index of the specified shape
     * @param pg the shape to look for
     * @return its 0-based index on the line
     */
    int getShapeIndex(Polygon pg) {
        return shapes.indexOf(pg);
    }

    /**
     * Remove a shape from the line
     * @param pg the shape to remove
     */
    public void removeShape(Polygon pg) {
        int index = shapes.indexOf(pg);
        if (index != -1) {
            if (index < words.size())
                words.remove(index);
            shapes.remove(index);
        }
    }

    public void removeShapeByID(int ID) {
        int index = -1;
        for (int i = 0; i < shapes.size(); i++) {
            Polygon pg = shapes.get(i);
            if (pg.ID == ID) {
                index = i;
                break;
            }
        }
        if (index != -1)
            shapes.remove(index);
    }

    /**
     * Add a shape to the line
     * @param index the index at which to add (or ==shapes.size() to add)
     * @param pg the shape to add
     * @param word the word to associate with it (or null)
     */
    void addShape(int index, Polygon pg, Word word) {
        shapes.add(index, pg);
        if (words.size() >= index)
            words.add(index, word);
    }

    /**
     * Reset the words and shapes arrays for a realignment
     */
    public void reset() {
        words.clear();
    }

    public void resetShapes() {
        shapes.clear();
    }

    public void setShapes(ArrayList<Polygon> shapes) {
        this.shapes = shapes;
    }

    /**
     * Remove some points from the line. Must be before we recognise words
     * @param removals the points to cull
     */
    public void removePoints(ArrayList<Point> removals) {
        for (Point p : removals)
            points.remove(p);
    }

    public void mergeWith(Line other) {
        points.addAll(other.points);
        sortPoints();
    }

    /**
     * Find the smallest distance to the line's segments
     * @param pt the point to measure towards
     * @return the minimum distance
     */
    public float closestDistTo(Point pt) {
        float minDist = Float.MAX_VALUE;
        for (int i = 1; i < points.size(); i++) {
            Point last = points.get(i - 1);
            Point curr = points.get(i);
            Segment seg = new Segment(last, curr);
            float dist = (float) seg.distFromLine(pt);
            if (dist < minDist)
                minDist = dist;
        }
        return minDist;
    }

    /**
     * Trim the line to the left-boundary of the first shape on the line
     */
    public void trimLeft() {
        if (shapes.size() > 0) {
            Polygon shape = shapes.get(0);
            Rectangle bounds = shape.getBounds();
            while (points.size() > 1 && points.get(1).x < bounds.x)
                points.remove(0);
            if (points.size() > 0)
                points.get(0).x = bounds.x;
        }
    }

    /**
     * Trim the line the the last shape on the line
     */
    public void trimRight() {
        if (shapes.size() > 0) {
            Polygon shape = shapes.get(shapes.size() - 1);
            Rectangle bounds = shape.getBounds();
            while (points.size() > 1 && points.get(points.size() - 2).x > bounds.x + bounds.width)
                points.remove(points.size() - 1);
            if (points.size() > 0)
                points.get(points.size() - 1).x = bounds.x;
        }
    }

    /**
     * Get the centroid of a set of shapes on this line
     * @param shapeIDs an array of the shape IDs or just 1
     * @return a Point being the average x,y values of the shape's points
     */
    public Point getCentroid(int[] shapeIDs) {
        Polygon[] chosen = new Polygon[shapeIDs.length];
        int i = 0;
        for (Polygon s : shapes) {
            for (int id : shapeIDs)
                if (id == s.ID)
                    chosen[i++] = s;
        }
        int totalX = 0;
        int totalY = 0;
        int N = 0;
        for (Polygon p : chosen) {
            ArrayList<Point> pts = p.toPoints();
            for (Point pt : pts) {
                totalX += pt.x;
                totalY += pt.y;
                N++;
            }
        }
        return new Point(totalX / N, totalY / N);
    }
}