// License: WTFPL. For details, see LICENSE file.
package iodb;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingConstants;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.data.coor.EastNorth;
import org.openstreetmap.josm.data.coor.LatLon;
import org.openstreetmap.josm.data.projection.Projection;
import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer;
import org.openstreetmap.josm.tools.ImageProvider;
/**
* A button which shows offset information. Must be spectacular, since it's the only
* non-JOptionPane GUI in the plugin.
*
* @author Zverik
* @license WTFPL
*/
public class OffsetDialogButton extends JButton {
private ImageryOffsetBase offset;
private JLabel distanceLabel;
private DirectionIcon directionArrow;
/**
* Initialize the button with an offset. Calculated all relevant values.
* @param offset An offset to display on the button.
*/
public OffsetDialogButton(ImageryOffsetBase offset) {
this.offset = offset;
layoutComponents();
}
/**
* Returns the offset associated with this button.
*/
public ImageryOffsetBase getOffset() {
return offset;
}
/**
* Update arrow for the offset location.
*/
public void updateLocation() {
LatLon center = ImageryOffsetTools.getMapCenter();
directionArrow.updateIcon(center);
double distance = center.greatCircleDistance(offset.getPosition());
distanceLabel.setText(ImageryOffsetTools.formatDistance(distance));
}
/**
* Adds a layout to this button and places labels, images and icons.
*/
private void layoutComponents() {
String authorAndDate = offset.isDeprecated()
? tr("Deprecated by {0} on {1}", offset.getAbandonAuthor(),
OffsetInfoAction.DATE_FORMAT.format(offset.getAbandonDate()))
: tr("Created by {0} on {1}", offset.getAuthor(),
OffsetInfoAction.DATE_FORMAT.format(offset.getDate()));
JLabel authorAndDateLabel = new JLabel(authorAndDate);
Font authorFont = new Font(authorAndDateLabel.getFont().getName(), Font.ITALIC, authorAndDateLabel.getFont().getSize());
authorAndDateLabel.setFont(authorFont);
directionArrow = new DirectionIcon(offset.getPosition());
distanceLabel = new JLabel("", directionArrow, SwingConstants.RIGHT);
distanceLabel.setHorizontalTextPosition(SwingConstants.LEFT);
Font distanceFont = new Font(distanceLabel.getFont().getName(), Font.PLAIN, distanceLabel.getFont().getSize());
distanceLabel.setFont(distanceFont);
updateLocation();
String description = offset.isDeprecated() ? offset.getAbandonReason() : offset.getDescription();
description = description.replace("<", "<").replace(">", ">");
JLabel descriptionLabel = new JLabel("<html><div style=\"width: 300px;\">"+description+"</div></html>");
Font descriptionFont = new Font(descriptionLabel.getFont().getName(), Font.BOLD, descriptionLabel.getFont().getSize());
descriptionLabel.setFont(descriptionFont);
OffsetIcon offsetIcon = new OffsetIcon(offset);
double offsetDistance = offset instanceof ImageryOffset
? offsetIcon.getDistance() : 0.0;
// ? ((ImageryOffset)offset).getImageryPos().greatCircleDistance(offset.getPosition()) : 0.0;
JLabel offsetLabel = new JLabel(offsetDistance > 0.2 ? ImageryOffsetTools.formatDistance(offsetDistance) : "",
offsetIcon, SwingConstants.CENTER);
Font offsetFont = new Font(offsetLabel.getFont().getName(), Font.PLAIN, offsetLabel.getFont().getSize() - 2);
offsetLabel.setFont(offsetFont);
offsetLabel.setHorizontalTextPosition(SwingConstants.CENTER);
offsetLabel.setVerticalTextPosition(SwingConstants.BOTTOM);
Box topLine = new Box(BoxLayout.X_AXIS);
topLine.add(authorAndDateLabel);
topLine.add(Box.createHorizontalGlue());
topLine.add(Box.createHorizontalStrut(10));
topLine.add(distanceLabel);
JPanel p = new JPanel(new BorderLayout(10, 5));
p.setOpaque(false);
p.add(topLine, BorderLayout.NORTH);
p.add(offsetLabel, BorderLayout.WEST);
p.add(descriptionLabel, BorderLayout.CENTER);
add(p);
}
/**
* Calculates length and direction for two points in the imagery offset object.
* @see #getLengthAndDirection(iodb.ImageryOffset, double, double)
*/
private double[] getLengthAndDirection(ImageryOffset offset) {
AbstractTileSourceLayer layer = ImageryOffsetTools.getTopImageryLayer();
double[] dxy = layer == null ? new double[] {0.0, 0.0} :
new double[] {layer.getDisplaySettings().getDx(), layer.getDisplaySettings().getDy()};
return getLengthAndDirection(offset, dxy[0], dxy[1]);
}
/**
* Calculates length and direction for two points in the imagery offset object
* taking into account an existing imagery layer offset.
*
* @see #getLengthAndDirection(iodb.ImageryOffset)
*/
public static double[] getLengthAndDirection(ImageryOffset offset, double dx, double dy) {
Projection proj = Main.getProjection();
EastNorth pos = proj.latlon2eastNorth(offset.getPosition());
LatLon correctedCenterLL = proj.eastNorth2latlon(pos.add(-dx, -dy));
double length = correctedCenterLL.greatCircleDistance(offset.getImageryPos());
double direction = length < 1e-2 ? 0.0 : correctedCenterLL.heading(offset.getImageryPos());
if (direction < 0)
direction += Math.PI * 2;
return new double[] {length, direction};
}
private static void drawArrow(Graphics g, int cx, int cy, double length, double direction) {
int dx = (int) Math.round(Math.sin(direction) * length / 2);
int dy = (int) Math.round(Math.cos(direction) * length / 2);
g.drawLine(cx - dx, cy - dy, cx + dx, cy + dy);
double wingLength = Math.max(length / 3, 4);
double d1 = direction - Math.PI / 6;
int dx1 = (int) Math.round(Math.sin(d1) * wingLength);
int dy1 = (int) Math.round(Math.cos(d1) * wingLength);
g.drawLine(cx + dx, cy + dy, cx + dx - dx1, cy + dy - dy1);
double d2 = direction + Math.PI / 6;
int dx2 = (int) Math.round(Math.sin(d2) * wingLength);
int dy2 = (int) Math.round(Math.cos(d2) * wingLength);
g.drawLine(cx + dx, cy + dy, cx + dx - dx2, cy + dy - dy2);
}
/**
* An offset icon. Displays a plain calibration icon for a geometry
* and an arrow for an imagery offset.
*/
private class OffsetIcon implements Icon {
private boolean isDeprecated;
private boolean isCalibration;
private double direction = -1.0;
private double distance;
private ImageIcon background;
/**
* Initialize the icon with an offset object. Calculates length and direction
* of an arrow if they're needed.
*/
OffsetIcon(ImageryOffsetBase offset) {
isDeprecated = offset.isDeprecated();
isCalibration = offset instanceof CalibrationObject;
if (offset instanceof ImageryOffset) {
background = ImageProvider.get("offset");
double[] ld = getLengthAndDirection((ImageryOffset) offset);
distance = ld[0];
direction = ld[1];
} else {
background = ImageProvider.get("calibration");
}
}
public double getDistance() {
return distance;
}
/**
* Paints the base image and adds to it according to the offset.
*/
@Override
public void paintIcon(Component comp, Graphics g, int x, int y) {
background.paintIcon(comp, g, x, y);
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
if (!isCalibration) {
g2.setColor(Color.black);
Point c = new Point(x + getIconWidth() / 2, y + getIconHeight() / 2);
if (distance < 1e-2) {
// no offset
g2.fillOval(c.x - 3, c.y - 3, 7, 7);
} else {
// draw an arrow
double arrowLength = distance < 10 ? getIconWidth() / 2 - 1 : getIconWidth() - 4;
g2.setStroke(new BasicStroke(2));
drawArrow(g2, c.x, c.y, arrowLength, direction);
}
}
if (isDeprecated) {
// big red X
g2.setColor(Color.red);
g2.setStroke(new BasicStroke(5, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.3f));
g2.drawLine(x + 2, y + 2, x + getIconWidth() - 2, y + getIconHeight() - 2);
g2.drawLine(x + 2, y + getIconHeight() - 2, x + getIconWidth() - 2, y + 2);
}
}
@Override
public int getIconWidth() {
return background.getIconWidth();
}
@Override
public int getIconHeight() {
return background.getIconHeight();
}
}
private static class DirectionIcon implements Icon {
private static final int SIZE = 10;
private LatLon to;
private double distance;
private double direction;
DirectionIcon(LatLon to) {
this.to = to;
}
public void updateIcon(LatLon from) {
distance = from.greatCircleDistance(to);
direction = to.heading(from);
}
/**
* Paints the base image and adds to it according to the offset.
*/
@Override
public void paintIcon(Component comp, Graphics g, int x, int y) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(Color.black);
Point c = new Point(x + getIconWidth() / 2, y + getIconHeight() / 2);
if (distance < 1) {
// no offset
int r = 2;
g2.fillOval(c.x - r, c.y - r, r * 2 + 1, r * 2 + 1);
} else {
// draw an arrow
g2.setStroke(new BasicStroke(1));
drawArrow(g2, c.x, c.y, SIZE, direction);
}
}
@Override
public int getIconWidth() {
return SIZE;
}
@Override
public int getIconHeight() {
return SIZE;
}
}
}