// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.plugins.measurement; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.Dimension; import java.awt.GridLayout; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.text.DecimalFormat; import java.util.Arrays; import java.util.Collection; import javax.swing.AbstractAction; import javax.swing.JLabel; import javax.swing.JPanel; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.data.SelectionChangedListener; import org.openstreetmap.josm.data.SystemOfMeasurement; import org.openstreetmap.josm.data.SystemOfMeasurement.SoMChangeListener; 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.osm.event.AbstractDatasetChangedEvent; import org.openstreetmap.josm.data.osm.event.DataChangedEvent; import org.openstreetmap.josm.data.osm.event.DataSetListener; import org.openstreetmap.josm.data.osm.event.NodeMovedEvent; import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent; import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent; import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent; import org.openstreetmap.josm.data.osm.event.TagsChangedEvent; import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent; import org.openstreetmap.josm.gui.SideButton; import org.openstreetmap.josm.gui.dialogs.ToggleDialog; import org.openstreetmap.josm.gui.help.HelpUtil; import org.openstreetmap.josm.gui.util.GuiHelper; import org.openstreetmap.josm.tools.ImageProvider; import org.openstreetmap.josm.tools.Shortcut; import org.openstreetmap.josm.tools.SubclassFilteredCollection; /** * A small tool dialog for displaying the current measurement data. * * @author ramack */ public class MeasurementDialog extends ToggleDialog implements SelectionChangedListener, DataSetListener, SoMChangeListener { private static final long serialVersionUID = 4708541586297950021L; /** * The reset button */ private SideButton resetButton; /** * The measurement label for the path length */ protected JLabel pathLengthLabel; /** * The measurement label for the currently selected segments */ protected JLabel selectLengthLabel; /** * The measurement label for area of the currently selected loop */ protected JLabel selectAreaLabel; /** * The measurement label for radius if the currently selected loop is a circle. */ protected JLabel selectRadiusLabel; /** * The measurement label for the segment angle, actually updated, if 2 nodes are selected */ protected JLabel segAngleLabel; private DataSet ds; private Collection<Way> ways; private Collection<Node> nodes; /** * Constructor */ public MeasurementDialog() { super(tr("Measured values"), "measure", tr("Open the measurement window."), Shortcut.registerShortcut("subwindow:measurement", tr("Toggle: {0}", tr("Measured values")), KeyEvent.VK_U, Shortcut.CTRL_SHIFT), 150); resetButton = new SideButton(new AbstractAction() { { putValue(NAME, tr("Reset")); new ImageProvider("dialogs", "select").getResource().attachImageIcon(this, true); putValue(SHORT_DESCRIPTION, tr("Reset current measurement results and delete measurement path.")); putValue("help", HelpUtil.ht("/Dialog/Measurement#Reset")); } @Override public void actionPerformed(ActionEvent e) { resetValues(); } }); JPanel valuePanel = new JPanel(new GridLayout(0,2)); valuePanel.add(new JLabel(tr("Path Length"))); pathLengthLabel = new JLabel(getDistText(0)); valuePanel.add(pathLengthLabel); valuePanel.add(new JLabel(tr("Selection Length"))); selectLengthLabel = new JLabel(getDistText(0)); valuePanel.add(selectLengthLabel); valuePanel.add(new JLabel(tr("Selection Area"))); selectAreaLabel = new JLabel(getAreaText(0)); valuePanel.add(selectAreaLabel); valuePanel.add(new JLabel(tr("Selection Radius"))); selectRadiusLabel = new JLabel(getRadiusText(0)); valuePanel.add(selectRadiusLabel); JLabel angle = new JLabel(tr("Angle")); angle.setToolTipText(tr("Angle between two selected Nodes")); valuePanel.add(angle); segAngleLabel = new JLabel("- \u00b0"); valuePanel.add(segAngleLabel); this.setPreferredSize(new Dimension(0, 92)); createLayout(valuePanel, false, Arrays.asList(new SideButton[] { resetButton })); DataSet.addSelectionListener(this); SystemOfMeasurement.addSoMChangeListener(this); } protected String getDistText(double v) { return SystemOfMeasurement.getSystemOfMeasurement().getDistText(v, new DecimalFormat("#0.000"), 1e-3); } protected String getAreaText(double v) { return SystemOfMeasurement.getSystemOfMeasurement().getAreaText(v, new DecimalFormat("#0.000"), 1e-3); } protected String getRadiusText(double v) { return SystemOfMeasurement.getSystemOfMeasurement().getDistText(v, new DecimalFormat("#0.000"), 1e-3); } protected String getAngleText(double v) { return new DecimalFormat("#0.0").format(v) + " \u00b0"; } /** * Cleans the active Measurement Layer */ public void resetValues(){ MeasurementPlugin.getCurrentLayer().reset(); } @Override public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) { double length = 0.0; double segAngle = 0.0; double area = 0.0; double radius = 0.0; Node lastNode = null; // Don't mix up way and nodes computation (fix #6872). Priority given to ways ways = new SubclassFilteredCollection<>(newSelection, Way.class::isInstance); if (ways.isEmpty()) { nodes = new SubclassFilteredCollection<>(newSelection, Node.class::isInstance); for (Node n : nodes) { if (n.getCoor() != null) { if (lastNode == null) { lastNode = n; } else { length += lastNode.getCoor().greatCircleDistance(n.getCoor()); segAngle = MeasurementLayer.angleBetween(lastNode.getCoor(), n.getCoor()); lastNode = n; } } } } else { nodes = null; for (Way w : ways) { Node lastN = null; double wayArea = 0.0; Double firstSegLength = null; boolean isCircle = true; for (Node n: w.getNodes()) { if (lastN != null && lastN.getCoor() != null && n.getCoor() != null) { final double segLength = lastN.getCoor().greatCircleDistance(n.getCoor()); if (firstSegLength == null) { firstSegLength = segLength; } if (isCircle && Math.abs(firstSegLength - segLength) > 0.000001) { isCircle = false; } length += segLength; //http://local.wasp.uwa.edu.au/~pbourke/geometry/polyarea/ wayArea += (MeasurementLayer.calcX(n.getCoor()) * MeasurementLayer.calcY(lastN.getCoor())) - (MeasurementLayer.calcY(n.getCoor()) * MeasurementLayer.calcX(lastN.getCoor())); segAngle = MeasurementLayer.angleBetween(lastN.getCoor(), n.getCoor()); } lastN = n; } if (lastN != null && lastN.equals(w.getNodes().iterator().next())) wayArea = Math.abs(wayArea / 2); else wayArea = 0; area += wayArea; } if (ways.size() == 1 && area > 0.0) { radius = length / (2 * Math.PI); } } final String lengthLabel = getDistText(length); final String angleLabel = getAngleText(segAngle); final String areaLabel = getAreaText(area); final String radiusLabel = getRadiusText(radius); GuiHelper.runInEDT(new Runnable() { @Override public void run() { selectLengthLabel.setText(lengthLabel); segAngleLabel.setText(angleLabel); selectAreaLabel.setText(areaLabel); selectRadiusLabel.setText(radiusLabel); } }); DataSet currentDs = Main.getLayerManager().getEditDataSet(); if (ds != currentDs) { if (ds != null) { ds.removeDataSetListener(this); } if (currentDs != null) { currentDs.addDataSetListener(this); } ds = currentDs; } } @Override public void destroy() { super.destroy(); SystemOfMeasurement.removeSoMChangeListener(this); DataSet.removeSelectionListener(this); if (ds != null) { ds.removeDataSetListener(this); ds = null; } } private boolean waysContain(Node n) { if (ways != null) { for (Way w : ways) { if (w.containsNode(n)) { return true; } } } return false; } @Override public void nodeMoved(NodeMovedEvent event) { Node n = event.getNode(); // Refresh selection if a node belonging to a selected member has moved (example: scale action) if ((nodes != null && nodes.contains(n)) || waysContain(n)) { selectionChanged(Main.getLayerManager().getEditDataSet().getSelected()); } } @Override public void primitivesAdded(PrimitivesAddedEvent event) {} @Override public void primitivesRemoved(PrimitivesRemovedEvent event) {} @Override public void tagsChanged(TagsChangedEvent event) {} @Override public void wayNodesChanged(WayNodesChangedEvent event) { } @Override public void relationMembersChanged(RelationMembersChangedEvent event) {} @Override public void otherDatasetChange(AbstractDatasetChangedEvent event) {} @Override public void dataChanged(DataChangedEvent event) {} @Override public void systemOfMeasurementChanged(String oldSoM, String newSoM) { // Refresh selection to take into account new system of measurement DataSet currentDs = Main.getLayerManager().getEditDataSet(); if (currentDs != null) { selectionChanged(currentDs.getSelected()); } } }