/* * The Unified Mapping Platform (JUMP) is an extensible, interactive GUI * for visualizing and manipulating spatial features with geometry and attributes. * * Copyright (C) 2003 Vivid Solutions * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * * For more information, contact: * * Vivid Solutions * Suite #1A * 2328 Government Street * Victoria BC V8T 5G5 * Canada * * (250)385-6040 * www.vividsolutions.com */ package com.vividsolutions.jump.workbench.ui; import java.util.*; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.Point; import com.vividsolutions.jts.util.Assert; import com.vividsolutions.jump.I18N; import com.vividsolutions.jump.feature.Feature; import com.vividsolutions.jump.workbench.model.Layer; import com.vividsolutions.jump.workbench.model.UndoableCommand; /** * Takes care of "rollback" (if any geometries are invalid) and undo, * for PlugIns and CursorTools that modify geometries. * <p> Also: * <UL> * <LI>warns the user if invalid geometries are found</LI> * <LI>invalidates the layer envelope cache</LI> * <LI>invalidates the geometry envelope caches</LI> * <LI>(undoably) removes features from the layer when their geometries are made empty</LI> * <LI>(undoably) adds features to the layer when they start with empty geometries </LI> * </UL></p> */ public class EditTransaction { private List features; private List originalGeometries; private List proposedGeometries; private Layer layer; private String name; private boolean rollingBackInvalidEdits; public static final String ROLLING_BACK_INVALID_EDITS_KEY = EditTransaction.class.getName() + " - ROLLING_BACK_INVALID_EDITS"; private LayerViewPanelContext layerViewPanelContext; public EditTransaction( Collection features, String name, Layer layer, boolean rollingBackInvalidEdits, boolean allowAddingAndRemovingFeatures, LayerViewPanel layerViewPanel) { this( features, name, layer, rollingBackInvalidEdits, allowAddingAndRemovingFeatures, layerViewPanel.getContext()); } /** * If you want to delete a feature, you can either (1) include the feature in * the features parameter, set allowAddingAndRemovingFeatures to true, * then call #setGeometry(feature, empty geometry); or (2) not include the feature in * the features parameter, instead using #deleteFeature * @param name Display name for undo. Use PlugIn#getName or CursorTool#getName. * @param layer the layer to which the features belong * @param allowAddingAndRemovingFeatures whether to treat empty * geometries as indications to add/remove features or as in fact empty geometries */ public EditTransaction( Collection features, String name, Layer layer, boolean rollingBackInvalidEdits, boolean allowAddingAndRemovingFeatures, LayerViewPanelContext layerViewPanelContext) { this.layerViewPanelContext = layerViewPanelContext; this.layer = layer; this.rollingBackInvalidEdits = rollingBackInvalidEdits; this.allowAddingAndRemovingFeatures = allowAddingAndRemovingFeatures; this.name = name; this.features = new ArrayList(features); //Clone the Geometries, and don't commit it until we're sure that no errors //occurred. [Jon Aquino] originalGeometries = geometryClones(features); proposedGeometries = geometryClones(features); } public static EditTransaction createTransactionOnSelection( SelectionEditor editor, SelectionManagerProxy selectionManagerProxy, LayerViewPanelContext layerViewPanelContext, String name, Layer layer, boolean rollingBackInvalidEdits, boolean allowAddingAndRemovingFeatures) { Map featureToNewGeometryMap = featureToNewGeometryMap(editor, selectionManagerProxy, layer); EditTransaction transaction = new EditTransaction( featureToNewGeometryMap.keySet(), name, layer, rollingBackInvalidEdits, allowAddingAndRemovingFeatures, layerViewPanelContext); transaction.setGeometries(featureToNewGeometryMap); return transaction; } public static Map featureToNewGeometryMap( SelectionEditor editor, SelectionManagerProxy selectionManagerProxy, Layer layer) { Map featureToNewGeometryMap = new HashMap(); for (Iterator i = selectionManagerProxy .getSelectionManager() .getFeaturesWithSelectedItems(layer) .iterator(); i.hasNext(); ) { Feature feature = (Feature) i.next(); Geometry newGeometry = (Geometry) feature.getGeometry().clone(); ArrayList selectedItems = new ArrayList(); for (Iterator j = selectionManagerProxy.getSelectionManager().getSelections().iterator(); j.hasNext(); ) { AbstractSelection selection = (AbstractSelection) j.next(); //Use #getSelectedItemIndices rather than #getSelectedItems, because //we want the selected items from newGeometry, not the original //Geometry (so that editor can freely modify them). [Jon Aquino] selectedItems.addAll( selection.items(newGeometry, selection.getSelectedItemIndices(layer, feature))); } newGeometry = editor.edit(newGeometry, selectedItems); featureToNewGeometryMap.put(feature, newGeometry); } return featureToNewGeometryMap; } public static interface SelectionEditor { /** * selectedItems may have the whole geometry, parts (collection elements), * or linestrings, or a mix of all three. But there will be no duplicate data * (that is, you can't select both the whole and one of its parts -- only the * whole geometry will be selected; similarly, you can't select a part and * one of its linestrings -- only the part will be selected). * @param geometryWithSelectedItems a clone of the geometry containing the selected items. * Because geometryWithSelectedItems is a clone, feel free to modify it, as no other * parties reference it. Then return it (or return something totally different). * @param selectedItems clones of the selected items (each of which have class Geometry). * selectedItems' elements are "live"; that is, they are objects taken from geometryWithSelectedItems. * So, for example, modifying selectedItem's coordinates will modify geometryWithSelectedItems' * coordinates. * @return a new Geometry for the Feature (typically geometryWithSelectedItems, but can * be a completely different Geometry), or an empty geometry to (undoably) remove the Feature from the Layer */ public Geometry edit(Geometry geometryWithSelectedItems, Collection selectedItems); } public Geometry getGeometry(int i) { return (Geometry) proposedGeometries.get(i); } public Geometry getGeometry(Feature feature) { return getGeometry(features.indexOf(feature)); } public void setGeometry(Feature feature, Geometry geometry) { setGeometry(features.indexOf(feature), geometry); } public void setGeometries(Map featureToGeometryMap) { // [michaudm 2009-05-16] using an index improves dramatically // performances for big transactions Map index = new HashMap(); int pos = 0; for (Iterator i = features.iterator() ; i.hasNext() ; ) { index.put(i.next(), pos++); } for (Iterator i = featureToGeometryMap.keySet().iterator(); i.hasNext();) { Feature feature = (Feature) i.next(); //setGeometry(feature, (Geometry) featureToGeometryMap.get(feature)); // [michaudm 2009-05-16] use index proposedGeometries.set(((Integer)index.get(feature)).intValue(), editor.removeRepeatedPoints((Geometry) featureToGeometryMap.get(feature))); } } public void setGeometry(int i, Geometry geometry) { proposedGeometries.set(i, editor.removeRepeatedPoints(geometry)); } private GeometryEditor editor = new GeometryEditor(); private boolean allowAddingAndRemovingFeatures; public static interface SuccessAction { public void run(); } public boolean commit() { return commit(Collections.singleton(this)); } public static boolean commit(Collection editTransactions) { return commit(editTransactions, new SuccessAction() { public void run() {} }); } /** * Commits several EditTransactions if their proposed geometries are all valid. * Useful for committing changes to several layers because an EditTransaction * handles one layer only. Gets the undo name and the UndoManager * from the first EditTransaction. * @param successAction run after the first execution (i.e. not after redos) if all * proposed geometries are valid (or rollingBackInvalidEdits is false) */ public static boolean commit(Collection editTransactions, SuccessAction successAction) { if (editTransactions.isEmpty()) { return true; } final ArrayList commands = new ArrayList(); for (Iterator i = editTransactions.iterator(); i.hasNext();) { EditTransaction editTransaction = (EditTransaction) i.next(); editTransaction.clearEnvelopeCaches(); if (!editTransaction.proposedGeometriesValid()) { if (editTransaction.rollingBackInvalidEdits) { editTransaction.layerViewPanelContext.warnUser( I18N.get("ui.EditTransaction.the-geometry-is-invalid-cancelled")); return false; } else { editTransaction.layerViewPanelContext.warnUser(I18N.get("ui.EditTransaction.the-new-geometry-is-invalid")); } } commands.add(editTransaction.createCommand()); } successAction.run(); UndoableCommand command = new UndoableCommand(((UndoableCommand) commands.iterator().next()).getName()) { public void execute() { for (Iterator i = commands.iterator(); i.hasNext();) { UndoableCommand subCommand = (UndoableCommand) i.next(); subCommand.execute(); } } public void unexecute() { for (Iterator i = commands.iterator(); i.hasNext();) { UndoableCommand subCommand = (UndoableCommand) i.next(); subCommand.unexecute(); } } }; command.execute(); ((EditTransaction) editTransactions.iterator().next()) .layer .getLayerManager() .getUndoableEditReceiver() .receive(command.toUndoableEdit()); return true; } /** * @param successAction will be run if the geometries are valid (or * OptionsPlugIn#isRollingBackInvalidEdits returns false), before the layer-change * events are fired. Useful for animations and other visual indicators which would * be slowed down if the layer-change events were fired first. * @return true if all the proposed geometries are valid */ public boolean commit(SuccessAction successAction) { return commit(Collections.singleton(this), successAction); } public void clearEnvelopeCaches() { for (int i = 0; i < proposedGeometries.size(); i++) { Geometry proposedGeometry = (Geometry) proposedGeometries.get(i); //Because the proposedGeometry is a clone, its cached envelope is old. //Invalidate the envelope. [Jon Aquino] proposedGeometry.geometryChanged(); } } public boolean proposedGeometriesValid() { for (int i = 0; i < proposedGeometries.size(); i++) { Geometry proposedGeometry = (Geometry) proposedGeometries.get(i); if (! proposedGeometry.isValid()) { return false; } } return true; } protected UndoableCommand createCommand() { UndoableCommand command = new UndoableCommand(name) { public void execute() { changeGeometries(proposedGeometries, originalGeometries, layer); } public void unexecute() { changeGeometries(originalGeometries, proposedGeometries, layer); } }; return command; } private List geometryClones(Collection features) { ArrayList geometryClones = new ArrayList(); for (Iterator i = features.iterator(); i.hasNext();) { Feature feature = (Feature) i.next(); geometryClones.add(feature.getGeometry().clone()); } return geometryClones; } /** * @param oldGeometries an empty geometry indicates that we should be re-adding the feature to the layer */ private void changeGeometries(List newGeometries, List oldGeometries, Layer layer) { ArrayList modifiedFeatures = new ArrayList(); ArrayList modifiedFeaturesOldClones = new ArrayList(); ArrayList featuresToAdd = new ArrayList(); ArrayList featuresToRemove = new ArrayList(); for (int i = 0; i < size(); i++) { Feature feature = (Feature) features.get(i); Geometry oldGeometry = (Geometry) oldGeometries.get(i); Geometry newGeometry = (Geometry) newGeometries.get(i); if (allowAddingAndRemovingFeatures && oldGeometry.isEmpty() && !newGeometry.isEmpty()) { featuresToAdd.add(feature); } else if ( allowAddingAndRemovingFeatures && newGeometry.isEmpty() // the second condition was preventing empty geometries to be removed // now, it should remove a feature which new geometry is empty, // but it does not. Why ? [mmichaud 2010-10-16] /*&& !oldGeometry.isEmpty()*/) { featuresToRemove.add(feature); } else { modifiedFeatures.add(feature); modifiedFeaturesOldClones.add(feature.clone()); feature.setGeometry(newGeometry); } } Layer.tryToInvalidateEnvelope(layer); //Important to fire the feature-removed event first (before the feature-added //and feature-modified events) so that any selections that need to be cleared //get cleared. [Jon Aquino] if (!featuresToRemove.isEmpty()) { layer.getFeatureCollectionWrapper().removeAll(featuresToRemove); } if (!featuresToAdd.isEmpty()) { layer.getFeatureCollectionWrapper().addAll(featuresToAdd); } if (!modifiedFeatures.isEmpty()) { layer.getLayerManager().fireGeometryModified( modifiedFeatures, layer, modifiedFeaturesOldClones); } } public int size() { return features.size(); } public Feature getFeature(int i) { return (Feature) features.get(i); } public void createFeature(Feature feature) { Assert.isTrue(allowAddingAndRemovingFeatures); Assert.isTrue(!features.contains(feature)); features.add(feature); originalGeometries.add(feature.getGeometry().getFactory().createGeometryCollection(new Geometry[0])); proposedGeometries.add(feature.getGeometry().clone()); } /** * @param feature must not have been passed into the constructor */ public void deleteFeature(Feature feature) { Assert.isTrue(allowAddingAndRemovingFeatures); Assert.isTrue(!features.contains(feature)); features.add(feature); originalGeometries.add(feature.getGeometry().clone()); proposedGeometries.add(feature.getGeometry().getFactory().createGeometryCollection(new Geometry[0])); } public Layer getLayer() { return layer; } public static int emptyGeometryCount(Collection transactions) { int count = 0; for (Iterator i = transactions.iterator(); i.hasNext(); ) { EditTransaction transaction = (EditTransaction) i.next(); count += transaction.getEmptyGeometryCount(); } return count; } private int getEmptyGeometryCount() { int count = 0; for (int i = 0; i < size(); i++) { if (getGeometry(i).isEmpty()) { count++; } } return count; } }