// License: GPL. For details, see LICENSE file.
package buildings_tools;
import static buildings_tools.BuildingsToolsPlugin.latlon2eastNorth;
import static org.openstreetmap.josm.tools.I18n.marktr;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.EventQueue;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.geom.GeneralPath;
import java.awt.image.BufferedImage;
import java.util.Collection;
import java.util.LinkedList;
import java.util.Map.Entry;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.actions.mapmode.MapMode;
import org.openstreetmap.josm.data.Bounds;
import org.openstreetmap.josm.data.SelectionChangedListener;
import org.openstreetmap.josm.data.coor.EastNorth;
import org.openstreetmap.josm.data.osm.DataSet;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.Way;
import org.openstreetmap.josm.data.preferences.ColorProperty;
import org.openstreetmap.josm.gui.MapFrame;
import org.openstreetmap.josm.gui.MapView;
import org.openstreetmap.josm.gui.layer.Layer;
import org.openstreetmap.josm.gui.layer.MapViewPaintable;
import org.openstreetmap.josm.gui.layer.OsmDataLayer;
import org.openstreetmap.josm.gui.util.KeyPressReleaseListener;
import org.openstreetmap.josm.gui.util.ModifierListener;
import org.openstreetmap.josm.tools.ImageProvider;
import org.openstreetmap.josm.tools.Shortcut;
@SuppressWarnings("serial")
public class DrawBuildingAction extends MapMode implements MapViewPaintable, SelectionChangedListener,
KeyPressReleaseListener, ModifierListener {
private enum Mode {
None, Drawing, DrawingWidth, DrawingAngFix
}
private final Cursor cursorCrosshair;
private final Cursor cursorJoinNode;
private Cursor currCursor;
private Cursor customCursor;
private Mode mode = Mode.None;
private Mode nextMode = Mode.None;
private Color selectedColor = Color.red;
private Point drawStartPos;
private Point mousePos;
final Building building = new Building();
public DrawBuildingAction(MapFrame mapFrame) {
super(tr("Draw buildings"), "building", tr("Draw buildings"),
Shortcut.registerShortcut("mapmode:buildings",
tr("Mode: {0}", tr("Draw buildings")),
KeyEvent.VK_B, Shortcut.DIRECT),
mapFrame, getCursor());
cursorCrosshair = getCursor();
cursorJoinNode = ImageProvider.getCursor("crosshair", "joinnode");
currCursor = cursorCrosshair;
}
private static Cursor getCursor() {
try {
return ImageProvider.getCursor("crosshair", "building");
} catch (Exception e) {
Main.error(e);
}
return Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR);
}
/**
* Displays the given cursor instead of the normal one.
*
* @param c One of the available cursors
*/
private void setCursor(final Cursor c) {
if (currCursor.equals(c))
return;
try {
// We invoke this to prevent strange things from happening
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
// Don't change cursor when mode has changed already
if (!(Main.map.mapMode instanceof DrawBuildingAction))
return;
Main.map.mapView.setCursor(c);
}
});
currCursor = c;
} catch (Exception e) {
Main.error(e);
}
}
private void showAddrDialog(Way w) {
AddressDialog dlg = new AddressDialog();
if (!alt) {
dlg.showDialog();
if (dlg.getValue() != 1)
return;
}
dlg.saveValues();
String tmp = dlg.getHouseNum();
if (tmp != null && !tmp.isEmpty())
w.put("addr:housenumber", tmp);
tmp = dlg.getStreetName();
if (tmp != null && !tmp.isEmpty())
w.put("addr:street", tmp);
}
@Override
public void enterMode() {
super.enterMode();
if (getLayerManager().getEditDataSet() == null) {
Main.map.selectSelectTool(false);
return;
}
selectedColor = new ColorProperty(marktr("selected"), selectedColor).get();
currCursor = cursorCrosshair;
Main.map.mapView.addMouseListener(this);
Main.map.mapView.addMouseMotionListener(this);
Main.map.mapView.addTemporaryLayer(this);
Main.map.keyDetector.addKeyListener(this);
Main.map.keyDetector.addModifierListener(this);
DataSet.addSelectionListener(this);
updateSnap(getLayerManager().getEditDataSet().getSelected());
}
@Override
public void exitMode() {
super.exitMode();
Main.map.mapView.removeMouseListener(this);
Main.map.mapView.removeMouseMotionListener(this);
Main.map.mapView.removeTemporaryLayer(this);
Main.map.keyDetector.removeKeyListener(this);
Main.map.keyDetector.removeModifierListener(this);
DataSet.removeSelectionListener(this);
if (mode != Mode.None)
Main.map.mapView.repaint();
mode = Mode.None;
}
public final void cancelDrawing() {
mode = Mode.None;
if (Main.map == null || Main.map.mapView == null)
return;
Main.map.statusLine.setHeading(-1);
Main.map.statusLine.setAngle(-1);
building.reset();
Main.map.mapView.repaint();
updateStatusLine();
}
@Override
public void modifiersChanged(int modifiers) {
boolean oldCtrl = ctrl;
boolean oldShift = shift;
updateKeyModifiers(modifiers);
if (ctrl != oldCtrl || shift != oldShift) {
processMouseEvent(null);
updCursor();
if (mode != Mode.None)
Main.map.mapView.repaint();
}
}
@Override
public void doKeyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
if (mode != Mode.None)
e.consume();
cancelDrawing();
}
}
@Override
public void doKeyReleased(KeyEvent e) {
}
private EastNorth getEastNorth() {
Node n;
if (ctrl) {
n = null;
} else {
n = Main.map.mapView.getNearestNode(mousePos, OsmPrimitive::isUsable);
}
if (n == null) {
return latlon2eastNorth(Main.map.mapView.getLatLon(mousePos.x, mousePos.y));
} else {
return latlon2eastNorth(n.getCoor());
}
}
private boolean isRectDrawing() {
return building.isRectDrawing() && (!shift || ToolSettings.isBBMode());
}
private Mode modeDrawing() {
EastNorth p = getEastNorth();
if (isRectDrawing()) {
building.setPlaceRect(p);
return shift ? Mode.DrawingAngFix : Mode.None;
} else {
building.setPlace(p, ToolSettings.getWidth(), ToolSettings.getLenStep(), shift);
Main.map.statusLine.setDist(building.getLength());
this.nextMode = ToolSettings.getWidth() == 0 ? Mode.DrawingWidth : Mode.None;
return this.nextMode;
}
}
private Mode modeDrawingWidth() {
building.setWidth(getEastNorth());
Main.map.statusLine.setDist(Math.abs(building.getWidth()));
return Mode.None;
}
private Mode modeDrawingAngFix() {
building.angFix(getEastNorth());
return Mode.None;
}
private void processMouseEvent(MouseEvent e) {
if (e != null) {
mousePos = e.getPoint();
updateKeyModifiers(e);
}
if (mode == Mode.None) {
nextMode = Mode.None;
return;
}
if (mode == Mode.Drawing) {
nextMode = modeDrawing();
} else if (mode == Mode.DrawingWidth) {
nextMode = modeDrawingWidth();
} else if (mode == Mode.DrawingAngFix) {
nextMode = modeDrawingAngFix();
} else {
throw new AssertionError("Invalid drawing mode");
}
}
@Override
public void paint(Graphics2D g, MapView mv, Bounds bbox) {
if (mode == Mode.None || building.getLength() == 0) {
return;
}
g.setColor(selectedColor);
g.setStroke(new BasicStroke(3, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
building.paint(g, mv);
g.setStroke(new BasicStroke(1));
}
private void drawingStart(MouseEvent e) {
mousePos = e.getPoint();
drawStartPos = mousePos;
Node n = Main.map.mapView.getNearestNode(mousePos, OsmPrimitive::isUsable);
if (n == null) {
building.setBase(latlon2eastNorth(Main.map.mapView.getLatLon(mousePos.x, mousePos.y)));
} else {
building.setBase(n);
}
mode = Mode.Drawing;
updateStatusLine();
}
private void drawingAdvance(MouseEvent e) {
processMouseEvent(e);
if (this.mode != Mode.None && this.nextMode == Mode.None) {
drawingFinish();
} else {
mode = this.nextMode;
updateStatusLine();
}
}
private void drawingFinish() {
if (building.getLength() != 0) {
Way w = building.create();
if (w != null) {
if (!alt || ToolSettings.isUsingAddr())
for (Entry<String, String> kv : ToolSettings.getTags().entrySet()) {
w.put(kv.getKey(), kv.getValue());
}
if (ToolSettings.isUsingAddr())
showAddrDialog(w);
if (ToolSettings.isAutoSelect()
&& (getLayerManager().getEditDataSet().getSelected().isEmpty() || shift)) {
getLayerManager().getEditDataSet().setSelected(w);
}
}
}
cancelDrawing();
}
@Override
public void mousePressed(MouseEvent e) {
if (e.getButton() != MouseEvent.BUTTON1)
return;
if (!Main.map.mapView.isActiveLayerDrawable())
return;
requestFocusInMapView();
if (mode == Mode.None)
drawingStart(e);
}
@Override
public void mouseDragged(MouseEvent e) {
processMouseEvent(e);
updCursor();
if (mode != Mode.None)
Main.map.mapView.repaint();
}
@Override
public void mouseReleased(MouseEvent e) {
if (e.getButton() != MouseEvent.BUTTON1)
return;
if (!Main.map.mapView.isActiveLayerDrawable())
return;
boolean dragged = true;
if (drawStartPos != null)
dragged = e.getPoint().distance(drawStartPos) > 10;
drawStartPos = null;
if (mode == Mode.Drawing && !dragged)
return;
if (mode == Mode.None)
return;
drawingAdvance(e);
}
private void updCursor() {
if (mousePos == null)
return;
if (!Main.isDisplayingMapView())
return;
Node n = null;
if (!ctrl)
n = Main.map.mapView.getNearestNode(mousePos, OsmPrimitive::isUsable);
if (n != null) {
setCursor(cursorJoinNode);
} else {
if (customCursor != null && (!ctrl || isRectDrawing()))
setCursor(customCursor);
else
setCursor(cursorCrosshair);
}
}
@Override
public void mouseMoved(MouseEvent e) {
if (!Main.map.mapView.isActiveLayerDrawable())
return;
processMouseEvent(e);
updCursor();
if (mode != Mode.None)
Main.map.mapView.repaint();
}
@Override
public String getModeHelpText() {
if (mode == Mode.None)
return tr("Point on the corner of the building to start drawing");
if (mode == Mode.Drawing)
return tr("Point on opposite end of the building");
if (mode == Mode.DrawingWidth)
return tr("Set width of the building");
return "";
}
@Override
public boolean layerIsSupported(Layer l) {
return l instanceof OsmDataLayer;
}
public final void updateSnap(Collection<? extends OsmPrimitive> newSelection) {
building.clearAngleSnap();
// update snap only if selection isn't too big
if (newSelection.size() <= 10) {
LinkedList<Node> nodes = new LinkedList<>();
LinkedList<Way> ways = new LinkedList<>();
for (OsmPrimitive p : newSelection) {
switch (p.getType()) {
case NODE:
nodes.add((Node) p);
break;
case WAY:
ways.add((Way) p);
break;
default:
break;
}
}
building.addAngleSnap(nodes.toArray(new Node[0]));
for (Way w : ways) {
building.addAngleSnap(w);
}
}
updateCustomCursor();
}
private void updateCustomCursor() {
Double angle = building.getDrawingAngle();
if (angle == null || !ToolSettings.isSoftCursor()) {
customCursor = null;
return;
}
final int R = 9; // crosshair outer radius
final int r = 3; // crosshair inner radius
BufferedImage img = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = img.createGraphics();
GeneralPath b = new GeneralPath();
b.moveTo(16 - Math.cos(angle) * R, 16 - Math.sin(angle) * R);
b.lineTo(16 - Math.cos(angle) * r, 16 - Math.sin(angle) * r);
b.moveTo(16 + Math.cos(angle) * R, 16 + Math.sin(angle) * R);
b.lineTo(16 + Math.cos(angle) * r, 16 + Math.sin(angle) * r);
b.moveTo(16 + Math.sin(angle) * R, 16 - Math.cos(angle) * R);
b.lineTo(16 + Math.sin(angle) * r, 16 - Math.cos(angle) * r);
b.moveTo(16 - Math.sin(angle) * R, 16 + Math.cos(angle) * R);
b.lineTo(16 - Math.sin(angle) * r, 16 + Math.cos(angle) * r);
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
g.setStroke(new BasicStroke(3, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
g.setColor(Color.WHITE);
g.draw(b);
g.setStroke(new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
g.setColor(Color.BLACK);
g.draw(b);
customCursor = Toolkit.getDefaultToolkit().createCustomCursor(img, new Point(16, 16), "custom crosshair");
updCursor();
}
@Override
public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
updateSnap(newSelection);
}
}