/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2016, Open Source Geospatial Foundation (OSGeo)
*
* 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 of the License.
*
* This library 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.
*
*/
package org.geotools.polylabel;
import java.util.PriorityQueue;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geotools.geometry.jts.GeometryBuilder;
import org.geotools.util.logging.Logging;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
/**
* Based on Vladimir Agafonkin's Algorithm https://www.mapbox.com/blog/polygon-center/
*
* @author Ian Turton
* @author Casper Børgesen
*/
public class PolyLabeller {
private static final Logger LOGGER = Logging.getLogger(PolyLabeller.class.getName());
static GeometryBuilder GB = new GeometryBuilder();
static Geometry getPolylabel(Geometry polygon, double precision) {
MultiPolygon multiPolygon;
if (polygon instanceof Polygon) {
multiPolygon = GB.multiPolygon((Polygon) polygon);
} else if (polygon instanceof MultiPolygon) {
multiPolygon = (MultiPolygon) polygon;
} else {
throw new IllegalStateException("Input polygon must be a Polygon or MultiPolygon");
}
if (polygon.isEmpty() || polygon.getArea() <= 0.0) {
throw new IllegalStateException("Can not label empty geometries");
}
if (!polygon.isValid()) {
throw new IllegalStateException("Can not label invalid geometries");
}
// find the bounding box of the outer ring
double minX, minY, maxX, maxY;
Envelope env = multiPolygon.getEnvelopeInternal();
minX = env.getMinX();
maxX = env.getMaxX();
minY = env.getMinY();
maxY = env.getMaxY();
double width = env.getWidth();
double height = env.getHeight();
double cellSize = Math.min(width, height);
double h = cellSize / 2.0;
// a priority queue of cells in order of their "potential" (max distance
// to polygon)
PriorityQueue<Cell> cellQueue = new PriorityQueue<>();
// cover polygon with initial cells
for (double x = minX; x < maxX; x += cellSize) {
for (double y = minY; y < maxY; y += cellSize) {
cellQueue.add(new Cell(x + h, y + h, h, multiPolygon));
}
}
// take centroid as the first best guess
Cell bestCell = getCentroidCell(multiPolygon);
int numProbes = cellQueue.size();
while (!cellQueue.isEmpty()) {
// pick the most promising cell from the queue
Cell cell = cellQueue.remove();
// update the best cell if we found a better one
if (cell.getD() > bestCell.getD()) {
bestCell = cell;
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer("found best " + (Math.round(1e4 * cell.getD()) / 1e4) + " after "
+ numProbes + " probes");
}
}
// do not drill down further if there's no chance of a better
// solution
if (cell.getMax() - bestCell.getD() <= precision)
continue;
// split the cell into four cells
h = cell.getH() / 2;
cellQueue.add(new Cell(cell.getX() - h, cell.getY() - h, h, multiPolygon));
cellQueue.add(new Cell(cell.getX() + h, cell.getY() - h, h, multiPolygon));
cellQueue.add(new Cell(cell.getX() - h, cell.getY() + h, h, multiPolygon));
cellQueue.add(new Cell(cell.getX() + h, cell.getY() + h, h, multiPolygon));
numProbes += 4;
}
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer("num probes: " + numProbes);
LOGGER.finer("best distance: " + bestCell.getD());
}
return bestCell.getPoint();
}
// get a cell centered on polygon centroid
private static Cell getCentroidCell(MultiPolygon poly) {
Point p = poly.getCentroid();
return new Cell(p.getX(), p.getY(), 0, poly);
}
}