/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2004-2008, 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.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.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.styling.TextSymbolizer.PolygonAlignOptions; import org.geotools.util.NumberRange; import org.geotools.util.logging.Logging; import org.opengis.feature.Feature; 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; /** * Default LabelCache Implementation. * * <p>The label cache sports a number of features that are enabled depending on * the programmatic configuration and the TextSymbolizer options.</p> * <p>The basic functionalitty of the label cache consist in finding the * best label position for each Feature according to the {@link TextSymbolizer} * specifications, and drawing it, provided it does not overlap with other labels.</p> * <p>This basic behaviour can be customized in a number of ways.</p> * * * <h2>Priority</h2> * <p>{@link TextSymbolizer#getPriority()} OGC Expression controls a label priority.</p> * <p>A label with high priority will be drawn before others, increasing its likeliness * to appear on the screen</p> * * @author jeichar * @author dblasby * @author Andrea Aime - OpenGeo * * @source $URL$ */ public final class LabelCacheImpl implements LabelCache { public enum LabelRenderingMode { /** * Always uses {@link Graphics2D#drawGlyphVector(java.awt.font.GlyphVector, float, float)} to * draw the straight labels. It's faster, straight and horizontal labels look better, * diagonal labels look worse, labels and halos are not perfectly centered */ STRING, /** * Always extracts the outline from the {@link GlyphVector} and paints it as a shape. It's * a bit slower, generates more antialiasing, ensures labels and halos are perfectly * centered */ OUTLINE, /** * Draws all diagonal lines in OUTLINE model, but horizontal ones in STRING mode. * Gives the best results when coupled with {@link RenderingHints#VALUE_FRACTIONALMETRICS_ON} * for good label/halo centering */ ADAPTIVE}; static final Logger LOGGER = Logging.getLogger(LabelCacheImpl.class); public double DEFAULT_PRIORITY = 1000.0; /** * The angle delta at which we switch from curved rendering to straight rendering */ public static double MIN_CURVED_DELTA = Math.PI / 60; /** Map<label, LabelCacheItem> the label cache */ protected Map<String, LabelCacheItem> labelCache = new HashMap<String, LabelCacheItem>(); /** non-grouped labels get thrown in here* */ protected ArrayList<LabelCacheItem> labelCacheNonGrouped = new ArrayList<LabelCacheItem>(); /** List of reserved areas of the screen for which labels should fear to tread */ private List<Rectangle2D> reserved = new ArrayList<Rectangle2D>(); // Anchor candidate values used when looping to find a point label that can be drawn 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; } /** * Sets the text rendering mode. */ public void setLabelRenderingMode(LabelRenderingMode mode) { this.labelRenderingMode = mode; } public void stop() { stop = true; activeLayers.clear(); } /** * @see org.geotools.renderer.lite.LabelCache#start() */ 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); } /** * @see org.geotools.renderer.lite.LabelCache#startLayer() */ public void startLayer(String layerId) { enabledLayers.add(layerId); activeLayers.add(layerId); } /** * get the priority from the symbolizer its an expression, so it will try to * evaluate it: 1. if its missing --> DEFAULT_PRIORITY 2. if its a number, * return that number 3. if its not a number, convert to string and try to * parse the number; return the number 4. otherwise, return DEFAULT_PRIORITY * * @param symbolizer * @param feature */ public double getPriority(TextSymbolizer symbolizer, Feature feature) { if (symbolizer.getPriority() == null) return DEFAULT_PRIORITY; // evaluate try { Double number = (Double) symbolizer.getPriority().evaluate(feature, Double.class); return number.doubleValue(); } catch (Exception e) { return DEFAULT_PRIORITY; } } /** * @see org.geotools.renderer.lite.LabelCache#put(org.geotools.renderer.style.TextStyle2D, * org.geotools.renderer.lite.LiteShape) */ public void put(String layerId, TextSymbolizer symbolizer, Feature feature, LiteShape2 shape, NumberRange scaleRange) { needsOrdering = true; try { // get label and geometry if(symbolizer.getLabel() == null) { return; } String label = (String) symbolizer.getLabel().evaluate(feature, String.class); if (label == null) return; if (label.length() == 0) { return; // dont label something with nothing! } 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 { // / --------- grouping case ---------------- // equals and hashcode of LabelCacheItem is the hashcode of // label and the // equals of the 2 labels so label can be used to find the // entry. // DJB: this is where the "grouping" of 'same label' features // occurs LabelCacheItem lci = (LabelCacheItem) labelCache.get(label); if (lci == null) // nothing in there yet! { lci = buildLabelCacheItem(layerId, symbolizer, feature, shape, scaleRange, label, priorityValue); labelCache.put(label, lci); } else { // add only in the non-default case or non-literal. Ie. // area() if ((symbolizer.getPriority() != null) && (!(symbolizer.getPriority() instanceof Literal))) lci.setPriority(lci.getPriority() + priorityValue); // djb-- // changed // because // you // do // not // always // want // to // add! lci.getGeoms().add(shape.getGeometry()); } } } catch (Exception e) { LOGGER.log(Level.SEVERE, "Error adding label to the label cache", e); } } public void put(Rectangle2D area) { reserved.add( area ); } private LabelCacheItem buildLabelCacheItem(String layerId, TextSymbolizer symbolizer, Feature 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.setPolygonAlign((PolygonAlignOptions) getEnumOption(symbolizer, POLYGONALIGN_KEY, DEFAULT_POLYGONALIGN)); item.setGraphicsResize(getGraphicResize(symbolizer)); item.setGraphicMargin(getGraphicMargin(symbolizer)); return item; } private Enum getEnumOption(TextSymbolizer symbolizer, String optionName, Enum defaultValue) { String value = symbolizer.getOption(optionName); if (value == null) return defaultValue; try { Enum enumValue = Enum.valueOf(defaultValue.getDeclaringClass(), value.toUpperCase()); return enumValue; } catch (Exception e) { return defaultValue; } } 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; } } /** * look at the options in the symbolizer for "group". return its value if * not present, return "DEFAULT_GROUP" * * @param symbolizer */ 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"); } /** * Parses the {@link GraphicResize} enum * @param symbolizer * @return */ private GraphicResize getGraphicResize(TextSymbolizer symbolizer) { String value = symbolizer.getOptions().get("graphic-resize"); if(value == null) { return GraphicResize.NONE; } else { return GraphicResize.valueOf(value.toUpperCase()); } } /** * Parses the graphic margin, if any * @param symbolizer * @return */ 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]}; } } } /** * @see org.geotools.renderer.lite.LabelCache#endLayer(java.awt.Graphics2D, * java.awt.Rectangle) */ public void endLayer(String layerId, Graphics2D graphics, Rectangle displayArea) { activeLayers.remove(layerId); } /** * Return a list with all the values in priority order. Both grouped and * non-grouped */ public List<LabelCacheItem> orderedLabels() { List<LabelCacheItem> al = getActiveLabels(); Collections.sort(al); Collections.reverse(al); return al; } /** * Returns a list of all active labels * * @return */ private List<LabelCacheItem> getActiveLabels() { // fill a list with the active labels 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; } /** * Is the label part of an active layer? * * @param layerIds * @return */ private boolean isActive(Set<String> layerIds) { for (String layerName : layerIds) { if (enabledLayers.contains(layerName)) return true; } return false; } /** * @see org.geotools.renderer.lite.LabelCache#end(java.awt.Graphics2D, * java.awt.Rectangle) */ 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 we are asked to antialias only text but we're drawing using // the outline // method, we need to re-enable graphics antialiasing during label // painting 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 ); // Hack: let's reduce the display area width and height by one pixel. // If the rendered image is 256x256, proper rendering of polygons and // lines occurr only if the display area is [0,0; 256,256], yet if you // try to render anything at [x,256] or [256,y] it won't show. // So, to avoid labels that happen to touch the border being cut // by one pixel, we reduce the display area. // Feels hackish, don't have a better solution at the moment thought displayArea = new Rectangle(displayArea); displayArea.width -= 1; displayArea.height -= 1; // prepare the geometry clipper clipper = new GeometryClipper(new Envelope(displayArea.getMinX(), displayArea.getMaxX(), displayArea.getMinY(), displayArea.getMaxY())); List<LabelCacheItem> items; // both grouped and non-grouped if (needsOrdering) { items = orderedLabels(); } else { items = getActiveLabels(); } LabelPainter painter = new LabelPainter(graphics, labelRenderingMode); for (LabelCacheItem labelItem : items) { if (stop) return; painter.setLabel(labelItem); try { // LabelCacheItem labelItem = (LabelCacheItem) // labelCache.get(labelIter.next()); // DJB: simplified this. Just send off to the point,line,or // polygon routine // NOTE: labelItem.getGeometry() returns the FIRST geometry, so // we're assuming that lines & points arent mixed // If they are, then the FIRST geometry determines how its // rendered (which is probably bad since it should be in // area,line,point order // TOD: as in NOTE above /* * Just use identity for tempTransform because display area is * 0,0,width,height and oldTransform may have a different * origin. OldTransform will be used later for drawing. -rg & je */ 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()); // the decimation can cause problems - we try to minimize it // do nothing e.printStackTrace(); } } } private Envelope toEnvelope(Rectangle2D bounds) { return new Envelope(bounds.getMinX(), bounds.getMaxX(), bounds.getMinY(), bounds.getMaxY()); } /** * how well does the label "fit" with the geometry. 1. points ALWAYS RETURNS * 1.0 2. lines ALWAYS RETURNS 1.0 (modify polygon method to handle rotated * labels) 3. polygon + assume: polylabels are unrotated + assume: polygon * could be invalid + dont worry about holes * * like to RETURN area of intersection between polygon and label bounds, but * thats expensive and likely to give us problems due to invalid polygons * SO, use a sample method - make a few points inside the label and see if * they're "close to" the polygon The method sucks, but works well... * * @param glyphVector * @param representativeGeom */ /** * @param glyphBounds * @param representativeGeom * @return */ private double goodnessOfFit(LabelPainter painter, AffineTransform transform, PreparedGeometry representativeGeom) { if (representativeGeom.getGeometry() instanceof Point) { return 1.0; } if (representativeGeom.getGeometry() instanceof LineString) { return 1.0; } if (representativeGeom.getGeometry() instanceof Polygon) { Rectangle2D glyphBounds = painter.getFullLabelBounds(); try { // do a sampling, how many points sitting on the labels are also // within a certain distance of the polygon? int count = 0; int n = 10; Coordinate c = new Coordinate(); Point pp = gf.createPoint(c); double[] gp = new double[2]; double[] tp = new double[2]; for (int i = 1; i < (painter.getLineCount() + 1); i++) { gp[1] = glyphBounds.getY() + ((double) glyphBounds.getHeight()) * (((double) i) / (painter.getLineCount() + 1)); for (int j = 1; j < (n + 1); j++) { gp[0] = glyphBounds.getX() + ((double) glyphBounds.getWidth()) * (((double) j) / (n + 1)); transform.transform(gp, 0, tp, 0, 1); c.x = tp[0]; c.y = tp[1]; pp.geometryChanged(); // useful to debug the sampling point positions // painter.graphics.setColor(Color.CYAN); // painter.graphics.drawRect((int) (c.x - 1), (int) (c.y - 1), 2, 2); 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(transform.createTransformedShape(glyphBounds).getBounds2D()); Envelope inter = intersection(ePoly, eglyph); if (inter != null) { return (inter.getWidth() * inter.getHeight()) / (eglyph.getWidth() * eglyph.getHeight()); } else { 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 we just want to label the longest line, remove the others if (!labelItem.labelAllGroup() && lines.size() > 1) { lines = Collections.singletonList(lines.get(0)); } // pre compute some labelling params final Rectangle2D textBounds = painter.getFullLabelBounds(); // ... use at least a 2 pixel step, no matter what the label length is 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; // repetition distance, if any int labelDistance = labelItem.getRepeat(); // min distance, if any int minDistance = labelItem.getMinGroupDistance(); LabelIndex groupLabels = new LabelIndex(); // Max displacement for the current label double labelOffset = labelItem.getMaxDisplacement(); boolean allowOverruns = labelItem.allowOverruns(); double maxAngleDelta = labelItem.getMaxAngleDelta(); int labelCount = 0; for (LineString line : lines) { // if we are following lines, use a simplified version of the line, // we don't want very small segments to influence the character // orientation if (labelItem.isFollowLineEnabled()) line = decimateLineString(line, step); // max distance between candidate label points, if any final double lineStringLength = line.getLength(); // if the line is too small compared to the label, don't label it // and exit right away, since the lines are sorted from longest to // shortest if ((!allowOverruns || labelItem.isFollowLineEnabled()) && line.getLength() < textBounds.getWidth()) return labelCount > 0; // create the candidate positions for the labels over the line. If // we can place just one // label or we're not supposed to replicate them, create the mid // position, otherwise // create mid and then create the sequence of before and after // labels 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; // this will generate a sequence like s, -2s, 3s, -4s, ... // which will make the cursor alternate on mid + s, mid - s, // mid + 2s, mid - 2s, mid + 3s, ... double signum = Math.signum(offset); offset = -1 * signum * (Math.abs(offset) + labelDistance); } } else { labelPositions = new double[1]; labelPositions[0] = lineStringLength / 2; } // Ok, now we try to paint each of the labels in each position, and // we take into // account that we might have to displace the labels 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; // label displacement loop boolean painted = false; while (Math.abs(currOffset) <= labelOffset * 2 && !painted) { // reset transform and other computation parameters tx.setToIdentity(); Rectangle2D labelEnvelope; double maxAngleChange = 0; // the line ordinates where we presume the label will start // and end (using full bounds, // thus taking into account shield and halo) double startOrdinate = cursor.getCurrentOrdinate() - textBounds.getWidth() / 2; double endOrdinate = cursor.getCurrentOrdinate() + textBounds.getWidth() / 2; // compute label bounds if (labelItem.followLineEnabled) { // curved label, but we might end up drawing a straight // one as an optimization maxAngleChange = cursor.getMaxAngleChange(startOrdinate, endOrdinate); if (maxAngleChange < MIN_CURVED_DELTA) { // if label will be painted as straight, use the // straight bounds setupLineTransform(painter, cursor, centroid, tx, true); labelEnvelope = tx.createTransformedShape(textBounds).getBounds2D(); } else { // otherwise use curved bounds, more expensive to // compute labelEnvelope = getCurvedLabelBounds(cursor, startOrdinate, endOrdinate, textBounds.getHeight() / 2); } } else { setupLineTransform(painter, cursor, centroid, tx, false); labelEnvelope = tx.createTransformedShape(textBounds).getBounds2D(); } // try to paint the label, the condition under which this // happens are complex if (displayArea.contains(labelEnvelope) && !(labelItem.isConflictResolutionEnabled() && paintedBounds.labelsWithinDistance(labelEnvelope, extraSpace)) && !groupLabels.labelsWithinDistance(labelEnvelope, minDistance)) { if (labelItem.isFollowLineEnabled()) { // for curved labels we never paint in case of // overrun if ((startOrdinate > 0 && endOrdinate <= cursor.getLineStringLength())) { if (maxAngleChange < maxAngleDelta) { // if the max angle is very small, draw it // like a straight line if (maxAngleChange < MIN_CURVED_DELTA) painter.paintStraightLabel(tx); else { painter.paintCurvedLabel(cursor); } painted = true; } } } else { // for straight labels, check overrun only if // required if ((allowOverruns || (startOrdinate > 0 && endOrdinate <= cursor .getLineStringLength()))) { painter.paintStraightLabel(tx); painted = true; } } } // if we actually painted the label, add the envelope to the // indexes and break out of the loop, // otherwise move to the next candidate position in the // displacement sequence if (painted) { labelCount++; groupLabels.addLabel(labelItem, labelEnvelope); if(labelItem.isConflictResolutionEnabled()) paintedBounds.addLabel(labelItem, labelEnvelope); } else { // this will generate a sequence like s, -2s, 3s, -4s, // ... // which will make the cursor alternate on mid + s, mid // - s, mid + 2s, mid - 2s, mid + 3s, ... 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]; // see if this one should be added 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); } /** * Sets up the transformation needed to position the label at the specified * point, using the positioning information loaded from the the text style * * @param tempTransform * @param centroid * @param textStyle * @param textBounds */ private void setupPointTransform(AffineTransform tempTransform, Point centroid, TextStyle2D textStyle, LabelPainter painter) { tempTransform.translate(centroid.getX(), centroid.getY()); double rotation = textStyle.getRotation(); if (Double.isNaN(rotation) || Double.isInfinite(rotation)) { // might legitimately happen if the rotation is computed out of an expression rotation = 0.0; } tempTransform.rotate(rotation); Rectangle2D textBounds = painter.getLabelBounds(); // This now does "centering" taking into account the anchoring // and the real positioning of the text bounds (the bounds are placed // so that the baseline is in the origin, and the text goes up in // the negative coordinates) double displacementX = (textStyle.getAnchorX() * (-textBounds.getWidth())) + textStyle.getDisplacementX(); double displacementY = (textStyle.getAnchorY() * (textBounds.getHeight())) - textStyle.getDisplacementY() - textBounds.getHeight() + painter.getLineHeight(); tempTransform.translate(displacementX, displacementY); } /** * Sets up the transformation needed to position the label at the current * location of the line string, using the positioning information loaded * from the the text style * * @param tempTransform * @param centroid * @param textStyle * @param textBounds */ 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(); // undo the above if its point placement! double rotation; double displacementX = 0; double displacementY = 0; if (textStyle.isPointPlacement() && !followLine) { // use the one the user supplied! rotation = textStyle.getRotation(); } else { // lineplacement if(painter.getLabel().isForceLeftToRightEnabled()) { rotation = cursor.getLabelOrientation(); } else { rotation = cursor.getCurrentAngle(); } // move it off the line displacementY -= textStyle.getPerpendicularOffset(); anchorX = 0.5; // centered 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); } /** * Gets a representative point and tries to place the label according to SLD. * If a maxDisplacement has been set and the default position does not work * a search for a better position is tried on concentric circles around the label * up until the radius of the circle becomes bigger than the max displacement */ private boolean paintPointLabel(LabelPainter painter, AffineTransform tempTransform, Rectangle displayArea, LabelIndex glyphs) throws Exception { LabelCacheItem labelItem = painter.getLabel(); // get the point onto the shape has to be painted Point point = getPointSetRepresentativeLocation(labelItem.getGeoms(), displayArea); if (point == null) return false; // prepare for the search loop TextStyle2D ts = labelItem.getTextStyle(); // ... use at least a 2 pixel step, no matter what the label length is 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 straight paint works we're good if(paintPointLabelInternal(painter, tx, displayArea, glyphs, labelItem, point, ts)) return true; // get a cloned text style that we can modify without issues TextStyle2D cloned = new TextStyle2D(ts); // ... and the closest quadrant angle that we'll use to start the search from int startAngle = getClosestStandardAngle(ts.getDisplacementX(), ts.getDisplacementY()); int angle = startAngle; while(radius <= labelItem.maxDisplacement) { // the offset is used to generate a x, -x, 2x, -2x, 3x, -3x sequence 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)); // using dx and dy would be easy but due to numeric approximations, // it's actually very hard to get it right so we use the angle double[] anchorPointCandidates; // normalize the angle so that it's between 0 and 360 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; } // try out various anchor point positions 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; } // make sure we do the jumps back and forth to generate the proper sequence if(angle <= startAngle) angle = angle + offset; else angle = angle - offset; } // increase the radius and move forward radius += step; } // we tried, we failed... return false; } /** * Returns the closest angle that is a multiple of 45° * @param x * @param y * @return an angle in degrees */ int getClosestStandardAngle(double x, double y) { double angle = Math.toDegrees(Math.atan2(y, x)); return (int) Math.round(angle / 45.0) * 45; } /** * Actually try to paint the label by setting up transformations, checking for * conflicts and so on * @param painter * @param tempTransform * @param displayArea * @param glyphs * @param labelItem * @param point * @param textStyle * @return * @throws Exception */ private boolean paintPointLabelInternal(LabelPainter painter, AffineTransform tempTransform, Rectangle displayArea, LabelIndex glyphs, LabelCacheItem labelItem, Point point, TextStyle2D textStyle) throws Exception { setupPointTransform(tempTransform, point, textStyle, painter); // check for overlaps and paint Rectangle2D transformed = tempTransform .createTransformedShape(painter.getFullLabelBounds()).getBounds2D(); if (!displayArea.contains(transformed) || (labelItem.isConflictResolutionEnabled() && glyphs.labelsWithinDistance(transformed, labelItem.getSpaceAround()))) { return false; } else { // painter.graphics.setStroke(new BasicStroke()); // painter.graphics.setColor(Color.BLACK); // painter.graphics.draw(transformed); painter.paintStraightLabel(tempTransform); if(labelItem.isConflictResolutionEnabled()) glyphs.addLabel(labelItem, transformed); return true; } } /** * returns the representative geometry (for further processing) * * TODO: handle lineplacement for a polygon (perhaps we're supposed to grab * the outside line and label it, but spec is unclear) */ 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) { // generalized polygons causes problems - this // tries to hide them. try { centroid = geom.getExteriorRing().getCentroid(); } catch (Exception ee) { try { centroid = geom.getFactory().createPoint(geom.getCoordinate()); } catch (Exception eee) { return false; // we're hooped } } } // check we're inside, if not, use a different approach PreparedGeometry pg = PreparedGeometryFactory.prepare(geom); if(!pg.contains(centroid)) { // resort to sampling, computing the intersection is slow and // due invalid geometries can easily break with an exception Envelope env = geom.getEnvelopeInternal(); double step = 5; int steps = (int) Math.round((env.getMaxX() - env.getMinX()) / step); Coordinate c = new Coordinate(); Point pp = gf.createPoint(c); c.y = centroid.getY(); int max = -1; int maxIdx = -1; int containCounter = -1; for (int i = 0; i < steps; i++) { c.x = env.getMinX() + step * i; pp.geometryChanged(); if(!pg.contains(pp)) { containCounter = 0; } else if(i == 0) { containCounter = 1; } else { containCounter++; if(containCounter > max) { max = containCounter; maxIdx = i; } } } if(maxIdx != -1) { int midIdx = max > 1 ? maxIdx - max / 2 : maxIdx; c.x = env.getMinX() + step * midIdx; pp.geometryChanged(); centroid = pp; } else { return false; } } // compute the transformation used to position the label TextStyle2DExt textStyle = new TextStyle2DExt(labelItem); 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; // candidate position was busy, let's circle out and find a good position // ... use at least a 2 pixel step, no matter what the label length is 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, TextStyle2DExt textStyle) throws Exception { // useful to debug the label/centroid relationship // painter.graphics.setColor(Color.RED); // painter.graphics.drawRect((int)(centroid.getX() - 2), (int) (centroid.getY() - 2), 2, 2); AffineTransform original = new AffineTransform(tempTransform); 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, tempTransform, pg) < painter.getLabel().getGoodnessOfFit()) { // try the alternate rotation if possible if(textStyle.flipRotation(pg.getGeometry())) { tempTransform.setTransform(original); setupPointTransform(tempTransform, centroid, textStyle, painter); transformed = tempTransform.createTransformedShape(painter.getFullLabelBounds()).getBounds2D(); if (!displayArea.contains(transformed) || (labelItem.isConflictResolutionEnabled() && glyphs.labelsWithinDistance(transformed, labelItem.getSpaceAround())) || goodnessOfFit(painter, tempTransform, pg) < painter.getLabel().getGoodnessOfFit()) { textStyle.flipRotation(pg.getGeometry()); return false; } } else { 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; } /** * * 1. get a list of points from the input geometries that are inside the * displayGeom NOTE: lines and polygons are reduced to their centroids (you * shouldnt really calling this with lines and polys) 2. choose the most * "central" of the points METRIC - choose anyone TODO: change metric to be * "closest to the centoid of the possible points" * * @param geoms * list of Point or MultiPoint (any other geometry types are * rejected * @param displayGeometry * @return a point or null (if there's nothing to draw) */ Point getPointSetRepresentativeLocation(List<Geometry> geoms, Rectangle displayArea) { // points that are inside the displayGeometry ArrayList<Point> pts = new ArrayList<Point>(); for (Geometry g : geoms) { if (!((g instanceof Point) || (g instanceof MultiPoint))) // handle // lines,polys, gc, etc.. g = g.getCentroid(); // will be point if (g instanceof Point) { Point point = (Point) g; if (displayArea.contains(point.getX(), point.getY())) // this is // robust! pts.add(point); // possible label location } 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); // possible label location } } } if (pts.size() == 0) return null; // do better metric than this: return (Point) pts.get(0); } /** * 1. make a list of all the geoms (not clipped) NOTE: reject points, * convert polygons to their exterior ring (you shouldnt be calling this * function with points and polys) 2. join the lines together 3. clip * resulting lines to display geometry 4. return longest line * * NOTE: the joining has multiple solution. For example, consider a Y (3 * lines): * * 1 2 * * * 3 * solutions are: 1->2 and 3 1->3 and 2 2->3 and 1 * * (see mergeLines() below for detail of the algorithm; its basically a * greedy algorithm that should form the 'longest' possible route through * the linework) * * NOTE: we clip after joining because there could be connections "going on" * outside the display bbox * * * @param geoms * @param removeOverlaps * @param displayGeometry * must be poly */ List<LineString> getLineSetRepresentativeLocation(List<Geometry> geoms, Rectangle displayArea, boolean removeOverlaps) { // go through each geometry in the set. // if its a polygon or multipolygon, get the boundary (reduce to a line) // if its a line, add it to "lines" // if its a multiline, add each component line to "lines" List<LineString> lines = new ArrayList<LineString>(); for (Geometry g : geoms) { accumulateLineStrings(g, lines); } if (lines.size() == 0) return null; // clip all the lines to the current bounds List<LineString> clippedLines = new ArrayList<LineString>(); for (LineString ls : lines) { // more robust clipper -- see its dox 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; // at this point "lines" now is a list of linestring // join this algo doesnt always do what you want it to do, but its // pretty good List<LineString> merged = mergeLines(clippedLines); // clippedLines is a list of LineString, all cliped (hopefully) to the // display geometry. we choose longest one if (merged.size() == 0) return null; // sort have the longest lines first 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; // reduce polygons to their boundaries if ((g instanceof Polygon) || (g instanceof MultiPolygon)) { g = g.getBoundary(); // line or multiline m // TODO: boundary included the inside rings, might want to // replace this with getExteriorRing() if (!((g instanceof LineString) || (g instanceof MultiLineString))) return 0; } // deal with line and multi line string, and finally with geom // collection if (g instanceof LineString) { if (g.getLength() != 0) { lines.add((LineString) g); return 1; } else { return 0; } } else if (g instanceof MultiLineString) {// multiline 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; } } /** * try to be more robust dont bother returning points * * This will try to solve robustness problems, but read code as to what it * does. It might return the unclipped line if there's a problem! * * @param line * @param bbox * MUST BE A BOUNDING BOX */ public MultiLineString clipLineString(LineString line) { Geometry clip = line; // djb -- jessie should do this during generalization line.geometryChanged(); if (clipper.getBounds().contains(line.getEnvelopeInternal())) { // shortcut -- entirely inside the display rectangle -- no clipping // required! 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) { // TODO: should try to expand the bounding box and re-do the // intersection, but line-bounding box // problems are quite rare. return line.getFactory().createMultiLineString(new LineString[] { line }); } } /** * 1. make a list of all the polygons clipped to the displayGeometry NOTE: * reject any points or lines 2. choose the largest of the clipped * geometries * * @param geoms * @param displayGeometry */ Polygon getPolySetRepresentativeLocation(List<Geometry> geoms, Rectangle displayArea) { List<Polygon> polys = new ArrayList<Polygon>(); // points that are // inside the Geometry displayGeometry = gf.toGeometry(toEnvelope(displayArea)); // go through each geometry in the input set // if its not a polygon or multipolygon ignore it // if its a polygon, add it to "polys" // if its a multipolgon, add each component to "polys" for (Geometry g : geoms) { if (!((g instanceof Polygon) || (g instanceof MultiPolygon))) continue; if (g instanceof Polygon) { polys.add((Polygon) g); } else { // multipoly for (int t = 0; t < g.getNumGeometries(); t++) { Polygon gg = (Polygon) g.getGeometryN(t); polys.add(gg); } } } if (polys.size() == 0) return null; // at this point "polys" is a list of polygons. Clip them 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)); } } // clippedPolys is a list of Polygon, all cliped (hopefully) to the // display geometry. we choose largest one if (clippedPolys.size() == 0) { return null; } double maxSize = -1; Polygon maxPoly = null; Polygon cpoly; for (int t = 0; t < clippedPolys.size(); t++) { cpoly = (Polygon) clippedPolys.get(t); final double area = cpoly.getArea(); if (area > maxSize) { maxPoly = cpoly; maxSize = area; } } // fast clipping may result in polygons with 0 area if(maxSize > 0) { return maxPoly; } else { return null; } } /** * try to do a more robust way of clipping a polygon to a bounding box. This * might return the orginal polygon if it cannot clip TODO: this is a bit * simplistic, there's lots more to do. * * @param poly * @param bbox * @param displayGeomEnv * * @return a MutliPolygon */ public MultiPolygon clipPolygon(Polygon poly, Polygon bbox, Envelope displayGeomEnv) { Geometry clip = poly; poly.geometryChanged();// djb -- jessie should do this during // generalization if (displayGeomEnv.contains(poly.getEnvelopeInternal())) { // shortcut -- entirely inside the display rectangle -- no clipping // required! Polygon[] polys = new Polygon[1]; polys[0] = (Polygon) clip; return poly.getFactory().createMultiPolygon(polys); } try { clip = clipper.clip(poly, false); } catch (Exception e) { // TODO: should try to expand the bounding box and re-do the // intersection. // TODO: also, try removing the interior rings of the polygon clip = poly;// just return the unclipped version } 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); } // otherwise we've got a point or line&point or empty 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; // its a GC 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); // dont think multiPolygon is possible, but not sure } // convert to multipoly 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); // build merged lines List<LineString> merged = new ArrayList<LineString>(lm.getMergedLineStrings()); if (merged.size() == 0) { return null; // shouldnt happen } else if (merged.size() == 1) { // simple case - no need to continue // merging return merged; } // coordinate -> list of incoming/outgoing lines 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); // SORT -- sorting is important because order does matter. // sorted long->short Collections.sort(merged_list, lineLengthComparator); return processNodes(merged_list, nodes); } /** * pull a line from the list, and: 1. if nothing connects to it (its * issolated), add it to "result" 2. otherwise, merge it at the start/end * with the LONGEST line there. 3. remove the original line, and the lines * it merged with from the hashtables 4. go again, with the merged line * * @param edges * @param nodes * @param result * */ public List<LineString> processNodes(List<LineString> edges, Map<Coordinate, List<LineString>> nodes) { List<LineString> result = new ArrayList<LineString>(); int index = 0; // index into edges while (index < edges.size()) // still more to do { // 1. get a line and remove it from the graph LineString ls = (LineString) edges.get(index); Coordinate key = ls.getCoordinateN(0); List<LineString> nodeList = nodes.get(key); if (nodeList == null) { // this was removed in an earlier iteration index++; continue; } else if (!nodeList.contains(ls)) { index++; continue; // already processed } removeFromHash(nodes, ls); // we're removing this from the network Coordinate key2 = ls.getCoordinateN(ls.getNumPoints() - 1); List<LineString> nodeList2 = nodes.get(key2); // case 1 -- this line is independent if ((nodeList.size() == 0) && (nodeList2.size() == 0)) { result.add(ls); index++; // move to next line continue; } if (nodeList.size() > 0) // touches something at the start { LineString ls2 = getLongest(nodeList); // merge with this one ls = merge(ls, ls2); removeFromHash(nodes, ls2); } if (nodeList2.size() > 0) // touches something at the start { LineString ls2 = getLongest(nodeList2); // merge with this one ls = merge(ls, ls2); removeFromHash(nodes, ls2); } // need for further processing edges.set(index, ls); // redo this one. 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); } } /** * reverse direction of points in a line */ 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])); } /** * If possible, merge the two lines together (ie. their start/end points are * equal) returns null if not possible * * @param major * @param minor */ 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)) { // reverse minor -> major return mergeSimple(reverse(minor), major); } else if (major_s.equals2D(minor_e)) { // minor -> major return mergeSimple(minor, major); } else if (major_e.equals2D(minor_s)) { // major -> minor return mergeSimple(major, minor); } else if (major_e.equals2D(minor_e)) { // major -> reverse(minor) return mergeSimple(major, reverse(minor)); } return null; // no merge } /** * simple linestring merge - l1 points then l2 points */ 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])); } /** * sorts a list of LineStrings by length (long=1st) * */ private final class LineLengthComparator implements java.util.Comparator<LineString> { public int compare(LineString o1, LineString o2) { // sort big->small return Double.compare(o2.getLength(), o1.getLength()); } } // djb: replaced because old one was from sun's Rectangle class 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; } }