// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.czechaddress.gui;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.util.List;
import java.util.Map;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.GroupLayout;
import javax.swing.Timer;
import org.openstreetmap.josm.Main;
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.gui.ExtendedDialog;
import org.openstreetmap.josm.plugins.czechaddress.CzechAddressPlugin;
import org.openstreetmap.josm.plugins.czechaddress.NotNullList;
import org.openstreetmap.josm.plugins.czechaddress.Preferences;
import org.openstreetmap.josm.plugins.czechaddress.PrimUtils;
import org.openstreetmap.josm.plugins.czechaddress.StatusListener;
import org.openstreetmap.josm.plugins.czechaddress.addressdatabase.AddressElement;
import org.openstreetmap.josm.plugins.czechaddress.addressdatabase.House;
import org.openstreetmap.josm.plugins.czechaddress.gui.utils.HalfCookedComboBoxModel;
import org.openstreetmap.josm.plugins.czechaddress.gui.utils.UniversalListRenderer;
import org.openstreetmap.josm.plugins.czechaddress.intelligence.Reasoner;
import org.openstreetmap.josm.plugins.czechaddress.proposal.AddKeyValueProposal;
import org.openstreetmap.josm.plugins.czechaddress.proposal.Proposal;
import org.openstreetmap.josm.plugins.czechaddress.proposal.ProposalContainer;
/**
* Dialog for adding/editing an address of a single primitive.
*
* <p><b>TODO:</b> This dialog does not dispose and disconnect from
* message handling system after being closed. Reproduce: Create a node
* using this dialog, delete it and update reasoner.</p>
*
* @author radomir.cernoch@gmail.com
*/
public class PointManipulatorDialog extends ExtendedDialog implements StatusListener {
private Timer updateMatchesTimer = null;
private Action updateMatchesAction;
private ProposalContainer proposalContainer;
/**
* Creates and shows the dialog.
* @param primitive the primitive, which should be edited by this dialog
*/
public PointManipulatorDialog(OsmPrimitive primitive) {
super(Main.parent, "Adresní bod",
new String[] {"OK", "Zrušit"}, true);
initComponents();
// Create action for delaying the database query...
updateMatchesAction = new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
updateMatches();
}
};
// Register for plugin-wide messages.
CzechAddressPlugin.addStatusListener(this);
updateLocation();
// A the beginning there are no proposals.
proposalContainer = new ProposalContainer(primitive);
proposalList.setModel(proposalContainer);
proposalList.setCellRenderer(new UniversalListRenderer());
// Init the "match" combobox.
matchesComboBox.setModel(new MatchesComboBoxModel());
matchesComboBox.setRenderer(new UniversalListRenderer());
if (primitive.get(PrimUtils.KEY_ADDR_CP) != null) {
alternateNumberEdit.setText(primitive.get(PrimUtils.KEY_ADDR_CP));
updateMatches();
}
// And finalize initializing the form.
setContent(mainPanel, false);
setButtonIcons(new String[] {"ok.png", "cancel.png"});
setDefaultButton(1);
setCancelButton(2);
setupDialog();
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
setAlwaysOnTop(false);
// TODO: Why does it always crash if the modality is set in constructor?
setModal(false);
}
@Override
protected void buttonAction(int i, ActionEvent evt) {
super.buttonAction(i, evt);
if (getValue() == 1) {
if (updateMatchesTimer != null && updateMatchesTimer.isRunning()) {
updateMatchesTimer.stop();
updateMatches();
}
proposalContainer.applyAll();
Main.getLayerManager().getEditDataSet().setSelected((Node) null); // TODO: This is an ugly hack.
Main.getLayerManager().getEditDataSet().setSelected(proposalContainer.getTarget());
AddressElement elem = (AddressElement) matchesComboBox.getSelectedItem();
if (elem != null) {
Reasoner r = Reasoner.getInstance();
synchronized (r) {
r.openTransaction();
r.doOverwrite(proposalContainer.getTarget(), elem);
r.closeTransaction();
}
}
}
CzechAddressPlugin.removeStatusListener(this);
}
/**
* Updates the dialog after a location has changed.
*/
public void updateLocation() {
locationEdit.setText(CzechAddressPlugin.getLocation().toString());
}
/**
* Updates the combobox with houses that match the current input.
*/
public void updateMatches() {
if (proposalContainer.getTarget().isDeleted())
setVisible(false);
OsmPrimitive prim = this.proposalContainer.getTarget();
Reasoner r = Reasoner.getInstance();
List<AddressElement> elems = new NotNullList<>();
synchronized (r) {
Map<String, String> backup = prim.getKeys();
r.openTransaction();
for (AddressElement elem : r.getCandidates(prim)) {
r.unOverwrite(prim, elem);
}
prim.setKeys(null);
prim.put(PrimUtils.KEY_ADDR_CP, alternateNumberEdit.getText());
r.update(prim);
elems.addAll(r.getCandidates(prim));
prim.setKeys(backup);
r.update(prim);
r.closeTransaction();
}
MatchesComboBoxModel matchesModel =
((MatchesComboBoxModel) matchesComboBox.getModel());
// Fill the combobox with suitable houses.
matchesModel.setElements(elems);
if (matchesModel.getSize() > 0) {
matchesComboBox.setSelectedIndex(0);
return;
}
// If there are no suitable houses, invent one!
House fakeHouse = new House(alternateNumberEdit.getText(), null);
fakeHouse.setParent(CzechAddressPlugin.getLocation());
proposalContainer.setProposals(
fakeHouse.getDiff(proposalContainer.getTarget()));
}
@Override
public void pluginStatusChanged(int message) {
// If location changes, we block the dialog until reasoning is done.
if (message == MESSAGE_LOCATION_CHANGED) {
updateLocation();
mainPanel.setEnabled(false);
}
}
/**
* This method is called from within the constructor to
* initialize the form.
*
* <p><b>WARNING:</b> Do NOT modify this code. The content of this method is
* always regenerated by the Form Editor.</p>
*/
// <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
private void initComponents() {
jLabel4 = new javax.swing.JLabel();
mainPanel = new javax.swing.JPanel();
jLabel1 = new javax.swing.JLabel();
alternateNumberEdit = new javax.swing.JTextField();
jLabel5 = new javax.swing.JLabel();
locationEdit = new javax.swing.JTextField();
changeLocationButton = new javax.swing.JButton();
jScrollPane1 = new javax.swing.JScrollPane();
proposalList = new javax.swing.JList<>();
matchesComboBox = new javax.swing.JComboBox<>();
jLabel6 = new javax.swing.JLabel();
statusLabel = new javax.swing.JLabel();
jLabel4.setText("jLabel4");
getContentPane().setLayout(new java.awt.GridLayout(1, 0));
jLabel1.setText("Číslo popisné:");
alternateNumberEdit.addKeyListener(new java.awt.event.KeyAdapter() {
@Override
public void keyReleased(java.awt.event.KeyEvent evt) {
PointManipulatorDialog.this.keyReleased(evt);
}
});
jLabel5.setText("Místo:");
locationEdit.setEditable(false);
locationEdit.setFocusable(false);
changeLocationButton.setText("Změnit");
changeLocationButton.setFocusable(false);
changeLocationButton.addActionListener(new java.awt.event.ActionListener() {
@Override
public void actionPerformed(java.awt.event.ActionEvent evt) {
changeLocationButtonActionPerformed(evt);
}
});
proposalList.addKeyListener(new java.awt.event.KeyAdapter() {
@Override
public void keyReleased(java.awt.event.KeyEvent evt) {
proposalListKeyReleased(evt);
}
});
jScrollPane1.setViewportView(proposalList);
//matchesComboBox.setModel(new javax.swing.DefaultComboBoxModel<String>(new String[] { "" }));
matchesComboBox.addItemListener(new java.awt.event.ItemListener() {
@Override
public void itemStateChanged(java.awt.event.ItemEvent evt) {
matchChanged(evt);
}
});
jLabel6.setText("Zaznam v databazi:");
statusLabel.setForeground(new java.awt.Color(176, 1, 1));
statusLabel.setText(" ");
javax.swing.GroupLayout mainPanelLayout = new javax.swing.GroupLayout(mainPanel);
mainPanel.setLayout(mainPanelLayout);
mainPanelLayout.setHorizontalGroup(
mainPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(mainPanelLayout.createSequentialGroup()
.addGroup(mainPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(jLabel6)
.addComponent(jLabel1)
.addComponent(jLabel5))
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addGroup(mainPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(javax.swing.GroupLayout.Alignment.TRAILING, mainPanelLayout.createSequentialGroup()
.addComponent(locationEdit, javax.swing.GroupLayout.DEFAULT_SIZE, 228, Short.MAX_VALUE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(changeLocationButton))
.addComponent(alternateNumberEdit, javax.swing.GroupLayout.DEFAULT_SIZE, 293, Short.MAX_VALUE)
.addComponent(matchesComboBox, 0, 293, Short.MAX_VALUE)))
.addComponent(statusLabel, javax.swing.GroupLayout.DEFAULT_SIZE, 433, Short.MAX_VALUE)
.addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 433, Short.MAX_VALUE)
);
mainPanelLayout.setVerticalGroup(
mainPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(mainPanelLayout.createSequentialGroup()
.addGroup(mainPanelLayout.createParallelGroup(GroupLayout.Alignment.BASELINE)
.addComponent(locationEdit, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
.addComponent(jLabel5)
.addComponent(changeLocationButton))
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addGroup(mainPanelLayout.createParallelGroup(GroupLayout.Alignment.BASELINE)
.addComponent(alternateNumberEdit, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
.addComponent(jLabel1))
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addGroup(mainPanelLayout.createParallelGroup(GroupLayout.Alignment.BASELINE)
.addComponent(jLabel6)
.addComponent(matchesComboBox, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE))
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(jScrollPane1, GroupLayout.DEFAULT_SIZE, 153, Short.MAX_VALUE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(statusLabel))
);
getContentPane().add(mainPanel);
} // </editor-fold>//GEN-END:initComponents
private void changeLocationButtonActionPerformed(java.awt.event.ActionEvent evt) { //GEN-FIRST:event_changeLocationButtonActionPerformed
CzechAddressPlugin.changeLocation();
} //GEN-LAST:event_changeLocationButtonActionPerformed
private void keyReleased(java.awt.event.KeyEvent evt) { //GEN-FIRST:event_keyReleased
if (updateMatchesTimer != null)
updateMatchesTimer.stop();
updateMatchesTimer = new Timer(300, updateMatchesAction);
updateMatchesTimer.setRepeats(false);
updateMatchesTimer.start();
} //GEN-LAST:event_keyReleased
private void proposalListKeyReleased(java.awt.event.KeyEvent evt) { //GEN-FIRST:event_proposalListKeyReleased
if (evt.getKeyCode() == KeyEvent.VK_DELETE) {
for (Object o : proposalList.getSelectedValuesList()) {
proposalContainer.removeProposal((Proposal) o);
}
}
} //GEN-LAST:event_proposalListKeyReleased
private void matchChanged(java.awt.event.ItemEvent evt) { //GEN-FIRST:event_matchChanged
if (matchesComboBox.getSelectedItem() == null) return;
AddressElement selectedElement = (AddressElement) matchesComboBox.getSelectedItem();
proposalContainer.setProposals(selectedElement.getDiff(proposalContainer.getTarget()));
Preferences p = Preferences.getInstance();
if (p.addNewTag && proposalContainer.getTarget().keySet().size() == 0)
proposalContainer.addProposal(new AddKeyValueProposal(
p.addNewTagKey, p.addNewTagValue));
if (p.addBuildingTag && proposalContainer.getTarget() instanceof Way
&& proposalContainer.getTarget().get("building") == null)
proposalContainer.addProposal(new AddKeyValueProposal("building", "yes"));
} //GEN-LAST:event_matchChanged
// Variables declaration - do not modify//GEN-BEGIN:variables
private javax.swing.JTextField alternateNumberEdit;
private javax.swing.JButton changeLocationButton;
private javax.swing.JLabel jLabel1;
private javax.swing.JLabel jLabel4;
private javax.swing.JLabel jLabel5;
private javax.swing.JLabel jLabel6;
private javax.swing.JScrollPane jScrollPane1;
private javax.swing.JTextField locationEdit;
private javax.swing.JPanel mainPanel;
private javax.swing.JComboBox<AddressElement> matchesComboBox;
private javax.swing.JList<Proposal> proposalList;
private javax.swing.JLabel statusLabel;
// End of variables declaration//GEN-END:variables
/**
* Container for all Houses, which match the given 'alternatenumber'.
*/
private class MatchesComboBoxModel extends HalfCookedComboBoxModel<AddressElement> {
private List<AddressElement> matches = null;
AddressElement selected = null;
public void setElements(List<AddressElement> elements) {
this.matches = elements;
selected = null;
notifyAllListeners();
}
@Override
public void setSelectedItem(Object anItem) {
if (matches == null) return;
selected = (AddressElement) anItem;
if (Reasoner.getInstance().translate(selected) != null)
statusLabel.setText("Vybraná adresa už v mapě existuje."+
" Potvrzením vznikne konflikt.");
else
statusLabel.setText(" ");
}
@Override
public Object getSelectedItem() {
return selected;
}
@Override
public int getSize() {
if (matches == null) return 0;
return matches.size();
}
@Override
public AddressElement getElementAt(int index) {
if (matches == null) return null;
if (index >= matches.size()) return null;
return matches.get(index);
}
}
}