// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.turnrestrictions.editor;
import java.text.MessageFormat;
import java.util.List;
import java.util.Observable;
import java.util.Set;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import org.openstreetmap.josm.data.osm.DataSet;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
import org.openstreetmap.josm.data.osm.PrimitiveId;
import org.openstreetmap.josm.data.osm.Relation;
import org.openstreetmap.josm.data.osm.TagCollection;
import org.openstreetmap.josm.data.osm.Way;
import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
import org.openstreetmap.josm.data.osm.event.DataSetListener;
import org.openstreetmap.josm.data.osm.event.DatasetEventManager;
import org.openstreetmap.josm.data.osm.event.DatasetEventManager.FireMode;
import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent;
import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent;
import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent;
import org.openstreetmap.josm.data.osm.event.TagsChangedEvent;
import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
import org.openstreetmap.josm.gui.layer.OsmDataLayer;
import org.openstreetmap.josm.gui.tagging.TagEditorModel;
import org.openstreetmap.josm.gui.tagging.TagModel;
import org.openstreetmap.josm.plugins.turnrestrictions.qa.IssuesModel;
import org.openstreetmap.josm.tools.CheckParameterUtil;
/**
* This is the model for the turn restriction editor. It keeps the editing state
* for a single turn restriction.
*
*/
public class TurnRestrictionEditorModel extends Observable implements DataSetListener {
/**
* Replies true if {@code tp1} and {@code tp2} have the same tags and
* the same members
*
* @param tp1 a turn restriction. Must not be null.
* @param tp2 a turn restriction . Must not be null.
* @return true if {@code tp1} and {@code tp2} have the same tags and
* the same members
* @throws IllegalArgumentException thrown if {@code tp1} is null
* @throws IllegalArgumentException thrown if {@code tp2} is null
*/
public static boolean hasSameMembersAndTags(Relation tp1, Relation tp2) throws IllegalArgumentException {
CheckParameterUtil.ensureParameterNotNull(tp1, "tp1");
CheckParameterUtil.ensureParameterNotNull(tp2, "tp2");
if (!TagCollection.from(tp1).asSet().equals(TagCollection.from(tp2).asSet())) return false;
if (tp1.getMembersCount() != tp2.getMembersCount()) return false;
for (int i = 0; i < tp1.getMembersCount(); i++) {
if (!tp1.getMember(i).equals(tp2.getMember(i))) return false;
}
return true;
}
private OsmDataLayer layer;
private final TagEditorModel tagEditorModel = new TagEditorModel();
private RelationMemberEditorModel memberModel;
private IssuesModel issuesModel;
private NavigationControler navigationControler;
private JosmSelectionListModel selectionModel;
/**
* Creates a model in the context of a {@link OsmDataLayer}
*
* @param layer the layer. Must not be null.
* @param navigationControler control to direct the user to specific UI components. Must not be null
* @throws IllegalArgumentException thrown if {@code layer} is null
*/
public TurnRestrictionEditorModel(OsmDataLayer layer, NavigationControler navigationControler) throws IllegalArgumentException {
CheckParameterUtil.ensureParameterNotNull(layer, "layer");
CheckParameterUtil.ensureParameterNotNull(navigationControler, "navigationControler");
this.layer = layer;
this.navigationControler = navigationControler;
memberModel = new RelationMemberEditorModel(layer);
memberModel.addTableModelListener(new RelationMemberModelListener());
issuesModel = new IssuesModel(this);
addObserver(issuesModel);
tagEditorModel.addTableModelListener(new TagEditorModelObserver());
selectionModel = new JosmSelectionListModel(layer);
}
/**
* Replies the model for the currently selected JOSM primitives
*/
public JosmSelectionListModel getJosmSelectionListModel() {
return selectionModel;
}
/**
* Sets the way participating in the turn restriction in a given role.
*
* @param role the role. Must not be null.
* @param way the way which participates in the turn restriction in the respective role.
* null, to remove the way with the given role.
* @exception IllegalArgumentException thrown if role is null
*/
public void setTurnRestrictionLeg(TurnRestrictionLegRole role, Way way) throws IllegalArgumentException {
CheckParameterUtil.ensureParameterNotNull(role, "role");
switch(role) {
case FROM:
memberModel.setFromPrimitive(way);
break;
case TO:
memberModel.setToPrimitive(way);
break;
}
}
/**
* Sets the way participating in the turn restriction in a given role.
*
* @param role the role. Must not be null.
* @param wayId the id of the way to set
* @exception IllegalArgumentException thrown if role is null
* @exception IllegalArgumentException thrown if wayId != null isn't the id of a way
* @exception IllegalStateException thrown the no way with this id was found in the dataset
*/
public void setTurnRestrictionLeg(TurnRestrictionLegRole role, PrimitiveId wayId) {
CheckParameterUtil.ensureParameterNotNull(role, "role");
if (wayId == null) {
setTurnRestrictionLeg(role, (Way) null);
return;
}
if (!wayId.getType().equals(OsmPrimitiveType.WAY)) {
throw new IllegalArgumentException(
MessageFormat.format("parameter ''wayId'' of type {0} expected, got {1}", OsmPrimitiveType.WAY, wayId.getType()));
}
OsmPrimitive p = layer.data.getPrimitiveById(wayId);
if (p == null) {
throw new IllegalStateException(MessageFormat.format("didn''t find way with id {0} in layer ''{1}''", wayId, layer.getName()));
}
setTurnRestrictionLeg(role, (Way) p);
}
/**
* <p>"Officially" a turn restriction should have exactly one member with
* role {@link TurnRestrictionLegRole#FROM FROM} and one member with role {@link TurnRestrictionLegRole#TO TO},
* both referring to an OSM {@link Way}. In order to deals with turn restrictions where these
* integrity constraints are violated, this model also supports relation with multiple or no
* 'from' or 'to' members.</p>
*
* <p>Replies the turn restriction legs with role {@code role}. If no leg with this
* role exists, an empty set is returned. If multiple legs exists, the set of referred
* primitives is returned.</p>
*
* @param role the role. Must not be null.
* @return the set of turn restriction legs with role {@code role}. The empty set, if
* no such turn restriction leg exists
* @throws IllegalArgumentException thrown if role is null
*/
public Set<OsmPrimitive> getTurnRestrictionLeg(TurnRestrictionLegRole role) {
CheckParameterUtil.ensureParameterNotNull(role, "role");
switch(role) {
case FROM: return memberModel.getFromPrimitives();
case TO: return memberModel.getToPrimitives();
}
// should not happen
return null;
}
/**
* Initializes the model from a relation representing a turn
* restriction
*
* @param turnRestriction the turn restriction
*/
protected void initFromTurnRestriction(Relation turnRestriction) {
// populate the member model
memberModel.populate(turnRestriction);
// make sure we have a restriction tag
TagCollection tags = TagCollection.from(turnRestriction);
tags.setUniqueForKey("type", "restriction");
tagEditorModel.initFromTags(tags);
setChanged();
notifyObservers();
}
/**
* Populates the turn restriction editor model with a turn restriction.
* {@code turnRestriction} is an arbitrary relation. A tag type=restriction
* isn't required. If it is missing, it is added here. {@code turnRestriction}
* must not be null and it must belong to a dataset.
*
* @param turnRestriction the turn restriction
* @throws IllegalArgumentException thrown if turnRestriction is null
* @throws IllegalArgumentException thrown if turnRestriction doesn't belong to a dataset
*/
public void populate(Relation turnRestriction) {
CheckParameterUtil.ensureParameterNotNull(turnRestriction, "turnRestriction");
if (turnRestriction.getDataSet() != null && turnRestriction.getDataSet() != layer.data) {
throw new IllegalArgumentException(
// don't translate - it's a technical message
MessageFormat.format("turnRestriction {0} must not belong to a different dataset than the dataset of layer ''{1}''",
turnRestriction.getId(), layer.getName())
);
}
initFromTurnRestriction(turnRestriction);
}
/**
* Applies the current state in the model to a turn restriction
*
* @param turnRestriction the turn restriction. Must not be null.
*/
public void apply(Relation turnRestriction) {
CheckParameterUtil.ensureParameterNotNull(turnRestriction, "turnRestriction");
TagCollection tags = tagEditorModel.getTagCollection();
turnRestriction.removeAll();
tags.applyTo(turnRestriction);
memberModel.applyTo(turnRestriction);
}
/**
* Replies the current tag value for the tag <tt>restriction</tt>.
* The empty tag, if there isn't a tag <tt>restriction</tt>.
*
* @return the tag value
*/
public String getRestrictionTagValue() {
TagCollection tags = tagEditorModel.getTagCollection();
if (!tags.hasTagsFor("restriction")) return "";
return tags.getJoinedValues("restriction");
}
/**
* Sets the current value for the restriction tag. If {@code value} is
* null or an empty string, the restriction tag is removed.
*
* @param value the value of the restriction tag
*/
public void setRestrictionTagValue(String value) {
if (value == null || value.trim().equals("")) {
tagEditorModel.delete("restriction");
} else {
TagModel tm = tagEditorModel.get("restriction");
if (tm != null) {
tm.setValue(value);
} else {
tagEditorModel.prepend(new TagModel("restriction", value.trim().toLowerCase()));
}
}
setChanged();
notifyObservers();
}
/**
* Replies the list of 'via' objects. The return value is an
* unmodifiable list.
*
* @return the list of 'via' objects
*/
public List<OsmPrimitive> getVias() {
return memberModel.getVias();
}
/**
* <p>Sets the list of vias for the edited turn restriction.</p>
*
* <p>If {@code vias} is null, all vias are removed. All primitives
* in {@code vias} must be assigned to a dataset and the dataset
* must be equal to the dataset of this editor model, see {@link #getDataSet()}</p>
*
* <p>null values in {@link vias} are skipped.</p>
*
* @param vias the list of vias
* @throws IllegalArgumentException thrown if one of the via objects belongs to the wrong dataset
*/
public void setVias(List<OsmPrimitive> vias) throws IllegalArgumentException {
memberModel.setVias(vias);
}
/**
* Replies the layer in whose context this editor is working
*
* @return the layer in whose context this editor is working
*/
public OsmDataLayer getLayer() {
return layer;
}
/**
* Registers this model with global event sources like {@link DatasetEventManager}
*/
public void registerAsEventListener() {
DatasetEventManager.getInstance().addDatasetListener(this, FireMode.IN_EDT);
}
/**
* Removes this model as listener from global event sources like {@link DatasetEventManager}
*/
public void unregisterAsEventListener() {
DatasetEventManager.getInstance().removeDatasetListener(this);
}
/**
* Replies the tag editor model
*
* @return the tag editor model
*/
public TagEditorModel getTagEditorModel() {
return tagEditorModel;
}
/**
* Replies the editor model for the relation members
*
* @return the editor model for the relation members
*/
public RelationMemberEditorModel getRelationMemberEditorModel() {
return memberModel;
}
/**
* Replies the model for the open issues in this turn restriction
* editor.
*
* @return the model for the open issues in this turn restriction
* editor
*/
public IssuesModel getIssuesModel() {
return issuesModel;
}
public NavigationControler getNavigationControler() {
return navigationControler;
}
/**
* Replies the current value of the tag "except", or the empty string
* if the tag doesn't exist.
*/
public ExceptValueModel getExcept() {
TagModel tag = tagEditorModel.get("except");
if (tag == null) return new ExceptValueModel("");
return new ExceptValueModel(tag.getValue());
}
/**
* Sets the current value of the tag "except". Removes the
* tag is {@code value} is null or consists of white
* space only.
*
* @param value the new value for 'except'
*/
public void setExcept(ExceptValueModel value) {
if (value == null || value.getValue().equals("")) {
if (tagEditorModel.get("except") != null) {
tagEditorModel.delete("except");
setChanged();
notifyObservers();
}
return;
}
TagModel tag = tagEditorModel.get("except");
if (tag == null) {
tagEditorModel.prepend(new TagModel("except", value.getValue()));
setChanged();
notifyObservers();
} else {
if (!tag.getValue().equals(value.getValue())) {
tag.setValue(value.getValue().trim());
setChanged();
notifyObservers();
}
}
}
/* ----------------------------------------------------------------------------------------- */
/* interface DataSetListener */
/* ----------------------------------------------------------------------------------------- */
protected boolean isAffectedByDataSetUpdate(DataSet ds, List<? extends OsmPrimitive> updatedPrimitives) {
if (ds != layer.data) return false;
if (updatedPrimitives == null || updatedPrimitives.isEmpty()) return false;
Set<OsmPrimitive> myPrimitives = memberModel.getMemberPrimitives();
int size1 = myPrimitives.size();
myPrimitives.retainAll(updatedPrimitives);
return size1 != myPrimitives.size();
}
@Override
public void dataChanged(DataChangedEvent event) {
// refresh the views
setChanged();
notifyObservers();
}
@Override
public void nodeMoved(NodeMovedEvent event) {
// may affect the display name of node in the list of vias
if (isAffectedByDataSetUpdate(event.getDataset(), event.getPrimitives())) {
setChanged();
notifyObservers();
}
}
@Override
public void otherDatasetChange(AbstractDatasetChangedEvent event) {/* irrelevant in this context */}
@Override
public void primitivesAdded(PrimitivesAddedEvent event) {/* irrelevant in this context */}
@Override
public void primitivesRemoved(PrimitivesRemovedEvent event) {
// relevant for the state of this model but not handled here. When the
// state of this model is applied to the dataset we check whether the
// the turn restriction refers to deleted or invisible primitives
}
@Override
public void relationMembersChanged(RelationMembersChangedEvent event) {/* irrelevant in this context */}
@Override
public void tagsChanged(TagsChangedEvent event) {
// may affect the display name of 'from', 'to' or 'via' elements
if (isAffectedByDataSetUpdate(event.getDataset(), event.getPrimitives())) {
setChanged();
notifyObservers();
}
}
@Override
public void wayNodesChanged(WayNodesChangedEvent event) {
// may affect the display name of 'from', 'to' or 'via' elements
if (isAffectedByDataSetUpdate(event.getDataset(), event.getPrimitives())) {
setChanged();
notifyObservers();
}
}
class RelationMemberModelListener implements TableModelListener {
@Override
public void tableChanged(TableModelEvent e) {
setChanged();
notifyObservers();
}
}
/* ----------------------------------------------------------------------------------------- */
/* inner classes */
/* ----------------------------------------------------------------------------------------- */
class TagEditorModelObserver implements TableModelListener {
@Override
public void tableChanged(TableModelEvent e) {
setChanged();
notifyObservers();
}
}
}