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

Last change on this file since 1499 was 1499, checked in by stoecker, 15 years ago

close #2302 - patch by jttt - optimizations and encapsulation

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