// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.turnrestrictions.editor;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import javax.swing.DefaultListSelectionModel;
import javax.swing.table.AbstractTableModel;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.PrimitiveId;
import org.openstreetmap.josm.data.osm.Relation;
import org.openstreetmap.josm.data.osm.RelationMember;
import org.openstreetmap.josm.gui.DefaultNameFormatter;
import org.openstreetmap.josm.gui.layer.OsmDataLayer;
import org.openstreetmap.josm.tools.CheckParameterUtil;
public class RelationMemberEditorModel extends AbstractTableModel {
private final ArrayList<RelationMemberModel> members = new ArrayList<>();
private OsmDataLayer layer;
private DefaultListSelectionModel rowSelectionModel;
private DefaultListSelectionModel colSelectionModel;
/**
* Creates a new model in the context of an {@link OsmDataLayer}. Internally allocates
* a row and a column selection model, see {@link #getRowSelectionModel()} and
* {@link #getColSelectionModel()}.
*
* @param layer the data layer. Must not be null.
* @exception IllegalArgumentException thrown if layer is null
*/
public RelationMemberEditorModel(OsmDataLayer layer) throws IllegalArgumentException {
CheckParameterUtil.ensureParameterNotNull(layer, "layer");
this.layer = layer;
rowSelectionModel = new DefaultListSelectionModel();
colSelectionModel = new DefaultListSelectionModel();
}
/**
* Creates a new model in the context of an {@link OsmDataLayer}
*
* @param layer layer the data layer. Must not be null.
* @param rowSelectionModel the row selection model. Must not be null.
* @param colSelectionModel the column selection model. Must not be null.
* @throws IllegalArgumentException thrown if layer is null
* @throws IllegalArgumentException thrown if rowSelectionModel is null
* @throws IllegalArgumentException thrown if colSelectionModel is null
*/
public RelationMemberEditorModel(OsmDataLayer layer, DefaultListSelectionModel rowSelectionModel,
DefaultListSelectionModel colSelectionModel) throws IllegalArgumentException {
CheckParameterUtil.ensureParameterNotNull(layer, "layer");
CheckParameterUtil.ensureParameterNotNull(rowSelectionModel, "rowSelectionModel");
CheckParameterUtil.ensureParameterNotNull(colSelectionModel, "colSelectionModel");
this.layer = layer;
this.rowSelectionModel = rowSelectionModel;
this.colSelectionModel = colSelectionModel;
}
/**
* Replies the row selection model used in this table model.
*
* @return the row selection model
*/
public DefaultListSelectionModel getRowSelectionModel() {
return rowSelectionModel;
}
/**
* Replies the column selection model used in this table model.
*
* @return the col selection model
*/
public DefaultListSelectionModel getColSelectionModel() {
return colSelectionModel;
}
/**
* Replies the set of {@link OsmPrimitive}s with the role {@code role}. If no
* such primitives exists, the empty set is returned.
*
* @return the set of {@link OsmPrimitive}s with the role {@code role}
*/
protected Set<OsmPrimitive> getPrimitivesWithRole(String role) {
HashSet<OsmPrimitive> ret = new HashSet<>();
for (RelationMemberModel rm: members) {
if (rm.getRole().equals(role)) {
OsmPrimitive p = layer.data.getPrimitiveById(rm.getTarget());
if (p != null) {
ret.add(p);
}
}
}
return ret;
}
/**
* Replies the list of {@link RelationMemberModel}s with the role {@code role}. If no
* such primitives exists, the empty set is returned.
*
* @return the set of {@link RelationMemberModel}s with the role {@code role}
*/
protected List<RelationMemberModel> getRelationMembersWithRole(String role) {
ArrayList<RelationMemberModel> ret = new ArrayList<>();
for (RelationMemberModel rm: members) {
if (rm.getRole().equals(role)) {
ret.add(rm);
}
}
return ret;
}
/**
* Removes all members with role {@code role}.
*
* @param role the role. Ignored if null.
* @return true if the list of members was modified; false, otherwise
*/
protected boolean removeMembersWithRole(String role) {
if (role == null) return false;
boolean isChanged = false;
for (Iterator<RelationMemberModel> it = members.iterator(); it.hasNext();) {
RelationMemberModel rm = it.next();
if (rm.getRole().equals(role)) {
it.remove();
isChanged = true;
}
}
return isChanged;
}
/**
* Replies the set of {@link OsmPrimitive}s with the role 'from'. If no
* such primitives exists, the empty set is returned.
*
* @return the set of {@link OsmPrimitive}s with the role 'from'
*/
public Set<OsmPrimitive> getFromPrimitives() {
return getPrimitivesWithRole("from");
}
/**
* Replies the set of {@link OsmPrimitive}s with the role 'to'. If no
* such primitives exists, the empty set is returned.
*
* @return the set of {@link OsmPrimitive}s with the role 'from'
*/
public Set<OsmPrimitive> getToPrimitives() {
return getPrimitivesWithRole("to");
}
/**
* Replies the list of 'via' objects in the order they occur in the
* member list. Replies an empty list if no vias exist
*/
public List<OsmPrimitive> getVias() {
ArrayList<OsmPrimitive> ret = new ArrayList<>();
for (RelationMemberModel rm: getRelationMembersWithRole("via")) {
ret.add(layer.data.getPrimitiveById(rm.getTarget()));
}
return ret;
}
/**
* Sets the list of vias. Removes all 'vias' if {@code vias} is null.
*
* null vias are skipped. A via must belong to the dataset of the layer in whose context
* this editor is working, otherwise an {@link IllegalArgumentException} is thrown.
*
* @param vias the vias.
* @exception IllegalArgumentException thrown if a via doesn't belong to the dataset of the layer
* in whose context this editor is working
*/
public void setVias(List<OsmPrimitive> vias) throws IllegalArgumentException {
boolean viasDeleted = removeMembersWithRole("via");
if (vias == null || vias.isEmpty()) {
if (viasDeleted) {
fireTableDataChanged();
}
return;
}
// check vias
for (OsmPrimitive via: vias) {
if (via == null) continue;
if (via.getDataSet() == null || via.getDataSet() != layer.data) {
throw new IllegalArgumentException(MessageFormat.format("via object ''{0}'' must belong to dataset of layer ''{1}''",
via.getDisplayName(DefaultNameFormatter.getInstance()), layer.getName()));
}
}
// add vias
for (OsmPrimitive via: vias) {
if (via == null) continue;
RelationMemberModel model = new RelationMemberModel("via", via);
members.add(model);
}
fireTableDataChanged();
}
/**
* Sets the turn restriction member with role {@code role}. Removes all
* members with role {@code role} if {@code id} is null.
*
* @param id the id
* @return true if the model was modified; false, otherwise
*/
protected boolean setPrimitiveWithRole(PrimitiveId id, String role) {
if (id == null) {
return removeMembersWithRole(role);
}
List<RelationMemberModel> fromMembers = getRelationMembersWithRole(role);
if (fromMembers.isEmpty()) {
RelationMemberModel rm = new RelationMemberModel(role, id);
members.add(rm);
return true;
} else if (fromMembers.size() == 1) {
RelationMemberModel rm = fromMembers.get(0);
if (!rm.getTarget().equals(id)) {
rm.setTarget(id);
return true;
}
return false;
} else {
removeMembersWithRole(role);
RelationMemberModel rm = new RelationMemberModel(role, id);
members.add(rm);
return true;
}
}
/**
* Sets the turn restriction member with role 'from'. Removes all
* members with role 'from' if {@code id} is null.
*
* @param id the id
*/
public void setFromPrimitive(PrimitiveId id) {
if (setPrimitiveWithRole(id, "from")) {
fireTableDataChanged();
}
}
/**
* Sets the turn restriction member with role 'to'. Removes all
* members with role 'to' if {@code id} is null.
*
* @param id the id
*/
public void setToPrimitive(PrimitiveId id) {
if (setPrimitiveWithRole(id, "to")) {
fireTableDataChanged();
}
}
/**
* Replies the set of {@link OsmPrimitive}s referred to by members in
* this model.
*
* @return the set of {@link OsmPrimitive}s referred to by members in
* this model.
*/
public Set<OsmPrimitive> getMemberPrimitives() {
Set<OsmPrimitive> ret = new HashSet<>();
for (RelationMemberModel rm: members) {
OsmPrimitive p = layer.data.getPrimitiveById(rm.getTarget());
if (p != null) ret.add(p);
}
return ret;
}
/**
* Populates the model with the relation member of a turn restriction. Clears
* the model if {@code tr} is null.
*
* @param tr the turn restriction
*/
public void populate(Relation tr) {
members.clear();
if (tr == null) {
fireTableDataChanged();
return;
}
for (RelationMember rm: tr.getMembers()) {
members.add(new RelationMemberModel(rm));
}
fireTableDataChanged();
}
/**
* Replaces the member of turn restriction {@code tr} by the relation members currently
* edited in this model.
*
* @param tr the turn restriction. Ignored if null.
*/
public void applyTo(Relation tr) {
if (tr == null) return;
List<RelationMember> newMembers = new ArrayList<>();
for (RelationMemberModel model: members) {
RelationMember rm = new RelationMember(model.getRole(), layer.data.getPrimitiveById(model.getTarget()));
newMembers.add(rm);
}
tr.setMembers(newMembers);
}
/**
* Clears the roles of all relation members currently selected in the
* table.
*/
protected void clearSelectedRoles() {
for (int i = 0; i < getRowCount(); i++) {
if (rowSelectionModel.isSelectedIndex(i)) {
members.get(i).setRole("");
}
}
}
/**
* Removes the currently selected rows from the model
*/
protected void removedSelectedMembers() {
for (int i = getRowCount()-1; i >= 0; i--) {
if (rowSelectionModel.isSelectedIndex(i)) {
members.remove(i);
}
}
}
/**
* Deletes the current selection.
*
* If only cells in the first column are selected, the roles of the selected
* members are reset to the empty string. Otherwise the selected members are
* removed from the model.
*
*/
public void deleteSelected() {
if (colSelectionModel.isSelectedIndex(0) && !colSelectionModel.isSelectedIndex(1)) {
clearSelectedRoles();
} else if (rowSelectionModel.getMinSelectionIndex() >= 0) {
removedSelectedMembers();
}
fireTableDataChanged();
}
protected List<Integer> getSelectedIndices() {
ArrayList<Integer> ret = new ArrayList<>();
for (int i = 0; i < members.size(); i++) {
if (rowSelectionModel.isSelectedIndex(i))
ret.add(i);
}
return ret;
}
public boolean canMoveUp() {
List<Integer> sel = getSelectedIndices();
if (sel.isEmpty()) return false;
return sel.get(0) > 0;
}
public boolean canMoveDown() {
List<Integer> sel = getSelectedIndices();
if (sel.isEmpty()) return false;
return sel.get(sel.size()-1) < members.size()-1;
}
public void moveUpSelected() {
if (!canMoveUp()) return;
List<Integer> sel = getSelectedIndices();
for (int idx: sel) {
RelationMemberModel m = members.remove(idx);
members.add(idx-1, m);
}
fireTableDataChanged();
rowSelectionModel.clearSelection();
colSelectionModel.setSelectionInterval(0, 1);
for (int idx: sel) {
rowSelectionModel.addSelectionInterval(idx-1, idx-1);
}
}
public void moveDownSelected() {
if (!canMoveDown()) return;
List<Integer> sel = getSelectedIndices();
for (int i = sel.size()-1; i >= 0; i--) {
int idx = sel.get(i);
RelationMemberModel m = members.remove(idx);
members.add(idx+1, m);
}
fireTableDataChanged();
rowSelectionModel.clearSelection();
colSelectionModel.setSelectionInterval(0, 1);
for (int idx: sel) {
rowSelectionModel.addSelectionInterval(idx+1, idx+1);
}
}
/**
* <p>Inserts a list of new relation members with the empty role for the primitives
* with id in {@code ids}. Inserts the new primitives at the position of the first
* selected row. If no row is selected, at the end of the list.</p>
*
* <p> null values are skipped. If there is an id for which there is no primitive in the context
* layer, if the primitive is deleted or invisible, an {@link IllegalArgumentException}
* is thrown and nothing is inserted.</p>
*
* @param ids the list of ids. Ignored if null.
* @throws IllegalArgumentException thrown if one of the ids can't be inserted
*/
public void insertMembers(Collection<PrimitiveId> ids) throws IllegalArgumentException {
if (ids == null) return;
ArrayList<RelationMemberModel> newMembers = new ArrayList<>();
for (PrimitiveId id: ids) {
OsmPrimitive p = layer.data.getPrimitiveById(id);
if (p == null) {
throw new IllegalArgumentException(tr("Cannot find object with id ''{0}'' in layer ''{1}''", id.toString(), layer.getName()));
}
if (p.isDeleted() || !p.isVisible()) {
throw new IllegalArgumentException(
tr("Cannot add object ''{0}'' as relation member because it is deleted or invisible in layer ''{1}''",
p.getDisplayName(DefaultNameFormatter.getInstance()), layer.getName()));
}
newMembers.add(new RelationMemberModel("", id));
}
if (newMembers.isEmpty()) return;
int insertPos = rowSelectionModel.getMinSelectionIndex();
if (insertPos >= 0) {
members.addAll(insertPos, newMembers);
} else {
members.addAll(newMembers);
}
fireTableDataChanged();
if (insertPos < 0) insertPos = 0;
colSelectionModel.setSelectionInterval(0, 1); // select both columns
rowSelectionModel.setSelectionInterval(insertPos, insertPos + newMembers.size()-1);
}
@Override
public int getColumnCount() {
return 2;
}
@Override
public int getRowCount() {
return members.size();
}
@Override
public Object getValueAt(int rowIndex, int columnIndex) {
switch(columnIndex) {
case 0: return members.get(rowIndex).getRole();
case 1: return layer.data.getPrimitiveById(members.get(rowIndex).getTarget());
}
return null;
}
@Override
public boolean isCellEditable(int rowIndex, int columnIndex) {
// only the column with the member roles is editable
return columnIndex == 0;
}
@Override
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
if (columnIndex != 0) return;
String role = (String) aValue;
RelationMemberModel model = members.get(rowIndex);
model.setRole(role);
fireTableCellUpdated(rowIndex, columnIndex);
}
}