source: josm/trunk/src/org/openstreetmap/josm/gui/dialogs/RelationEditor.java@ 1376

Last change on this file since 1376 was 1376, checked in by framm, 15 years ago

layout change for relation editor

  • Property svn:eol-style set to native
File size: 23.4 KB
Line 
1package org.openstreetmap.josm.gui.dialogs;
2
3import static org.openstreetmap.josm.tools.I18n.marktr;
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.BorderLayout;
7import java.awt.Dimension;
8import java.awt.FlowLayout;
9import java.awt.GridBagLayout;
10import java.awt.GridLayout;
11import java.awt.event.ActionEvent;
12import java.awt.event.ActionListener;
13import java.awt.event.KeyEvent;
14import java.beans.PropertyChangeEvent;
15import java.beans.PropertyChangeListener;
16import java.io.IOException;
17import java.text.Collator;
18import java.util.ArrayList;
19import java.util.Arrays;
20import java.util.Collection;
21import java.util.Collections;
22import java.util.Comparator;
23import java.util.Map.Entry;
24
25import javax.swing.BoxLayout;
26import javax.swing.JButton;
27import javax.swing.JFrame;
28import javax.swing.JLabel;
29import javax.swing.JOptionPane;
30import javax.swing.JPanel;
31import javax.swing.JScrollPane;
32import javax.swing.JTabbedPane;
33import javax.swing.JTable;
34import javax.swing.ListSelectionModel;
35import javax.swing.event.ListSelectionEvent;
36import javax.swing.event.ListSelectionListener;
37import javax.swing.event.TableModelEvent;
38import javax.swing.event.TableModelListener;
39import javax.swing.table.DefaultTableModel;
40
41import org.openstreetmap.josm.Main;
42import org.openstreetmap.josm.command.AddCommand;
43import org.openstreetmap.josm.command.ChangeCommand;
44import org.openstreetmap.josm.data.osm.DataSet;
45import org.openstreetmap.josm.data.osm.DataSource;
46import org.openstreetmap.josm.data.osm.OsmPrimitive;
47import org.openstreetmap.josm.data.osm.Relation;
48import org.openstreetmap.josm.data.osm.RelationMember;
49import org.openstreetmap.josm.data.osm.visitor.MergeVisitor;
50import org.openstreetmap.josm.gui.OsmPrimitivRenderer;
51import org.openstreetmap.josm.io.OsmServerObjectReader;
52import org.openstreetmap.josm.tools.GBC;
53import org.openstreetmap.josm.tools.ImageProvider;
54import org.xml.sax.SAXException;
55
56/**
57 * This dialog is for editing relations.
58 *
59 * In the basic form, it provides two tables, one with the relation tags
60 * and one with the relation members. (Relation tags can be edited through
61 * the normal properties dialog as well, if you manage to get a relation
62 * selected!)
63 *
64 * @author Frederik Ramm <frederik@remote.org>
65 *
66 */
67public class RelationEditor extends JFrame {
68
69 /**
70 * The relation that this editor is working on, and the clone made for
71 * editing.
72 */
73 private final Relation relation;
74 private final Relation clone;
75 private JLabel status;
76
77 /**
78 * True if the relation is ordered (API 0.6). False for API 0.5.
79 */
80 boolean ordered;
81
82 /**
83 * The property data.
84 */
85 private final DefaultTableModel propertyData = new DefaultTableModel() {
86 @Override public boolean isCellEditable(int row, int column) {
87 return true;
88 }
89 @Override public Class<?> getColumnClass(int columnIndex) {
90 return String.class;
91 }
92 };
93
94 /**
95 * The membership data.
96 */
97 private final DefaultTableModel memberData = new DefaultTableModel() {
98 @Override public boolean isCellEditable(int row, int column) {
99 return column == 0;
100 }
101 @Override public Class<?> getColumnClass(int columnIndex) {
102 return columnIndex == 1 ? OsmPrimitive.class : String.class;
103 }
104 };
105
106 /**
107 * The properties and membership lists.
108 */
109 private final JTable propertyTable = new JTable(propertyData);
110 private final JTable memberTable = new JTable(memberData);
111
112 // =================== FIXME FIXME FIXME =====================
113 // As soon as API 0.5 is dead, drop all the collation stuff from here ...
114
115 /**
116 * Collator for sorting the roles and entries of the member table.
117 */
118 private static final Collator collator;
119 static {
120 collator = Collator.getInstance();
121 collator.setStrength(Collator.PRIMARY);
122 }
123
124 /**
125 * Compare role strings.
126 */
127 private static int compareRole(String s1, String s2) {
128 int last1 = s1.lastIndexOf('_');
129 if (last1 > 0) {
130 int last2 = s2.lastIndexOf('_');
131 if (last2 == last1) {
132 String prefix1 = s1.substring(0, last1);
133 String prefix2 = s2.substring(0, last2);
134
135 if (prefix1.equalsIgnoreCase(prefix2)) {
136 // Both roles have the same prefix, now determine the
137 // suffix.
138 String suffix1 = s1.substring(last1 + 1, s1.length());
139 String suffix2 = s2.substring(last2 + 1, s2.length());
140
141 if (suffix1.matches("\\d+") && suffix2.matches("\\d+")) {
142 // Suffix is an number -> compare it.
143 int i1 = Integer.parseInt(suffix1);
144 int i2 = Integer.parseInt(suffix2);
145
146 return i1 - i2;
147 }
148 }
149 }
150 }
151 if(s1.length() == 0 && s2.length() != 0)
152 return 1;
153 else if(s2.length() == 0 && s1.length() != 0)
154 return -1;
155
156 // Default handling if the role name is nothing like "stop_xx"
157 return collator.compare(s1, s2);
158 }
159
160
161 /**
162 * Compare two OsmPrimitives.
163 */
164 private static int compareMembers(OsmPrimitive o1, OsmPrimitive o2) {
165 return collator.compare(o1.getName(), o2.getName());
166 }
167
168 private final Comparator<RelationMember> memberComparator = new Comparator<RelationMember>() {
169 public int compare(RelationMember r1, RelationMember r2) {
170 int roleResult = compareRole(r1.role, r2.role);
171
172 if (roleResult == 0)
173 roleResult = compareMembers(r1.member, r2.member);
174
175 return roleResult;
176 }
177 };
178
179 // =================== FIXME FIXME FIXME =====================
180 // ... until here, and also get rid of the "Collections.sort..." below.
181
182 /**
183 * Creates a new relation editor for the given relation. The relation
184 * will be saved if the user selects "ok" in the editor.
185 *
186 * If no relation is given, will create an editor for a new relation.
187 *
188 * @param relation relation to edit, or null to create a new one.
189 */
190 public RelationEditor(Relation relation)
191 {
192 this(relation, null);
193 }
194
195 /**
196 * Creates a new relation editor for the given relation. The relation
197 * will be saved if the user selects "ok" in the editor.
198 *
199 * If no relation is given, will create an editor for a new relation.
200 *
201 * @param relation relation to edit, or null to create a new one.
202 */
203 public RelationEditor(Relation relation, Collection<RelationMember> selectedMembers )
204 {
205 super(relation == null ? tr("Create new relation") :
206 relation.id == 0 ? tr ("Edit new relation") :
207 tr("Edit relation #{0}", relation.id));
208 this.relation = relation;
209
210 ordered = Main.pref.get("osm-server.version", "0.5").equals("0.6");
211
212 if (relation == null) {
213 // create a new relation
214 this.clone = new Relation();
215 } else {
216 // edit an existing relation
217 this.clone = new Relation(relation);
218 if (!ordered) Collections.sort(this.clone.members, memberComparator);
219 }
220
221 getContentPane().setLayout(new BorderLayout());
222 JTabbedPane tabPane = new JTabbedPane();
223 getContentPane().add(tabPane, BorderLayout.CENTER);
224
225 // (ab)use JOptionPane to make this look familiar;
226 // hook up with JOptionPane's property change event
227 // to detect button click
228 final JOptionPane okcancel = new JOptionPane("",
229 JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_CANCEL_OPTION, null);
230 getContentPane().add(okcancel, BorderLayout.SOUTH);
231
232 okcancel.addPropertyChangeListener(new PropertyChangeListener() {
233 public void propertyChange(PropertyChangeEvent event) {
234 if (event.getPropertyName().equals(JOptionPane.VALUE_PROPERTY) && event.getNewValue() != null) {
235 if ((Integer)event.getNewValue() == JOptionPane.OK_OPTION) {
236 // clicked ok!
237 if (RelationEditor.this.relation == null) {
238 Main.main.undoRedo.add(new AddCommand(clone));
239 DataSet.fireSelectionChanged(Main.ds.getSelected());
240 } else if (!RelationEditor.this.relation.realEqual(clone, true)) {
241 Main.main.undoRedo.add(new ChangeCommand(RelationEditor.this.relation, clone));
242 DataSet.fireSelectionChanged(Main.ds.getSelected());
243 }
244 }
245 setVisible(false);
246 }
247 }
248 });
249
250 /* I don't like this text any more. Let's just get on with doing the specialist
251 * editors instead of talking about it.
252 JLabel help = new JLabel("<html><em>"+
253 tr("This is the basic relation editor which allows you to change the relation's tags " +
254 "as well as the members. In addition to this we should have a smart editor that " +
255 "detects the type of relationship and limits your choices in a sensible way.")+"</em></html>");
256 getContentPane().add(help, BorderLayout.NORTH);
257 */
258 try { setAlwaysOnTop(true); } catch (SecurityException sx) {}
259
260 // Basic Editor panel has two blocks;
261 // a tag table at the top and a membership list below.
262
263 // setting up the properties table
264
265 propertyData.setColumnIdentifiers(new String[]{tr("Key"),tr("Value")});
266 propertyTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
267 propertyData.addTableModelListener(new TableModelListener() {
268 public void tableChanged(TableModelEvent tme) {
269 if (tme.getType() == TableModelEvent.UPDATE) {
270 int row = tme.getFirstRow();
271
272 if (!(tme.getColumn() == 0 && row == propertyData.getRowCount() -1)) {
273 clone.entrySet().clear();
274 for (int i = 0; i < propertyData.getRowCount(); i++) {
275 String key = propertyData.getValueAt(i, 0).toString();
276 String value = propertyData.getValueAt(i, 1).toString();
277 if (key.length() > 0 && value.length() > 0) clone.put(key, value);
278 }
279 refreshTables();
280 }
281 }
282 }
283 });
284 propertyTable.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
285
286 // setting up the member table
287
288 memberData.setColumnIdentifiers(new String[]{tr("Role"),tr("Occupied By")});
289 memberTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
290 memberTable.getColumnModel().getColumn(1).setCellRenderer(new OsmPrimitivRenderer());
291 memberData.addTableModelListener(new TableModelListener() {
292 public void tableChanged(TableModelEvent tme) {
293 if (tme.getType() == TableModelEvent.UPDATE && tme.getColumn() == 0) {
294 int row = tme.getFirstRow();
295 clone.members.get(row).role = memberData.getValueAt(row, 0).toString();
296 }
297 }
298 });
299 ListSelectionModel lsm = memberTable.getSelectionModel();
300 lsm.addListSelectionListener(new ListSelectionListener() {
301 public void valueChanged(ListSelectionEvent e) {
302 ArrayList<OsmPrimitive> sel;
303 int cnt = memberTable.getSelectedRowCount();
304 if(cnt > 0)
305 {
306 sel = new ArrayList<OsmPrimitive>(cnt);
307 for (int i : memberTable.getSelectedRows())
308 sel.add((OsmPrimitive)memberTable.getValueAt(i, 1));
309 }
310 else
311 {
312 cnt = memberTable.getRowCount();
313 sel = new ArrayList<OsmPrimitive>(cnt);
314 for (int i = 0; i < cnt; ++i)
315 sel.add((OsmPrimitive)memberTable.getValueAt(i, 1));
316 }
317 Main.ds.setSelected(sel);
318 }
319 });
320 memberTable.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
321
322 // combine both tables and wrap them in a scrollPane
323 JPanel bothTables = new JPanel();
324 bothTables.setLayout(new GridBagLayout());
325 bothTables.add(new JLabel(tr("Tags (empty value deletes tag)")), GBC.eol().fill(GBC.HORIZONTAL));
326 bothTables.add(new JScrollPane(propertyTable), GBC.eop().fill(GBC.BOTH));
327 bothTables.add(status = new JLabel(tr("Members")), GBC.eol().fill(GBC.HORIZONTAL));
328 if (ordered) {
329 JPanel upDownPanel = new JPanel();
330 upDownPanel.setLayout(new BoxLayout(upDownPanel, BoxLayout.Y_AXIS));
331
332 upDownPanel.add(createButton(null, "moveup", tr("Move the currently selected members up"),
333 KeyEvent.VK_U, new ActionListener() {
334 public void actionPerformed(ActionEvent e) {
335 moveMembers(-1);
336 }
337 }));
338 upDownPanel.add(createButton(null, "movedown", tr("Move the currently selected members down"),
339 KeyEvent.VK_N, new ActionListener() {
340 public void actionPerformed(ActionEvent e) {
341 moveMembers(1);
342 }
343 }));
344
345
346 bothTables.add(new JScrollPane(memberTable), GBC.std().fill(GBC.BOTH));
347 bothTables.add(upDownPanel, GBC.eol().fill(GBC.VERTICAL));
348 } else {
349 bothTables.add(new JScrollPane(memberTable), GBC.eol().fill(GBC.BOTH));
350 }
351
352 // this is not exactly pretty but the four buttons simply don't fit in one line.
353 // we should have smaller buttons for situations like this.
354 JPanel buttonPanel = new JPanel(new GridLayout(2,2));
355
356 buttonPanel.add(createButton(marktr("Add Selected"),"addselected",
357 tr("Add all currently selected objects as members"), KeyEvent.VK_A, new ActionListener() {
358 public void actionPerformed(ActionEvent e) {
359 addSelected();
360 }
361 }));
362
363 buttonPanel.add(createButton(marktr("Remove Selected"),"removeselected",
364 tr("Remove all currently selected objects from relation"), KeyEvent.VK_R, new ActionListener() {
365 public void actionPerformed(ActionEvent e) {
366 deleteSelected();
367 }
368 }));
369
370 buttonPanel.add(createButton(marktr("Remove"),"remove",
371 tr("Remove the member in the current table row from this relation"), KeyEvent.VK_D, new ActionListener() {
372 public void actionPerformed(ActionEvent e) {
373 int[] rows = memberTable.getSelectedRows();
374 RelationMember mem = new RelationMember();
375 for (int row : rows) {
376 mem.role = memberTable.getValueAt(row, 0).toString();
377 mem.member = (OsmPrimitive) memberTable.getValueAt(row, 1);
378 clone.members.remove(mem);
379 }
380 refreshTables();
381 }
382 }));
383
384 /*buttonPanel.add(createButton(marktr("Select"),"select",
385 tr("Highlight the member from the current table row as JOSM's selection"), KeyEvent.VK_S, new ActionListener() {
386 public void actionPerformed(ActionEvent e) {
387 ArrayList<OsmPrimitive> sel;
388 int cnt = memberTable.getSelectedRowCount();
389 if(cnt > 0)
390 {
391 sel = new ArrayList<OsmPrimitive>(cnt);
392 for (int i : memberTable.getSelectedRows())
393 sel.add((OsmPrimitive)memberTable.getValueAt(i, 1));
394 }
395 else
396 {
397 cnt = memberTable.getRowCount();
398 sel = new ArrayList<OsmPrimitive>(cnt);
399 for (int i = 0; i < cnt; ++i)
400 sel.add((OsmPrimitive)memberTable.getValueAt(i, 1));
401 }
402 Main.ds.setSelected(sel);
403 }
404 }));*/
405 buttonPanel.add(createButton(marktr("Download Members"),"downloadincomplete",
406 tr("Download all incomplete ways and nodes in relation"), KeyEvent.VK_L, new ActionListener() {
407 public void actionPerformed(ActionEvent e) {
408 downloadRelationMembers();
409 refreshTables();
410 }
411 }));
412
413 bothTables.add(buttonPanel, GBC.eop().fill(GBC.HORIZONTAL));
414
415 tabPane.add(bothTables, "Basic");
416
417 refreshTables();
418
419 if (selectedMembers != null) {
420 boolean scrolled = false;
421 for (int i = 0; i < memberData.getRowCount(); i++) {
422 for (RelationMember m : selectedMembers) {
423 if (m.member == memberData.getValueAt(i, 1)
424 && m.role.equals(memberData.getValueAt(i, 0))) {
425 memberTable.addRowSelectionInterval(i, i);
426 if (!scrolled) {
427 // Ensure that the first member is visible
428 memberTable.scrollRectToVisible(memberTable.getCellRect(i, 0, true));
429 scrolled = true;
430 }
431 break;
432 }
433 }
434
435 }
436 }
437
438 setSize(new Dimension(600, 500));
439 setLocationRelativeTo(Main.parent);
440 }
441
442 private void refreshTables() {
443 // re-load property data
444
445 propertyData.setRowCount(0);
446 for (Entry<String, String> e : clone.entrySet()) {
447 propertyData.addRow(new Object[]{e.getKey(), e.getValue()});
448 }
449 propertyData.addRow(new Object[]{"", ""});
450
451 // re-load membership data
452
453 memberData.setRowCount(0);
454 for (RelationMember em : clone.members) {
455 memberData.addRow(new Object[]{em.role, em.member});
456 }
457 status.setText(tr("Members: {0}", clone.members.size()));
458 }
459
460 private JButton createButton(String name, String iconName, String tooltip, int mnemonic, ActionListener actionListener) {
461 JButton b = new JButton((name == null) ? null : tr(name), ImageProvider.get("dialogs", iconName));
462 b.setActionCommand(name);
463 b.addActionListener(actionListener);
464 b.setToolTipText(tooltip);
465 b.setMnemonic(mnemonic);
466 b.putClientProperty("help", "Dialog/Properties/"+name);
467 return b;
468 }
469
470 private void addSelected() {
471 for (OsmPrimitive p : Main.ds.getSelected()) {
472 boolean skip = false;
473 // ordered relations may have the same member multiple times.
474 // TODO: visual indication of the fact that one is there more than once?
475 if (!ordered)
476 {
477 for (RelationMember rm : clone.members) {
478 if (rm.member == p || p == relation)
479 {
480 skip = true;
481 break;
482 }
483 }
484 }
485 if (!skip)
486 {
487 RelationMember em = new RelationMember();
488 em.member = p;
489 em.role = "";
490 // when working with ordered relations, we make an effort to
491 // add the element before the first selected member.
492 int[] rows = memberTable.getSelectedRows();
493 if (ordered && rows.length > 0) {
494 clone.members.add(rows[0], em);
495 } else {
496 clone.members.add(em);
497 }
498 }
499 }
500 refreshTables();
501 }
502
503 private void deleteSelected() {
504 for (OsmPrimitive p : Main.ds.getSelected()) {
505 Relation c = new Relation(clone);
506 for (RelationMember rm : c.members) {
507 if (rm.member == p)
508 {
509 RelationMember mem = new RelationMember();
510 mem.role = rm.role;
511 mem.member = rm.member;
512 clone.members.remove(mem);
513 }
514 }
515 }
516 refreshTables();
517 }
518
519 private void moveMembers(int direction) {
520 int[] rows = memberTable.getSelectedRows();
521 if (rows.length == 0) return;
522
523 // check if user attempted to move anything beyond the boundary of the list
524 if (rows[0] + direction < 0) return;
525 if (rows[rows.length-1] + direction >= clone.members.size()) return;
526
527 RelationMember m[] = new RelationMember[clone.members.size()];
528
529 // first move all selected rows from the member list into a new array,
530 // displaced by the move amount
531 for (Integer i: rows) {
532 m[i+direction] = clone.members.get(i);
533 clone.members.set(i, null);
534 }
535
536 // now fill the empty spots in the destination array with the remaining
537 // elements.
538 int i = 0;
539 for (RelationMember rm : clone.members) {
540 if (rm != null) {
541 while (m[i] != null) i++;
542 m[i++] = rm;
543 }
544 }
545
546 // and write the array back into the member list.
547 clone.members.clear();
548 clone.members.addAll(Arrays.asList(m));
549 refreshTables();
550 ListSelectionModel lsm = memberTable.getSelectionModel();
551 lsm.setValueIsAdjusting(true);
552 for (Integer j: rows) {
553 lsm.addSelectionInterval(j + direction, j + direction);
554 }
555 lsm.setValueIsAdjusting(false);
556 }
557
558 private void downloadRelationMembers() {
559 boolean download = false;
560 for (RelationMember member : clone.members) {
561 if (member.member.incomplete) {
562 download = true;
563 break;
564 }
565 }
566 if (download) {
567 OsmServerObjectReader reader = new OsmServerObjectReader(clone.id, OsmServerObjectReader.TYPE_REL, true);
568 try {
569 DataSet dataSet = reader.parseOsm();
570 if (dataSet != null) {
571 final MergeVisitor visitor = new MergeVisitor(Main.main
572 .editLayer().data, dataSet);
573 for (final OsmPrimitive osm : dataSet.allPrimitives())
574 osm.visit(visitor);
575 visitor.fixReferences();
576
577 // copy the merged layer's data source info
578 for (DataSource src : dataSet.dataSources)
579 Main.main.editLayer().data.dataSources.add(src);
580 Main.main.editLayer().fireDataChange();
581
582 if (visitor.conflicts.isEmpty())
583 return;
584 final ConflictDialog dlg = Main.map.conflictDialog;
585 dlg.add(visitor.conflicts);
586 JOptionPane.showMessageDialog(Main.parent,
587 tr("There were conflicts during import."));
588 if (!dlg.isVisible())
589 dlg.action
590 .actionPerformed(new ActionEvent(this, 0, ""));
591 }
592
593 } catch (SAXException e) {
594 e.printStackTrace();
595 JOptionPane.showMessageDialog(this,tr("Error parsing server response.")+": "+e.getMessage(),
596 tr("Error"), JOptionPane.ERROR_MESSAGE);
597 } catch (IOException e) {
598 e.printStackTrace();
599 JOptionPane.showMessageDialog(this,tr("Cannot connect to server.")+": "+e.getMessage(),
600 tr("Error"), JOptionPane.ERROR_MESSAGE);
601 }
602 }
603 }
604}
Note: See TracBrowser for help on using the repository browser.