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

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

removed usage of tab stops

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