/*
* The Unified Mapping Platform (JUMP) is an extensible, interactive GUI
* for visualizing and manipulating spatial features with geometry and attributes.
*
* Copyright (C) 2003 Vivid Solutions
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*
* For more information, contact:
*
* Vivid Solutions
* Suite #1A
* 2328 Government Street
* Victoria BC V8T 5G5
* Canada
*
* (250)385-6040
* www.vividsolutions.com
*/
package com.vividsolutions.jump.workbench.ui.renderer.style;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.font.TextLayout;
import java.awt.geom.*;
import java.util.Iterator;
import java.util.List;
import java.awt.BasicStroke;
import java.awt.Stroke;
import org.openjump.core.ui.util.ScreenScale;
import com.vividsolutions.jts.geom.*;
import com.vividsolutions.jts.index.quadtree.Quadtree;
import com.vividsolutions.jts.util.Assert;
import com.vividsolutions.jump.I18N;
import com.vividsolutions.jump.feature.Feature;
import com.vividsolutions.jump.geom.Angle;
import com.vividsolutions.jump.geom.CoordUtil;
import com.vividsolutions.jump.geom.InteriorPointFinder;
import com.vividsolutions.jump.util.CoordinateArrays;
import com.vividsolutions.jump.workbench.model.Layer;
import com.vividsolutions.jump.workbench.ui.GUIUtil;
import com.vividsolutions.jump.workbench.ui.Viewport;
import java.text.DateFormat;
import java.text.NumberFormat;
import java.util.Date;
public class LabelStyle implements Style {
public final static int FONT_BASE_SIZE = 12;
public final static String ABOVE_LINE = "ABOVE_LINE"; //LDB: keep these for Project file
public final static String ON_LINE = "ON_LINE";
public final static String BELOW_LINE = "BELOW_LINE";
public final static String[] verticalAlignmentLookup = {ABOVE_LINE, ON_LINE,BELOW_LINE};
// At the moment, internationalization is of no use as the UI display
// an image in the vertical alignment ComboBox used [mmichaud 2007-06-02]
// Disabled image in ComboBox and replaced with existing I18N text [LDB 2007-08-27]
public static String ABOVE_LINE_TEXT = I18N.get("ui.renderer.style.LabelStyle.ABOVE_LINE");
public static String ON_LINE_TEXT = I18N.get("ui.renderer.style.LabelStyle.ON_LINE");
public static String BELOW_LINE_TEXT =I18N.get("ui.renderer.style.LabelStyle.BELOW_LINE");
public static final String FID_COLUMN = "$FID";
public final static String JUSTIFY_CENTER_TEXT =
I18N.get("ui.renderer.style.LabelStyle.center");
public final static String JUSTIFY_LEFT_TEXT =
I18N.get("ui.renderer.style.LabelStyle.left");
public final static String JUSTIFY_RIGHT_TEXT =
I18N.get("ui.renderer.style.LabelStyle.right");
public final static int JUSTIFY_CENTER = 0; //LDB: in retrospect, should have used text lookup as above
public final static int JUSTIFY_LEFT = 1; // for readabiilty of Project XML file
public final static int JUSTIFY_RIGHT = 2;
private GeometryFactory factory = new GeometryFactory();
private Color originalColor;
private AffineTransform originalTransform;
private Layer layer;
private Geometry viewportRectangle = null;
private InteriorPointFinder interiorPointFinder = new InteriorPointFinder();
private Quadtree labelsDrawn = null;
private String attribute = LabelStyle.FID_COLUMN;
private String angleAttribute = ""; //"" means no angle attribute [Jon Aquino]
private String heightAttribute = ""; //"" means no height attribute [Jon Aquino]
private boolean enabled = false;
private Color color = Color.black;
private Font font = new Font("Dialog", Font.PLAIN, FONT_BASE_SIZE);
private boolean scaling = false;
private double height = 12;
private boolean hidingOverlappingLabels = true;
public String verticalAlignment = ABOVE_LINE;
private boolean outlineShowing = false;
private Color outlineColor = new Color(230,230,230, 192);
private double outlineWidth = 4d;
private Stroke outlineStroke = new BasicStroke(4f,
BasicStroke.CAP_BUTT,BasicStroke.JOIN_BEVEL);
private boolean hideAtScale = false;
private double scaleToHideAt = 20000d;
private int horizontalAlignment = JUSTIFY_CENTER;
public LabelStyle() {}
public void initialize(Layer layer) {
labelsDrawn = new Quadtree();
viewportRectangle = null;
//Set the vertices' fill colour to the layer's line colour
this.layer = layer;
//-- [sstein] added again to initialize correct language
//-- [mmichaud] internationalization unused at the moment
//ABOVE_LINE = I18N.get("ui.renderer.style.LabelStyle.ABOVE_LINE");
//ON_LINE = I18N.get("ui.renderer.style.LabelStyle.ON_LINE");
//BELOW_LINE =I18N.get("ui.renderer.style.LabelStyle.BELOW_LINE");
//--
}
public void paint(Feature f, Graphics2D g, Viewport viewport)
throws NoninvertibleTransformException {
Object attributeValue = getAttributeValue(f);
// added .trim() 2007-07-13 [mmichaud]
if ((attributeValue == null) || (attributeValue.toString().trim().length() == 0)) {
return; //LDB formerly toString().length() == 0
}
if (attributeValue instanceof Date) {
DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.DEFAULT);
attributeValue = dateFormat.format((Date) attributeValue);
} else if (attributeValue instanceof Double) {
NumberFormat numberFormat = NumberFormat.getNumberInstance();
attributeValue = numberFormat.format(((Double) attributeValue).doubleValue());
} else if (attributeValue instanceof Integer) {
NumberFormat numberFormat = NumberFormat.getIntegerInstance();
attributeValue = numberFormat.format(((Integer) attributeValue).intValue());
}
if (isHidingAtScale()){
double scale = height / getFont().getSize2D();
if (isScaling()) {
scale *= viewport.getScale();
}
double realScale = ScreenScale.getHorizontalMapScale(viewport);
if (realScale > scaleToHideAt)
return;
}
Geometry viewportIntersection = intersection(f.getGeometry(), viewport);
if (viewportIntersection == null) {
return;
}
ModelSpaceLabelSpec spec = modelSpaceLabelSpec(viewportIntersection);
Point2D labelCentreInViewSpace =
viewport.toViewPoint(new Point2D.Double(spec.location.x, spec.location.y));
paint(
g,
attributeValue.toString().trim(), // added .trim() 2007-07-13 [mmichaud]
viewport,//.getScale(),
labelCentreInViewSpace,
angle(f, getAngleAttribute(), spec.angle),
height(f, getHeightAttribute(), getHeight()),
spec.linear);
}
/**
* Gets the appropriate attribute value, if one exists.
* If for some reason the attribute column does not exist, return null
*
* @param f
* @return the value of the attribute
* @return null if the attribute column does not exist
*/
private Object getAttributeValue(Feature f)
{
if (getAttribute().equals(LabelStyle.FID_COLUMN))
return f.getID() + "";
if (! f.getSchema().hasAttribute(getAttribute()))
return null;
return f.getAttribute(getAttribute());
}
public static double angle(
Feature feature,
String angleAttributeName,
double defaultAngle) {
if (angleAttributeName.equals("")) {
return defaultAngle;
}
Object angleAttribute = feature.getAttribute(angleAttributeName);
if (angleAttribute == null) {
return defaultAngle;
}
try {
return Angle.toRadians(Double.parseDouble(angleAttribute.toString().trim()));
} catch (NumberFormatException e) {
return defaultAngle;
}
}
private ModelSpaceLabelSpec modelSpaceLabelSpec(Geometry geometry)
throws NoninvertibleTransformException {
if (geometry.getDimension() == 1) {
return modelSpaceLabelSpec1D(geometry);
}
if ( geometry.getDimension() == 0) { //LDB: treat points as linear to justify them
return new ModelSpaceLabelSpec(geometry.getCoordinate(), 0d, true);
}
if ((verticalAlignment.equals(ABOVE_LINE))
|| (verticalAlignment.equals(BELOW_LINE))) {
return new ModelSpaceLabelSpec(findPoint(geometry), 0, true);
}
return new ModelSpaceLabelSpec(interiorPointFinder.findPoint(geometry), 0, false);
}
public Coordinate findPoint(Geometry geometry) {
if (geometry.isEmpty())
return new Coordinate(0, 0);
Envelope envelope = geometry.getEnvelopeInternal();
double x = (envelope.getMinX() + envelope.getMaxX()) / 2d;
double y = (envelope.getMinY() + envelope.getMaxY()) / 2d;
if (verticalAlignment.equals(ABOVE_LINE))
y = envelope.getMaxY();
else if (verticalAlignment.equals(BELOW_LINE))
y = envelope.getMinY();
if (horizontalAlignment == JUSTIFY_LEFT)
x = envelope.getMinX();
else if (horizontalAlignment == JUSTIFY_RIGHT)
x = envelope.getMaxX();
return new Coordinate(x, y);
}
private ModelSpaceLabelSpec modelSpaceLabelSpec1D(Geometry geometry) {
LineSegment longestSegment = longestSegment(geometry);
return new ModelSpaceLabelSpec(
(horizontalAlignment == JUSTIFY_CENTER) ?
CoordUtil.average(longestSegment.p0, longestSegment.p1) :
(horizontalAlignment == JUSTIFY_LEFT) ?
longestSegment.p0 : longestSegment.p1 ,
angle(longestSegment),
true);
}
private double angle(LineSegment segment) {
double angle = Angle.angle(segment.p0, segment.p1);
//Don't want upside-down labels! [Jon Aquino]
if (angle < (-Math.PI / 2d)) {
angle += Math.PI;
}
if (angle > (Math.PI / 2d)) {
angle -= Math.PI;
}
return angle;
}
private LineSegment longestSegment(Geometry geometry) {
double maxSegmentLength = -1;
Coordinate c0 = null;
Coordinate c1 = null;
List arrays = CoordinateArrays.toCoordinateArrays(geometry, false);
for (Iterator i = arrays.iterator(); i.hasNext();) {
Coordinate[] coordinates = (Coordinate[]) i.next();
for (int j = 1; j < coordinates.length; j++) { //start 1
if (coordinates[j - 1].distance(coordinates[j]) > maxSegmentLength) {
maxSegmentLength = coordinates[j - 1].distance(coordinates[j]);
c0 = coordinates[j - 1];
c1 = coordinates[j];
}
}
}
return new LineSegment(c0, c1);
}
public static double height(
Feature feature,
String heightAttributeName,
double defaultHeight) {
if (heightAttributeName.equals("")) {
return defaultHeight;
}
Object heightAttribute = feature.getAttribute(heightAttributeName);
if (heightAttribute == null) {
return defaultHeight;
}
try {
return Double.parseDouble(heightAttribute.toString().trim());
} catch (NumberFormatException e) {
return defaultHeight;
}
}
public void paint(
Graphics2D g,
String text,
Viewport viewport,
//double viewportScale,
Point2D viewCentre,
double angle,
double height,
boolean linear) {
setup(g);
try {
double viewportScale = viewport.getScale();
double scale = height / getFont().getSize2D();
if (isScaling()) {
scale *= viewportScale;
}
TextLayout layout = new TextLayout(text, getFont(), g.getFontRenderContext());
AffineTransform transform = g.getTransform();
configureTransform(transform, viewCentre, scale, layout, angle, linear);
g.setTransform(transform);
if (isHidingOverlappingLabels()) {
Area transformedLabelBounds =
new Area(layout.getBounds()).createTransformedArea(transform);
Envelope transformedLabelBoundsEnvelope =
envelope(transformedLabelBounds);
if (collidesWithExistingLabel(transformedLabelBounds,
transformedLabelBoundsEnvelope)) {
return;
}
labelsDrawn.insert(
transformedLabelBoundsEnvelope,
transformedLabelBounds);
}
if (outlineShowing) {
g.setColor(outlineColor);
g.setStroke(outlineStroke);
g.draw(layout.getOutline(null));
}
g.setColor(getColor());
layout.draw(g, 0, 0);
} finally {
cleanup(g);
}
}
private Envelope envelope(Shape shape) {
Rectangle2D bounds = shape.getBounds2D();
return new Envelope(
bounds.getMinX(),
bounds.getMaxX(),
bounds.getMinY(),
bounds.getMaxY());
}
private boolean collidesWithExistingLabel(
Area transformedLabelBounds,
Envelope transformedLabelBoundsEnvelope) {
List potentialCollisions = labelsDrawn.query(transformedLabelBoundsEnvelope);
for (Iterator i = potentialCollisions.iterator(); i.hasNext();) {
Area potentialCollision = (Area) i.next();
Area intersection = new Area(potentialCollision);
intersection.intersect(transformedLabelBounds);
if (!intersection.isEmpty()) {
return true;
}
}
return false;
}
private void setup(Graphics2D g) {
originalTransform = g.getTransform();
originalColor = g.getColor();
}
private void cleanup(Graphics2D g) {
g.setTransform(originalTransform);
g.setColor(originalColor);
}
/**
* Even though a feature's envelope intersects the viewport, the feature
* itself may not intersect the viewport. In this case, this method
* returns null.
*/
private Geometry intersection(Geometry geometry, Viewport viewport) {
Geometry geo;
try { //LDB: need to catch the NoninvertibleTransformException instead of just throwing it
geo = geometry.intersection(viewportRectangle(viewport));
} catch (NoninvertibleTransformException e){
return null;
}
if (geo.getNumGeometries() == 0){
return null;
}
return geo;
}
private Geometry viewportRectangle(Viewport viewport)
throws NoninvertibleTransformException {
if (viewportRectangle == null) {
Envelope e =
viewport.toModelEnvelope(
0,
viewport.getPanel().getWidth(),
0,
viewport.getPanel().getHeight());
viewportRectangle =
factory.createPolygon(
factory.createLinearRing(
new Coordinate[] {
new Coordinate(e.getMinX(), e.getMinY()),
new Coordinate(e.getMinX(), e.getMaxY()),
new Coordinate(e.getMaxX(), e.getMaxY()),
new Coordinate(e.getMaxX(), e.getMinY()),
new Coordinate(e.getMinX(), e.getMinY())}),
null);
}
return viewportRectangle;
}
private void configureTransform(
AffineTransform transform,
Point2D viewCentre,
double scale,
TextLayout layout,
double angle,
boolean linear) {
double xTranslation = viewCentre.getX();
double yTranslation = viewCentre.getY() + ((scale * GUIUtil.trueAscent(layout)) / 2d);
if (linear) {
xTranslation -= horizontalAlignmentOffset(scale * layout.getBounds().getWidth());
yTranslation -= verticalAlignmentOffset(scale * layout.getBounds().getHeight());
}
else {
xTranslation -= ((scale * layout.getBounds().getWidth()) / 2d);
}
//Negate the angle because the positive y-axis points downwards.
//See the #rotate JavaDoc. [Jon Aquino]
transform.rotate(-angle, viewCentre.getX(), viewCentre.getY());
transform.translate(xTranslation, yTranslation);
transform.scale(scale, scale);
}
private double verticalAlignmentOffset(double scaledLabelHeight) {
if (getVerticalAlignment().equals(ON_LINE)) {
return 0;
}
double buffer = 3;
double offset =
buffer
+ (layer.getBasicStyle().getLineWidth() / 2d)
+ (scaledLabelHeight / 2d);
if (getVerticalAlignment().equals(ABOVE_LINE)) {
return offset;
}
if (getVerticalAlignment().equals(BELOW_LINE)) {
return -offset;
}
Assert.shouldNeverReachHere();
return 0;
}
private double horizontalAlignmentOffset(double width) {
if (getHorizontalAlignment()==JUSTIFY_CENTER) {
return width/2d;
}
if (getHorizontalAlignment()==JUSTIFY_LEFT) {
return 0;
}
if (getHorizontalAlignment()==JUSTIFY_RIGHT) {
return width; //LDB: see hack in modelSpaceLabelSpec1D
}
Assert.shouldNeverReachHere();
return 0;
}
/**
* @return approximate alignment offset for estimation
*/
public double getVerticalAlignmentOffset() {
return verticalAlignmentOffset(getHeight()) - getHeight()/2;
}
/**
* @return approximate alignment offset for estimation
*/
public double getHorizontalAlignmentOffset(String text) {
return horizontalAlignmentOffset(text.length() * getHeight() * 0.6);
}
public String getAttribute() {
return attribute;
}
public String getAngleAttribute() {
return angleAttribute;
}
public String getHeightAttribute() {
return heightAttribute;
}
public boolean isEnabled() {
return enabled;
}
public Color getColor() {
return color;
}
public Font getFont() {
return font;
}
public boolean isScaling() {
return scaling;
}
public double getHeight() {
return height;
}
public boolean isHidingOverlappingLabels() {
return hidingOverlappingLabels;
}
public boolean isHidingAtScale() {
return hideAtScale;
}
public boolean getHideAtScale() {
return hideAtScale;
}
public String getVerticalAlignment() {
return verticalAlignment;
}
public int getHorizontalAlignment() {
return horizontalAlignment;
}
public boolean getHidingOverlappingLabels() {
return hidingOverlappingLabels;
}
public boolean getOutlineShowing(){
return outlineShowing;
}
public double getOutlineWidth(){
return outlineWidth;
}
public double getScaleToHideAt(){
return scaleToHideAt;
}
public Color getOutlineColor(){
return outlineColor;
}
public void setVerticalAlignment(String verticalAlignment) {
this.verticalAlignment = verticalAlignment;
}
public void setHorizontalAlignment(int horizontalAlignment) {
this.horizontalAlignment = horizontalAlignment;
}
public void setAttribute(String attribute) {
this.attribute = attribute;
}
public void setAngleAttribute(String angleAttribute) {
this.angleAttribute = angleAttribute;
}
public void setHeightAttribute(String heightAttribute) {
this.heightAttribute = heightAttribute;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public void setColor(Color color) {
this.color = color;
}
public void setFont(Font font) {
this.font = font;
}
public void setScaling(boolean scaling) {
this.scaling = scaling;
}
public void setHeight(double height) {
this.height = height;
}
public void setHidingOverlappingLabels(boolean hidingOverlappingLabels) {
this.hidingOverlappingLabels = hidingOverlappingLabels;
}
public void setOutlineShowing( boolean outlineShowing){
this.outlineShowing = outlineShowing;
}
public void setOutlineWidth( double outlineWidth){
this.outlineWidth = outlineWidth;
this.outlineStroke = new BasicStroke((float) outlineWidth,
BasicStroke.CAP_BUTT,BasicStroke.JOIN_BEVEL);
}
public void setScaleToHideAt( double scaleToHideAt){
this.scaleToHideAt = scaleToHideAt;
}
public void setOutlineColor( Color outlineColor, int alpha){
this.outlineColor = new Color(outlineColor.getRed(),
outlineColor.getGreen(),
outlineColor.getBlue(),
alpha);
}
public void setOutlineColor( Color outlineColor){
if (outlineColor != null) {
int alpha = this.outlineColor.getAlpha();
setOutlineColor( outlineColor, alpha);
}
}
public void setHideAtScale( boolean hideAtScale){
this.hideAtScale = hideAtScale;
}
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
Assert.shouldNeverReachHere();
return null;
}
}
private class ModelSpaceLabelSpec {
public double angle;
public Coordinate location;
public boolean linear;
public ModelSpaceLabelSpec(Coordinate location, double angle, boolean linear) {
this.location = location;
this.angle = angle;
this.linear = linear;
}
}
}