001: /*
002: * The Unified Mapping Platform (JUMP) is an extensible, interactive GUI
003: * for visualizing and manipulating spatial features with geometry and attributes.
004: *
005: * Copyright (C) 2003 Vivid Solutions
006: *
007: * This program is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU General Public License
009: * as published by the Free Software Foundation; either version 2
010: * of the License, or (at your option) any later version.
011: *
012: * This program is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
015: * GNU General Public License for more details.
016: *
017: * You should have received a copy of the GNU General Public License
018: * along with this program; if not, write to the Free Software
019: * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
020: *
021: * For more information, contact:
022: *
023: * Vivid Solutions
024: * Suite #1A
025: * 2328 Government Street
026: * Victoria BC V8T 5G5
027: * Canada
028: *
029: * (250)385-6040
030: * www.vividsolutions.com
031: */
032: package com.vividsolutions.jump.workbench.ui.renderer.style;
033:
034: import java.awt.Color;
035: import java.awt.Font;
036: import java.awt.Graphics2D;
037: import java.awt.Shape;
038: import java.awt.font.TextLayout;
039: import java.awt.geom.*;
040: import java.util.Iterator;
041: import java.util.List;
042:
043: import com.vividsolutions.jts.geom.*;
044: import com.vividsolutions.jts.index.quadtree.Quadtree;
045: import com.vividsolutions.jts.util.Assert;
046: import com.vividsolutions.jump.I18N;
047: import com.vividsolutions.jump.feature.Feature;
048: import com.vividsolutions.jump.geom.Angle;
049: import com.vividsolutions.jump.geom.CoordUtil;
050: import com.vividsolutions.jump.geom.InteriorPointFinder;
051: import com.vividsolutions.jump.util.CoordinateArrays;
052: import com.vividsolutions.jump.workbench.model.Layer;
053: import com.vividsolutions.jump.workbench.ui.GUIUtil;
054: import com.vividsolutions.jump.workbench.ui.Viewport;
055:
056: public class LabelStyle implements Style {
057: public final static int FONT_BASE_SIZE = 12;
058: public final static String ABOVE_LINE = "ABOVE_LINE";
059: public final static String ON_LINE = "ON_LINE";
060: public final static String BELOW_LINE = "BELOW_LINE";
061: // At the moment, niternationalization is of no use as the UI display
062: // an image in the vertical alignment ComboBox used [mmichaud 2007-06-02]
063: //public static String ABOVE_LINE = I18N.get("ui.renderer.style.LabelStyle.ABOVE_LINE");
064: //public static String ON_LINE = I18N.get("ui.renderer.style.LabelStyle.ON_LINE");
065: //public static String BELOW_LINE =I18N.get("ui.renderer.style.LabelStyle.BELOW_LINE");
066: public static final String FID_COLUMN = "$FID";
067: private GeometryFactory factory = new GeometryFactory();
068: private Color originalColor;
069: private AffineTransform originalTransform;
070: private Layer layer;
071: private Geometry viewportRectangle = null;
072: private InteriorPointFinder interiorPointFinder = new InteriorPointFinder();
073: private Quadtree labelsDrawn = null;
074: private String attribute = LabelStyle.FID_COLUMN;
075: private String angleAttribute = ""; //"" means no angle attribute [Jon Aquino]
076: private String heightAttribute = ""; //"" means no height attribute [Jon Aquino]
077: private boolean enabled = false;
078: private Color color = Color.black;
079: private Font font = new Font("Dialog", Font.PLAIN, FONT_BASE_SIZE);
080: private boolean scaling = false;
081: private double height = 12;
082: private boolean hidingOverlappingLabels = true;
083: public String verticalAlignment = ABOVE_LINE;
084:
085: public LabelStyle() {
086: }
087:
088: public void initialize(Layer layer) {
089: labelsDrawn = new Quadtree();
090: viewportRectangle = null;
091: //Set the vertices' fill colour to the layer's line colour
092: this .layer = layer;
093: //-- [sstein] added again to initialize correct language
094: //-- [mmichaud] internationalization unused at the moment
095: //ABOVE_LINE = I18N.get("ui.renderer.style.LabelStyle.ABOVE_LINE");
096: //ON_LINE = I18N.get("ui.renderer.style.LabelStyle.ON_LINE");
097: //BELOW_LINE =I18N.get("ui.renderer.style.LabelStyle.BELOW_LINE");
098: //--
099: }
100:
101: public void paint(Feature f, Graphics2D g, Viewport viewport)
102: throws NoninvertibleTransformException {
103: Object attribute = getAttributeValue(f);
104: // added .trim() 2007-07-13 [mmichaud]
105: if ((attribute == null)
106: || (attribute.toString().trim().length() == 0)) {
107: return;
108: }
109: Geometry viewportIntersection = intersection(f.getGeometry(),
110: viewport);
111: if (viewportIntersection == null) {
112: return;
113: }
114: ModelSpaceLabelSpec spec = modelSpaceLabelSpec(viewportIntersection);
115: Point2D labelCentreInViewSpace = viewport
116: .toViewPoint(new Point2D.Double(spec.location.x,
117: spec.location.y));
118: paint(g, attribute.toString().trim(), // added .trim() 2007-07-13 [mmichaud]
119: viewport.getScale(), labelCentreInViewSpace, angle(f,
120: getAngleAttribute(), spec.angle), height(f,
121: getHeightAttribute(), getHeight()), spec.linear);
122: }
123:
124: /**
125: * Gets the appropriate attribute value, if one exists.
126: * If for some reason the attribute column does not exist, return null
127: *
128: * @param f
129: * @return the value of the attribute
130: * @return null if the attribute column does not exist
131: */
132: private Object getAttributeValue(Feature f) {
133: if (getAttribute().equals(LabelStyle.FID_COLUMN))
134: return f.getID() + "";
135: if (!f.getSchema().hasAttribute(getAttribute()))
136: return null;
137: return f.getAttribute(getAttribute());
138: }
139:
140: public static double angle(Feature feature,
141: String angleAttributeName, double defaultAngle) {
142: if (angleAttributeName.equals("")) {
143: return defaultAngle;
144: }
145: Object angleAttribute = feature
146: .getAttribute(angleAttributeName);
147: if (angleAttribute == null) {
148: return defaultAngle;
149: }
150: try {
151: return Angle.toRadians(Double.parseDouble(angleAttribute
152: .toString().trim()));
153: } catch (NumberFormatException e) {
154: return defaultAngle;
155: }
156: }
157:
158: private ModelSpaceLabelSpec modelSpaceLabelSpec(Geometry geometry)
159: throws NoninvertibleTransformException {
160: if (geometry.getDimension() == 1) {
161: return modelSpaceLabelSpec1D(geometry);
162: }
163: return new ModelSpaceLabelSpec(interiorPointFinder
164: .findPoint(geometry), 0, false);
165: }
166:
167: private ModelSpaceLabelSpec modelSpaceLabelSpec1D(Geometry geometry) {
168: LineSegment longestSegment = longestSegment(geometry);
169: return new ModelSpaceLabelSpec(CoordUtil.average(
170: longestSegment.p0, longestSegment.p1),
171: angle(longestSegment), true);
172: }
173:
174: private double angle(LineSegment segment) {
175: double angle = Angle.angle(segment.p0, segment.p1);
176: //Don't want upside-down labels! [Jon Aquino]
177: if (angle < (-Math.PI / 2d)) {
178: angle += Math.PI;
179: }
180: if (angle > (Math.PI / 2d)) {
181: angle -= Math.PI;
182: }
183: return angle;
184: }
185:
186: private LineSegment longestSegment(Geometry geometry) {
187: double maxSegmentLength = -1;
188: Coordinate c0 = null;
189: Coordinate c1 = null;
190: List arrays = CoordinateArrays.toCoordinateArrays(geometry,
191: false);
192: for (Iterator i = arrays.iterator(); i.hasNext();) {
193: Coordinate[] coordinates = (Coordinate[]) i.next();
194: for (int j = 1; j < coordinates.length; j++) { //start 1
195: if (coordinates[j - 1].distance(coordinates[j]) > maxSegmentLength) {
196: maxSegmentLength = coordinates[j - 1]
197: .distance(coordinates[j]);
198: c0 = coordinates[j - 1];
199: c1 = coordinates[j];
200: }
201: }
202: }
203: return new LineSegment(c0, c1);
204: }
205:
206: public static double height(Feature feature,
207: String heightAttributeName, double defaultHeight) {
208: if (heightAttributeName.equals("")) {
209: return defaultHeight;
210: }
211: Object heightAttribute = feature
212: .getAttribute(heightAttributeName);
213: if (heightAttribute == null) {
214: return defaultHeight;
215: }
216: try {
217: return Double
218: .parseDouble(heightAttribute.toString().trim());
219: } catch (NumberFormatException e) {
220: return defaultHeight;
221: }
222: }
223:
224: public void paint(Graphics2D g, String text, double viewportScale,
225: Point2D viewCentre, double angle, double height,
226: boolean linear) {
227: setup(g);
228: try {
229: double scale = height / getFont().getSize2D();
230: if (isScaling()) {
231: scale *= viewportScale;
232: }
233: g.setColor(getColor());
234: TextLayout layout = new TextLayout(text, getFont(), g
235: .getFontRenderContext());
236: AffineTransform transform = g.getTransform();
237: configureTransform(transform, viewCentre, scale, layout,
238: angle, linear);
239: g.setTransform(transform);
240: if (isHidingOverlappingLabels()) {
241: Area transformedLabelBounds = new Area(layout
242: .getBounds()).createTransformedArea(transform);
243: Envelope transformedLabelBoundsEnvelope = envelope(transformedLabelBounds);
244: if (collidesWithExistingLabel(transformedLabelBounds,
245: transformedLabelBoundsEnvelope)) {
246: return;
247: }
248: labelsDrawn.insert(transformedLabelBoundsEnvelope,
249: transformedLabelBounds);
250: }
251: layout.draw(g, 0, 0);
252: } finally {
253: cleanup(g);
254: }
255: }
256:
257: private Envelope envelope(Shape shape) {
258: Rectangle2D bounds = shape.getBounds2D();
259: return new Envelope(bounds.getMinX(), bounds.getMaxX(), bounds
260: .getMinY(), bounds.getMaxY());
261: }
262:
263: private boolean collidesWithExistingLabel(
264: Area transformedLabelBounds,
265: Envelope transformedLabelBoundsEnvelope) {
266: List potentialCollisions = labelsDrawn
267: .query(transformedLabelBoundsEnvelope);
268: for (Iterator i = potentialCollisions.iterator(); i.hasNext();) {
269: Area potentialCollision = (Area) i.next();
270: Area intersection = new Area(potentialCollision);
271: intersection.intersect(transformedLabelBounds);
272: if (!intersection.isEmpty()) {
273: return true;
274: }
275: }
276: return false;
277: }
278:
279: private void setup(Graphics2D g) {
280: originalTransform = g.getTransform();
281: originalColor = g.getColor();
282: }
283:
284: private void cleanup(Graphics2D g) {
285: g.setTransform(originalTransform);
286: g.setColor(originalColor);
287: }
288:
289: /**
290: * Even though a feature's envelope intersects the viewport, the feature
291: * itself may not intersect the viewport. In this case, this method
292: * returns null.
293: */
294: private Geometry intersection(Geometry geometry, Viewport viewport)
295: throws NoninvertibleTransformException {
296: return geometry.intersection(viewportRectangle(viewport));
297: }
298:
299: private Geometry viewportRectangle(Viewport viewport)
300: throws NoninvertibleTransformException {
301: if (viewportRectangle == null) {
302: Envelope e = viewport.toModelEnvelope(0, viewport
303: .getPanel().getWidth(), 0, viewport.getPanel()
304: .getHeight());
305: viewportRectangle = factory
306: .createPolygon(factory
307: .createLinearRing(new Coordinate[] {
308: new Coordinate(e.getMinX(), e
309: .getMinY()),
310: new Coordinate(e.getMinX(), e
311: .getMaxY()),
312: new Coordinate(e.getMaxX(), e
313: .getMaxY()),
314: new Coordinate(e.getMaxX(), e
315: .getMinY()),
316: new Coordinate(e.getMinX(), e
317: .getMinY()) }), null);
318: }
319: return viewportRectangle;
320: }
321:
322: private void configureTransform(AffineTransform transform,
323: Point2D viewCentre, double scale, TextLayout layout,
324: double angle, boolean linear) {
325: double xTranslation = viewCentre.getX()
326: - ((scale * layout.getBounds().getWidth()) / 2d);
327: double yTranslation = viewCentre.getY()
328: + ((scale * GUIUtil.trueAscent(layout)) / 2d);
329: if (linear) {
330: yTranslation -= verticalAlignmentOffset(scale
331: * layout.getBounds().getHeight());
332: }
333: //Negate the angle because the positive y-axis points downwards.
334: //See the #rotate JavaDoc. [Jon Aquino]
335: transform.rotate(-angle, viewCentre.getX(), viewCentre.getY());
336: transform.translate(xTranslation, yTranslation);
337: transform.scale(scale, scale);
338: }
339:
340: private double verticalAlignmentOffset(double scaledLabelHeight) {
341: if (getVerticalAlignment().equals(ON_LINE)) {
342: return 0;
343: }
344: double buffer = 3;
345: double offset = buffer
346: + (layer.getBasicStyle().getLineWidth() / 2d)
347: + (scaledLabelHeight / 2d);
348: if (getVerticalAlignment().equals(ABOVE_LINE)) {
349: return offset;
350: }
351: if (getVerticalAlignment().equals(BELOW_LINE)) {
352: return -offset;
353: }
354: Assert.shouldNeverReachHere();
355: return 0;
356: }
357:
358: public String getAttribute() {
359: return attribute;
360: }
361:
362: public String getAngleAttribute() {
363: return angleAttribute;
364: }
365:
366: public String getHeightAttribute() {
367: return heightAttribute;
368: }
369:
370: public boolean isEnabled() {
371: return enabled;
372: }
373:
374: public Color getColor() {
375: return color;
376: }
377:
378: public Font getFont() {
379: return font;
380: }
381:
382: public boolean isScaling() {
383: return scaling;
384: }
385:
386: public double getHeight() {
387: return height;
388: }
389:
390: public boolean isHidingOverlappingLabels() {
391: return hidingOverlappingLabels;
392: }
393:
394: public String getVerticalAlignment() {
395: return verticalAlignment;
396: }
397:
398: public void setVerticalAlignment(String verticalAlignment) {
399: this .verticalAlignment = verticalAlignment;
400: }
401:
402: public void setAttribute(String attribute) {
403: this .attribute = attribute;
404: }
405:
406: public void setAngleAttribute(String angleAttribute) {
407: this .angleAttribute = angleAttribute;
408: }
409:
410: public void setHeightAttribute(String heightAttribute) {
411: this .heightAttribute = heightAttribute;
412: }
413:
414: public void setEnabled(boolean enabled) {
415: this .enabled = enabled;
416: }
417:
418: public void setColor(Color color) {
419: this .color = color;
420: }
421:
422: public void setFont(Font font) {
423: this .font = font;
424: }
425:
426: public void setScaling(boolean scaling) {
427: this .scaling = scaling;
428: }
429:
430: public void setHeight(double height) {
431: this .height = height;
432: }
433:
434: public void setHidingOverlappingLabels(
435: boolean hidingOverlappingLabels) {
436: this .hidingOverlappingLabels = hidingOverlappingLabels;
437: }
438:
439: public Object clone() {
440: try {
441: return super .clone();
442: } catch (CloneNotSupportedException e) {
443: Assert.shouldNeverReachHere();
444: return null;
445: }
446: }
447:
448: private class ModelSpaceLabelSpec {
449: public double angle;
450: public Coordinate location;
451: public boolean linear;
452:
453: public ModelSpaceLabelSpec(Coordinate location, double angle,
454: boolean linear) {
455: this.location = location;
456: this.angle = angle;
457: this.linear = linear;
458: }
459: }
460: }
|