001: /*
002: * The Unified Mapping Platform (JUMP) is an extensible, interactive GUI
003: * for visualizing and manipulating spatial features with geometry and attributes.
004: *
005: * Copyright (C) 2003 Vivid Solutions
006: *
007: * This program is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU General Public License
009: * as published by the Free Software Foundation; either version 2
010: * of the License, or (at your option) any later version.
011: *
012: * This program is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
015: * GNU General Public License for more details.
016: *
017: * You should have received a copy of the GNU General Public License
018: * along with this program; if not, write to the Free Software
019: * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
020: *
021: * For more information, contact:
022: *
023: * Vivid Solutions
024: * Suite #1A
025: * 2328 Government Street
026: * Victoria BC V8T 5G5
027: * Canada
028: *
029: * (250)385-6040
030: * www.vividsolutions.com
031: */
032:
033: package com.vividsolutions.jump.workbench.ui;
034:
035: import java.util.*;
036:
037: import com.vividsolutions.jts.geom.Geometry;
038: import com.vividsolutions.jts.geom.Point;
039: import com.vividsolutions.jts.util.Assert;
040: import com.vividsolutions.jump.I18N;
041: import com.vividsolutions.jump.feature.Feature;
042: import com.vividsolutions.jump.workbench.model.Layer;
043: import com.vividsolutions.jump.workbench.model.UndoableCommand;
044:
045: /**
046: * Takes care of "rollback" (if any geometries are invalid) and undo,
047: * for PlugIns and CursorTools that modify geometries.
048: * <p> Also:
049: * <UL>
050: * <LI>warns the user if invalid geometries are found</LI>
051: * <LI>invalidates the layer envelope cache</LI>
052: * <LI>invalidates the geometry envelope caches</LI>
053: * <LI>(undoably) removes features from the layer when their geometries are made empty</LI>
054: * <LI>(undoably) adds features to the layer when they start with empty geometries </LI>
055: * </UL></p>
056: */
057: public class EditTransaction {
058: private List features;
059: private List originalGeometries;
060: private List proposedGeometries;
061: private Layer layer;
062: private String name;
063: private boolean rollingBackInvalidEdits;
064:
065: public static final String ROLLING_BACK_INVALID_EDITS_KEY = EditTransaction.class
066: .getName()
067: + " - ROLLING_BACK_INVALID_EDITS";
068:
069: private LayerViewPanelContext layerViewPanelContext;
070:
071: public EditTransaction(Collection features, String name,
072: Layer layer, boolean rollingBackInvalidEdits,
073: boolean allowAddingAndRemovingFeatures,
074: LayerViewPanel layerViewPanel) {
075: this (features, name, layer, rollingBackInvalidEdits,
076: allowAddingAndRemovingFeatures, layerViewPanel
077: .getContext());
078: }
079:
080: /**
081: * If you want to delete a feature, you can either (1) include the feature in
082: * the features parameter, set allowAddingAndRemovingFeatures to true,
083: * then call #setGeometry(feature, empty geometry); or (2) not include the feature in
084: * the features parameter, instead using #deleteFeature
085: * @param name Display name for undo. Use PlugIn#getName or CursorTool#getName.
086: * @param layer the layer to which the features belong
087: * @param allowAddingAndRemovingFeatures whether to treat empty
088: * geometries as indications to add/remove features or as in fact empty geometries
089: */
090: public EditTransaction(Collection features, String name,
091: Layer layer, boolean rollingBackInvalidEdits,
092: boolean allowAddingAndRemovingFeatures,
093: LayerViewPanelContext layerViewPanelContext) {
094: this .layerViewPanelContext = layerViewPanelContext;
095: this .layer = layer;
096: this .rollingBackInvalidEdits = rollingBackInvalidEdits;
097: this .allowAddingAndRemovingFeatures = allowAddingAndRemovingFeatures;
098: this .name = name;
099: this .features = new ArrayList(features);
100:
101: //Clone the Geometries, and don't commit it until we're sure that no errors
102: //occurred. [Jon Aquino]
103: originalGeometries = geometryClones(features);
104: proposedGeometries = geometryClones(features);
105: }
106:
107: public static EditTransaction createTransactionOnSelection(
108: SelectionEditor editor,
109: SelectionManagerProxy selectionManagerProxy,
110: LayerViewPanelContext layerViewPanelContext, String name,
111: Layer layer, boolean rollingBackInvalidEdits,
112: boolean allowAddingAndRemovingFeatures) {
113: Map featureToNewGeometryMap = featureToNewGeometryMap(editor,
114: selectionManagerProxy, layer);
115: EditTransaction transaction = new EditTransaction(
116: featureToNewGeometryMap.keySet(), name, layer,
117: rollingBackInvalidEdits,
118: allowAddingAndRemovingFeatures, layerViewPanelContext);
119: transaction.setGeometries(featureToNewGeometryMap);
120: return transaction;
121: }
122:
123: public static Map featureToNewGeometryMap(SelectionEditor editor,
124: SelectionManagerProxy selectionManagerProxy, Layer layer) {
125: Map featureToNewGeometryMap = new HashMap();
126: for (Iterator i = selectionManagerProxy.getSelectionManager()
127: .getFeaturesWithSelectedItems(layer).iterator(); i
128: .hasNext();) {
129: Feature feature = (Feature) i.next();
130: Geometry newGeometry = (Geometry) feature.getGeometry()
131: .clone();
132: ArrayList selectedItems = new ArrayList();
133: for (Iterator j = selectionManagerProxy
134: .getSelectionManager().getSelections().iterator(); j
135: .hasNext();) {
136: AbstractSelection selection = (AbstractSelection) j
137: .next();
138: //Use #getSelectedItemIndices rather than #getSelectedItems, because
139: //we want the selected items from newGeometry, not the original
140: //Geometry (so that editor can freely modify them). [Jon Aquino]
141: selectedItems
142: .addAll(selection
143: .items(newGeometry, selection
144: .getSelectedItemIndices(layer,
145: feature)));
146: }
147: newGeometry = editor.edit(newGeometry, selectedItems);
148: featureToNewGeometryMap.put(feature, newGeometry);
149: }
150: return featureToNewGeometryMap;
151: }
152:
153: public static interface SelectionEditor {
154: /**
155: * selectedItems may have the whole geometry, parts (collection elements),
156: * or linestrings, or a mix of all three. But there will be no duplicate data
157: * (that is, you can't select both the whole and one of its parts -- only the
158: * whole geometry will be selected; similarly, you can't select a part and
159: * one of its linestrings -- only the part will be selected).
160: * @param geometryWithSelectedItems a clone of the geometry containing the selected items.
161: * Because geometryWithSelectedItems is a clone, feel free to modify it, as no other
162: * parties reference it. Then return it (or return something totally different).
163: * @param selectedItems clones of the selected items (each of which have class Geometry).
164: * selectedItems' elements are "live"; that is, they are objects taken from geometryWithSelectedItems.
165: * So, for example, modifying selectedItem's coordinates will modify geometryWithSelectedItems'
166: * coordinates.
167: * @return a new Geometry for the Feature (typically geometryWithSelectedItems, but can
168: * be a completely different Geometry), or an empty geometry to (undoably) remove the Feature from the Layer
169: */
170: public Geometry edit(Geometry geometryWithSelectedItems,
171: Collection selectedItems);
172: }
173:
174: public Geometry getGeometry(int i) {
175: return (Geometry) proposedGeometries.get(i);
176: }
177:
178: public Geometry getGeometry(Feature feature) {
179: return getGeometry(features.indexOf(feature));
180: }
181:
182: public void setGeometry(Feature feature, Geometry geometry) {
183: setGeometry(features.indexOf(feature), geometry);
184: }
185:
186: public void setGeometries(Map featureToGeometryMap) {
187: for (Iterator i = featureToGeometryMap.keySet().iterator(); i
188: .hasNext();) {
189: Feature feature = (Feature) i.next();
190: setGeometry(feature, (Geometry) featureToGeometryMap
191: .get(feature));
192: }
193: }
194:
195: public void setGeometry(int i, Geometry geometry) {
196: proposedGeometries
197: .set(i, editor.removeRepeatedPoints(geometry));
198: }
199:
200: private GeometryEditor editor = new GeometryEditor();
201:
202: private boolean allowAddingAndRemovingFeatures;
203:
204: public static interface SuccessAction {
205: public void run();
206: }
207:
208: public boolean commit() {
209: return commit(Collections.singleton(this ));
210: }
211:
212: public static boolean commit(Collection editTransactions) {
213: return commit(editTransactions, new SuccessAction() {
214: public void run() {
215: }
216: });
217: }
218:
219: /**
220: * Commits several EditTransactions if their proposed geometries are all valid.
221: * Useful for committing changes to several layers because an EditTransaction
222: * handles one layer only. Gets the undo name and the UndoManager
223: * from the first EditTransaction.
224: * @param successAction run after the first execution (i.e. not after redos) if all
225: * proposed geometries are valid (or rollingBackInvalidEdits is false)
226: */
227: public static boolean commit(Collection editTransactions,
228: SuccessAction successAction) {
229: if (editTransactions.isEmpty()) {
230: return true;
231: }
232: final ArrayList commands = new ArrayList();
233: for (Iterator i = editTransactions.iterator(); i.hasNext();) {
234: EditTransaction editTransaction = (EditTransaction) i
235: .next();
236: editTransaction.clearEnvelopeCaches();
237: if (!editTransaction.proposedGeometriesValid()) {
238: if (editTransaction.rollingBackInvalidEdits) {
239: editTransaction.layerViewPanelContext
240: .warnUser(I18N
241: .get("ui.EditTransaction.the-geometry-is-invalid-cancelled"));
242: return false;
243: } else {
244: editTransaction.layerViewPanelContext
245: .warnUser(I18N
246: .get("ui.EditTransaction.the-new-geometry-is-invalid"));
247: }
248: }
249: commands.add(editTransaction.createCommand());
250: }
251: successAction.run();
252: UndoableCommand command = new UndoableCommand(
253: ((UndoableCommand) commands.iterator().next())
254: .getName()) {
255: public void execute() {
256: for (Iterator i = commands.iterator(); i.hasNext();) {
257: UndoableCommand subCommand = (UndoableCommand) i
258: .next();
259: subCommand.execute();
260: }
261: }
262:
263: public void unexecute() {
264: for (Iterator i = commands.iterator(); i.hasNext();) {
265: UndoableCommand subCommand = (UndoableCommand) i
266: .next();
267: subCommand.unexecute();
268: }
269: }
270: };
271: command.execute();
272: ((EditTransaction) editTransactions.iterator().next()).layer
273: .getLayerManager().getUndoableEditReceiver().receive(
274: command.toUndoableEdit());
275: return true;
276: }
277:
278: /**
279: * @param successAction will be run if the geometries are valid (or
280: * OptionsPlugIn#isRollingBackInvalidEdits returns false), before the layer-change
281: * events are fired. Useful for animations and other visual indicators which would
282: * be slowed down if the layer-change events were fired first.
283: * @return true if all the proposed geometries are valid
284: */
285: public boolean commit(SuccessAction successAction) {
286: return commit(Collections.singleton(this ), successAction);
287: }
288:
289: public void clearEnvelopeCaches() {
290: for (int i = 0; i < proposedGeometries.size(); i++) {
291: Geometry proposedGeometry = (Geometry) proposedGeometries
292: .get(i);
293: //Because the proposedGeometry is a clone, its cached envelope is old.
294: //Invalidate the envelope. [Jon Aquino]
295: proposedGeometry.geometryChanged();
296: }
297: }
298:
299: public boolean proposedGeometriesValid() {
300: for (int i = 0; i < proposedGeometries.size(); i++) {
301: Geometry proposedGeometry = (Geometry) proposedGeometries
302: .get(i);
303: if (!proposedGeometry.isValid()) {
304: return false;
305: }
306: }
307: return true;
308: }
309:
310: protected UndoableCommand createCommand() {
311: UndoableCommand command = new UndoableCommand(name) {
312: public void execute() {
313: changeGeometries(proposedGeometries,
314: originalGeometries, layer);
315: }
316:
317: public void unexecute() {
318: changeGeometries(originalGeometries,
319: proposedGeometries, layer);
320: }
321: };
322: return command;
323: }
324:
325: private List geometryClones(Collection features) {
326: ArrayList geometryClones = new ArrayList();
327:
328: for (Iterator i = features.iterator(); i.hasNext();) {
329: Feature feature = (Feature) i.next();
330: geometryClones.add(feature.getGeometry().clone());
331: }
332:
333: return geometryClones;
334: }
335:
336: /**
337: * @param oldGeometries an empty geometry indicates that we should be re-adding the feature to the layer
338: */
339: private void changeGeometries(List newGeometries,
340: List oldGeometries, Layer layer) {
341: ArrayList modifiedFeatures = new ArrayList();
342: ArrayList modifiedFeaturesOldClones = new ArrayList();
343: ArrayList featuresToAdd = new ArrayList();
344: ArrayList featuresToRemove = new ArrayList();
345: for (int i = 0; i < size(); i++) {
346: Feature feature = (Feature) features.get(i);
347: Geometry oldGeometry = (Geometry) oldGeometries.get(i);
348: Geometry newGeometry = (Geometry) newGeometries.get(i);
349: if (allowAddingAndRemovingFeatures && oldGeometry.isEmpty()
350: && !newGeometry.isEmpty()) {
351: featuresToAdd.add(feature);
352: } else if (allowAddingAndRemovingFeatures
353: && newGeometry.isEmpty() && !oldGeometry.isEmpty()) {
354: featuresToRemove.add(feature);
355: } else {
356: modifiedFeatures.add(feature);
357: modifiedFeaturesOldClones.add(feature.clone());
358: feature.setGeometry(newGeometry);
359: }
360: }
361:
362: Layer.tryToInvalidateEnvelope(layer);
363: //Important to fire the feature-removed event first (before the feature-added
364: //and feature-modified events) so that any selections that need to be cleared
365: //get cleared. [Jon Aquino]
366: if (!featuresToRemove.isEmpty()) {
367: layer.getFeatureCollectionWrapper().removeAll(
368: featuresToRemove);
369: }
370: if (!featuresToAdd.isEmpty()) {
371: layer.getFeatureCollectionWrapper().addAll(featuresToAdd);
372: }
373: if (!modifiedFeatures.isEmpty()) {
374: layer.getLayerManager().fireGeometryModified(
375: modifiedFeatures, layer, modifiedFeaturesOldClones);
376: }
377: }
378:
379: public int size() {
380: return features.size();
381: }
382:
383: public Feature getFeature(int i) {
384: return (Feature) features.get(i);
385: }
386:
387: public void createFeature(Feature feature) {
388: Assert.isTrue(allowAddingAndRemovingFeatures);
389: Assert.isTrue(!features.contains(feature));
390: features.add(feature);
391: originalGeometries.add(new Point(null, null, 0));
392: proposedGeometries.add(feature.getGeometry().clone());
393: }
394:
395: /**
396: * @param feature must not have been passed into the constructor
397: */
398: public void deleteFeature(Feature feature) {
399: Assert.isTrue(allowAddingAndRemovingFeatures);
400: Assert.isTrue(!features.contains(feature));
401: features.add(feature);
402: originalGeometries.add(feature.getGeometry().clone());
403: proposedGeometries.add(new Point(null, null, 0));
404: }
405:
406: public Layer getLayer() {
407: return layer;
408: }
409:
410: public static int emptyGeometryCount(Collection transactions) {
411: int count = 0;
412: for (Iterator i = transactions.iterator(); i.hasNext();) {
413: EditTransaction transaction = (EditTransaction) i.next();
414: count += transaction.getEmptyGeometryCount();
415: }
416: return count;
417: }
418:
419: private int getEmptyGeometryCount() {
420: int count = 0;
421: for (int i = 0; i < size(); i++) {
422: if (getGeometry(i).isEmpty()) {
423: count++;
424: }
425: }
426: return count;
427: }
428: }
|