package org.geotools.renderer.label;
import static org.geotools.styling.TextSymbolizer.*;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geotools.geometry.jts.Decimator;
import org.geotools.geometry.jts.GeometryClipper;
import org.geotools.geometry.jts.LiteShape2;
import org.geotools.renderer.label.LabelCacheItem.GraphicResize;
import org.geotools.renderer.lite.LabelCache;
import org.geotools.renderer.style.SLDStyleFactory;
import org.geotools.renderer.style.TextStyle2D;
import org.geotools.styling.TextSymbolizer;
import org.geotools.util.NumberRange;
import org.geotools.util.logging.Logging;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.filter.expression.Literal;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryCollection;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.LinearRing;
import com.vividsolutions.jts.geom.MultiLineString;
import com.vividsolutions.jts.geom.MultiPoint;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
import com.vividsolutions.jts.geom.prep.PreparedGeometry;
import com.vividsolutions.jts.geom.prep.PreparedGeometryFactory;
import com.vividsolutions.jts.operation.linemerge.LineMerger;
import com.vividsolutions.jts.precision.EnhancedPrecisionOp;
public final class LabelCacheImpl implements LabelCache {
public enum LabelRenderingMode {
STRING,
OUTLINE,
ADAPTIVE};
static final Logger LOGGER = Logging.getLogger(LabelCacheImpl.class);
public double DEFAULT_PRIORITY = 1000.0;
public static double MIN_CURVED_DELTA = Math.PI / 60;
protected Map<String, LabelCacheItem> labelCache = new HashMap<String, LabelCacheItem>();
protected ArrayList<LabelCacheItem> labelCacheNonGrouped = new ArrayList<LabelCacheItem>();
private List<Rectangle2D> reserved = new ArrayList<Rectangle2D>();
static final double[] RIGHT_ANCHOR_CANDIDATES = new double[] {0,0.5, 0,0, 0,1};
static final double[] MID_ANCHOR_CANDIDATES = new double[] {0.5,0.5, 0,0.5, 1,0.5};
static final double[] LEFT_ANCHOR_CANDIDATES = new double[] {1,0.5, 1,0, 1,1};
protected LabelRenderingMode labelRenderingMode = LabelRenderingMode.STRING;
protected SLDStyleFactory styleFactory = new SLDStyleFactory();
boolean stop = false;
Set<String> enabledLayers = new HashSet<String>();
Set<String> activeLayers = new HashSet<String>();
LineLengthComparator lineLengthComparator = new LineLengthComparator();
GeometryFactory gf = new GeometryFactory();
GeometryClipper clipper;
private boolean needsOrdering = false;
public void enableLayer(String layerId) {
needsOrdering = true;
enabledLayers.add(layerId);
}
public LabelRenderingMode getLabelRenderingMode() {
return labelRenderingMode;
}
public void setLabelRenderingMode(LabelRenderingMode mode) {
this.labelRenderingMode = mode;
}
public void stop() {
stop = true;
activeLayers.clear();
}
public void start() {
stop = false;
}
public void clear() {
if (!activeLayers.isEmpty()) {
throw new IllegalStateException(activeLayers
+ " are layers that started rendering but have not completed,"
+ " stop() or endLayer() must be called before clear is called");
}
needsOrdering = true;
labelCache.clear();
labelCacheNonGrouped.clear();
enabledLayers.clear();
}
public void clear(String layerId) {
if (activeLayers.contains(layerId)) {
throw new IllegalStateException(layerId
+ " is still rendering, end the layer before calling clear.");
}
needsOrdering = true;
for (Iterator<LabelCacheItem> iter = labelCache.values().iterator(); iter.hasNext();) {
LabelCacheItem item = iter.next();
if (item.getLayerIds().contains(layerId))
iter.remove();
}
for (Iterator<LabelCacheItem> iter = labelCacheNonGrouped.iterator(); iter.hasNext();) {
LabelCacheItem item = iter.next();
if (item.getLayerIds().contains(layerId))
iter.remove();
}
enabledLayers.remove(layerId);
}
public void disableLayer(String layerId) {
needsOrdering = true;
enabledLayers.remove(layerId);
}
public void startLayer(String layerId) {
enabledLayers.add(layerId);
activeLayers.add(layerId);
}
public double getPriority(TextSymbolizer symbolizer, SimpleFeature feature) {
if (symbolizer.getPriority() == null)
return DEFAULT_PRIORITY;
try {
Double number = (Double) symbolizer.getPriority().evaluate(feature, Double.class);
return number.doubleValue();
} catch (Exception e) {
return DEFAULT_PRIORITY;
}
}
public void put(String layerId, TextSymbolizer symbolizer, SimpleFeature feature,
LiteShape2 shape, NumberRange scaleRange) {
needsOrdering = true;
try {
String label = (String) symbolizer.getLabel().evaluate(feature, String.class);
if (label == null)
return;
label = label.trim();
if (label.length() == 0) {
return; }
double priorityValue = getPriority(symbolizer, feature);
boolean group = getBooleanOption(symbolizer, TextSymbolizer.GROUP_KEY, false);
if (!(group)) {
LabelCacheItem item = buildLabelCacheItem(layerId, symbolizer, feature, shape,
scaleRange, label, priorityValue);
labelCacheNonGrouped.add(item);
} else {
LabelCacheItem lci = (LabelCacheItem) labelCache.get(label);
if (lci == null) {
lci = buildLabelCacheItem(layerId, symbolizer, feature, shape, scaleRange,
label, priorityValue);
labelCache.put(label, lci);
} else {
if ((symbolizer.getPriority() != null)
&& (!(symbolizer.getPriority() instanceof Literal)))
lci.setPriority(lci.getPriority() + priorityValue);
lci.getGeoms().add(shape.getGeometry());
}
}
} catch (Exception e) {
LOGGER.log(Level.FINE, "Error adding label to the label cache", e);
}
}
public void put(Rectangle2D area) {
reserved.add( area );
}
private LabelCacheItem buildLabelCacheItem(String layerId, TextSymbolizer symbolizer,
SimpleFeature feature, LiteShape2 shape, NumberRange scaleRange, String label,
double priorityValue) {
TextStyle2D textStyle = (TextStyle2D) styleFactory.createStyle(feature, symbolizer,
scaleRange);
LabelCacheItem item = new LabelCacheItem(layerId, textStyle, shape, label);
item.setPriority(priorityValue);
item.setSpaceAround(getIntOption(symbolizer, SPACE_AROUND_KEY, DEFAULT_SPACE_AROUND));
item.setMaxDisplacement(getIntOption(symbolizer, MAX_DISPLACEMENT_KEY,
DEFAULT_MAX_DISPLACEMENT));
item.setMinGroupDistance(getIntOption(symbolizer, MIN_GROUP_DISTANCE_KEY,
DEFAULT_MIN_GROUP_DISTANCE));
item.setRepeat(getIntOption(symbolizer, LABEL_REPEAT_KEY, DEFAULT_LABEL_REPEAT));
item.setLabelAllGroup(getBooleanOption(symbolizer, LABEL_ALL_GROUP_KEY,
DEFAULT_LABEL_ALL_GROUP));
item.setRemoveGroupOverlaps(getBooleanOption(symbolizer, "removeOverlaps",
DEFAULT_REMOVE_OVERLAPS));
item.setAllowOverruns(getBooleanOption(symbolizer, ALLOW_OVERRUNS_KEY,
DEFAULT_ALLOW_OVERRUNS));
item.setFollowLineEnabled(getBooleanOption(symbolizer, FOLLOW_LINE_KEY, DEFAULT_FOLLOW_LINE));
double maxAngleDelta = getDoubleOption(symbolizer, MAX_ANGLE_DELTA_KEY, DEFAULT_MAX_ANGLE_DELTA);
item.setMaxAngleDelta(Math.toRadians(maxAngleDelta));
item.setAutoWrap(getIntOption(symbolizer, AUTO_WRAP_KEY, DEFAULT_AUTO_WRAP));
item.setForceLeftToRightEnabled(getBooleanOption(symbolizer, FORCE_LEFT_TO_RIGHT_KEY, DEFAULT_FORCE_LEFT_TO_RIGHT));
item.setConflictResolutionEnabled(getBooleanOption(symbolizer, CONFLICT_RESOLUTION_KEY, DEFAULT_CONFLICT_RESOLUTION));
item.setGoodnessOfFit(getDoubleOption(symbolizer, GOODNESS_OF_FIT_KEY, DEFAULT_GOODNESS_OF_FIT));
item.setGraphicsResize(getGraphicResize(symbolizer));
item.setGraphicMargin(getGraphicMargin(symbolizer));
return item;
}
private int getIntOption(TextSymbolizer symbolizer, String optionName, int defaultValue) {
String value = symbolizer.getOption(optionName);
if (value == null)
return defaultValue;
try {
return Integer.parseInt(value);
} catch (Exception e) {
return defaultValue;
}
}
private double getDoubleOption(TextSymbolizer symbolizer, String optionName, double defaultValue) {
String value = symbolizer.getOption(optionName);
if (value == null)
return defaultValue;
try {
return Double.parseDouble(value);
} catch (Exception e) {
return defaultValue;
}
}
private boolean getBooleanOption(TextSymbolizer symbolizer, String optionName,
boolean defaultValue) {
String value = symbolizer.getOption(optionName);
if (value == null)
return defaultValue;
return value.equalsIgnoreCase("yes") || value.equalsIgnoreCase("true")
|| value.equalsIgnoreCase("1");
}
private GraphicResize getGraphicResize(TextSymbolizer symbolizer) {
String value = symbolizer.getOptions().get("graphic-resize");
if(value == null) {
return GraphicResize.NONE;
} else {
return GraphicResize.valueOf(value.toUpperCase());
}
}
private int[] getGraphicMargin(TextSymbolizer symbolizer) {
String value = symbolizer.getOptions().get("graphic-margin");
if(value == null) {
return null;
} else {
String[] values = value.trim().split("\\s+");
if(values.length == 0) {
return null;
} else if(values.length > 4) {
throw new IllegalArgumentException("The graphic margin is to be specified with 1, 2 or 4 values");
}
int[] parsed = new int[values.length];
for (int i = 0; i < parsed.length; i++) {
parsed[i] = Integer.parseInt(values[i]);
}
if(parsed.length == 4) {
return parsed;
} else if(parsed.length == 3) {
return new int[] {parsed[0], parsed[1], parsed[2], parsed[1]};
} else if(parsed.length == 2) {
return new int[] {parsed[0], parsed[1], parsed[0], parsed[1]};
} else {
return new int[] {parsed[0], parsed[0], parsed[0], parsed[0]};
}
}
}
public void endLayer(String layerId, Graphics2D graphics, Rectangle displayArea) {
activeLayers.remove(layerId);
}
public List<LabelCacheItem> orderedLabels() {
List<LabelCacheItem> al = getActiveLabels();
Collections.sort(al);
Collections.reverse(al);
return al;
}
private List<LabelCacheItem> getActiveLabels() {
List<LabelCacheItem> al = new ArrayList<LabelCacheItem>();
for (LabelCacheItem item : labelCache.values()) {
if (isActive(item.getLayerIds()))
al.add(item);
}
for (LabelCacheItem item : labelCacheNonGrouped) {
if (isActive(item.getLayerIds()))
al.add(item);
}
return al;
}
private boolean isActive(Set<String> layerIds) {
for (String layerName : layerIds) {
if (enabledLayers.contains(layerName))
return true;
}
return false;
}
public void end(Graphics2D graphics, Rectangle displayArea) {
final Object antialiasing = graphics.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
final Object textAntialiasing = graphics
.getRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING);
try {
if (labelRenderingMode != LabelRenderingMode.STRING
&& antialiasing == RenderingHints.VALUE_ANTIALIAS_OFF
&& textAntialiasing == RenderingHints.VALUE_TEXT_ANTIALIAS_ON) {
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
}
paintLabels(graphics, displayArea);
} finally {
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antialiasing);
}
}
void paintLabels(Graphics2D graphics, Rectangle displayArea) {
if (!activeLayers.isEmpty()) {
throw new IllegalStateException(activeLayers
+ " are layers that started rendering but have not completed,"
+ " stop() or endLayer() must be called before end() is called");
}
LabelIndex glyphs = new LabelIndex();
glyphs.reserveArea( reserved );
displayArea = new Rectangle(displayArea);
displayArea.width -= 1;
displayArea.height -= 1;
clipper = new GeometryClipper(new Envelope(displayArea.getMinX(), displayArea.getMaxX(), displayArea.getMinY(), displayArea.getMaxY()));
List<LabelCacheItem> items; if (needsOrdering) {
items = orderedLabels();
} else {
items = getActiveLabels();
}
LabelPainter painter = new LabelPainter(graphics, labelRenderingMode);
for (LabelCacheItem labelItem : items) {
if (stop)
return;
painter.setLabel(labelItem);
try {
AffineTransform tempTransform = new AffineTransform();
Geometry geom = labelItem.getGeometry();
if ((geom instanceof Point) || (geom instanceof MultiPoint))
paintPointLabel(painter, tempTransform, displayArea, glyphs);
else if (((geom instanceof LineString) && !(geom instanceof LinearRing))
|| (geom instanceof MultiLineString))
paintLineLabels(painter, tempTransform, displayArea, glyphs);
else if (geom instanceof Polygon || geom instanceof MultiPolygon
|| geom instanceof LinearRing)
paintPolygonLabel(painter, tempTransform, displayArea, glyphs);
} catch (Exception e) {
System.out.println("Issues painting " + labelItem.getLabel());
e.printStackTrace();
}
}
}
private Envelope toEnvelope(Rectangle2D bounds) {
return new Envelope(bounds.getMinX(), bounds.getMaxX(), bounds.getMinY(), bounds.getMaxY());
}
private double goodnessOfFit(LabelPainter painter, Rectangle2D glyphBounds,
PreparedGeometry representativeGeom) {
if (representativeGeom.getGeometry() instanceof Point) {
return 1.0;
}
if (representativeGeom.getGeometry() instanceof LineString) {
return 1.0;
}
if (representativeGeom.getGeometry() instanceof Polygon) {
try {
int count = 0;
int n = 10;
Coordinate c = new Coordinate();
Point pp = gf.createPoint(c);
for (int i = 1; i < (painter.getLineCount() + 1); i++) {
double y = glyphBounds.getY() + ((double) glyphBounds.getHeight())
* (((double) i) / (painter.getLineCount() + 1));
for (int j = 1; j < (n + 1); j++) {
c.x = glyphBounds.getX() + ((double) glyphBounds.getWidth())
* (((double) j) / (n + 1));
c.y = y;
pp.geometryChanged();
if (representativeGeom.contains(pp))
count++;
}
}
return ((double) count) / (n * painter.getLineCount());
} catch (Exception e) {
Geometry g = representativeGeom.getGeometry();
g.geometryChanged();
Envelope ePoly = g.getEnvelopeInternal();
Envelope eglyph = toEnvelope(glyphBounds);
Envelope inter = intersection(ePoly, eglyph);
if (inter != null)
return (inter.getWidth() * inter.getHeight())
/ (eglyph.getWidth() * eglyph.getHeight());
return 0.0;
}
}
return 0.0;
}
private boolean paintLineLabels(LabelPainter painter, AffineTransform originalTransform,
Rectangle displayArea, LabelIndex paintedBounds) throws Exception {
final LabelCacheItem labelItem = painter.getLabel();
List<LineString> lines = (List<LineString>) getLineSetRepresentativeLocation(labelItem
.getGeoms(), displayArea, labelItem.removeGroupOverlaps());
if (lines == null || lines.size() == 0)
return false;
if (!labelItem.labelAllGroup() && lines.size() > 1) {
lines = Collections.singletonList(lines.get(0));
}
final Rectangle2D textBounds = painter.getFullLabelBounds();
final double step = painter.getAscent() > 2 ? painter.getAscent() : 2;
int space = labelItem.getSpaceAround();
int haloRadius = Math.round(labelItem.getTextStyle().getHaloFill() != null ? labelItem
.getTextStyle().getHaloRadius() : 0);
int extraSpace = space + haloRadius;
int labelDistance = labelItem.getRepeat();
int minDistance = labelItem.getMinGroupDistance();
LabelIndex groupLabels = new LabelIndex();
double labelOffset = labelItem.getMaxDisplacement();
boolean allowOverruns = labelItem.allowOverruns();
double maxAngleDelta = labelItem.getMaxAngleDelta();
int labelCount = 0;
for (LineString line : lines) {
if (labelItem.isFollowLineEnabled())
line = decimateLineString(line, step);
final double lineStringLength = line.getLength();
if ((!allowOverruns || labelItem.isFollowLineEnabled())
&& line.getLength() < textBounds.getWidth())
return labelCount > 0;
double[] labelPositions;
if (labelDistance > 0 && labelDistance < lineStringLength / 2) {
labelPositions = new double[(int) (lineStringLength / labelDistance)];
labelPositions[0] = lineStringLength / 2;
double offset = labelDistance;
for (int i = 1; i < labelPositions.length; i++) {
labelPositions[i] = labelPositions[i - 1] + offset;
double signum = Math.signum(offset);
offset = -1 * signum * (Math.abs(offset) + labelDistance);
}
} else {
labelPositions = new double[1];
labelPositions[0] = lineStringLength / 2;
}
LineStringCursor cursor = new LineStringCursor(line);
AffineTransform tx = new AffineTransform();
for (int i = 0; i < labelPositions.length; i++) {
cursor.moveTo(labelPositions[i]);
Coordinate centroid = cursor.getCurrentPosition();
double currOffset = 0;
boolean painted = false;
while (Math.abs(currOffset) <= labelOffset * 2 && !painted) {
tx.setToIdentity();
Rectangle2D labelEnvelope;
double maxAngleChange = 0;
double startOrdinate = cursor.getCurrentOrdinate() - textBounds.getWidth() / 2;
double endOrdinate = cursor.getCurrentOrdinate() + textBounds.getWidth() / 2;
if (labelItem.followLineEnabled) {
maxAngleChange = cursor.getMaxAngleChange(startOrdinate, endOrdinate);
if (maxAngleChange < MIN_CURVED_DELTA) {
setupLineTransform(painter, cursor, centroid, tx, true);
labelEnvelope = tx.createTransformedShape(textBounds).getBounds2D();
} else {
labelEnvelope = getCurvedLabelBounds(cursor, startOrdinate,
endOrdinate, textBounds.getHeight() / 2);
}
} else {
setupLineTransform(painter, cursor, centroid, tx, false);
labelEnvelope = tx.createTransformedShape(textBounds).getBounds2D();
}
if (displayArea.contains(labelEnvelope)
&& !(labelItem.isConflictResolutionEnabled() && paintedBounds.labelsWithinDistance(labelEnvelope, extraSpace))
&& !groupLabels.labelsWithinDistance(labelEnvelope, minDistance)) {
if (labelItem.isFollowLineEnabled()) {
if ((startOrdinate > 0 && endOrdinate <= cursor.getLineStringLength())) {
if (maxAngleChange < maxAngleDelta) {
if (maxAngleChange < MIN_CURVED_DELTA)
painter.paintStraightLabel(tx);
else {
painter.paintCurvedLabel(cursor);
}
painted = true;
}
}
} else {
if ((allowOverruns || (startOrdinate > 0 && endOrdinate <= cursor
.getLineStringLength()))) {
painter.paintStraightLabel(tx);
painted = true;
}
}
}
if (painted) {
labelCount++;
groupLabels.addLabel(labelItem, labelEnvelope);
if(labelItem.isConflictResolutionEnabled())
paintedBounds.addLabel(labelItem, labelEnvelope);
} else {
double signum = Math.signum(currOffset);
if (signum == 0) {
currOffset = step;
} else {
currOffset = -1 * signum * (Math.abs(currOffset) + step);
}
cursor.moveRelative(currOffset);
cursor.getCurrentPosition(centroid);
}
}
}
}
return labelCount > 0;
}
private Rectangle2D getCurvedLabelBounds(LineStringCursor cursor, double startOrdinate,
double endOrdinate, double bufferSize) {
LineString cut = cursor.getSubLineString(startOrdinate, endOrdinate);
Envelope e = cut.getEnvelopeInternal();
e.expandBy(bufferSize);
return new Rectangle2D.Double(e.getMinX(), e.getMinY(), e.getWidth(), e.getHeight());
}
private LineString decimateLineString(LineString line, double step) {
Coordinate[] inputCoordinates = line.getCoordinates();
List<Coordinate> simplified = new ArrayList<Coordinate>();
Coordinate prev = inputCoordinates[0];
simplified.add(prev);
for (int i = 1; i < inputCoordinates.length; i++) {
Coordinate curr = inputCoordinates[i];
if ((Math.abs(curr.x - prev.x) > step) || (Math.abs(curr.y - prev.y)) > step) {
simplified.add(curr);
prev = curr;
}
}
if (simplified.size() == 1)
simplified.add(inputCoordinates[inputCoordinates.length - 1]);
Coordinate[] newCoords = (Coordinate[]) simplified
.toArray(new Coordinate[simplified.size()]);
return line.getFactory().createLineString(newCoords);
}
private void setupPointTransform(AffineTransform tempTransform, Point centroid,
TextStyle2D textStyle, LabelPainter painter) {
tempTransform.translate(centroid.getX(), centroid.getY());
double rotation = textStyle.getRotation();
if (rotation != rotation) rotation = 0.0;
if (Double.isInfinite(rotation))
rotation = 0;
tempTransform.rotate(rotation);
Rectangle2D textBounds = painter.getLabelBounds();
double displacementX = (textStyle.getAnchorX() * (-textBounds.getWidth()))
+ textStyle.getDisplacementX();
double displacementY = (textStyle.getAnchorY() * (textBounds.getHeight()))
- textStyle.getDisplacementY() - textBounds.getHeight() + painter.getLineHeight();
tempTransform.translate(displacementX, displacementY);
}
private void setupLineTransform(LabelPainter painter, LineStringCursor cursor,
Coordinate centroid, AffineTransform tempTransform, boolean followLine) {
tempTransform.translate(centroid.x, centroid.y);
TextStyle2D textStyle = painter.getLabel().getTextStyle();
double anchorX = textStyle.getAnchorX();
double anchorY = textStyle.getAnchorY();
double rotation;
double displacementX = 0;
double displacementY = 0;
if (textStyle.isPointPlacement() && !followLine) {
rotation = textStyle.getRotation();
} else { if(painter.getLabel().isForceLeftToRightEnabled())
rotation = cursor.getLabelOrientation();
else
rotation = cursor.getCurrentAngle();
displacementY -= textStyle.getPerpendicularOffset();
anchorX = 0.5; anchorY = painter.getLinePlacementYAnchor();
}
Rectangle2D textBounds = painter.getLabelBounds();
displacementX = (anchorX * (-textBounds.getWidth())) + textStyle.getDisplacementX();
displacementY += (anchorY * (textBounds.getHeight())) - textStyle.getDisplacementY();
if (Double.isNaN(rotation) || Double.isInfinite(rotation))
rotation = 0.0;
tempTransform.rotate(rotation);
tempTransform.translate(displacementX, displacementY);
}
private boolean paintPointLabel(LabelPainter painter, AffineTransform tempTransform,
Rectangle displayArea, LabelIndex glyphs) throws Exception {
LabelCacheItem labelItem = painter.getLabel();
Point point = getPointSetRepresentativeLocation(labelItem.getGeoms(), displayArea);
if (point == null)
return false;
TextStyle2D ts = labelItem.getTextStyle();
final double step = painter.getAscent() > 2 ? painter.getAscent() : 2;
double radius = Math.sqrt(ts.getDisplacementX() * ts.getDisplacementX()
+ ts.getDisplacementY() * ts.getDisplacementY());
AffineTransform tx = new AffineTransform(tempTransform);
if(paintPointLabelInternal(painter, tx, displayArea, glyphs, labelItem, point, ts))
return true;
TextStyle2D cloned = new TextStyle2D(ts);
int startAngle = getClosestStandardAngle(ts.getDisplacementX(), ts.getDisplacementY());
int angle = startAngle;
while(radius <= labelItem.maxDisplacement) {
for (int offset = 45; offset <= 360; offset = offset + 45) {
double dx = radius * Math.cos(Math.toRadians(angle));
double dy = radius * Math.sin(Math.toRadians(angle));
double[] anchorPointCandidates;
int normAngle = angle % 360;
if(normAngle < 0)
normAngle = 360 + normAngle;
if(normAngle < 90 || normAngle > 270) {
anchorPointCandidates = RIGHT_ANCHOR_CANDIDATES;
} else if(normAngle > 90 && normAngle < 270) {
anchorPointCandidates = LEFT_ANCHOR_CANDIDATES;
} else {
anchorPointCandidates = MID_ANCHOR_CANDIDATES;
}
for (int i = 0; i < anchorPointCandidates.length; i +=2) {
double ax = anchorPointCandidates[i];
double ay = anchorPointCandidates[i + 1];
cloned.setAnchorX(ax);
cloned.setAnchorY(ay);
cloned.setDisplacementX(dx);
cloned.setDisplacementY(dy);
tx = new AffineTransform(tempTransform);
if(paintPointLabelInternal(painter, tx, displayArea, glyphs, labelItem, point, cloned))
return true;
}
if(angle <= startAngle)
angle = angle + offset;
else
angle = angle - offset;
}
radius += step;
}
return false;
}
int getClosestStandardAngle(double x, double y) {
double angle = Math.toDegrees(Math.atan2(y, x));
return (int) Math.round(angle / 45.0) * 45;
}
private boolean paintPointLabelInternal(LabelPainter painter, AffineTransform tempTransform,
Rectangle displayArea, LabelIndex glyphs, LabelCacheItem labelItem, Point point,
TextStyle2D textStyle) throws Exception {
setupPointTransform(tempTransform, point, textStyle, painter);
Rectangle2D transformed = tempTransform
.createTransformedShape(painter.getFullLabelBounds()).getBounds2D();
if (!displayArea.contains(transformed)
|| (labelItem.isConflictResolutionEnabled() &&
glyphs.labelsWithinDistance(transformed, labelItem.getSpaceAround()))) {
return false;
} else {
painter.paintStraightLabel(tempTransform);
if(labelItem.isConflictResolutionEnabled())
glyphs.addLabel(labelItem, transformed);
return true;
}
}
private boolean paintPolygonLabel(LabelPainter painter, AffineTransform tempTransform,
Rectangle displayArea, LabelIndex glyphs) throws Exception {
LabelCacheItem labelItem = painter.getLabel();
Polygon geom = getPolySetRepresentativeLocation(labelItem.getGeoms(), displayArea);
if (geom == null)
return false;
Point centroid;
try {
centroid = geom.getCentroid();
} catch (Exception e) {
try {
centroid = geom.getExteriorRing().getCentroid();
} catch (Exception ee) {
try {
centroid = geom.getFactory().createPoint(geom.getCoordinate());
} catch (Exception eee) {
return false; }
}
}
PreparedGeometry pg = PreparedGeometryFactory.prepare(geom);
if(!pg.contains(centroid)) {
Envelope env = geom.getEnvelopeInternal();
LineString bisector = geom.getFactory().createLineString(new Coordinate[] {
new Coordinate(env.getMinX(), centroid.getY()),
new Coordinate(env.getMaxX(), centroid.getY()) });
Geometry intersection = bisector.intersection(geom);
Envelope widestEnv = widestGeometry(intersection).getEnvelopeInternal();
double midX = (widestEnv.getMinX() + widestEnv.getMaxX()) / 2;
centroid = geom.getFactory().createPoint(new Coordinate(midX, centroid.getY()));
}
TextStyle2D textStyle = new TextStyle2D(labelItem.getTextStyle());
if(labelItem.getMaxDisplacement() > 0) {
textStyle.setDisplacementX(0);
textStyle.setDisplacementY(0);
textStyle.setAnchorX(0.5);
textStyle.setAnchorY(0.5);
}
AffineTransform tx = new AffineTransform(tempTransform);
if(paintPolygonLabelInternal(painter, tx, displayArea, glyphs, labelItem,
pg, centroid, textStyle))
return true;
final double step = painter.getAscent() > 2 ? painter.getAscent() : 2;
double radius = step;
Coordinate c = new Coordinate(centroid.getCoordinate());
Coordinate cc = centroid.getCoordinate();
Point testPoint = centroid.getFactory().createPoint(c);
while(radius < labelItem.getMaxDisplacement()) {
for(int angle = 0; angle < 360; angle += 45) {
double dx = Math.cos(Math.toRadians(angle)) * radius;
double dy = Math.sin(Math.toRadians(angle)) * radius;
c.x = cc.x + dx;
c.y = cc.y + dy;
testPoint.geometryChanged();
if(!pg.contains(testPoint))
continue;
textStyle.setDisplacementX(dx);
textStyle.setDisplacementY(dy);
tx = new AffineTransform(tempTransform);
if(paintPolygonLabelInternal(painter, tx, displayArea, glyphs, labelItem,
pg, centroid, textStyle))
return true;
}
radius += step;
}
return false;
}
private boolean paintPolygonLabelInternal(LabelPainter painter, AffineTransform tempTransform,
Rectangle displayArea, LabelIndex glyphs, LabelCacheItem labelItem, PreparedGeometry pg,
Point centroid, TextStyle2D textStyle) throws Exception {
setupPointTransform(tempTransform, centroid, textStyle, painter);
Rectangle2D transformed = tempTransform
.createTransformedShape(painter.getFullLabelBounds()).getBounds2D();
if (!displayArea.contains(transformed)
|| (labelItem.isConflictResolutionEnabled()
&& glyphs.labelsWithinDistance(transformed, labelItem.getSpaceAround()))
|| goodnessOfFit(painter, transformed, pg) < painter.getLabel().getGoodnessOfFit())
return false;
painter.paintStraightLabel(tempTransform);
if(labelItem.isConflictResolutionEnabled())
glyphs.addLabel(labelItem, transformed);
return true;
}
Geometry widestGeometry(Geometry geometry) {
if (!(geometry instanceof GeometryCollection)) {
return geometry;
}
return widestGeometry((GeometryCollection) geometry);
}
Geometry widestGeometry(GeometryCollection gc) {
if (gc.isEmpty()) {
return gc;
}
Geometry widest = gc.getGeometryN(0);
for (int i = 1; i < gc.getNumGeometries(); i++) {
Geometry curr = gc.getGeometryN(i);
if (curr.getEnvelopeInternal().getWidth() > widest.getEnvelopeInternal().getWidth()) {
widest = curr;
}
}
return widest;
}
Point getPointSetRepresentativeLocation(List<Geometry> geoms, Rectangle displayArea) {
ArrayList<Point> pts = new ArrayList<Point>();
for (Geometry g : geoms) {
if (!((g instanceof Point) || (g instanceof MultiPoint))) g = g.getCentroid(); if (g instanceof Point) {
Point point = (Point) g;
if (displayArea.contains(point.getX(), point.getY())) pts.add(point); } else if (g instanceof MultiPoint) {
for (int t = 0; t < g.getNumGeometries(); t++) {
Point gg = (Point) g.getGeometryN(t);
if (displayArea.contains(gg.getX(), gg.getY()))
pts.add(gg); }
}
}
if (pts.size() == 0)
return null;
return (Point) pts.get(0);
}
List<LineString> getLineSetRepresentativeLocation(List<Geometry> geoms, Rectangle displayArea,
boolean removeOverlaps) {
List<LineString> lines = new ArrayList<LineString>();
for (Geometry g : geoms) {
accumulateLineStrings(g, lines);
}
if (lines.size() == 0)
return null;
List<LineString> clippedLines = new ArrayList<LineString>();
for (LineString ls : lines) {
MultiLineString ll = clipLineString(ls);
if ((ll != null) && (!(ll.isEmpty()))) {
for (int t = 0; t < ll.getNumGeometries(); t++)
clippedLines.add((LineString) ll.getGeometryN(t));
}
}
if (removeOverlaps) {
List<LineString> cleanedLines = new ArrayList<LineString>();
List<Geometry> bufferCache = new ArrayList<Geometry>();
for (LineString ls : clippedLines) {
Geometry g = ls;
for (int i = 0; i < cleanedLines.size(); i++) {
LineString cleaned = cleanedLines.get(i);
if (g.getEnvelopeInternal().intersects(cleaned.getEnvelopeInternal())) {
Geometry buffer = bufferCache.get(i);
if (buffer == null) {
buffer = cleaned.buffer(2);
bufferCache.set(i, buffer);
}
g = g.difference(buffer);
}
}
int added = accumulateLineStrings(g, cleanedLines);
for (int i = 0; i < added; i++) {
bufferCache.add(null);
}
}
clippedLines = cleanedLines;
}
if (clippedLines == null || clippedLines.size() == 0)
return null;
List<LineString> merged = mergeLines(clippedLines);
if (merged.size() == 0)
return null;
Collections.sort(merged, new LineLengthComparator());
return merged;
}
private int accumulateLineStrings(Geometry g, List<LineString> lines) {
if (!((g instanceof LineString) || (g instanceof MultiLineString) || (g instanceof Polygon) || (g instanceof MultiPolygon)))
return 0;
if ((g instanceof Polygon) || (g instanceof MultiPolygon)) {
g = g.getBoundary(); if (!((g instanceof LineString) || (g instanceof MultiLineString)))
return 0;
}
if (g instanceof LineString) {
if (g.getLength() != 0) {
lines.add((LineString) g);
return 1;
} else {
return 0;
}
} else if (g instanceof MultiLineString) { for (int t = 0; t < g.getNumGeometries(); t++) {
LineString gg = (LineString) g.getGeometryN(t);
lines.add(gg);
}
return g.getNumGeometries();
} else {
int count = 0;
for (int t = 0; t < g.getNumGeometries(); t++) {
count += accumulateLineStrings(g.getGeometryN(t), lines);
}
return count;
}
}
public MultiLineString clipLineString(LineString line) {
Geometry clip = line;
line.geometryChanged();
if (clipper.getBounds().contains(line.getEnvelopeInternal())) {
LineString[] lns = new LineString[1];
lns[0] = (LineString) clip;
return line.getFactory().createMultiLineString(lns);
}
try {
Geometry g = clipper.clip(line, false);
if(g == null) {
return null;
} else if(g instanceof LineString){
return line.getFactory().createMultiLineString(new LineString[] { (LineString) g });
} else {
return (MultiLineString) g;
}
} catch (Exception e) {
return line.getFactory().createMultiLineString(new LineString[] { line });
}
}
Polygon getPolySetRepresentativeLocation(List<Geometry> geoms, Rectangle displayArea) {
List<Polygon> polys = new ArrayList<Polygon>(); Geometry displayGeometry = gf.toGeometry(toEnvelope(displayArea));
for (Geometry g : geoms) {
if (!((g instanceof Polygon) || (g instanceof MultiPolygon)))
continue;
if (g instanceof Polygon) {
polys.add((Polygon) g);
} else {
for (int t = 0; t < g.getNumGeometries(); t++) {
Polygon gg = (Polygon) g.getGeometryN(t);
polys.add(gg);
}
}
}
if (polys.size() == 0)
return null;
List<Polygon> clippedPolys = new ArrayList<Polygon>();
Envelope displayGeomEnv = displayGeometry.getEnvelopeInternal();
for (Polygon p : polys) {
MultiPolygon pp = clipPolygon(p, (Polygon) displayGeometry, displayGeomEnv);
if ((pp != null) && (!(pp.isEmpty()))) {
for (int t = 0; t < pp.getNumGeometries(); t++)
clippedPolys.add((Polygon) pp.getGeometryN(t));
}
}
if (clippedPolys.size() == 0)
return null;
if (clippedPolys.size() == 1)
return (Polygon) clippedPolys.get(0);
double maxSize = -1;
Polygon maxPoly = null;
Polygon cpoly;
for (int t = 0; t < clippedPolys.size(); t++) {
cpoly = (Polygon) clippedPolys.get(t);
if (cpoly.getArea() > maxSize) {
maxPoly = cpoly;
maxSize = cpoly.getArea();
}
}
return maxPoly;
}
public MultiPolygon clipPolygon(Polygon poly, Polygon bbox, Envelope displayGeomEnv) {
Geometry clip = poly;
poly.geometryChanged(); if (displayGeomEnv.contains(poly.getEnvelopeInternal())) {
Polygon[] polys = new Polygon[1];
polys[0] = (Polygon) clip;
return poly.getFactory().createMultiPolygon(polys);
}
try {
clip = clipper.clip(poly, false);
} catch (Exception e) {
clip = poly; }
if (clip instanceof MultiPolygon)
return (MultiPolygon) clip;
if (clip instanceof Polygon) {
Polygon[] polys = new Polygon[1];
polys[0] = (Polygon) clip;
return poly.getFactory().createMultiPolygon(polys);
}
if (clip instanceof Point)
return null;
if (clip instanceof MultiPoint)
return null;
if (clip instanceof LineString)
return null;
if (clip instanceof MultiLineString)
return null;
if (clip == null)
return null;
GeometryCollection gc = (GeometryCollection) clip;
List<Polygon> polys = new ArrayList<Polygon>();
Geometry g;
for (int t = 0; t < gc.getNumGeometries(); t++) {
g = gc.getGeometryN(t);
if (g instanceof Polygon)
polys.add((Polygon) g);
}
if (polys.size() == 0)
return null;
return poly.getFactory().createMultiPolygon((Polygon[]) polys.toArray(new Polygon[1]));
}
private List<LineString> mergeLines(Collection<LineString> lines) {
LineMerger lm = new LineMerger();
lm.add(lines);
List<LineString> merged = new ArrayList<LineString>(lm.getMergedLineStrings());
if (merged.size() == 0) {
return null; } else if (merged.size() == 1) { return merged;
}
Map<Coordinate, List<LineString>> nodes = new HashMap<Coordinate, List<LineString>>(merged
.size() * 2);
for (LineString ls : merged) {
putInNodeHash(ls.getCoordinateN(0), ls, nodes);
putInNodeHash(ls.getCoordinateN(ls.getNumPoints() - 1), ls, nodes);
}
List<LineString> merged_list = new ArrayList<LineString>(merged);
Collections.sort(merged_list, lineLengthComparator);
return processNodes(merged_list, nodes);
}
public List<LineString> processNodes(List<LineString> edges,
Map<Coordinate, List<LineString>> nodes) {
List<LineString> result = new ArrayList<LineString>();
int index = 0; while (index < edges.size()) {
LineString ls = (LineString) edges.get(index);
Coordinate key = ls.getCoordinateN(0);
List<LineString> nodeList = nodes.get(key);
if (nodeList == null) { index++;
continue;
} else if (!nodeList.contains(ls)) {
index++;
continue; }
removeFromHash(nodes, ls);
Coordinate key2 = ls.getCoordinateN(ls.getNumPoints() - 1);
List<LineString> nodeList2 = nodes.get(key2);
if ((nodeList.size() == 0) && (nodeList2.size() == 0)) {
result.add(ls);
index++; continue;
}
if (nodeList.size() > 0) {
LineString ls2 = getLongest(nodeList); ls = merge(ls, ls2);
removeFromHash(nodes, ls2);
}
if (nodeList2.size() > 0) {
LineString ls2 = getLongest(nodeList2); ls = merge(ls, ls2);
removeFromHash(nodes, ls2);
}
edges.set(index, ls); putInNodeHash(ls.getCoordinateN(0), ls, nodes);
putInNodeHash(ls.getCoordinateN(ls.getNumPoints() - 1), ls, nodes);
}
return result;
}
public void removeFromHash(Map<Coordinate, List<LineString>> nodes, LineString ls) {
Coordinate key = ls.getCoordinateN(0);
List<LineString> nodeList = nodes.get(key);
if (nodeList != null) {
nodeList.remove(ls);
}
key = ls.getCoordinateN(ls.getNumPoints() - 1);
nodeList = nodes.get(key);
if (nodeList != null) {
nodeList.remove(ls);
}
}
private LineString getLongest(List<LineString> al) {
if (al.size() == 1)
return al.get(0);
double maxLength = -1;
LineString result = null;
for (LineString l : al) {
if (l.getLength() > maxLength) {
result = l;
maxLength = l.getLength();
}
}
return result;
}
private void putInNodeHash(Coordinate node, LineString ls,
Map<Coordinate, List<LineString>> nodes) {
List<LineString> nodeList = (List<LineString>) nodes.get(node);
if (nodeList == null) {
nodeList = new ArrayList<LineString>();
nodeList.add(ls);
nodes.put(node, nodeList);
} else {
nodeList.add(ls);
}
}
private LineString reverse(LineString l) {
List<Coordinate> clist = Arrays.asList(l.getCoordinates());
Collections.reverse(clist);
return l.getFactory().createLineString((Coordinate[]) clist.toArray(new Coordinate[1]));
}
private LineString merge(LineString major, LineString minor) {
Coordinate major_s = major.getCoordinateN(0);
Coordinate major_e = major.getCoordinateN(major.getNumPoints() - 1);
Coordinate minor_s = minor.getCoordinateN(0);
Coordinate minor_e = minor.getCoordinateN(minor.getNumPoints() - 1);
if (major_s.equals2D(minor_s)) {
return mergeSimple(reverse(minor), major);
} else if (major_s.equals2D(minor_e)) {
return mergeSimple(minor, major);
} else if (major_e.equals2D(minor_s)) {
return mergeSimple(major, minor);
} else if (major_e.equals2D(minor_e)) {
return mergeSimple(major, reverse(minor));
}
return null; }
private LineString mergeSimple(LineString l1, LineString l2) {
List<Coordinate> clist = new ArrayList<Coordinate>(Arrays.asList(l1.getCoordinates()));
clist.addAll(Arrays.asList(l2.getCoordinates()));
return l1.getFactory().createLineString((Coordinate[]) clist.toArray(new Coordinate[1]));
}
private final class LineLengthComparator implements java.util.Comparator<LineString> {
public int compare(LineString o1, LineString o2) {
return Double.compare(o2.getLength(), o1.getLength());
}
}
private Envelope intersection(Envelope e1, Envelope e2) {
Envelope r = e1.intersection(e2);
if (r.getWidth() < 0)
return null;
if (r.getHeight() < 0)
return null;
return r;
}
}