// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.plugins.AddrInterpolation; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.BorderLayout; import java.awt.Checkbox; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.Frame; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedList; import java.util.regex.Pattern; import javax.swing.BorderFactory; import javax.swing.ButtonGroup; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JRadioButton; import javax.swing.JTextField; import javax.swing.border.Border; import javax.swing.border.TitledBorder; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.command.AddCommand; import org.openstreetmap.josm.command.ChangeCommand; import org.openstreetmap.josm.command.ChangePropertyCommand; import org.openstreetmap.josm.command.Command; import org.openstreetmap.josm.command.DeleteCommand; import org.openstreetmap.josm.command.SequenceCommand; import org.openstreetmap.josm.data.coor.LatLon; 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.Relation; import org.openstreetmap.josm.data.osm.RelationMember; import org.openstreetmap.josm.data.osm.Way; import org.openstreetmap.josm.gui.widgets.UrlLabel; import org.openstreetmap.josm.tools.ImageProvider; public class AddrInterpolationDialog extends JDialog implements ActionListener { private Way selectedStreet = null; private Way addrInterpolationWay = null; private Relation associatedStreetRelation = null; private ArrayList<Node> houseNumberNodes = null; // Additional nodes with addr:housenumber private static String lastIncrement = ""; private static int lastAccuracyIndex = 0; private static String lastCity = ""; private static String lastState = ""; private static String lastPostCode = ""; private static String lastCountry = ""; private static String lastFullAddress = ""; private static boolean lastConvertToHousenumber = false; // Edit controls private EscapeDialog dialog = null; private JRadioButton streetNameButton = null; private JRadioButton streetRelationButton = null; private JTextField startTextField = null; private JTextField endTextField = null; private JTextField incrementTextField = null; private JTextField cityTextField = null; private JTextField stateTextField = null; private JTextField postCodeTextField = null; private JTextField countryTextField = null; private JTextField fullTextField = null; private Checkbox cbConvertToHouseNumbers = null; private boolean relationChanged = false; // Whether to re-trigger data changed for relation // Track whether interpolation method is known so that auto detect doesn't override a previous choice. private boolean interpolationMethodSet = false; // NOTE: The following 2 arrays must match in number of elements and position // Tag values for map (Except that 'Numeric' is replaced by actual # on map) String[] addrInterpolationTags = {"odd", "even", "all", "alphabetic", "Numeric"}; String[] addrInterpolationStrings = {tr("Odd"), tr("Even"), tr("All"), tr("Alphabetic"), tr("Numeric") }; // Translatable names for display private final int NumericIndex = 4; private JComboBox<String> addrInterpolationList = null; // NOTE: The following 2 arrays must match in number of elements and position String[] addrInclusionTags = {"actual", "estimate", "potential" }; // Tag values for map String[] addrInclusionStrings = {tr("Actual"), tr("Estimate"), tr("Potential") }; // Translatable names for display private JComboBox<String> addrInclusionList = null; // For tracking edit changes as group for undo private Collection<Command> commandGroup = null; private Relation editedRelation = null; public AddrInterpolationDialog(String name) { if (!FindAndSaveSelections()) { return; } JPanel editControlsPane = CreateEditControls(); ShowDialog(editControlsPane, name); } private void ShowDialog(JPanel editControlsPane, String name) { dialog = new EscapeDialog((Frame) Main.parent, name, true); dialog.add(editControlsPane); dialog.setSize(new Dimension(300, 500)); dialog.setLocation(new Point(100, 300)); // Listen for windowOpened event to set focus dialog.addWindowListener(new WindowAdapter() { @Override public void windowOpened(WindowEvent e) { if (addrInterpolationWay != null) { startTextField.requestFocus(); } else { cityTextField.requestFocus(); } } }); dialog.setVisible(true); lastIncrement = incrementTextField.getText(); lastCity = cityTextField.getText(); lastState = stateTextField.getText(); lastPostCode = postCodeTextField.getText(); lastCountry = countryTextField.getText(); lastFullAddress = fullTextField.getText(); lastConvertToHousenumber = cbConvertToHouseNumbers.getState(); } // Create edit control items and return JPanel on which they reside private JPanel CreateEditControls() { JPanel editControlsPane = new JPanel(); GridBagLayout gridbag = new GridBagLayout(); GridBagConstraints c = new GridBagConstraints(); editControlsPane.setLayout(gridbag); editControlsPane.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15)); String streetName = selectedStreet.get("name"); String streetRelation = FindRelation(); if (streetRelation.equals("")) { streetRelation = " (Create new)"; } streetNameButton = new JRadioButton(tr("Name: {0}", streetName)); streetRelationButton = new JRadioButton(tr("Relation: {0}", streetRelation)); if (associatedStreetRelation == null) { streetNameButton.setSelected(true); } else { streetRelationButton.setSelected(true); } // Create edit controls for street / relation radio buttons ButtonGroup group = new ButtonGroup(); group.add(streetNameButton); group.add(streetRelationButton); JPanel radioButtonPanel = new JPanel(new BorderLayout()); radioButtonPanel.setBorder(BorderFactory.createTitledBorder(tr("Associate with street using:"))); radioButtonPanel.add(streetNameButton, BorderLayout.NORTH); radioButtonPanel.add(streetRelationButton, BorderLayout.SOUTH); // Add to edit panel c.gridx = 0; c.gridwidth = 2; // # of columns to span c.fill = GridBagConstraints.HORIZONTAL; // Full width c.gridwidth = GridBagConstraints.REMAINDER; //end row editControlsPane.add(radioButtonPanel, c); JLabel numberingLabel = new JLabel(tr("Numbering Scheme:")); addrInterpolationList = new JComboBox<>(addrInterpolationStrings); JLabel incrementLabel = new JLabel(tr("Increment:")); incrementTextField = new JTextField(lastIncrement, 100); incrementTextField.setEnabled(false); JLabel startLabel = new JLabel(tr("Starting #:")); JLabel endLabel = new JLabel(tr("Ending #:")); startTextField = new JTextField(10); endTextField = new JTextField(10); JLabel inclusionLabel = new JLabel(tr("Accuracy:")); addrInclusionList = new JComboBox<>(addrInclusionStrings); addrInclusionList.setSelectedIndex(lastAccuracyIndex); // Preload any values already set in map GetExistingMapKeys(); JLabel[] textLabels = {startLabel, endLabel, numberingLabel, incrementLabel, inclusionLabel}; Component[] editFields = {startTextField, endTextField, addrInterpolationList, incrementTextField, addrInclusionList}; AddEditControlRows(textLabels, editFields, editControlsPane); cbConvertToHouseNumbers = new Checkbox(tr("Convert way to individual house numbers."), null, lastConvertToHousenumber); // cbConvertToHouseNumbers.setSelected(lastConvertToHousenumber); // Address interpolation fields not valid if Way not selected if (addrInterpolationWay == null) { addrInterpolationList.setEnabled(false); startTextField.setEnabled(false); endTextField.setEnabled(false); cbConvertToHouseNumbers.setEnabled(false); } JPanel optionPanel = CreateOptionalFields(); c.gridx = 0; c.gridwidth = 2; // # of columns to span c.fill = GridBagConstraints.BOTH; // Full width c.gridwidth = GridBagConstraints.REMAINDER; //end row editControlsPane.add(optionPanel, c); KeyAdapter enterProcessor = new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_ENTER) { if (ValidateAndSave()) { dialog.dispose(); } } } }; // Make Enter == OK click on fields using this adapter endTextField.addKeyListener(enterProcessor); cityTextField.addKeyListener(enterProcessor); addrInterpolationList.addKeyListener(enterProcessor); incrementTextField.addKeyListener(enterProcessor); // Watch when Interpolation Method combo box is selected so that // it can auto-detect method based on entered numbers. addrInterpolationList.addFocusListener(new FocusAdapter() { @Override public void focusGained(FocusEvent fe) { if (!interpolationMethodSet) { if (AutoDetectInterpolationMethod()) { interpolationMethodSet = true; // Don't auto detect over a previous choice } } } }); // Watch when Interpolation Method combo box is changed so that // Numeric increment box can be enabled or disabled. addrInterpolationList.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { int selectedIndex = addrInterpolationList.getSelectedIndex(); incrementTextField.setEnabled(selectedIndex == NumericIndex); // Enable or disable numeric field } }); editControlsPane.add(cbConvertToHouseNumbers, c); if (houseNumberNodes.size() > 0) { JLabel houseNumberNodeNote = new JLabel(tr("Will associate {0} additional house number nodes", houseNumberNodes.size())); editControlsPane.add(houseNumberNodeNote, c); } editControlsPane.add(new UrlLabel("http://wiki.openstreetmap.org/wiki/JOSM/Plugins/AddrInterpolation", tr("More information about this feature"), 2), c); c.gridx = 0; c.gridwidth = 1; //next-to-last c.fill = GridBagConstraints.NONE; //reset to default c.weightx = 0.0; c.insets = new Insets(15, 0, 0, 0); c.anchor = GridBagConstraints.LINE_END; JButton okButton = new JButton(tr("OK"), ImageProvider.get("ok")); editControlsPane.add(okButton, c); c.gridx = 1; c.gridwidth = GridBagConstraints.REMAINDER; //end row c.weightx = 1.0; c.anchor = GridBagConstraints.LINE_START; JButton cancelButton = new JButton(tr("Cancel"), ImageProvider.get("cancel")); editControlsPane.add(cancelButton, c); okButton.setActionCommand("ok"); okButton.addActionListener(this); cancelButton.setActionCommand("cancel"); cancelButton.addActionListener(this); return editControlsPane; } // Call after both starting and ending housenumbers have been entered - usually when // combo box gets focus. // Return true if a method was detected private boolean AutoDetectInterpolationMethod() { String startValueString = ReadTextField(startTextField); String endValueString = ReadTextField(endTextField); if ((startValueString == null) || (endValueString == null)) { // Not all values entered yet return false; } // String[] addrInterpolationTags = { "odd", "even", "all", "alphabetic", ### }; // Tag values for map if (isLong(startValueString) && isLong(endValueString)) { // Have 2 numeric values long startValue = Long.parseLong(startValueString); long endValue = Long.parseLong(endValueString); if (isEven(startValue)) { if (isEven(endValue)) { SelectInterpolationMethod("even"); } else { SelectInterpolationMethod("all"); } } else { if (!isEven(endValue)) { SelectInterpolationMethod("odd"); } else { SelectInterpolationMethod("all"); } } } else { // Test for possible alpha char startingChar = startValueString.charAt(startValueString.length()-1); char endingChar = endValueString.charAt(endValueString.length()-1); if ((!IsNumeric("" + startingChar)) && (!IsNumeric("" + endingChar))) { // Both end with alpha SelectInterpolationMethod("alphabetic"); return true; } if ((IsNumeric("" + startingChar)) && (!IsNumeric("" + endingChar))) { endingChar = Character.toUpperCase(endingChar); if ((endingChar >= 'A') && (endingChar <= 'Z')) { // First is a number, last is Latin alpha SelectInterpolationMethod("alphabetic"); return true; } } // Did not detect alpha return false; } return true; } // Set Interpolation Method combo box to method specified by 'currentMethod' (an OSM key value) private void SelectInterpolationMethod(String currentMethod) { int currentIndex = 0; if (isLong(currentMethod)) { // Valid number: Numeric increment method currentIndex = addrInterpolationTags.length-1; incrementTextField.setText(currentMethod); incrementTextField.setEnabled(true); } else { // Must scan OSM key values because combo box is already loaded with translated strings for (int i = 0; i < addrInterpolationTags.length; i++) { if (addrInterpolationTags[i].equals(currentMethod)) { currentIndex = i; break; } } } addrInterpolationList.setSelectedIndex(currentIndex); } // Set Inclusion Method combo box to method specified by 'currentMethod' (an OSM key value) private void SelectInclusion(String currentMethod) { int currentIndex = 0; // Must scan OSM key values because combo box is already loaded with translated strings for (int i = 0; i < addrInclusionTags.length; i++) { if (addrInclusionTags[i].equals(currentMethod)) { currentIndex = i; break; } } addrInclusionList.setSelectedIndex(currentIndex); } // Create optional control fields in a group box private JPanel CreateOptionalFields() { JPanel editControlsPane = new JPanel(); GridBagLayout gridbag = new GridBagLayout(); editControlsPane.setLayout(gridbag); editControlsPane.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); JLabel[] optionalTextLabels = {new JLabel(tr("City:")), new JLabel(tr("State:")), new JLabel(tr("Post Code:")), new JLabel(tr("Country:")), new JLabel(tr("Full Address:"))}; cityTextField = new JTextField(lastCity, 100); stateTextField = new JTextField(lastState, 100); postCodeTextField = new JTextField(lastPostCode, 20); countryTextField = new JTextField(lastCountry, 2); fullTextField = new JTextField(lastFullAddress, 300); // Special processing for addr:country code, max length and uppercase countryTextField.addKeyListener(new KeyAdapter() { @Override public void keyTyped(KeyEvent e) { JTextField jtextfield = (JTextField) e.getSource(); String text = jtextfield.getText(); int length = text.length(); if (length == jtextfield.getColumns()) { e.consume(); } else if (length > jtextfield.getColumns()) { // show error message ?? e.consume(); } else { // Accept key; convert to upper case if (!e.isActionKey()) { e.setKeyChar(Character.toUpperCase(e.getKeyChar())); } } } }); Component[] optionalEditFields = {cityTextField, stateTextField, postCodeTextField, countryTextField, fullTextField}; AddEditControlRows(optionalTextLabels, optionalEditFields, editControlsPane); JPanel optionPanel = new JPanel(new BorderLayout()); Border groupBox = BorderFactory.createEtchedBorder(); TitledBorder titleBorder = BorderFactory.createTitledBorder(groupBox, tr("Optional Information:"), TitledBorder.LEFT, TitledBorder.TOP); optionPanel.setBorder(titleBorder); optionPanel.add(editControlsPane, BorderLayout.CENTER); return optionPanel; } // Populate dialog for any possible existing settings if editing an existing Address interpolation way private void GetExistingMapKeys() { // Check all nodes for optional addressing data // Address interpolation nodes will overwrite these value if they contain optional data for (Node node : houseNumberNodes) { CheckNodeForAddressTags(node); } if (addrInterpolationWay != null) { String currentMethod = addrInterpolationWay.get("addr:interpolation"); if (currentMethod != null) { SelectInterpolationMethod(currentMethod); interpolationMethodSet = true; // Don't auto detect over a previous choice } String currentInclusion = addrInterpolationWay.get("addr:inclusion"); if (currentInclusion != null) { SelectInclusion(currentInclusion); } Node firstNode = addrInterpolationWay.getNode(0); Node lastNode = addrInterpolationWay.getNode(addrInterpolationWay.getNodesCount()-1); // Get any existing start / end # values String value = firstNode.get("addr:housenumber"); if (value != null) { startTextField.setText(value); } value = lastNode.get("addr:housenumber"); if (value != null) { endTextField.setText(value); } CheckNodeForAddressTags(firstNode); CheckNodeForAddressTags(lastNode); } } // Check for any existing address data. If found, // overwrite any previous data private void CheckNodeForAddressTags(Node checkNode) { // Interrogate possible existing optional values String value = checkNode.get("addr:city"); if (value != null) { lastCity = value; } value = checkNode.get("addr:state"); if (value != null) { lastState = value; } value = checkNode.get("addr:postcode"); if (value != null) { lastPostCode = value; } value = checkNode.get("addr:country"); if (value != null) { lastCountry = value; } value = checkNode.get("addr:full"); if (value != null) { lastFullAddress = value; } } // Look for a possible 'associatedStreet' type of relation for selected street // Returns relation description, or an empty string private String FindRelation() { String relationDescription = null; DataSet currentDataSet = Main.getLayerManager().getEditDataSet(); if (currentDataSet != null) { for (Relation relation : currentDataSet.getRelations()) { String relationType = relation.get("type"); if (relationType != null) { if (relationType.equals("associatedStreet")) { for (RelationMember relationMember : relation.getMembers()) { if (relationMember.isWay()) { Way way = (Way) relationMember.getMember(); // System.out.println("Name: " + way.get("name") ); if (way == selectedStreet) { associatedStreetRelation = relation; relationDescription = Long.toString(way.getId()); String streetName = ""; if (relation.getKeys().containsKey("name")) { streetName = relation.get("name"); } else { // Relation is unnamed - use street name streetName = selectedStreet.get("name"); } relationDescription += " (" + streetName + ")"; return relationDescription; } } } } } } } return ""; } // We can proceed only if there is both a named way (the 'street') and // one un-named way (the address interpolation way ) selected. // The plugin menu item is enabled after a single way is selected to display a more meaningful // message (a new user may not realize that they need to select both the street and // address interpolation way first). // Also, selected street and working address interpolation ways are saved. private boolean FindAndSaveSelections() { boolean isValid = false; int namedWayCount = 0; int unNamedWayCount = 0; DataSet currentDataSet = Main.getLayerManager().getEditDataSet(); if (currentDataSet != null) { for (OsmPrimitive osm : currentDataSet.getSelectedWays()) { Way way = (Way) osm; if (way.getKeys().containsKey("name")) { namedWayCount++; this.selectedStreet = way; } else { unNamedWayCount++; this.addrInterpolationWay = way; } } // Get additional nodes with addr:housenumber tags: // Either selected or in the middle of the Address Interpolation way // Do not include end points of Address Interpolation way in this set yet. houseNumberNodes = new ArrayList<>(); // Check selected nodes for (OsmPrimitive osm : currentDataSet.getSelectedNodes()) { Node node = (Node) osm; if (node.getKeys().containsKey("addr:housenumber")) { houseNumberNodes.add(node); } } if (addrInterpolationWay != null) { // Check nodes in middle of address interpolation way if (addrInterpolationWay.getNodesCount() > 2) { for (int i = 1; i < (addrInterpolationWay.getNodesCount()-2); i++) { Node testNode = addrInterpolationWay.getNode(i); if (testNode.getKeys().containsKey("addr:housenumber")) { houseNumberNodes.add(testNode); } } } } } if (namedWayCount != 1) { JOptionPane.showMessageDialog( Main.parent, tr("Please select a street to associate with address interpolation way"), tr("Error"), JOptionPane.ERROR_MESSAGE ); } else { // Avoid 2 error dialogs if both conditions don't match if (unNamedWayCount != 1) { // Allow for street + house number nodes only to be selected (no address interpolation way). if (houseNumberNodes.size() > 0) { isValid = true; } else { JOptionPane.showMessageDialog( Main.parent, tr("Please select address interpolation way for this street"), tr("Error"), JOptionPane.ERROR_MESSAGE ); } } else { isValid = true; } } return isValid; } /** * Add rows of edit controls - with labels in the left column, and controls in the right * column on the gridbag of the specified container. */ private void AddEditControlRows(JLabel[] labels, Component[] editFields, Container container) { GridBagConstraints c = new GridBagConstraints(); c.anchor = GridBagConstraints.EAST; int numLabels = labels.length; for (int i = 0; i < numLabels; i++) { c.gridx = 0; c.gridwidth = 1; //next-to-last c.fill = GridBagConstraints.NONE; //reset to default c.weightx = 0.0; //reset to default container.add(labels[i], c); c.gridx = 1; c.gridwidth = GridBagConstraints.REMAINDER; //end row c.fill = GridBagConstraints.HORIZONTAL; c.weightx = 1.0; container.add(editFields[i], c); } } public void actionPerformed(ActionEvent e) { if ("ok".equals(e.getActionCommand())) { if (ValidateAndSave()) { dialog.dispose(); } } else if ("cancel".equals(e.getActionCommand())) { dialog.dispose(); } } // For Alpha interpolation, return base string // For example: "22A" -> "22" // For example: "A" -> "" // Input string must not be empty private String BaseAlpha(String strValue) { if (strValue.length() > 0) { return strValue.substring(0, strValue.length()-1); } else { return ""; } } private char LastChar(String strValue) { if (strValue.length() > 0) { return strValue.charAt(strValue.length()-1); } else { return 0; } } // Test for valid positive long int private boolean isLong(String input) { try { Long val = Long.parseLong(input); return (val > 0); } catch (Exception e) { return false; } } private boolean isEven(Long input) { return ((input % 2) == 0); } private static Pattern p = Pattern.compile("^[0-9]+$"); private static boolean IsNumeric(String s) { return p.matcher(s).matches(); } private void InterpolateAlphaSection(int startNodeIndex, int endNodeIndex, String endValueString, char startingChar, char endingChar) { String baseAlpha = BaseAlpha(endValueString); int nSegments = endNodeIndex - startNodeIndex; double[] segmentLengths = new double[nSegments]; // Total length of address interpolation way section double totalLength = CalculateSegmentLengths(startNodeIndex, endNodeIndex, segmentLengths); int nHouses = endingChar - startingChar-1; // # of house number nodes to create if (nHouses > 0) { double houseSpacing = totalLength / (nHouses+1); Node lastHouseNode = addrInterpolationWay.getNode(startNodeIndex); int currentSegment = 0; // Segment being used to place new house # node char currentChar = startingChar; while (nHouses > 0) { double distanceNeeded = houseSpacing; // Move along segments until we can place the new house number while (distanceNeeded > segmentLengths[currentSegment]) { distanceNeeded -= segmentLengths[currentSegment]; currentSegment++; lastHouseNode = addrInterpolationWay.getNode(startNodeIndex + currentSegment); } // House number is to be positioned in current segment. double proportion = distanceNeeded / segmentLengths[currentSegment]; Node toNode = addrInterpolationWay.getNode(startNodeIndex + 1 + currentSegment); LatLon newHouseNumberPosition = lastHouseNode.getCoor().interpolate(toNode.getCoor(), proportion); Node newHouseNumberNode = new Node(newHouseNumberPosition); currentChar++; if ((currentChar > 'Z') && (currentChar < 'a')) { // Wraparound past uppercase Z: go directly to lower case a currentChar = 'a'; } String newHouseNumber = baseAlpha + currentChar; newHouseNumberNode.put("addr:housenumber", newHouseNumber); commandGroup.add(new AddCommand(newHouseNumberNode)); houseNumberNodes.add(newHouseNumberNode); // Street, etc information to be added later lastHouseNode = newHouseNumberNode; segmentLengths[currentSegment] -= distanceNeeded; // Track amount used nHouses--; } } } private void CreateAlphaInterpolation(String startValueString, String endValueString) { char startingChar = LastChar(startValueString); char endingChar = LastChar(endValueString); if (isLong(startValueString)) { // Special case of numeric first value, followed by 'A' startingChar = 'A'-1; } // Search for possible anchors from the 2nd node to 2nd from last, interpolating between each anchor int startIndex = 0; // Index into first interpolation zone of address interpolation way for (int i = 1; i < addrInterpolationWay.getNodesCount()-1; i++) { Node testNode = addrInterpolationWay.getNode(i); String endNodeNumber = testNode.get("addr:housenumber"); if (endNodeNumber != null) { // This is a potential anchor node if (endNodeNumber != "") { char anchorChar = LastChar(endNodeNumber); if ((anchorChar > startingChar) && (anchorChar < endingChar)) { // Lies within the expected range InterpolateAlphaSection(startIndex, i, endNodeNumber, startingChar, anchorChar); // For next interpolation section startingChar = anchorChar; startValueString = endNodeNumber; startIndex = i; } } } } // End nodes do not actually contain housenumber value yet (command has not executed), so use user-entered value InterpolateAlphaSection(startIndex, addrInterpolationWay.getNodesCount()-1, endValueString, startingChar, endingChar); } private double CalculateSegmentLengths(int startNodeIndex, int endNodeIndex, double[] segmentLengths) { Node fromNode = addrInterpolationWay.getNode(startNodeIndex); double totalLength = 0.0; int nSegments = segmentLengths.length; for (int segment = 0; segment < nSegments; segment++) { Node toNode = addrInterpolationWay.getNode(startNodeIndex + 1 + segment); segmentLengths[segment] = fromNode.getCoor().greatCircleDistance(toNode.getCoor()); totalLength += segmentLengths[segment]; fromNode = toNode; } return totalLength; } private void InterpolateNumericSection(int startNodeIndex, int endNodeIndex, long startingAddr, long endingAddr, long increment) { int nSegments = endNodeIndex - startNodeIndex; double[] segmentLengths = new double[nSegments]; // Total length of address interpolation way section double totalLength = CalculateSegmentLengths(startNodeIndex, endNodeIndex, segmentLengths); int nHouses = (int) ((endingAddr - startingAddr) / increment) -1; if (nHouses > 0) { double houseSpacing = totalLength / (nHouses+1); Node lastHouseNode = addrInterpolationWay.getNode(startNodeIndex); int currentSegment = 0; // Segment being used to place new house # node long currentHouseNumber = startingAddr; while (nHouses > 0) { double distanceNeeded = houseSpacing; // Move along segments until we can place the new house number while (distanceNeeded > segmentLengths[currentSegment]) { distanceNeeded -= segmentLengths[currentSegment]; currentSegment++; lastHouseNode = addrInterpolationWay.getNode(startNodeIndex + currentSegment); } // House number is to be positioned in current segment. double proportion = distanceNeeded / segmentLengths[currentSegment]; Node toNode = addrInterpolationWay.getNode(startNodeIndex + 1 + currentSegment); LatLon newHouseNumberPosition = lastHouseNode.getCoor().interpolate(toNode.getCoor(), proportion); Node newHouseNumberNode = new Node(newHouseNumberPosition); currentHouseNumber += increment; String newHouseNumber = Long.toString(currentHouseNumber); newHouseNumberNode.put("addr:housenumber", newHouseNumber); commandGroup.add(new AddCommand(newHouseNumberNode)); houseNumberNodes.add(newHouseNumberNode); // Street, etc information to be added later lastHouseNode = newHouseNumberNode; segmentLengths[currentSegment] -= distanceNeeded; // Track amount used nHouses--; } } } private void CreateNumericInterpolation(String startValueString, String endValueString, long increment) { long startingAddr = Long.parseLong(startValueString); long endingAddr = Long.parseLong(endValueString); // Search for possible anchors from the 2nd node to 2nd from last, interpolating between each anchor int startIndex = 0; // Index into first interpolation zone of address interpolation way for (int i = 1; i < addrInterpolationWay.getNodesCount()-1; i++) { Node testNode = addrInterpolationWay.getNode(i); String strEndNodeNumber = testNode.get("addr:housenumber"); if (strEndNodeNumber != null) { // This is a potential anchor node if (isLong(strEndNodeNumber)) { long anchorAddrNumber = Long.parseLong(strEndNodeNumber); if ((anchorAddrNumber > startingAddr) && (anchorAddrNumber < endingAddr)) { // Lies within the expected range InterpolateNumericSection(startIndex, i, startingAddr, anchorAddrNumber, increment); // For next interpolation section startingAddr = anchorAddrNumber; startValueString = strEndNodeNumber; startIndex = i; } } } } // End nodes do not actually contain housenumber value yet (command has not executed), so use user-entered value InterpolateNumericSection(startIndex, addrInterpolationWay.getNodesCount()-1, startingAddr, endingAddr, increment); } // Called if user has checked "Convert to House Numbers" checkbox. private void ConvertWayToHousenumbers(String selectedMethod, String startValueString, String endValueString, String incrementString) { // - Use nodes labeled with 'same type' as interim anchors in the middle of the way to identify unequal spacing. // - Ignore nodes of different type; for example '25b' is ignored in sequence 5..15 // Calculate required number of house numbers to create if (selectedMethod.equals("alphabetic")) { CreateAlphaInterpolation(startValueString, endValueString); } else { long increment = 1; if (selectedMethod.equals("odd") || selectedMethod.equals("even")) { increment = 2; } else if (selectedMethod.equals("Numeric")) { increment = Long.parseLong(incrementString); } CreateNumericInterpolation(startValueString, endValueString, increment); } RemoveAddressInterpolationWay(); } private void RemoveAddressInterpolationWay() { // Remove way - nodes of the way remain commandGroup.add(new DeleteCommand(addrInterpolationWay)); // Remove untagged nodes for (int i = 1; i < addrInterpolationWay.getNodesCount()-1; i++) { Node testNode = addrInterpolationWay.getNode(i); if (!testNode.hasKeys()) { commandGroup.add(new DeleteCommand(testNode)); } } addrInterpolationWay = null; } private boolean ValidateAndSave() { String startValueString = ReadTextField(startTextField); String endValueString = ReadTextField(endTextField); String incrementString = ReadTextField(incrementTextField); String city = ReadTextField(cityTextField); String state = ReadTextField(stateTextField); String postCode = ReadTextField(postCodeTextField); String country = ReadTextField(countryTextField); String fullAddress = ReadTextField(fullTextField); String selectedMethod = GetInterpolationMethod(); if (addrInterpolationWay != null) { Long startAddr = 0L, endAddr = 0L; if (!selectedMethod.equals("alphabetic")) { Long[] addrArray = {startAddr, endAddr}; if (!ValidAddressNumbers(startValueString, endValueString, addrArray)) { return false; } startAddr = addrArray[0]; endAddr = addrArray[1]; } String errorMessage = ""; if (selectedMethod.equals("odd")) { if (isEven(startAddr) || isEven(endAddr)) { errorMessage = tr("Expected odd numbers for addresses"); } } else if (selectedMethod.equals("even")) { if (!isEven(startAddr) || !isEven(endAddr)) { errorMessage = tr("Expected even numbers for addresses"); } } else if (selectedMethod.equals("all")) { } else if (selectedMethod.equals("alphabetic")) { errorMessage = ValidateAlphaAddress(startValueString, endValueString); } else if (selectedMethod.equals("Numeric")) { if (!ValidNumericIncrementString(incrementString, startAddr, endAddr)) { errorMessage = tr("Expected valid number for increment"); } } if (!errorMessage.equals("")) { JOptionPane.showMessageDialog(Main.parent, errorMessage, tr("Error"), JOptionPane.ERROR_MESSAGE); return false; } } if (country != null) { if (country.length() != 2) { JOptionPane.showMessageDialog(Main.parent, tr("Country code must be 2 letters"), tr("Error"), JOptionPane.ERROR_MESSAGE); return false; } } // Entries are valid ... save in map commandGroup = new LinkedList<>(); String streetName = selectedStreet.get("name"); if (addrInterpolationWay != null) { Node firstNode = addrInterpolationWay.getNode(0); Node lastNode = addrInterpolationWay.getNode(addrInterpolationWay.getNodesCount()-1); // De-select address interpolation way; leave street selected DataSet currentDataSet = Main.getLayerManager().getEditDataSet(); if (currentDataSet != null) { currentDataSet.clearSelection(addrInterpolationWay); currentDataSet.clearSelection(lastNode); // Workaround for JOSM Bug #3838 } String interpolationTagValue = selectedMethod; if (selectedMethod.equals("Numeric")) { // The interpolation method is the number for 'Numeric' case interpolationTagValue = incrementString; } if (cbConvertToHouseNumbers.getState()) { // Convert way to house numbers is checked. // Create individual nodes and delete interpolation way ConvertWayToHousenumbers(selectedMethod, startValueString, endValueString, incrementString); } else { // Address interpolation way will remain commandGroup.add(new ChangePropertyCommand(addrInterpolationWay, "addr:interpolation", interpolationTagValue)); commandGroup.add(new ChangePropertyCommand(addrInterpolationWay, "addr:inclusion", GetInclusionMethod())); } commandGroup.add(new ChangePropertyCommand(firstNode, "addr:housenumber", startValueString)); commandGroup.add(new ChangePropertyCommand(lastNode, "addr:housenumber", endValueString)); // Add address interpolation house number nodes to main house number node list for common processing houseNumberNodes.add(firstNode); houseNumberNodes.add(lastNode); } if (streetRelationButton.isSelected()) { // Relation button was selected if (associatedStreetRelation == null) { CreateRelation(streetName); // relationChanged = true; (not changed since it was created) } // Make any additional changes only to the copy editedRelation = new Relation(associatedStreetRelation); if (addrInterpolationWay != null) { AddToRelation(associatedStreetRelation, addrInterpolationWay, "house"); } } // For all nodes, add to relation and // Add optional text fields to all nodes if specified for (Node node : houseNumberNodes) { if (streetRelationButton.isSelected()) { AddToRelation(associatedStreetRelation, node, "house"); } if ((city != null) || (streetNameButton.isSelected())) { // Include street unconditionally if adding nodes only or city name specified commandGroup.add(new ChangePropertyCommand(node, "addr:street", streetName)); } // Set or remove remaining optional fields commandGroup.add(new ChangePropertyCommand(node, "addr:city", city)); commandGroup.add(new ChangePropertyCommand(node, "addr:state", state)); commandGroup.add(new ChangePropertyCommand(node, "addr:postcode", postCode)); commandGroup.add(new ChangePropertyCommand(node, "addr:country", country)); commandGroup.add(new ChangePropertyCommand(node, "addr:full", fullAddress)); } if (relationChanged) { commandGroup.add(new ChangeCommand(associatedStreetRelation, editedRelation)); } Main.main.undoRedo.add(new SequenceCommand(tr("Address Interpolation"), commandGroup)); Main.map.repaint(); return true; } private boolean ValidNumericIncrementString(String incrementString, long startingAddr, long endingAddr) { if (!isLong(incrementString)) { return false; } long testIncrement = Long.parseLong(incrementString); if ((testIncrement <= 0) || (testIncrement > endingAddr)) { return false; } if (((endingAddr - startingAddr) % testIncrement) != 0) { return false; } return true; } // Create Associated Street relation, add street, and add to list of commands to perform private void CreateRelation(String streetName) { associatedStreetRelation = new Relation(); associatedStreetRelation.put("name", streetName); associatedStreetRelation.put("type", "associatedStreet"); RelationMember newStreetMember = new RelationMember("street", selectedStreet); associatedStreetRelation.addMember(newStreetMember); commandGroup.add(new AddCommand(associatedStreetRelation)); } // Read from dialog text box, removing leading and trailing spaces // Return the string, or null for a zero length string private String ReadTextField(JTextField field) { String value = field.getText(); if (value != null) { value = value.trim(); if (value.equals("")) { value = null; } } return value; } // Test if relation contains specified member // If not already present, it is added private void AddToRelation(Relation relation, OsmPrimitive testMember, String role) { boolean isFound = false; for (RelationMember relationMember : relation.getMembers()) { if (testMember == relationMember.getMember()) { isFound = true; break; } } if (!isFound) { RelationMember newMember = new RelationMember(role, testMember); editedRelation.addMember(newMember); relationChanged = true; } } // Check alphabetic style address // Last character of both must be alpha // Last character of ending must be greater than starting // Return empty error message if OK private String ValidateAlphaAddress(String startValueString, String endValueString) { String errorMessage = ""; if (startValueString.equals("") || endValueString.equals("")) { errorMessage = tr("Please enter valid number for starting and ending address"); } else { char startingChar = LastChar(startValueString); char endingChar = LastChar(endValueString); boolean isOk = false; if ((IsNumeric("" + startingChar)) && (!IsNumeric("" + endingChar))) { endingChar = Character.toUpperCase(endingChar); if ((endingChar >= 'A') && (endingChar <= 'Z')) { // First is a number, last is Latin alpha isOk = true; } } else if ((!IsNumeric("" + startingChar)) && (!IsNumeric("" + endingChar))) { // Both are alpha isOk = true; } if (!isOk) { errorMessage = tr("Alphabetic address must end with a letter"); } // if a number is included, validate that it is the same number if (endValueString.length() > 1) { // Get number portion of first item: may or may not have letter suffix String numStart = BaseAlpha(startValueString); if (IsNumeric(startValueString)) { numStart = startValueString; } String numEnd = BaseAlpha(endValueString); if (!numStart.equals(numEnd)) { errorMessage = tr("Starting and ending numbers must be the same for alphabetic addresses"); } } // ?? Character collation in all languages ?? if (startingChar >= endingChar) { errorMessage = tr("Starting address letter must be less than ending address letter"); } } return errorMessage; } // Convert string addresses to numeric, with error check private boolean ValidAddressNumbers(String startValueString, String endValueString, Long[] addrArray) { String errorMessage = ""; if (!isLong(startValueString)) { errorMessage = tr("Please enter valid number for starting address"); } if (!isLong(endValueString)) { errorMessage = tr("Please enter valid number for ending address"); } if (errorMessage.equals("")) { addrArray[0] = Long.parseLong(startValueString); addrArray[1] = Long.parseLong(endValueString); if (addrArray[1] <= addrArray[0]) { errorMessage = tr("Starting address number must be less than ending address number"); } } if (errorMessage.equals("")) { return true; } else { JOptionPane.showMessageDialog(Main.parent, errorMessage, tr("Error"), JOptionPane.ERROR_MESSAGE); return false; } } private String GetInterpolationMethod() { int selectedIndex = addrInterpolationList.getSelectedIndex(); return addrInterpolationTags[selectedIndex]; } private String GetInclusionMethod() { int selectedIndex = addrInclusionList.getSelectedIndex(); lastAccuracyIndex = selectedIndex; return addrInclusionTags[selectedIndex]; } }