package com.revolsys.swing.map.overlay;
import java.awt.Cursor;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Toolkit;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.geom.Point2D;
import java.beans.PropertyChangeEvent;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import javax.measure.Measure;
import javax.measure.quantity.Area;
import javax.measure.quantity.Length;
import javax.measure.unit.NonSI;
import javax.measure.unit.SI;
import javax.measure.unit.Unit;
import com.revolsys.awt.WebColors;
import com.revolsys.collection.map.Maps;
import com.revolsys.datatype.DataType;
import com.revolsys.datatype.DataTypes;
import com.revolsys.geometry.cs.CoordinateSystem;
import com.revolsys.geometry.cs.ProjectedCoordinateSystem;
import com.revolsys.geometry.model.Geometry;
import com.revolsys.geometry.model.GeometryFactory;
import com.revolsys.geometry.model.LineString;
import com.revolsys.geometry.model.LinearRing;
import com.revolsys.geometry.model.Point;
import com.revolsys.geometry.model.Polygon;
import com.revolsys.geometry.model.Punctual;
import com.revolsys.geometry.model.vertex.Vertex;
import com.revolsys.io.BaseCloseable;
import com.revolsys.swing.Icons;
import com.revolsys.swing.map.MapPanel;
import com.revolsys.swing.map.Viewport2D;
import com.revolsys.swing.map.layer.record.renderer.GeometryStyleRenderer;
import com.revolsys.swing.map.layer.record.renderer.TextStyleRenderer;
import com.revolsys.swing.map.layer.record.style.GeometryStyle;
import com.revolsys.swing.map.layer.record.style.TextStyle;
import com.revolsys.util.number.Doubles;
public class MeasureOverlay extends AbstractOverlay {
private static final Cursor CURSOR = Icons.getCursor("cursor_ruler", 8, 7);
private static final Geometry EMPTY_GEOMETRY = GeometryFactory.wgs84().geometry();
public static final String MEASURE = "Measure";
private static final NumberFormat MEASURE_FORMAT = new DecimalFormat("#,##0.00");
private static final SelectedRecordsVertexRenderer MEASURE_RENDERER = new SelectedRecordsVertexRenderer(
WebColors.Magenta, false);
private static final GeometryStyle POLYGON_STYLE = GeometryStyle.polygon(WebColors.Black,
WebColors.newAlpha(WebColors.Magenta, 75));
private static final long serialVersionUID = 1L;
private boolean dragged = false;
private DataType measureDataType;
private Geometry measureGeometry = EMPTY_GEOMETRY;
private String measureLabel;
private List<CloseLocation> mouseOverLocations = Collections.emptyList();
public MeasureOverlay(final MapPanel map) {
super(map);
addOverlayAction( //
MEASURE, //
CURSOR, //
ZoomOverlay.ACTION_PAN, //
ZoomOverlay.ACTION_ZOOM, //
ZoomOverlay.ACTION_ZOOM_BOX //
);
}
private void cancel() {
modeMeasureClear();
}
public void clearMouseOverGeometry() {
if (isOverlayAction(MEASURE)) {
setMapCursor(CURSOR);
}
this.mouseOverLocations = Collections.emptyList();
clearSnapLocations();
}
protected void clearMouseOverLocations() {
setXorGeometry(null);
clearMouseOverGeometry();
getMap().clearToolTipText();
repaint();
}
private Geometry deleteVertex() {
Geometry geometry = getMeasureGeometry();
for (final CloseLocation location : getMouseOverLocations()) {
final int[] vertexId = location.getVertexId();
if (vertexId != null) {
if (geometry instanceof Point) {
return null;
} else if (geometry instanceof LineString) {
final LineString line = (LineString)geometry;
if (line.getVertexCount() == 2) {
if (vertexId.length == 1) {
if (vertexId[0] == 0) {
return line.getPoint(1);
} else {
return line.getPoint(0);
}
}
}
} else if (geometry instanceof Polygon) {
final Polygon polygon = (Polygon)geometry;
final LinearRing ring = polygon.getRing(0);
if (ring.getVertexCount() == 4) {
if (vertexId.length == 2) {
final GeometryFactory geometryFactory = geometry.getGeometryFactory();
final Vertex point0 = ring.getVertex(0);
final Vertex point1 = ring.getVertex(1);
final Vertex point2 = ring.getVertex(2);
switch (vertexId[1]) {
case 0:
return geometryFactory.lineString(point1, point2);
case 1:
return geometryFactory.lineString(point2, point0);
default:
return geometryFactory.lineString(point0, point1);
}
}
}
}
try {
geometry = geometry.deleteVertex(vertexId);
} catch (final Exception e) {
Toolkit.getDefaultToolkit().beep();
return geometry;
}
}
}
return geometry;
}
public DataType getMeasureDataType() {
return this.measureDataType;
}
public Geometry getMeasureGeometry() {
return this.measureGeometry;
}
public List<CloseLocation> getMouseOverLocations() {
return this.mouseOverLocations;
}
protected Geometry getVertexGeometry(final MouseEvent event, final CloseLocation location) {
final Geometry geometry = location.getGeometry();
int previousPointOffset;
int[] vertexId = location.getVertexId();
if (vertexId == null) {
previousPointOffset = 0;
vertexId = location.getSegmentId();
} else {
previousPointOffset = -1;
}
final GeometryFactory geometryFactory = getGeometryFactory2d();
if (geometry instanceof Point) {
} else {
final Point point = getPoint(geometryFactory, event);
final Vertex vertex = geometry.getVertex(vertexId);
Point previousPoint = null;
Point nextPoint = null;
if (previousPointOffset == 0) {
previousPoint = vertex;
} else {
previousPoint = vertex.getLinePrevious();
}
nextPoint = vertex.getLineNext();
final List<LineString> lines = new ArrayList<>();
if (previousPoint != null && !previousPoint.isEmpty()) {
lines.add(newXorLine(geometryFactory, previousPoint, point));
}
if (nextPoint != null && !nextPoint.isEmpty()) {
lines.add(newXorLine(geometryFactory, nextPoint, point));
}
if (!lines.isEmpty()) {
return geometryFactory.lineal(lines);
}
}
return null;
}
@Override
public void keyPressed(final KeyEvent event) {
final int keyCode = event.getKeyCode();
if (keyCode == KeyEvent.VK_M) {
if (isOverlayAction(MEASURE)) {
setMeasureGeometry(EMPTY_GEOMETRY);
} else {
setOverlayAction(MEASURE);
}
} else if (keyCode == KeyEvent.VK_ESCAPE) {
if (isOverlayAction(MEASURE) && !this.dragged) {
cancel();
}
} else if (keyCode == KeyEvent.VK_BACK_SPACE || keyCode == KeyEvent.VK_DELETE) {
if (!getMouseOverLocations().isEmpty()) {
final Geometry geometry = deleteVertex();
setMeasureGeometry(geometry);
clearMouseOverLocations();
}
}
}
private void modeMeasureClear() {
clearOverlayAction(MEASURE);
setMeasureGeometry(EMPTY_GEOMETRY);
this.dragged = false;
final DataType oldValue = this.measureDataType;
this.measureDataType = null;
firePropertyChange("measureDataType", oldValue, null);
}
protected boolean modeMeasureClick(final MouseEvent event) {
final int modifiers = event.getModifiersEx();
if (modifiers == 0 && event.getButton() == MouseEvent.BUTTON1) {
if (isOverlayAction(MEASURE)) {
final int clickCount = event.getClickCount();
Point point = getSnapPoint();
if (point == null) {
point = getPoint(event);
}
if (clickCount == 1) {
final Geometry measureGeometry = getMeasureGeometry();
final GeometryFactory geometryFactory = getGeometryFactory2d();
if (measureGeometry.isEmpty()) {
setMeasureGeometry(point);
} else if (measureGeometry instanceof Point) {
final Point from = (Point)measureGeometry;
if (!from.equals(point)) {
final LineString line = geometryFactory.lineString(from, point);
setMeasureGeometry(line);
}
} else if (this.measureDataType == DataTypes.LINE_STRING) {
if (measureGeometry instanceof LineString) {
LineString line = (LineString)measureGeometry;
final Point to = line.getToPoint();
if (!to.equals(point)) {
line = line.appendVertex(point);
setMeasureGeometry(line);
}
}
} else {
if (measureGeometry instanceof LineString) {
LineString line = (LineString)measureGeometry;
final Point from = line.getToVertex(0);
if (!from.equals(point)) {
line = line.appendVertex(point);
setMeasureGeometry(line);
}
if (line.getVertexCount() > 2) {
if (!line.isClosed()) {
final Vertex firstPoint = line.getVertex(0);
line = line.appendVertex(firstPoint);
}
setMeasureGeometry(geometryFactory.polygon(line));
}
} else if (measureGeometry instanceof Polygon) {
final Polygon polygon = (Polygon)measureGeometry;
setMeasureGeometry(polygon.appendVertex(point, 0));
}
}
event.consume();
repaint();
return true;
}
}
}
return false;
}
protected boolean modeMeasureDrag(final MouseEvent event) {
if (isOverlayAction(MEASURE)) {
this.dragged = true;
if (getMouseOverLocations().isEmpty()) {
modeMeasureUpdateXorGeometry();
} else {
getMap().clearToolTipText();
Geometry xorGeometry = null;
for (final CloseLocation location : getMouseOverLocations()) {
final Geometry locationGeometry = getVertexGeometry(event, location);
if (locationGeometry != null) {
if (xorGeometry == null) {
xorGeometry = locationGeometry;
} else {
xorGeometry = xorGeometry.union(locationGeometry);
}
}
}
setXorGeometry(xorGeometry);
if (!hasSnapPoint()) {
setMapCursor(CURSOR_NODE_ADD);
}
return true;
}
}
return false;
}
protected boolean modeMeasureFinish(final MouseEvent event) {
if (isOverlayAction(MEASURE)) {
if (!getMouseOverLocations().isEmpty()) {
if (event.getButton() == MouseEvent.BUTTON1) {
for (final CloseLocation location : getMouseOverLocations()) {
final Geometry geometry = location.getGeometry();
final GeometryFactory geometryFactory = getGeometryFactory2d();
final Point point;
if (getSnapPoint() == null) {
point = getPoint(geometryFactory, event);
} else {
point = getSnapPoint().newGeometry(geometryFactory);
}
final int[] vertexIndex = location.getVertexId();
Geometry newGeometry;
final Point newPoint = point;
if (vertexIndex == null) {
final int[] segmentIndex = location.getSegmentId();
final int[] newIndex = segmentIndex.clone();
newIndex[newIndex.length - 1] = newIndex[newIndex.length - 1] + 1;
newGeometry = geometry.insertVertex(newPoint, newIndex);
} else {
newGeometry = geometry.moveVertex(newPoint, vertexIndex);
}
setMeasureGeometry(newGeometry);
}
return true;
}
}
this.dragged = false;
}
return false;
}
protected boolean modeMeasureMove(final MouseEvent event) {
if (isOverlayAction(MEASURE)) {
final MapPanel map = getMap();
final CloseLocation location = map.findCloseLocation(null, null, this.measureGeometry);
final List<CloseLocation> locations = new ArrayList<>();
if (location != null) {
locations.add(location);
}
final boolean hasMouseOver = setMouseOverLocations(locations);
// TODO make work with multi-part
if (!hasMouseOver) {
modeMeasureUpdateXorGeometry();
}
return true;
}
return false;
}
protected boolean modeMeasureStart(final MouseEvent event) {
final int modifiers = event.getModifiersEx();
if (modifiers == InputEvent.BUTTON1_DOWN_MASK) {
if (isOverlayAction(MEASURE)) {
if (!getMouseOverLocations().isEmpty()) {
repaint();
return true;
}
}
}
return false;
}
protected void modeMeasureUpdateXorGeometry() {
final Point point = getOverlayPoint();
if (!hasSnapPoint()) {
setMapCursor(CURSOR);
}
Geometry xorGeometry = null;
if (this.measureGeometry.isEmpty()) {
} else {
Vertex fromPoint;
final Vertex toPoint;
if (this.measureGeometry instanceof LineString) {
fromPoint = this.measureGeometry.getVertex(0);
toPoint = this.measureGeometry.getToVertex(0);
} else {
fromPoint = this.measureGeometry.getVertex(0, 0);
toPoint = this.measureGeometry.getToVertex(0, 0);
}
final GeometryFactory geometryFactory = getGeometryFactory2d();
if (toPoint != null && !toPoint.isEmpty()) {
if (this.measureGeometry instanceof Point) {
xorGeometry = geometryFactory.lineString(toPoint, point);
} else {
if (toPoint.equals(fromPoint) || this.measureDataType == DataTypes.LINE_STRING) {
xorGeometry = newXorLine(geometryFactory, toPoint, point);
} else {
final Point p1 = geometryFactory.point(toPoint);
final Point p3 = geometryFactory.point(fromPoint);
final GeometryFactory viewportGeometryFactory = getViewportGeometryFactory2d();
xorGeometry = viewportGeometryFactory.lineString(p1, point, p3);
}
}
}
}
setXorGeometry(xorGeometry);
}
@Override
public void mouseClicked(final MouseEvent event) {
if (modeMeasureClick(event)) {
}
}
@Override
public void mouseDragged(final MouseEvent event) {
if (modeMeasureDrag(event)) {
}
}
@Override
public void mouseEntered(final MouseEvent e) {
modeMeasureMove(e);
}
@Override
public void mouseExited(final MouseEvent e) {
setXorGeometry(null);
}
@Override
public void mouseMoved(final MouseEvent event) {
if (modeMeasureMove(event)) {
}
}
@Override
public void mousePressed(final MouseEvent event) {
if (modeMeasureStart(event)) {
}
}
@Override
public void mouseReleased(final MouseEvent event) {
if (modeMeasureFinish(event)) {
modeMeasureMove(event);
}
}
@Override
protected void paintComponent(final Viewport2D viewport, final Graphics2D graphics) {
if (!this.measureGeometry.isEmpty()) {
final GeometryFactory geometryFactory = getGeometryFactory2d();
try (
BaseCloseable transformCloseable = viewport.setUseModelCoordinates(graphics, true)) {
MEASURE_RENDERER.paintSelected(viewport, graphics, geometryFactory, this.measureGeometry);
if (this.measureGeometry instanceof Polygon) {
final Polygon polygon = (Polygon)this.measureGeometry;
GeometryStyleRenderer.renderPolygon(viewport, graphics, polygon, POLYGON_STYLE);
}
}
if (!(this.measureGeometry instanceof Punctual)) {
final TextStyle measureTextStyle = new TextStyle();
measureTextStyle.setTextBoxColor(WebColors.Violet);
measureTextStyle.setTextSize(Measure.valueOf(14, NonSI.PIXEL));
measureTextStyle.setTextFaceName(Font.MONOSPACED);
Point textPoint;
measureTextStyle.setTextHorizontalAlignment("right");
if (this.measureDataType == DataTypes.POLYGON && this.measureGeometry instanceof Polygon) {
measureTextStyle.setTextDx(Measure.valueOf(-5, NonSI.PIXEL));
measureTextStyle.setTextPlacementType("vertex(n-1)");
measureTextStyle.setTextVerticalAlignment("middle");
textPoint = this.measureGeometry.getToVertex(0, 1);
} else {
measureTextStyle.setTextDx(Measure.valueOf(-7, NonSI.PIXEL));
measureTextStyle.setTextDy(Measure.valueOf(-2, NonSI.PIXEL));
measureTextStyle.setTextPlacementType("vertex(n)");
measureTextStyle.setTextVerticalAlignment("top");
textPoint = this.measureGeometry.getToVertex(0);
}
TextStyleRenderer.renderText(viewport, graphics, this.measureLabel, textPoint,
measureTextStyle);
}
}
drawXorGeometry(graphics);
}
@Override
public void propertyChange(final PropertyChangeEvent event) {
final String propertyName = event.getPropertyName();
if (propertyName.equals("overlayAction")) {
final MapPanel map = getMap();
if (!map.hasOverlayAction(MEASURE)) {
modeMeasureClear();
}
}
}
public void setMeasureDataType(final DataType measureDataType) {
final DataType oldValue = this.measureDataType;
if (oldValue != measureDataType) {
this.measureDataType = measureDataType;
setMeasureGeometry(EMPTY_GEOMETRY);
this.dragged = false;
this.measureDataType = measureDataType;
firePropertyChange("measureDataType", oldValue, measureDataType);
}
}
public void setMeasureGeometry(Geometry measureGeometry) {
if (measureGeometry == null) {
measureGeometry = EMPTY_GEOMETRY;
}
if (measureGeometry != this.measureGeometry) {
this.measureGeometry = measureGeometry;
if (measureGeometry == null) {
this.measureLabel = "";
} else {
Unit<Length> lengthUnit = SI.METRE;
final CoordinateSystem coordinateSystem = measureGeometry.getCoordinateSystem();
if (coordinateSystem instanceof ProjectedCoordinateSystem) {
lengthUnit = coordinateSystem.getLengthUnit();
}
final double length = measureGeometry.getLength(lengthUnit);
@SuppressWarnings("unchecked")
final Unit<Area> areaUnit = (Unit<Area>)lengthUnit.times(lengthUnit);
final double area = measureGeometry.getArea(areaUnit);
final String unitString = lengthUnit.toString();
synchronized (MEASURE_FORMAT) {
final StringBuilder label = new StringBuilder();
final String lengthString = MEASURE_FORMAT.format(Doubles.makePrecise(100, length));
label.append(lengthString);
label.append(unitString);
if (this.measureDataType == DataTypes.POLYGON && measureGeometry instanceof Polygon) {
final String areaString = MEASURE_FORMAT.format(Doubles.makePrecise(100, area));
label.append(" \n");
label.append(areaString);
label.append(unitString);
label.append('\u00B2');
}
this.measureLabel = label.toString();
}
}
setXorGeometry(null);
}
}
protected boolean setMouseOverLocations(final List<CloseLocation> mouseOverLocations) {
if (this.mouseOverLocations.equals(mouseOverLocations)) {
return !this.mouseOverLocations.isEmpty();
} else {
this.mouseOverLocations = mouseOverLocations;
setSnapPoint(null);
setXorGeometry(null);
return updateMouseOverLocations();
}
}
public void toggleMeasureMode(final DataType measureDataType) {
final MapPanel map = getMap();
if (this.measureDataType == null) {
if (map.setOverlayAction(MEASURE)) {
setMeasureDataType(measureDataType);
}
} else if (measureDataType == this.measureDataType && map.hasOverlayAction(MEASURE)) {
modeMeasureClear();
} else {
if (map.setOverlayAction(MEASURE)) {
setMeasureDataType(measureDataType);
}
}
}
private boolean updateMouseOverLocations() {
final MapPanel map = getMap();
if (this.mouseOverLocations.isEmpty()) {
map.clearToolTipText();
return false;
} else {
final Map<String, Set<CloseLocation>> vertexLocations = new TreeMap<>();
final Map<String, Set<CloseLocation>> segmentLocations = new TreeMap<>();
for (final CloseLocation location : this.mouseOverLocations) {
if (location.getVertexId() == null) {
Maps.addToSet(segmentLocations, "Measure", location);
} else {
Maps.addToSet(vertexLocations, "Measure", location);
}
}
final StringBuilder text = new StringBuilder("<html>");
appendLocations(text, "Move Vertices", vertexLocations);
appendLocations(text, "Insert Vertices", segmentLocations);
text.append("</html>");
final Point2D eventPoint = getEventPosition();
map.setToolTipText(eventPoint, text);
if (vertexLocations.isEmpty()) {
setMapCursor(CURSOR_LINE_ADD_NODE);
} else {
setMapCursor(CURSOR_NODE_EDIT);
}
}
return true;
}
}