0001: /*
0002: * GeoTools - OpenSource mapping toolkit
0003: * http://geotools.org
0004: * (C) 2005-2006, Geotools Project Managment Committee (PMC)
0005: * (C) 2004, Refractions Research Inc.
0006: * (c) others
0007: *
0008: * This library is free software; you can redistribute it and/or
0009: * modify it under the terms of the GNU Lesser General Public
0010: * License as published by the Free Software Foundation;
0011: * version 2.1 of the License.
0012: *
0013: * This library is distributed in the hope that it will be useful,
0014: * but WITHOUT ANY WARRANTY; without even the implied warranty of
0015: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
0016: * Lesser General Public License for more details.
0017: */
0018: package org.geotools.renderer.lite;
0019:
0020: import java.awt.AlphaComposite;
0021: import java.awt.BasicStroke;
0022: import java.awt.Color;
0023: import java.awt.Composite;
0024: import java.awt.Graphics2D;
0025: import java.awt.Paint;
0026: import java.awt.Rectangle;
0027: import java.awt.font.FontRenderContext;
0028: import java.awt.font.GlyphVector;
0029: import java.awt.geom.AffineTransform;
0030: import java.awt.geom.Rectangle2D;
0031: import java.util.ArrayList;
0032: import java.util.Arrays;
0033: import java.util.Collection;
0034: import java.util.Collections;
0035: import java.util.HashMap;
0036: import java.util.HashSet;
0037: import java.util.Hashtable;
0038: import java.util.Iterator;
0039: import java.util.List;
0040: import java.util.Map;
0041: import java.util.Set;
0042:
0043: import javax.media.jai.util.Range;
0044:
0045: import org.geotools.feature.Feature;
0046: import org.geotools.geometry.jts.Decimator;
0047: import org.geotools.geometry.jts.LiteShape2;
0048: import org.geotools.renderer.style.SLDStyleFactory;
0049: import org.geotools.renderer.style.TextStyle2D;
0050: import org.geotools.styling.TextSymbolizer;
0051: import org.opengis.filter.expression.Literal;
0052:
0053: import com.vividsolutions.jts.geom.Coordinate;
0054: import com.vividsolutions.jts.geom.CoordinateSequence;
0055: import com.vividsolutions.jts.geom.Envelope;
0056: import com.vividsolutions.jts.geom.Geometry;
0057: import com.vividsolutions.jts.geom.GeometryCollection;
0058: import com.vividsolutions.jts.geom.GeometryFactory;
0059: import com.vividsolutions.jts.geom.LineString;
0060: import com.vividsolutions.jts.geom.LinearRing;
0061: import com.vividsolutions.jts.geom.MultiLineString;
0062: import com.vividsolutions.jts.geom.MultiPoint;
0063: import com.vividsolutions.jts.geom.MultiPolygon;
0064: import com.vividsolutions.jts.geom.Point;
0065: import com.vividsolutions.jts.geom.Polygon;
0066: import com.vividsolutions.jts.operation.linemerge.LineMerger;
0067: import com.vividsolutions.jts.precision.EnhancedPrecisionOp;
0068:
0069: /**
0070: * Default LabelCache Implementation
0071: *
0072: * DJB (major changes on May 11th, 2005): 1.The old version of the labeler, if
0073: * given a *set* of points, lines, or polygons justed labels the first item in
0074: * the set. The sets are formed when you want to only put a single "Main St" on
0075: * the map even if you have a bunch of small "Main St" segments.
0076: *
0077: * I changed this to be much much wiser.
0078: *
0079: * Basically, the new way looks at the set of geometries that its going to put a
0080: * label on and find the "best" one that represents it. That geometry is then
0081: * labeled (see below for details on where that label is placed).
0082: *
0083: * 2. I changed the actual drawing routines;
0084: *
0085: * 1. get the "representative geometry" 2. for points, label as before 3. for
0086: * lines, find the middle point on the line (old version just averaged start and
0087: * end points) and centre label on that point (rotated) 4. for polygon, put the
0088: * label in the middle
0089: *
0090: * 3.
0091: *
0092: * ie. for lines, try the label at the 1/3, 1/2, and 2/3 location. Metric is how
0093: * close the label bounding box is to the line.
0094: *
0095: * ie. for polygons, bisect the polygon (about the centroid) in to North, South,
0096: * East and West polygons. Use the location that has the label best inside the
0097: * polygon.
0098: *
0099: * After this is done, you can start doing constraint relaxation...
0100: *
0101: * 4. TODO: deal with labels going off the edge of the screen (much reduced
0102: * now). 5. TODO: add a "minimum quality" parameter (ie. if you're labeling a
0103: * tiny polygon with a tiny label, dont bother). Metrics are descibed in #3. 6.
0104: * TODO: add ability for SLD to tweak parameters (ie. "always label").
0105: *
0106: *
0107: * ------------------------------------------------------------------------------------------
0108: * I've added extra functionality; a) priority -- if you set the <Priority> in a
0109: * TextSymbolizer, then you can control the order of labelling ** see mailing
0110: * list for more details b) <VendorOption name="group">no</VendorOption> ---
0111: * turns off grouping for this symbolizer c) <VendorOption name="spaceAround">5</VendorOption> --
0112: * do not put labels within 5 pixels of this label.
0113: *
0114: * @author jeichar
0115: * @author dblasby
0116: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/render/src/main/java/org/geotools/renderer/lite/LabelCacheDefault.java $
0117: */
0118: public final class LabelCacheDefault implements LabelCache {
0119:
0120: /**
0121: * labels that arent this good will not be shown
0122: */
0123: public double MIN_GOODNESS_FIT = 0.7;
0124:
0125: public double DEFAULT_PRIORITY = 1000.0;
0126:
0127: /** Map<label, LabelCacheItem> the label cache */
0128: protected Map labelCache = new HashMap();
0129:
0130: /** non-grouped labels get thrown in here* */
0131: protected ArrayList labelCacheNonGrouped = new ArrayList();
0132:
0133: public boolean DEFAULT_GROUP = false; // what to do if there's no grouping option
0134:
0135: public int DEFAULT_SPACEAROUND = 0;
0136:
0137: /**
0138: * When true, the text is rendered as its GlyphVector outline (as a geometry) instead of using
0139: * drawGlypVector. Pro: labels and halos are perfectly centered, some people prefer the
0140: * extra antialiasing obtained. Cons: possibly slower, some people do not like the
0141: * extra antialiasing :)
0142: */
0143: protected boolean outlineRenderingEnabled = false;
0144:
0145: protected SLDStyleFactory styleFactory = new SLDStyleFactory();
0146: boolean stop = false;
0147: Set enabledLayers = new HashSet();
0148: Set activeLayers = new HashSet();
0149:
0150: LineLengthComparator lineLengthComparator = new LineLengthComparator();
0151:
0152: private boolean needsOrdering = false;
0153:
0154: public void stop() {
0155: stop = true;
0156: activeLayers.clear();
0157: }
0158:
0159: /**
0160: * @see org.geotools.renderer.lite.LabelCache#start()
0161: */
0162: public void start() {
0163: stop = false;
0164: }
0165:
0166: public void clear() {
0167: if (!activeLayers.isEmpty()) {
0168: throw new IllegalStateException(
0169: activeLayers
0170: + " are layers that started rendering but have not completed,"
0171: + " stop() or endLayer() must be called before clear is called");
0172: }
0173: needsOrdering = true;
0174: labelCache.clear();
0175: labelCacheNonGrouped.clear();
0176: enabledLayers.clear();
0177: }
0178:
0179: public void clear(String layerId) {
0180: if (activeLayers.contains(layerId)) {
0181: throw new IllegalStateException(
0182: layerId
0183: + " is still rendering, end the layer before calling clear.");
0184: }
0185: needsOrdering = true;
0186:
0187: for (Iterator iter = labelCache.values().iterator(); iter
0188: .hasNext();) {
0189: LabelCacheItem item = (LabelCacheItem) iter.next();
0190: if (item.getLayerIds().contains(layerId))
0191: iter.remove();
0192: }
0193: for (Iterator iter = labelCacheNonGrouped.iterator(); iter
0194: .hasNext();) {
0195: LabelCacheItem item = (LabelCacheItem) iter.next();
0196: if (item.getLayerIds().contains(layerId))
0197: iter.remove();
0198: }
0199:
0200: enabledLayers.remove(layerId);
0201:
0202: }
0203:
0204: public void disableLayer(String layerId) {
0205: needsOrdering = true;
0206: enabledLayers.remove(layerId);
0207: }
0208:
0209: /**
0210: * @see org.geotools.renderer.lite.LabelCache#startLayer()
0211: */
0212: public void startLayer(String layerId) {
0213: enabledLayers.add(layerId);
0214: activeLayers.add(layerId);
0215: }
0216:
0217: /**
0218: * get the priority from the symbolizer its an expression, so it will try to
0219: * evaluate it: 1. if its missing --> DEFAULT_PRIORITY 2. if its a number,
0220: * return that number 3. if its not a number, convert to string and try to
0221: * parse the number; return the number 4. otherwise, return DEFAULT_PRIORITY
0222: *
0223: * @param symbolizer
0224: * @param feature
0225: */
0226: public double getPriority(TextSymbolizer symbolizer, Feature feature) {
0227: if (symbolizer.getPriority() == null)
0228: return DEFAULT_PRIORITY;
0229:
0230: // evaluate
0231: try {
0232: Double number = (Double) symbolizer.getPriority().evaluate(
0233: feature, Double.class);
0234: return number.doubleValue();
0235: } catch (Exception e) {
0236: return DEFAULT_PRIORITY;
0237: }
0238: }
0239:
0240: /**
0241: * @see org.geotools.renderer.lite.LabelCache#put(org.geotools.renderer.style.TextStyle2D,
0242: * org.geotools.renderer.lite.LiteShape)
0243: */
0244: public void put(String layerId, TextSymbolizer symbolizer,
0245: Feature feature, LiteShape2 shape, Range scaleRange) {
0246: needsOrdering = true;
0247: try {
0248: //get label and geometry
0249: String label = (String) symbolizer.getLabel().evaluate(
0250: feature, String.class);
0251:
0252: if (label == null)
0253: return;
0254:
0255: label = label.trim();
0256: if (label.length() == 0) {
0257: return; // dont label something with nothing!
0258: }
0259: double priorityValue = getPriority(symbolizer, feature);
0260: boolean group = isGrouping(symbolizer);
0261: if (!(group)) {
0262: TextStyle2D textStyle = (TextStyle2D) styleFactory
0263: .createStyle(feature, symbolizer, scaleRange);
0264:
0265: LabelCacheItem item = new LabelCacheItem(layerId,
0266: textStyle, shape, label);
0267: item.setPriority(priorityValue);
0268: item.setSpaceAround(getSpaceAround(symbolizer));
0269: labelCacheNonGrouped.add(item);
0270: } else { // / --------- grouping case ----------------
0271:
0272: // equals and hashcode of LabelCacheItem is the hashcode of
0273: // label and the
0274: // equals of the 2 labels so label can be used to find the
0275: // entry.
0276:
0277: // DJB: this is where the "grouping" of 'same label' features
0278: // occurs
0279: LabelCacheItem lci = (LabelCacheItem) labelCache
0280: .get(label);
0281: if (lci == null) // nothing in there yet!
0282: {
0283: TextStyle2D textStyle = (TextStyle2D) styleFactory
0284: .createStyle(feature, symbolizer,
0285: scaleRange);
0286: LabelCacheItem item = new LabelCacheItem(layerId,
0287: textStyle, shape, label);
0288: item.setPriority(priorityValue);
0289: item.setSpaceAround(getSpaceAround(symbolizer));
0290: labelCache.put(label, item);
0291: } else {
0292: // add only in the non-default case or non-literal. Ie.
0293: // area()
0294: if ((symbolizer.getPriority() != null)
0295: && (!(symbolizer.getPriority() instanceof Literal)))
0296: lci.setPriority(lci.getPriority()
0297: + priorityValue); // djb--
0298: // changed
0299: // because
0300: // you
0301: // do
0302: // not
0303: // always
0304: // want
0305: // to
0306: // add!
0307:
0308: lci.getGeoms().add(shape.getGeometry());
0309: }
0310: }
0311: } catch (Exception e) // DJB: protection if there's a problem with the
0312: // decimation (getGeometry() can be null)
0313: {
0314: // do nothing
0315: }
0316: }
0317:
0318: /**
0319: * pull space around from the sybolizer options - defaults to
0320: * DEFAULT_SPACEAROUND.
0321: *
0322: * <0 means "I can overlap other labels" be careful with this.
0323: *
0324: * @param symbolizer
0325: */
0326: private int getSpaceAround(TextSymbolizer symbolizer) {
0327: String value = symbolizer.getOption("spaceAround");
0328: if (value == null)
0329: return DEFAULT_SPACEAROUND;
0330: try {
0331: return Integer.parseInt(value);
0332: } catch (Exception e) {
0333: return DEFAULT_SPACEAROUND;
0334: }
0335: }
0336:
0337: /**
0338: * look at the options in the symbolizer for "group". return its value if
0339: * not present, return "DEFAULT_GROUP"
0340: *
0341: * @param symbolizer
0342: */
0343: private boolean isGrouping(TextSymbolizer symbolizer) {
0344: String value = symbolizer.getOption("group");
0345: if (value == null)
0346: return DEFAULT_GROUP;
0347: return value.equalsIgnoreCase("yes")
0348: || value.equalsIgnoreCase("true")
0349: || value.equalsIgnoreCase("1");
0350: }
0351:
0352: /**
0353: * @see org.geotools.renderer.lite.LabelCache#endLayer(java.awt.Graphics2D,
0354: * java.awt.Rectangle)
0355: */
0356: public void endLayer(String layerId, Graphics2D graphics,
0357: Rectangle displayArea) {
0358: activeLayers.remove(layerId);
0359: }
0360:
0361: /**
0362: * return a list with all the values in priority order. Both grouped and
0363: * non-grouped
0364: *
0365: *
0366: */
0367: public List orderedLabels() {
0368: ArrayList al = getActiveLabels();
0369:
0370: Collections.sort(al);
0371: Collections.reverse(al);
0372: return al;
0373: }
0374:
0375: private ArrayList getActiveLabels() {
0376: Collection c = labelCache.values();
0377: ArrayList al = new ArrayList(); // modifiable (ie. sortable)
0378: for (Iterator iter = c.iterator(); iter.hasNext();) {
0379: LabelCacheItem item = (LabelCacheItem) iter.next();
0380: if (isActive(item.getLayerIds()))
0381: al.add(item);
0382: }
0383:
0384: for (Iterator iter = labelCacheNonGrouped.iterator(); iter
0385: .hasNext();) {
0386: LabelCacheItem item = (LabelCacheItem) iter.next();
0387: if (isActive(item.getLayerIds()))
0388: al.add(item);
0389: }
0390: return al;
0391: }
0392:
0393: private boolean isActive(Set layerIds) {
0394: for (Iterator iter = layerIds.iterator(); iter.hasNext();) {
0395: String string = (String) iter.next();
0396: if (enabledLayers.contains(string))
0397: return true;
0398:
0399: }
0400: return false;
0401: }
0402:
0403: /**
0404: * @see org.geotools.renderer.lite.LabelCache#end(java.awt.Graphics2D,
0405: * java.awt.Rectangle)
0406: */
0407: public void end(Graphics2D graphics, Rectangle displayArea) {
0408: if (!activeLayers.isEmpty()) {
0409: throw new IllegalStateException(
0410: activeLayers
0411: + " are layers that started rendering but have not completed,"
0412: + " stop() or endLayer() must be called before end() is called");
0413: }
0414: List glyphs = new ArrayList();
0415:
0416: // Hack: let's reduce the display area width and height by one pixel.
0417: // If the rendered image is 256x256, proper rendering of polygons and
0418: // lines occurr only if the display area is [0,0; 256,256], yet if you
0419: // try to render anything at [x,256] or [256,y] it won't show.
0420: // So, to avoid labels that happen to touch the border being cut
0421: // by one pixel, we reduce the display area.
0422: // Feels hackish, don't have a better solution at the moment thought
0423: displayArea = new Rectangle(displayArea);
0424: displayArea.width -= 1;
0425: displayArea.height -= 1;
0426:
0427: GeometryFactory factory = new GeometryFactory();
0428: Geometry displayGeom = factory.toGeometry(new Envelope(
0429: displayArea.getMinX(), displayArea.getMaxX(),
0430: displayArea.getMinY(), displayArea.getMaxY()));
0431:
0432: List items; // both grouped and non-grouped
0433: if (needsOrdering) {
0434: items = orderedLabels();
0435: } else {
0436: items = getActiveLabels();
0437: }
0438: for (Iterator labelIter = items.iterator(); labelIter.hasNext();) {
0439: if (stop)
0440: return;
0441: try {
0442: // LabelCacheItem labelItem = (LabelCacheItem)
0443: // labelCache.get(labelIter.next());
0444: LabelCacheItem labelItem = (LabelCacheItem) labelIter
0445: .next();
0446: labelItem.getTextStyle().setLabel(labelItem.getLabel());
0447: GlyphVector glyphVector = labelItem.getTextStyle()
0448: .getTextGlyphVector(graphics);
0449:
0450: // DJB: simplified this. Just send off to the point,line,or
0451: // polygon routine
0452: // NOTE: labelItem.getGeometry() returns the FIRST geometry, so
0453: // we're assuming that lines & points arent mixed
0454: // If they are, then the FIRST geometry determines how its
0455: // rendered (which is probably bad since it should be in
0456: // area,line,point order
0457: // TOD: as in NOTE above
0458:
0459: Geometry geom = labelItem.getGeometry();
0460:
0461: AffineTransform oldTransform = graphics.getTransform();
0462: /*
0463: * Just use identity for tempTransform because display area is 0,0,width,height
0464: * and oldTransform may have a different origin. OldTransform will be used later
0465: * for drawing.
0466: * -rg & je
0467: */
0468: AffineTransform tempTransform = new AffineTransform();
0469:
0470: Geometry representativeGeom = null;
0471:
0472: if ((geom instanceof Point)
0473: || (geom instanceof MultiPoint))
0474: representativeGeom = paintPointLabel(glyphVector,
0475: labelItem, tempTransform, displayGeom);
0476: else if (((geom instanceof LineString) && !(geom instanceof LinearRing))
0477: || (geom instanceof MultiLineString))
0478: representativeGeom = paintLineLabel(glyphVector,
0479: labelItem, tempTransform, displayGeom);
0480: else if (geom instanceof Polygon
0481: || geom instanceof MultiPolygon
0482: || geom instanceof LinearRing)
0483: representativeGeom = paintPolygonLabel(glyphVector,
0484: labelItem, tempTransform, displayGeom);
0485:
0486: // DJB: this is where overlapping labels are forbidden (first
0487: // out of the map has priority)
0488: Rectangle glyphBounds = glyphVector.getPixelBounds(
0489: null, 0, 0);
0490:
0491: glyphBounds = tempTransform.createTransformedShape(
0492: glyphBounds).getBounds();
0493:
0494: // is this offscreen? We assume offscreen as anything that is outside
0495: // or crosses the rendering borders, since in tiled rendering
0496: // we have to insulate ourself from other tiles
0497: if (!(displayArea.contains(glyphBounds)))
0498: continue;
0499:
0500: // we wind up using the translated shield location a number of
0501: // times, in overlap calculations, offscreen
0502: // calculations, etc. Let's just pre-calculate it here, as we do
0503: // the offscreen calculation.
0504: Rectangle2D shieldBounds = null;
0505: if (labelItem.getTextStyle().getGraphic() != null) {
0506: Rectangle area = labelItem.getTextStyle()
0507: .getGraphicDimensions();
0508: Rectangle untransformedBounds = glyphVector
0509: .getPixelBounds(
0510: new FontRenderContext(
0511: new AffineTransform(),
0512: true, false), 0, 0);
0513: // center the graphics on the labels back
0514: double[] shieldVerts = new double[] {
0515: -area.width / 2 + untransformedBounds.x
0516: - untransformedBounds.width / 2,
0517: -area.height / 2 + untransformedBounds.y
0518: - untransformedBounds.height / 2,
0519: area.width / 2, area.height / 2 };
0520: // transform to rendered space
0521: tempTransform.transform(shieldVerts, 0,
0522: shieldVerts, 0, 2);
0523: shieldBounds = new Rectangle2D.Double(
0524: shieldVerts[0] + glyphBounds.width / 2,
0525: shieldVerts[1] + glyphBounds.height / 2,
0526: shieldVerts[2] - shieldVerts[0],
0527: shieldVerts[3] - shieldVerts[1]);
0528: // if glyph is only partially outside of the display area, don't render it
0529: // for the same req
0530: if (!displayArea.contains(shieldBounds))
0531: continue;
0532: }
0533:
0534: // take into account radius so that halo do not overwrite other labels
0535: // that are too close to the current one
0536: int space = labelItem.getSpaceAround();
0537: int haloRadius = Math.round(labelItem.getTextStyle()
0538: .getHaloFill() != null ? labelItem
0539: .getTextStyle().getHaloRadius() : 0);
0540: if (space >= 0) // if <0 then its okay to have overlapping items
0541: {
0542: if (overlappingItems(glyphBounds, glyphs, space
0543: + haloRadius))
0544: continue;
0545: if (shieldBounds != null
0546: && overlappingItems(shieldBounds
0547: .getBounds(), glyphs, space))
0548: continue;
0549: }
0550:
0551: if (goodnessOfFit(glyphVector, tempTransform,
0552: representativeGeom) < MIN_GOODNESS_FIT)
0553: continue;
0554:
0555: try {
0556: /*
0557: * Merge the tempTransform with the transform provided by graphics. This is the
0558: * proper transform that should be used for drawing.
0559: * -je & rg
0560: */
0561: AffineTransform newTransform = new AffineTransform(
0562: oldTransform);
0563: newTransform.concatenate(tempTransform);
0564: graphics.setTransform(newTransform);
0565:
0566: if (labelItem.getTextStyle().getGraphic() != null) {
0567:
0568: // draw the label shield first, underneath the halo
0569: LiteShape2 tempShape = new LiteShape2(
0570: new GeometryFactory()
0571: .createPoint(new Coordinate(
0572: glyphBounds.width / 2.0,
0573: -1.0
0574: * glyphBounds.height
0575: / 2.0)), null,
0576: null, false, false);
0577:
0578: // labels should always draw, so we'll just force this
0579: // one to draw by setting it's min/max scale to 0<10 and
0580: // then
0581: // drawing at scale 5.0 on the next line
0582: labelItem.getTextStyle().getGraphic()
0583: .setMinMaxScale(0.0, 10.0);
0584: new StyledShapePainter(this ).paint(graphics,
0585: tempShape, labelItem.getTextStyle()
0586: .getGraphic(), 5.0);
0587: graphics.setTransform(tempTransform);
0588: }
0589:
0590: java.awt.Shape outline = glyphVector.getOutline();
0591: if (labelItem.getTextStyle().getHaloFill() != null) {
0592: graphics.setPaint(labelItem.getTextStyle()
0593: .getHaloFill());
0594: graphics.setComposite(labelItem.getTextStyle()
0595: .getHaloComposite());
0596:
0597: graphics.setStroke(new BasicStroke(
0598: 2f * haloRadius, BasicStroke.CAP_ROUND,
0599: BasicStroke.JOIN_ROUND));
0600: graphics.draw(outline);
0601: }
0602: // DJB: added this because several people were using
0603: // "font-color" instead of fill
0604: // It legal to have a label w/o fill (which means dont
0605: // render it)
0606: // This causes people no end of trouble.
0607: // If they dont want to colour it, then they should use a
0608: // filter
0609: // DEFAULT (no <Fill>) --> BLACK
0610: // NOTE: re-reading the spec says this is the correct
0611: // assumption.
0612: Paint fill = labelItem.getTextStyle().getFill();
0613: Composite comp = labelItem.getTextStyle()
0614: .getComposite();
0615: if (fill == null) {
0616: fill = Color.BLACK;
0617: comp = AlphaComposite.getInstance(
0618: AlphaComposite.SRC_OVER, 1.0f); // 100% opaque
0619: }
0620: if (fill != null) {
0621: graphics.setPaint(fill);
0622: graphics.setComposite(comp);
0623: if (outlineRenderingEnabled)
0624: graphics.fill(outline);
0625: else
0626: graphics.drawGlyphVector(glyphVector, 0, 0);
0627: Rectangle bounds = glyphVector.getPixelBounds(
0628: new FontRenderContext(tempTransform,
0629: true, false), 0, 0);
0630: int extraSpace = labelItem.getSpaceAround();
0631: if (extraSpace >= 0) // if <0 then we dont record
0632: // (something can overwrite it)
0633: {
0634: bounds = new Rectangle(bounds.x
0635: - extraSpace,
0636: bounds.y - extraSpace, bounds.width
0637: + extraSpace, bounds.height
0638: + extraSpace);
0639: if ((shieldBounds != null)) {
0640: bounds.add(shieldBounds);
0641: }
0642: bounds.grow(haloRadius, haloRadius);
0643: glyphs.add(bounds);
0644: }
0645: }
0646: } finally {
0647: graphics.setTransform(oldTransform);
0648: }
0649: } catch (Exception e) {
0650: // the decimation can cause problems - we
0651: // try to minimize it
0652: // do nothing
0653: }
0654: }
0655: }
0656:
0657: /**
0658: * how well does the label "fit" with the geometry. 1. points ALWAYS RETURNS
0659: * 1.0 2. lines ALWAYS RETURNS 1.0 (modify polygon method to handle rotated
0660: * labels) 3. polygon + assume: polylabels are unrotated + assume: polygon
0661: * could be invalid + dont worry about holes
0662: *
0663: * like to RETURN area of intersection between polygon and label bounds, but
0664: * thats expensive and likely to give us problems due to invalid polygons
0665: * SO, use a sample method - make a few points inside the label and see if
0666: * they're "close to" the polygon The method sucks, but works well...
0667: *
0668: * @param glyphVector
0669: * @param tempTransform
0670: * @param representativeGeom
0671: */
0672: private double goodnessOfFit(GlyphVector glyphVector,
0673: AffineTransform tempTransform, Geometry representativeGeom) {
0674: if (representativeGeom instanceof Point) {
0675: return 1.0;
0676: }
0677: if (representativeGeom instanceof LineString) {
0678: return 1.0;
0679: }
0680: if (representativeGeom instanceof Polygon) {
0681: Rectangle glyphBounds = glyphVector.getPixelBounds(
0682: new FontRenderContext(tempTransform, true, false),
0683: 0, 0);
0684: try {
0685: Polygon p = simplifyPoly((Polygon) representativeGeom);
0686: int count = 0;
0687: int n = 10;
0688: double mindistance = (glyphBounds.height);
0689: for (int t = 1; t < (n + 1); t++) {
0690: Coordinate c = new Coordinate(glyphBounds.x
0691: + ((double) glyphBounds.width)
0692: * (((double) t) / (n + 1)), glyphBounds
0693: .getCenterY());
0694: Point pp = new Point(c, representativeGeom
0695: .getPrecisionModel(), representativeGeom
0696: .getSRID());
0697: if (p.distance(pp) < mindistance)
0698:
0699: {
0700: count++;
0701: }
0702: }
0703: return ((double) count) / n;
0704: } catch (Exception e) {
0705: representativeGeom.geometryChanged(); // djb -- jessie should
0706: // do this during
0707: // generalization
0708: Envelope ePoly = representativeGeom
0709: .getEnvelopeInternal();
0710: Envelope eglyph = new Envelope(glyphBounds.x,
0711: glyphBounds.x + glyphBounds.width,
0712: glyphBounds.y, glyphBounds.y
0713: + glyphBounds.height);
0714: Envelope inter = intersection(ePoly, eglyph);
0715: if (inter != null)
0716: return (inter.getWidth() * inter.getHeight())
0717: / (eglyph.getWidth() * eglyph.getHeight());
0718: return 0.0;
0719: }
0720: }
0721: return 0.0;
0722: }
0723:
0724: /**
0725: * Remove holes from a polygon
0726: *
0727: * @param polygon
0728: */
0729: private Polygon simplifyPoly(Polygon polygon) {
0730: if (polygon.getNumInteriorRing() == 0)
0731: return polygon;
0732:
0733: LineString outer = polygon.getExteriorRing();
0734: if (outer.getStartPoint().distance(outer.getEndPoint()) != 0) {
0735: List clist = new ArrayList(Arrays.asList(outer
0736: .getCoordinates()));
0737: clist.add(outer.getStartPoint().getCoordinate());
0738: outer = outer.getFactory().createLinearRing(
0739: (Coordinate[]) clist.toArray(new Coordinate[clist
0740: .size()]));
0741: }
0742: LinearRing r = (LinearRing) outer;
0743:
0744: return outer.getFactory().createPolygon(r, null);
0745: }
0746:
0747: /**
0748: * Determines whether labelItems overlaps a previously rendered label.
0749: *
0750: * @param glyphs
0751: * list of bounds of previously rendered glyphs/shields.
0752: * @param bounds
0753: * new rectangle to check
0754: * @param extraSpace
0755: * extra space added to edges of bounds during check
0756: * @return true if labelItem overlaps a previously rendered glyph.
0757: */
0758: private boolean overlappingItems(Rectangle bounds, List glyphs,
0759: int extraSpace) {
0760: bounds = new Rectangle(bounds.x - extraSpace, bounds.y
0761: - extraSpace, bounds.width + extraSpace, bounds.height
0762: + extraSpace);
0763: Rectangle oldBounds;
0764: for (Iterator iter = glyphs.iterator(); iter.hasNext();) {
0765: oldBounds = (Rectangle) iter.next();
0766: if (oldBounds.intersects(bounds))
0767: return true;
0768: }
0769: return false;
0770: }
0771:
0772: private Geometry paintLineLabel(GlyphVector glyphVector,
0773: LabelCacheItem labelItem, AffineTransform tempTransform,
0774: Geometry displayGeom) {
0775: LineString line = (LineString) getLineSetRepresentativeLocation(
0776: labelItem.getGeoms(), displayGeom);
0777:
0778: if (line == null)
0779: return null;
0780:
0781: TextStyle2D textStyle = labelItem.getTextStyle();
0782:
0783: paintLineStringLabel(glyphVector, line, textStyle,
0784: tempTransform);
0785: return line;
0786: }
0787:
0788: /**
0789: * This handles point and line placement.
0790: *
0791: * 1. lineplacement -- calculate a rotation and location (and does the perp
0792: * offset) 2. pointplacement -- reduce line to a point and ignore the
0793: * calculated rotation
0794: *
0795: * @param glyphVector
0796: * @param line
0797: * @param textStyle
0798: * @param tempTransform
0799: */
0800: private void paintLineStringLabel(GlyphVector glyphVector,
0801: LineString line, TextStyle2D textStyle,
0802: AffineTransform tempTransform) {
0803: //Point start = line.getStartPoint();
0804: //Point end = line.getEndPoint();
0805: //double dx = end.getX() - start.getX();
0806: //double dy = end.getY() - start.getY();
0807: //double slope = dy / dx;
0808: //double theta = Math.atan(slope);
0809: // double rotation=theta;
0810:
0811: Rectangle2D textBounds = glyphVector.getVisualBounds();
0812: Point centroid = middleLine(line, 0.5); // DJB: changed from centroid to
0813: // "middle point" -- see
0814: // middleLine() dox
0815: // DJB: this is also where you could do "voting" and looking at other
0816: // locations on the line to label (ie. 0.33,0.66)
0817: tempTransform.translate(centroid.getX(), centroid.getY());
0818: double displacementX = 0;
0819: double displacementY = 0;
0820:
0821: // DJB: this now does "centering"
0822: // displacementX = (textStyle.getAnchorX() +
0823: // (-textBounds.getWidth()/2.0))
0824: // + textStyle.getDisplacementX();
0825: // displacementY = (textStyle.getAnchorY() +
0826: // (textBounds.getHeight()/2.0))
0827: // - textStyle.getDisplacementY();
0828:
0829: double anchorX = textStyle.getAnchorX();
0830: double anchorY = textStyle.getAnchorY();
0831:
0832: // undo the above if its point placement!
0833: double rotation;
0834: if (textStyle.isPointPlacement()) {
0835: rotation = textStyle.getRotation(); // use the one the user
0836: // supplied!
0837: } else // lineplacement
0838: {
0839: rotation = middleTheta(line, 0.5);
0840: displacementY -= textStyle.getPerpendicularOffset(); // move it
0841: // off the
0842: // line
0843: anchorX = 0.5; // centered
0844: anchorY = 0.5; // centered, sitting on line
0845: }
0846:
0847: displacementX = (anchorX * (-textBounds.getWidth()))
0848: + textStyle.getDisplacementX();
0849: displacementY += (anchorY * (textBounds.getHeight()))
0850: - textStyle.getDisplacementY();
0851:
0852: if (rotation != rotation) // IEEE def'n x=x for all x except when x is
0853: // NaN
0854: rotation = 0.0;
0855: if (Double.isInfinite(rotation))
0856: rotation = 0; // weird number
0857: tempTransform.rotate(rotation);
0858: tempTransform.translate(displacementX, displacementY);
0859: }
0860:
0861: /**
0862: * Simple to paint a point (or set of points) Just choose the first one and
0863: * paint it!
0864: *
0865: */
0866: private Geometry paintPointLabel(GlyphVector glyphVector,
0867: LabelCacheItem labelItem, AffineTransform tempTransform,
0868: Geometry displayGeom) {
0869: // get the point onto the shape has to be painted
0870: Point point = getPointSetRepresentativeLocation(labelItem
0871: .getGeoms(), displayGeom);
0872: if (point == null)
0873: return null;
0874:
0875: TextStyle2D textStyle = labelItem.getTextStyle();
0876: Rectangle2D textBounds = glyphVector.getVisualBounds();
0877: tempTransform.translate(point.getX(), point.getY());
0878: double displacementX = 0;
0879: double displacementY = 0;
0880:
0881: // DJB: this probably isnt doing what you think its doing - see others
0882: displacementX = (textStyle.getAnchorX() * (-textBounds
0883: .getWidth()))
0884: + textStyle.getDisplacementX();
0885: displacementY = (textStyle.getAnchorY() * (textBounds
0886: .getHeight()))
0887: - textStyle.getDisplacementY();
0888:
0889: if (!textStyle.isPointPlacement()) {
0890: // lineplacement. We're cheating here, since we cannot line label a
0891: // point
0892: displacementY -= textStyle.getPerpendicularOffset(); // just move
0893: // it up
0894: // (yes, its
0895: // cheating)
0896: }
0897:
0898: double rotation = textStyle.getRotation();
0899: if (rotation != rotation) // IEEE def'n x=x for all x except when x is
0900: // NaN
0901: rotation = 0.0;
0902: if (Double.isInfinite(rotation))
0903: rotation = 0; // weird number
0904:
0905: tempTransform.rotate(rotation);
0906: tempTransform.translate(displacementX, displacementY);
0907: return point;
0908: }
0909:
0910: /**
0911: * returns the representative geometry (for further processing)
0912: *
0913: * TODO: handle lineplacement for a polygon (perhaps we're supposed to grab
0914: * the outside line and label it, but spec is unclear)
0915: */
0916: private Geometry paintPolygonLabel(GlyphVector glyphVector,
0917: LabelCacheItem labelItem, AffineTransform tempTransform,
0918: Geometry displayGeom) {
0919: Polygon geom = getPolySetRepresentativeLocation(labelItem
0920: .getGeoms(), displayGeom);
0921: if (geom == null)
0922: return null;
0923:
0924: Point centroid;
0925:
0926: try {
0927: centroid = geom.getCentroid(); // this where you would do the
0928: // north/south/west/east stuff
0929: } catch (Exception e) // generalized polygons causes problems - this
0930: // tries to hid them.
0931: {
0932: try {
0933: centroid = geom.getExteriorRing().getCentroid();
0934: } catch (Exception ee) {
0935: try {
0936: centroid = geom.getFactory().createPoint(
0937: geom.getCoordinate());
0938: } catch (Exception eee) {
0939: return null; // we're hooped
0940: }
0941: }
0942: }
0943:
0944: TextStyle2D textStyle = labelItem.getTextStyle();
0945: Rectangle2D textBounds = glyphVector.getVisualBounds();
0946: tempTransform.translate(centroid.getX(), centroid.getY());
0947: double displacementX = 0;
0948: double displacementY = 0;
0949:
0950: // DJB: this now does "centering"
0951: displacementX = (textStyle.getAnchorX() * (-textBounds
0952: .getWidth()))
0953: + textStyle.getDisplacementX();
0954: displacementY = (textStyle.getAnchorY() * (textBounds
0955: .getHeight()))
0956: - textStyle.getDisplacementY();
0957:
0958: if (!textStyle.isPointPlacement()) {
0959: // lineplacement. We're cheating here, since we've reduced the
0960: // polygon to a point, when we should be trying to do something
0961: // a little smarter (like find its median axis!)
0962: displacementY -= textStyle.getPerpendicularOffset(); // just move
0963: // it up
0964: // (yes, its
0965: // cheating)
0966: }
0967:
0968: double rotation = textStyle.getRotation();
0969: if (rotation != rotation) // IEEE def'n x=x for all x except when x is
0970: // NaN
0971: rotation = 0.0;
0972: if (Double.isInfinite(rotation))
0973: rotation = 0; // weird number
0974:
0975: tempTransform.rotate(rotation);
0976: tempTransform.translate(displacementX, displacementY);
0977: return geom;
0978: }
0979:
0980: /**
0981: *
0982: * 1. get a list of points from the input geometries that are inside the
0983: * displayGeom NOTE: lines and polygons are reduced to their centroids (you
0984: * shouldnt really calling this with lines and polys) 2. choose the most
0985: * "central" of the points METRIC - choose anyone TODO: change metric to be
0986: * "closest to the centoid of the possible points"
0987: *
0988: * @param geoms
0989: * list of Point or MultiPoint (any other geometry types are
0990: * rejected
0991: * @param displayGeometry
0992: * @return a point or null (if there's nothing to draw)
0993: */
0994: Point getPointSetRepresentativeLocation(List geoms,
0995: Geometry displayGeometry) {
0996: ArrayList pts = new ArrayList(); // points that are inside the
0997: // displayGeometry
0998:
0999: Iterator it = geoms.iterator();
1000: Geometry g;
1001: while (it.hasNext()) {
1002: g = (Geometry) it.next();
1003: if (!((g instanceof Point) || (g instanceof MultiPoint))) // handle
1004: // lines,polys,
1005: // gc,
1006: // etc..
1007: g = g.getCentroid(); // will be point
1008: if (g instanceof Point) {
1009: if (displayGeometry.intersects(g)) // this is robust!
1010: pts.add(g); // possible label location
1011: } else if (g instanceof MultiPoint) {
1012: for (int t = 0; t < g.getNumGeometries(); t++) {
1013: Point gg = (Point) g.getGeometryN(t);
1014: if (displayGeometry.intersects(gg))
1015: pts.add(gg); // possible label location
1016: }
1017: }
1018: }
1019: if (pts.size() == 0)
1020: return null;
1021:
1022: // do better metric than this:
1023: return (Point) pts.get(0);
1024: }
1025:
1026: /**
1027: * 1. make a list of all the geoms (not clipped) NOTE: reject points,
1028: * convert polygons to their exterior ring (you shouldnt be calling this
1029: * function with points and polys) 2. join the lines together 3. clip
1030: * resulting lines to display geometry 4. return longest line
1031: *
1032: * NOTE: the joining has multiple solution. For example, consider a Y (3
1033: * lines): * * 1 2 * * * 3 * solutions are: 1->2 and 3 1->3 and 2 2->3 and 1
1034: *
1035: * (see mergeLines() below for detail of the algorithm; its basically a
1036: * greedy algorithm that should form the 'longest' possible route through
1037: * the linework)
1038: *
1039: * NOTE: we clip after joining because there could be connections "going on"
1040: * outside the display bbox
1041: *
1042: *
1043: * @param geoms
1044: * @param displayGeometry
1045: * must be poly
1046: */
1047: LineString getLineSetRepresentativeLocation(List geoms,
1048: Geometry displayGeometry) {
1049: ArrayList lines = new ArrayList(); // points that are inside the
1050: // displayGeometry
1051:
1052: Iterator it = geoms.iterator();
1053: Geometry g;
1054: // go through each geometry in the set.
1055: // if its a polygon or multipolygon, get the boundary (reduce to a line)
1056: // if its a line, add it to "lines"
1057: // if its a multiline, add each component line to "lines"
1058: while (it.hasNext()) {
1059: g = (Geometry) it.next();
1060: if (!((g instanceof LineString)
1061: || (g instanceof MultiLineString)
1062: || (g instanceof Polygon) || (g instanceof MultiPolygon)))
1063: continue;
1064:
1065: if ((g instanceof Polygon) || (g instanceof MultiPolygon)) {
1066: g = g.getBoundary(); // line or multiline m
1067: // TODO: boundary included the inside rings, might want to
1068: // replace this with getExteriorRing()
1069: if (!((g instanceof LineString) || (g instanceof MultiLineString)))
1070: continue; // protection
1071: } else if (g instanceof LineString) {
1072: if (g.getLength() != 0)
1073: lines.add(g);
1074: } else // multiline
1075: {
1076: for (int t = 0; t < g.getNumGeometries(); t++) {
1077: LineString gg = (LineString) g.getGeometryN(t);
1078: lines.add(gg);
1079: }
1080: }
1081: }
1082: if (lines.size() == 0)
1083: return null;
1084:
1085: // at this point "lines" now is a list of linestring
1086:
1087: // join
1088: // this algo doesnt always do what you want it to do, but its pretty
1089: // good
1090: Collection merged = this .mergeLines(lines);
1091:
1092: // clip to bounding box
1093: ArrayList clippedLines = new ArrayList();
1094: it = merged.iterator();
1095: LineString l;
1096: MultiLineString ll;
1097: Envelope displayGeomEnv = displayGeometry.getEnvelopeInternal();
1098: while (it.hasNext()) {
1099: l = (LineString) it.next();
1100: ll = clipLineString(l, (Polygon) displayGeometry,
1101: displayGeomEnv);
1102: if ((ll != null) && (!(ll.isEmpty()))) {
1103: for (int t = 0; t < ll.getNumGeometries(); t++)
1104: clippedLines.add(ll.getGeometryN(t)); // more robust
1105: // clipper -- see
1106: // its dox
1107: }
1108: }
1109:
1110: // clippedLines is a list of LineString, all cliped (hopefully) to the
1111: // display geometry. we choose longest one
1112: if (clippedLines.size() == 0)
1113: return null;
1114: double maxLen = -1;
1115: LineString maxLine = null;
1116: LineString cline;
1117: for (int t = 0; t < clippedLines.size(); t++) {
1118: cline = (LineString) clippedLines.get(t);
1119: if (cline.getLength() > maxLen) {
1120: maxLine = cline;
1121: maxLen = cline.getLength();
1122: }
1123: }
1124: return maxLine; // longest resulting line
1125: }
1126:
1127: /**
1128: * try to be more robust dont bother returning points
1129: *
1130: * This will try to solve robustness problems, but read code as to what it
1131: * does. It might return the unclipped line if there's a problem!
1132: *
1133: * @param line
1134: * @param bbox
1135: * MUST BE A BOUNDING BOX
1136: */
1137: public MultiLineString clipLineString(LineString line,
1138: Polygon bbox, Envelope displayGeomEnv) {
1139:
1140: Geometry clip = line;
1141: line.geometryChanged();// djb -- jessie should do this during
1142: // generalization
1143: if (displayGeomEnv.contains(line.getEnvelopeInternal())) {
1144: // shortcut -- entirely inside the display rectangle -- no clipping
1145: // required!
1146: LineString[] lns = new LineString[1];
1147: lns[0] = (LineString) clip;
1148: return line.getFactory().createMultiLineString(lns);
1149: }
1150: try {
1151: // the representative geometry does not need to be accurate, let's
1152: // simplify it further before doing the overlay to reduce the overlay cost
1153: Decimator d = new Decimator(10, 10);
1154: d.decimate(line);
1155: line.geometryChanged();
1156: clip = EnhancedPrecisionOp.intersection(line, bbox);
1157: } catch (Exception e) {
1158: // TODO: should try to expand the bounding box and re-do the
1159: // intersection, but line-bounding box
1160: // problems are quite rare.
1161: clip = line;// just return the unclipped version
1162: }
1163: if (clip instanceof MultiLineString)
1164: return (MultiLineString) clip;
1165: if (clip instanceof LineString) {
1166: LineString[] lns = new LineString[1];
1167: lns[0] = (LineString) clip;
1168: return line.getFactory().createMultiLineString(lns);
1169: }
1170: // otherwise we've got a point or line&point or empty
1171: if (clip instanceof Point)
1172: return null;
1173: if (clip instanceof MultiPoint)
1174: return null;
1175:
1176: // its a GC (Line intersection Poly cannot be a polygon/multipoly)
1177: GeometryCollection gc = (GeometryCollection) clip;
1178: ArrayList lns = new ArrayList();
1179: Geometry g;
1180: for (int t = 0; t < gc.getNumGeometries(); t++) {
1181: g = gc.getGeometryN(t);
1182: if (g instanceof LineString)
1183: lns.add(g);
1184: // dont think multilinestring is possible, but not sure
1185: }
1186:
1187: // convert to multilinestring
1188: if (lns.size() == 0)
1189: return null;
1190:
1191: return line.getFactory().createMultiLineString(
1192: (LineString[]) lns.toArray(new LineString[1]));
1193:
1194: }
1195:
1196: /**
1197: * 1. make a list of all the polygons clipped to the displayGeometry NOTE:
1198: * reject any points or lines 2. choose the largest of the clipped
1199: * geometries
1200: *
1201: * @param geoms
1202: * @param displayGeometry
1203: */
1204: Polygon getPolySetRepresentativeLocation(List geoms,
1205: Geometry displayGeometry) {
1206: ArrayList polys = new ArrayList(); // points that are inside the
1207: // displayGeometry
1208:
1209: Iterator it = geoms.iterator();
1210: Geometry g;
1211: // go through each geometry in the input set
1212: // if its not a polygon or multipolygon ignore it
1213: // if its a polygon, add it to "polys"
1214: // if its a multipolgon, add each component to "polys"
1215: while (it.hasNext()) {
1216: g = (Geometry) it.next();
1217: if (!((g instanceof Polygon) || (g instanceof MultiPolygon)))
1218: continue;
1219:
1220: if (g instanceof Polygon) {
1221: polys.add(g);
1222: } else // multipoly
1223: {
1224: for (int t = 0; t < g.getNumGeometries(); t++) {
1225: Polygon gg = (Polygon) g.getGeometryN(t);
1226: polys.add(gg);
1227: }
1228: }
1229: }
1230: if (polys.size() == 0)
1231: return null;
1232:
1233: // at this point "polys" is a list of polygons
1234:
1235: // clip
1236: ArrayList clippedPolys = new ArrayList();
1237: it = polys.iterator();
1238: Polygon p;
1239: MultiPolygon pp;
1240: Envelope displayGeomEnv = displayGeometry.getEnvelopeInternal();
1241: while (it.hasNext()) {
1242: p = (Polygon) it.next();
1243: pp = clipPolygon(p, (Polygon) displayGeometry,
1244: displayGeomEnv);
1245: if ((pp != null) && (!(pp.isEmpty()))) {
1246: for (int t = 0; t < pp.getNumGeometries(); t++)
1247: clippedPolys.add(pp.getGeometryN(t)); // more robust
1248: // version -- see
1249: // dox
1250: }
1251: }
1252: // clippedPolys is a list of Polygon, all cliped (hopefully) to the
1253: // display geometry. we choose largest one
1254: if (clippedPolys.size() == 0)
1255: return null;
1256: double maxSize = -1;
1257: Polygon maxPoly = null;
1258: Polygon cpoly;
1259: for (int t = 0; t < clippedPolys.size(); t++) {
1260: cpoly = (Polygon) clippedPolys.get(t);
1261: if (cpoly.getArea() > maxSize) {
1262: maxPoly = cpoly;
1263: maxSize = cpoly.getArea();
1264: }
1265: }
1266: return maxPoly;
1267: }
1268:
1269: /**
1270: * try to do a more robust way of clipping a polygon to a bounding box. This
1271: * might return the orginal polygon if it cannot clip TODO: this is a bit
1272: * simplistic, there's lots more to do.
1273: *
1274: * @param poly
1275: * @param bbox
1276: * @param displayGeomEnv
1277: *
1278: * @return a MutliPolygon
1279: */
1280: public MultiPolygon clipPolygon(Polygon poly, Polygon bbox,
1281: Envelope displayGeomEnv) {
1282:
1283: Geometry clip = poly;
1284: poly.geometryChanged();// djb -- jessie should do this during
1285: // generalization
1286: if (displayGeomEnv.contains(poly.getEnvelopeInternal())) {
1287: // shortcut -- entirely inside the display rectangle -- no clipping
1288: // required!
1289: Polygon[] polys = new Polygon[1];
1290: polys[0] = (Polygon) clip;
1291: return poly.getFactory().createMultiPolygon(polys);
1292: }
1293:
1294: try {
1295: // the representative geometry does not need to be accurate, let's
1296: // simplify it further before doing the overlay to reduce the overlay cost
1297: Decimator d = new Decimator(10, 10);
1298: d.decimate(poly);
1299: poly.geometryChanged();
1300: clip = EnhancedPrecisionOp.intersection(poly, bbox);
1301: } catch (Exception e) {
1302: // TODO: should try to expand the bounding box and re-do the
1303: // intersection.
1304: // TODO: also, try removing the interior rings of the polygon
1305:
1306: clip = poly;// just return the unclipped version
1307: }
1308: if (clip instanceof MultiPolygon)
1309: return (MultiPolygon) clip;
1310: if (clip instanceof Polygon) {
1311: Polygon[] polys = new Polygon[1];
1312: polys[0] = (Polygon) clip;
1313: return poly.getFactory().createMultiPolygon(polys);
1314: }
1315: // otherwise we've got a point or line&point or empty
1316: if (clip instanceof Point)
1317: return null;
1318: if (clip instanceof MultiPoint)
1319: return null;
1320: if (clip instanceof LineString)
1321: return null;
1322: if (clip instanceof MultiLineString)
1323: return null;
1324:
1325: // its a GC
1326: GeometryCollection gc = (GeometryCollection) clip;
1327: ArrayList plys = new ArrayList();
1328: Geometry g;
1329: for (int t = 0; t < gc.getNumGeometries(); t++) {
1330: g = gc.getGeometryN(t);
1331: if (g instanceof Polygon)
1332: plys.add(g);
1333: // dont think multiPolygon is possible, but not sure
1334: }
1335:
1336: // convert to multipoly
1337: if (plys.size() == 0)
1338: return null;
1339:
1340: return poly.getFactory().createMultiPolygon(
1341: (Polygon[]) plys.toArray(new Polygon[1]));
1342: }
1343:
1344: /**
1345: * see middlePoint() find the segment that the point is apart of, and return
1346: * the slope.
1347: *
1348: * @param l
1349: * @param percent
1350: */
1351: double middleTheta(LineString l, double percent) {
1352: if (percent >= 1.0)
1353: percent = 0.99; // for precision
1354: if (percent <= 0)
1355: percent = 0.01; // for precision
1356:
1357: double len = l.getLength();
1358: double dist = percent * len;
1359:
1360: double running_sum_dist = 0;
1361: CoordinateSequence pts = l.getCoordinateSequence();
1362: double segmentLen;
1363: double dx;
1364: double dy;
1365: double slope;
1366: final int length = pts.size();
1367: Coordinate curr = new Coordinate();
1368: Coordinate next = new Coordinate();
1369: for (int i = 0; i < length - 1; i++) {
1370: pts.getCoordinate(i, curr);
1371: pts.getCoordinate(i + 1, next);
1372: segmentLen = curr.distance(next);
1373:
1374: if ((running_sum_dist + segmentLen) >= dist) {
1375: // it is on this segment pts[i] to pts[i+1]
1376: dx = (next.x - curr.x);
1377: dy = (next.y - curr.y);
1378: slope = dy / dx;
1379: return Math.atan(slope);
1380: }
1381: running_sum_dist += segmentLen;
1382: }
1383: return 0;
1384: }
1385:
1386: /**
1387: * calculate the middle of a line. The returning point will be x% (0.5 =
1388: * 50%) along the line and on the line.
1389: *
1390: *
1391: * @param l
1392: * @param percent
1393: * 0=start, 0.5=middle, 1.0=end
1394: */
1395: Point middleLine(LineString l, double percent) {
1396: if (percent >= 1.0)
1397: percent = 0.99; // for precision
1398: if (percent <= 0)
1399: percent = 0.01; // for precision
1400:
1401: double len = l.getLength();
1402: double dist = percent * len;
1403:
1404: double running_sum_dist = 0;
1405: Coordinate[] pts = l.getCoordinates();
1406: double segmentLen;
1407: final int length = pts.length;
1408: double r;
1409: Coordinate c;
1410: for (int i = 0; i < length - 1; i++) {
1411: segmentLen = pts[i].distance(pts[i + 1]);
1412:
1413: if ((running_sum_dist + segmentLen) >= dist) {
1414: // it is on this segment
1415: r = (dist - running_sum_dist) / segmentLen;
1416: c = new Coordinate(pts[i].x + (pts[i + 1].x - pts[i].x)
1417: * r, pts[i].y + (pts[i + 1].y - pts[i].y) * r);
1418: return l.getFactory().createPoint(c);
1419: }
1420: running_sum_dist += segmentLen;
1421: }
1422:
1423: return l.getEndPoint(); // precision protection
1424: }
1425:
1426: Collection mergeLines(Collection lines) {
1427: LineMerger lm = new LineMerger();
1428: lm.add(lines);
1429: Collection merged = lm.getMergedLineStrings(); // merged lines
1430:
1431: // Collection merged = lines;
1432:
1433: if (merged.size() == 0) {
1434: return null; // shouldnt happen
1435: }
1436: if (merged.size() == 1) // simple case - no need to continue merging
1437: {
1438: return merged;
1439: }
1440:
1441: Hashtable nodes = new Hashtable(merged.size() * 2); // coordinate ->
1442: // list of lines
1443: Iterator it = merged.iterator();
1444: while (it.hasNext()) {
1445: LineString ls = (LineString) it.next();
1446: putInNodeHash(ls, nodes);
1447: }
1448:
1449: ArrayList result = new ArrayList();
1450: ArrayList merged_list = new ArrayList(merged);
1451:
1452: // SORT -- sorting is important because order does matter.
1453: Collections.sort(merged_list, lineLengthComparator); // sorted
1454: // long->short
1455: processNodes(merged_list, nodes, result);
1456: // this looks for differences between the two methods.
1457: // Collection a = mergeLines2(lines);
1458: // if (a.size() != result.size())
1459: // {
1460: // System.out.println("bad");
1461: // boolean bb= false;
1462: // if (bb)
1463: // {
1464: // Collection b = mergeLines(lines);
1465: // }
1466: // }
1467: return result;
1468: }
1469:
1470: /**
1471: * pull a line from the list, and: 1. if nothing connects to it (its
1472: * issolated), add it to "result" 2. otherwise, merge it at the start/end
1473: * with the LONGEST line there. 3. remove the original line, and the lines
1474: * it merged with from the hashtables 4. go again, with the merged line
1475: *
1476: * @param edges
1477: * @param nodes
1478: * @param result
1479: *
1480: */
1481: public void processNodes(List edges, Hashtable nodes,
1482: ArrayList result) {
1483: int index = 0; // index into edges
1484: while (index < edges.size()) // still more to do
1485: {
1486: // 1. get a line and remove it from the graph
1487: LineString ls = (LineString) edges.get(index);
1488: Coordinate key = ls.getCoordinateN(0);
1489: ArrayList nodeList = (ArrayList) nodes.get(key);
1490: if (nodeList == null) // this was removed in an earlier iteration
1491: {
1492: index++;
1493: continue;
1494: }
1495: if (!nodeList.contains(ls)) {
1496: index++;
1497: continue; // already processed
1498: }
1499: removeFromHash(nodes, ls); // we're removing this from the network
1500:
1501: Coordinate key2 = ls.getCoordinateN(ls.getNumPoints() - 1);
1502: ArrayList nodeList2 = (ArrayList) nodes.get(key2);
1503:
1504: // case 1 -- this line is independent
1505: if ((nodeList.size() == 0) && (nodeList2.size() == 0)) {
1506: result.add(ls);
1507: index++; // move to next line
1508: continue;
1509: }
1510:
1511: if (nodeList.size() > 0) // touches something at the start
1512: {
1513: LineString ls2 = getLongest(nodeList); // merge with this one
1514: ls = merge(ls, ls2);
1515: removeFromHash(nodes, ls2);
1516: }
1517: if (nodeList2.size() > 0) // touches something at the start
1518: {
1519: LineString ls2 = getLongest(nodeList2); // merge with this one
1520: ls = merge(ls, ls2);
1521: removeFromHash(nodes, ls2);
1522: }
1523: // need for further processing
1524: edges.set(index, ls); // redo this one.
1525: putInNodeHash(ls, nodes); // put in network
1526: }
1527: }
1528:
1529: public void removeFromHash(Hashtable nodes, LineString ls) {
1530: Coordinate key = ls.getCoordinateN(0);
1531: ArrayList nodeList = (ArrayList) nodes.get(key);
1532: if (nodeList != null) {
1533: nodeList.remove(ls);
1534: }
1535: key = ls.getCoordinateN(ls.getNumPoints() - 1);
1536: nodeList = (ArrayList) nodes.get(key);
1537: if (nodeList != null) {
1538: nodeList.remove(ls);
1539: }
1540: }
1541:
1542: public LineString getLongest(ArrayList al) {
1543: if (al.size() == 1)
1544: return (LineString) (al.get(0));
1545: double maxLength = -1;
1546: LineString result = null;
1547: final int size = al.size();
1548: LineString l;
1549: for (int t = 0; t < size; t++) {
1550: l = (LineString) al.get(t);
1551: if (l.getLength() > maxLength) {
1552: result = l;
1553: maxLength = l.getLength();
1554: }
1555: }
1556: return result;
1557: }
1558:
1559: public void putInNodeHash(LineString ls, Hashtable nodes) {
1560: Coordinate key = ls.getCoordinateN(0);
1561: ArrayList nodeList = (ArrayList) nodes.get(key);
1562: if (nodeList == null) {
1563: nodeList = new ArrayList();
1564: nodeList.add(ls);
1565: nodes.put(key, nodeList);
1566: } else
1567: nodeList.add(ls);
1568: key = ls.getCoordinateN(ls.getNumPoints() - 1);
1569: nodeList = (ArrayList) nodes.get(key);
1570: if (nodeList == null) {
1571: nodeList = new ArrayList();
1572: nodeList.add(ls);
1573: nodes.put(key, nodeList);
1574: } else
1575: nodeList.add(ls);
1576: }
1577:
1578: /**
1579: * merges a set of lines together into a (usually) smaller set. This one's
1580: * pretty dumb, we use the JTS method (which doesnt merge on degree 3 nodes)
1581: * and try to construct less lines.
1582: *
1583: * There's multiple solutions, but we do this the easy way. Usually you will
1584: * not be given more than 3 lines (especially after jts is finished with).
1585: *
1586: * Find a line, find a lines that it "connects" to and add it. Keep going.
1587: *
1588: * DONE: be smarter - use length so the algorithm becomes greedy.
1589: *
1590: * This isnt 100% correct, but usually it does the right thing.
1591: *
1592: * NOTE: this is O(N^2), but N tends to be <10
1593: *
1594: * @param lines
1595: */
1596: Collection mergeLines2(Collection lines) {
1597: LineMerger lm = new LineMerger();
1598: lm.add(lines);
1599: Collection merged = lm.getMergedLineStrings(); // merged lines
1600: // Collection merged = lines;
1601:
1602: if (merged.size() == 0) {
1603: return null; // shouldnt happen
1604: }
1605: if (merged.size() == 1) // simple case - no need to continue merging
1606: {
1607: return merged;
1608: }
1609:
1610: // key to this algorithm is the sorting by line length!
1611:
1612: // basic method:
1613: // 1. grab the first line in the list of lines to be merged
1614: // 2. search through the rest of lines (longer ones = first checked) for
1615: // a line that can be merged
1616: // 3. if you find one, great, merge it and do 2 things - a) update the
1617: // search geometry with the merged geometry and b) delete the other
1618: // geometry
1619: // if not, keep looking
1620: // 4. go back to step #1, but use the next longest line
1621: // 5. keep going until you've completely gone through the list and no
1622: // merging's taken place
1623:
1624: ArrayList mylines = new ArrayList(merged);
1625:
1626: boolean keep_going = true;
1627: while (keep_going) {
1628: keep_going = false; // no news is bad news
1629: Collections.sort(mylines, lineLengthComparator); // sorted
1630: final int size = mylines.size(); // long->short
1631: LineString major, minor, merge;
1632: for (int t = 0; t < size; t++) // for each line
1633: {
1634: major = (LineString) mylines.get(t); // this is the
1635: // search
1636: // geometry
1637: // (step #1)
1638: if (major != null) {
1639: for (int i = t + 1; i < mylines.size(); i++) // search
1640: // forward
1641: // for a
1642: // joining
1643: // thing
1644: {
1645: minor = (LineString) mylines.get(i); // forward
1646: // scan
1647: if (minor != null) // protection because we remove an
1648: // already match line!
1649: {
1650: merge = merge(major, minor); // step 3
1651: // (null =
1652: // not
1653: // mergeable)
1654: if (merge != null) {
1655: // step 3a
1656: keep_going = true;
1657: mylines.set(i, null);
1658: mylines.set(t, merge);
1659: major = merge;
1660: }
1661: }
1662: }
1663: }
1664: }
1665: // remove any null items in the list (see step 3a)
1666:
1667: mylines = (ArrayList) removeNulls(mylines);
1668:
1669: }
1670:
1671: // return result
1672: return removeNulls(mylines);
1673:
1674: }
1675:
1676: /**
1677: * given a list, return a new list thats the same as the first, but has no
1678: * null values in it.
1679: *
1680: * @param l
1681: */
1682: ArrayList removeNulls(List l) {
1683: ArrayList al = new ArrayList();
1684: Iterator it = l.iterator();
1685: Object o;
1686: while (it.hasNext()) {
1687: o = it.next();
1688: if (o != null) {
1689: al.add(o);
1690: }
1691: }
1692: return al;
1693: }
1694:
1695: /**
1696: * reverse direction of points in a line
1697: */
1698: LineString reverse(LineString l) {
1699: List clist = Arrays.asList(l.getCoordinates());
1700: Collections.reverse(clist);
1701: return l.getFactory().createLineString(
1702: (Coordinate[]) clist.toArray(new Coordinate[1]));
1703: }
1704:
1705: /**
1706: * if possible, merge the two lines together (ie. their start/end points are
1707: * equal) returns null if not possible
1708: *
1709: * @param major
1710: * @param minor
1711: */
1712: LineString merge(LineString major, LineString minor) {
1713: Coordinate major_s = major.getCoordinateN(0);
1714: Coordinate major_e = major
1715: .getCoordinateN(major.getNumPoints() - 1);
1716: Coordinate minor_s = minor.getCoordinateN(0);
1717: Coordinate minor_e = minor
1718: .getCoordinateN(minor.getNumPoints() - 1);
1719:
1720: if (major_s.equals2D(minor_s)) {
1721: // reverse minor -> major
1722: return mergeSimple(reverse(minor), major);
1723:
1724: } else if (major_s.equals2D(minor_e)) {
1725: // minor -> major
1726: return mergeSimple(minor, major);
1727: } else if (major_e.equals2D(minor_s)) {
1728: // major -> minor
1729: return mergeSimple(major, minor);
1730: } else if (major_e.equals2D(minor_e)) {
1731: // major -> reverse(minor)
1732: return mergeSimple(major, reverse(minor));
1733: }
1734: return null; // no merge
1735: }
1736:
1737: /**
1738: * simple linestring merge - l1 points then l2 points
1739: */
1740: private LineString mergeSimple(LineString l1, LineString l2) {
1741: ArrayList clist = new ArrayList(Arrays.asList(l1
1742: .getCoordinates()));
1743: clist.addAll(Arrays.asList(l2.getCoordinates()));
1744:
1745: return l1.getFactory().createLineString(
1746: (Coordinate[]) clist.toArray(new Coordinate[1]));
1747: }
1748:
1749: /**
1750: * sorts a list of LineStrings by length (long=1st)
1751: *
1752: */
1753: private final class LineLengthComparator implements
1754: java.util.Comparator {
1755: public int compare(Object o1, Object o2) // note order - this sort
1756: // big->small
1757: {
1758: return Double.compare(((LineString) o2).getLength(),
1759: ((LineString) o1).getLength());
1760: }
1761: }
1762:
1763: // djb: replaced because old one was from sun's Rectangle class
1764: private Envelope intersection(Envelope e1, Envelope e2) {
1765: Envelope r = e1.intersection(e2);
1766: if (r.getWidth() < 0)
1767: return null;
1768: if (r.getHeight() < 0)
1769: return null;
1770: return r;
1771: }
1772:
1773: public void enableLayer(String layerId) {
1774: needsOrdering = true;
1775: enabledLayers.add(layerId);
1776: }
1777:
1778: public boolean isOutlineRenderingEnabled() {
1779: return outlineRenderingEnabled;
1780: }
1781:
1782: /**
1783: * Sets the text rendering mode.
1784: * When true, the text is rendered as its GlyphVector outline (as a geometry) instead of using
1785: * drawGlypVector. Pro: labels and halos are perfectly centered, some people prefer the
1786: * extra antialiasing obtained. Cons: possibly slower, some people do not like the
1787: * extra antialiasing :)
1788: */
1789: public void setOutlineRenderingEnabled(
1790: boolean outlineRenderingEnabled) {
1791: this.outlineRenderingEnabled = outlineRenderingEnabled;
1792: }
1793:
1794: }
|