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.plugin.analysis;
034:
035: import java.awt.event.ActionEvent;
036: import java.awt.event.ActionListener;
037:
038: import java.util.ArrayList;
039: import java.util.Collection;
040: import java.util.HashMap;
041: import java.util.Iterator;
042: import java.util.List;
043: import java.util.Map;
044: import java.util.Set;
045:
046: import javax.swing.DefaultComboBoxModel;
047: import javax.swing.JCheckBox;
048: import javax.swing.JComboBox;
049:
050: import com.vividsolutions.jts.geom.*;
051: import com.vividsolutions.jts.geom.util.*;
052: import com.vividsolutions.jts.operation.linemerge.LineMerger;
053:
054: import com.vividsolutions.jump.I18N;
055: import com.vividsolutions.jump.feature.AttributeType;
056: import com.vividsolutions.jump.feature.BasicFeature;
057: import com.vividsolutions.jump.feature.Feature;
058: import com.vividsolutions.jump.feature.FeatureCollection;
059: import com.vividsolutions.jump.feature.FeatureDataset;
060: import com.vividsolutions.jump.feature.FeatureDatasetFactory;
061: import com.vividsolutions.jump.feature.FeatureSchema;
062: import com.vividsolutions.jump.task.TaskMonitor;
063: import com.vividsolutions.jump.workbench.model.Layer;
064: import com.vividsolutions.jump.workbench.model.StandardCategoryNames;
065: import com.vividsolutions.jump.workbench.plugin.AbstractPlugIn;
066: import com.vividsolutions.jump.workbench.plugin.PlugInContext;
067: import com.vividsolutions.jump.workbench.plugin.ThreadedPlugIn;
068: import com.vividsolutions.jump.workbench.ui.GUIUtil;
069: import com.vividsolutions.jump.workbench.ui.MultiInputDialog;
070: import com.vividsolutions.jump.workbench.ui.images.IconLoader;
071:
072: /**
073: * UnionByAttribute plugin is used to perform selective unions based on attribute values.
074: * All the features having a same attribute value are unioned together.<br>
075: * An option can be used to eliminate null or empty attribute values. Useful to merge,
076: * for example, all named rivers, but not unnamed one, even if they are in the same layer.<br>
077: * An other option makes it possible to total numeric fields of unioned features.<br>
078: */
079: public class UnionByAttributePlugIn extends AbstractPlugIn implements
080: ThreadedPlugIn {
081:
082: private final static String LAYER = I18N
083: .get("ui.plugin.analysis.UnionByAttributePlugIn.layer");
084: private final static String ATTRIBUTE = I18N
085: .get("ui.plugin.analysis.UnionByAttributePlugIn.attribute");
086: private final static String IGNORE_EMPTY = I18N
087: .get("ui.plugin.analysis.UnionByAttributePlugIn.ignore-empty");
088: private final static String MERGE_LINES = I18N
089: .get("ui.plugin.analysis.UnionByAttributePlugIn.merge-lines");
090: private final static String TOTAL_NUMERIC_FIELDS = I18N
091: .get("ui.plugin.analysis.UnionByAttributePlugIn.total-numeric-fields");
092:
093: private MultiInputDialog dialog;
094:
095: private GeometryFactory fact;
096:
097: public UnionByAttributePlugIn() {
098: }
099:
100: public String getName() {
101: return I18N
102: .get("ui.plugin.analysis.UnionByAttributePlugIn.union-by-attribute");
103: }
104:
105: /*
106: public void initialize(PlugInContext context) throws Exception {
107: context.getFeatureInstaller().addMainMenuItem(
108: this, "Tools", "Find Unaligned Segments...", null, new MultiEnableCheck()
109: .add(context.getCheckFactory().createWindowWithLayerNamePanelMustBeActiveCheck())
110: .add(context.getCheckFactory().createAtLeastNLayersMustExistCheck(1)));
111: }
112: */
113:
114: public boolean execute(PlugInContext context) throws Exception {
115:
116: initDialog(context);
117: dialog.setVisible(true);
118:
119: if (!dialog.wasOKPressed()) {
120: return false;
121: }
122:
123: return true;
124: }
125:
126: private void initDialog(PlugInContext context) {
127:
128: dialog = new MultiInputDialog(
129: context.getWorkbenchFrame(),
130: I18N
131: .get("ui.plugin.analysis.UnionByAttributePlugIn.union-by-attribute"),
132: true);
133:
134: dialog.setSideBarImage(IconLoader.icon("UnionByAttribute.gif"));
135:
136: dialog
137: .setSideBarDescription(I18N
138: .get("ui.plugin.analysis.UnionByAttributePlugIn.creates-a-new-layer-containing-the-unions-of-features-having-a-common-attribute-value"));
139:
140: dialog.addLayerComboBox(LAYER, context.getCandidateLayer(0),
141: context.getLayerManager());
142:
143: List list = getFieldsFromLayerWithoutGeometry(context
144: .getCandidateLayer(0));
145: Object val = list.size() > 0 ? list.iterator().next() : null;
146: final JComboBox jcb_attribute = dialog
147: .addComboBox(
148: ATTRIBUTE,
149: val,
150: list,
151: I18N
152: .get("ui.plugin.analysis.UnionByAttributePlugIn.select-attribute"));
153: if (list.size() == 0)
154: jcb_attribute.setEnabled(false);
155:
156: final JCheckBox jcb_ignore_empty = dialog.addCheckBox(
157: IGNORE_EMPTY, true);
158: if (list.size() == 0)
159: jcb_ignore_empty.setEnabled(false);
160:
161: dialog.addSeparator();
162: final JCheckBox jcb_merge_lines = dialog.addCheckBox(
163: MERGE_LINES, true);
164: final JCheckBox jcb_total_numeric_fields = dialog.addCheckBox(
165: TOTAL_NUMERIC_FIELDS, true);
166:
167: dialog.getComboBox(LAYER).addActionListener(
168: new ActionListener() {
169: public void actionPerformed(ActionEvent e) {
170: List list = getFieldsFromLayerWithoutGeometry();
171: if (list.size() == 0) {
172: jcb_attribute
173: .setModel(new DefaultComboBoxModel(
174: new String[0]));
175: jcb_attribute.setEnabled(false);
176: jcb_ignore_empty.setEnabled(false);
177: }
178: jcb_attribute
179: .setModel(new DefaultComboBoxModel(list
180: .toArray(new String[0])));
181: }
182: });
183:
184: GUIUtil.centreOnWindow(dialog);
185: }
186:
187: public void run(TaskMonitor monitor, PlugInContext context)
188: throws Exception {
189:
190: monitor.allowCancellationRequests();
191:
192: // Get options from the dialog
193: Layer layer = dialog.getLayer(LAYER);
194: FeatureCollection fc = layer.getFeatureCollectionWrapper();
195: FeatureSchema schema = fc.getFeatureSchema();
196: String att = dialog.getText(ATTRIBUTE);
197: boolean ignore_empty = dialog.getBoolean(IGNORE_EMPTY);
198: boolean merge_lines = dialog.getBoolean(MERGE_LINES);
199: boolean total_numeric_fields = dialog
200: .getBoolean(TOTAL_NUMERIC_FIELDS);
201:
202: if (fc.getFeatures().size() > 0
203: && ((Feature) fc.getFeatures().get(0)).getGeometry() != null) {
204: fact = ((Feature) fc.getFeatures().get(0)).getGeometry()
205: .getFactory();
206: } else {
207: context
208: .getWorkbenchFrame()
209: .warnUser(
210: I18N
211: .get("ui.plugin.analysis.UnionByAttributePlugIn.no-data-to-be-unioned"));
212: return;
213: }
214:
215: // Create the schema for the output dataset
216: FeatureSchema newSchema = new FeatureSchema();
217: newSchema.addAttribute("GEOMETRY", AttributeType.GEOMETRY);
218: newSchema.addAttribute(att, schema.getAttributeType(att));
219: if (total_numeric_fields) {
220: for (int i = 0, max = schema.getAttributeCount(); i < max; i++) {
221: if (schema.getAttributeType(i) == AttributeType.INTEGER
222: || schema.getAttributeType(i) == AttributeType.DOUBLE)
223: newSchema.addAttribute(schema.getAttributeName(i),
224: schema.getAttributeType(i));
225: }
226: }
227:
228: // Order features by attribute value in a map
229: Map map = new HashMap();
230: monitor
231: .report(I18N
232: .get("ui.plugin.analysis.UnionByAttributePlugIn.union-by-attribute"));
233: //monitor.report(-1, -1, I18N.get("ui.plugin.analysis.UnionByAttributePlugIn.sorting"));
234: for (Iterator i = fc.iterator(); i.hasNext();) {
235: Feature f = (Feature) i.next();
236: Object key = f.getAttribute(att);
237: if (ignore_empty
238: && (key == null || key.toString().trim().length() == 0)) {
239: continue;
240: } else if (!map.containsKey(key)) {
241: FeatureCollection fd = new FeatureDataset(fc
242: .getFeatureSchema());
243: fd.add(f);
244: map.put(key, fd);
245: } else {
246: ((FeatureCollection) map.get(key)).add(f);
247: }
248: }
249:
250: // Computing the result
251: int count = 1;
252: FeatureCollection resultfc = new FeatureDataset(newSchema);
253: for (Iterator i = map.keySet().iterator(); i.hasNext();) {
254: monitor
255: .report(I18N
256: .get("ui.plugin.analysis.UnionByAttributePlugIn.union-by-attribute")
257: + " (" + count++ + "/" + map.size() + ")");
258: Object key = i.next();
259: FeatureCollection fca = (FeatureCollection) map.get(key);
260: if (fca.size() > 0) {
261: Feature feature = union(monitor, fca, merge_lines,
262: total_numeric_fields);
263: feature.setAttribute(att, key);
264: Feature newFeature = new BasicFeature(newSchema);
265: for (int j = 0, max = newSchema.getAttributeCount(); j < max; j++) {
266: newFeature.setAttribute(j,
267: feature.getAttribute(newSchema
268: .getAttributeName(j)));
269: }
270: resultfc.add(newFeature);
271: }
272: }
273: context.getLayerManager().addCategory(
274: StandardCategoryNames.RESULT);
275: context.addLayer(StandardCategoryNames.RESULT, layer.getName()
276: + "-" + att + " (union)", resultfc);
277: }
278:
279: /**
280: * New method for union. Instead of the naive algorithm looping over the features and
281: * unioning each time, this one union small groups of features which are closed to each
282: * other, then iterates over the result.
283: * The difference is not so important for small datasets, but for large datasets, the
284: * difference may of 5 minutes versus 5 hours.
285: */
286: private Feature union(TaskMonitor monitor, FeatureCollection fc,
287: boolean merge_lines, boolean total) {
288: List[] geometries = getGeometries(fc.iterator());
289: List polygons = geometries[2];
290: List lines = geometries[1];
291: List points = geometries[0];
292:
293: Geometry pointsUnion = fact.buildGeometry(new ArrayList());
294: Geometry lineStringsUnion = fact.buildGeometry(new ArrayList());
295: Geometry polygonsUnion = fact.buildGeometry(new ArrayList());
296:
297: // Union Points
298: if (points.size() > 0) {
299: pointsUnion = fact.createMultiPoint((Point[]) points
300: .toArray(new Point[0]));
301: }
302:
303: // Union LineString
304: if (lines.size() > 0) {
305: Geometry multiLineGeom = fact.createMultiLineString(fact
306: .toLineStringArray(lines));
307: Geometry unionInput = fact.createMultiLineString(null);
308: Geometry minLine = extractPoint(lines);
309: if (minLine != null)
310: unionInput = minLine;
311: lineStringsUnion = multiLineGeom.union(unionInput);
312: if (merge_lines) {
313: lineStringsUnion = mergeLines(lineStringsUnion);
314: }
315: }
316:
317: // Union Polygons
318: if (polygons.size() > 0) {
319: int iteration = 1;
320: int nbIteration = 1 + (int) (Math.log(polygons.size()) / Math
321: .log(4));
322: while (polygons.size() > 1) {
323: monitor
324: .report(
325: iteration++,
326: nbIteration,
327: I18N
328: .get("ui.plugin.analysis.UnionByAttributePlugIn.union-by-attribute"));
329: final int cellSize = 1 + (int) Math.sqrt(polygons
330: .size());
331: java.util.Comparator comparator = new java.util.Comparator() {
332: public int compare(Object o1, Object o2) {
333: if (o1 == null || o2 == null)
334: return 0;
335: Envelope env1 = ((Geometry) o1)
336: .getEnvelopeInternal();
337: Envelope env2 = ((Geometry) o2)
338: .getEnvelopeInternal();
339: double indice1 = env1.getMinX() / cellSize
340: + cellSize
341: * ((int) env1.getMinY() / cellSize);
342: double indice2 = env2.getMinX() / cellSize
343: + cellSize
344: * ((int) env2.getMinY() / cellSize);
345: // Bug fixed on 2007-06-27 : must never return 0
346: return indice1 >= indice2 ? 1
347: : indice1 < indice2 ? -1 : 0;
348: }
349:
350: public boolean equals(Object obj) {
351: return this .equals(obj);
352: }
353: };
354: java.util.TreeSet treeSet = new java.util.TreeSet(
355: comparator);
356: treeSet.addAll(polygons);
357: // Testes with groups of 4, 8 and 16 (4 is better than 8 which is better than 16
358: // for large datasets).
359: polygons = union(monitor, treeSet, 4);
360: }
361: }
362: if (polygons.size() > 0)
363: polygonsUnion = (Geometry) polygons.get(0);
364: Geometry union;
365: if (polygonsUnion.isEmpty()) {
366: if (lineStringsUnion.isEmpty()) {
367: if (pointsUnion.isEmpty())
368: union = fact.buildGeometry(new ArrayList());
369: else
370: union = pointsUnion;
371: } else if (pointsUnion.isEmpty())
372: union = lineStringsUnion;
373: else
374: union = lineStringsUnion.union(pointsUnion);
375: } else if (lineStringsUnion.isEmpty()) {
376: if (pointsUnion.isEmpty())
377: union = polygonsUnion;
378: else
379: union = polygonsUnion.union(pointsUnion);
380: } else {
381: if (pointsUnion.isEmpty())
382: union = polygonsUnion.union(lineStringsUnion);
383: // Can't union poly + linestring + points, because the intermediate result
384: // is GeometryCollection which is non unionable
385: //else union = polygonsUnion.union(lineStringsUnion).union(pointsUnion);
386: else
387: union = polygonsUnion.union(lineStringsUnion);
388: }
389:
390: FeatureSchema schema = fc.getFeatureSchema();
391: Feature feature = new BasicFeature(schema);
392: feature.setGeometry(union);
393: if (total) {
394: feature = totalNumericValues(fc, feature);
395: }
396: return feature;
397: }
398:
399: /**
400: * Method unioning an ordered set of geometries by small groups.
401: */
402: private List union(TaskMonitor monitor, Set set, int groupSize) {
403: List unionGeometryList = new ArrayList();
404: Geometry currUnion = null;
405: int size = set.size();
406: int count = 0;
407: for (Iterator i = set.iterator(); i.hasNext();) {
408: Geometry geom = (Geometry) i.next();
409: if (count % groupSize == 0)
410: currUnion = geom;
411: else {
412: currUnion = currUnion.union(geom);
413: if (groupSize - count % groupSize == 1)
414: unionGeometryList.add(currUnion);
415: }
416: count++;
417: }
418: if (groupSize - count % groupSize != 0) {
419: unionGeometryList.add(currUnion);
420: }
421: return unionGeometryList;
422: }
423:
424: // Set the sum of fc collection numeric attribute values into feature numeric attributes.
425: private Feature totalNumericValues(FeatureCollection fc,
426: Feature feature) {
427: FeatureSchema schema = fc.getFeatureSchema();
428: for (Iterator it = fc.iterator(); it.hasNext();) {
429: Feature f = (Feature) it.next();
430: for (int i = 0, max = schema.getAttributeCount(); i < max; i++) {
431: if (schema.getAttributeType(i) == AttributeType.INTEGER) {
432: Object val = feature.getAttribute(i);
433: int val1 = (val == null) ? 0 : ((Integer) val)
434: .intValue();
435: val = f.getAttribute(i);
436: val1 = (val == null) ? val1 : val1
437: + ((Integer) val).intValue();
438: feature.setAttribute(i, new Integer(val1));
439: } else if (schema.getAttributeType(i) == AttributeType.DOUBLE) {
440: Object val = feature.getAttribute(i);
441: double val1 = (val == null) ? 0 : ((Double) val)
442: .doubleValue();
443: val = f.getAttribute(i);
444: val1 = (val == null) ? val1 : val1
445: + ((Double) val).doubleValue();
446: feature.setAttribute(i, new Double(val1));
447: }
448: }
449: }
450: return feature;
451: }
452:
453: private List[] getGeometries(Iterator featureIterator) {
454: List[] lists = new ArrayList[] { new ArrayList(),
455: new ArrayList(), new ArrayList() };
456: for (Iterator i = featureIterator; i.hasNext();) {
457: Geometry g = (Geometry) ((Feature) i.next()).getGeometry();
458: if (g instanceof Point)
459: lists[0].add(g);
460: else if (g instanceof LineString)
461: lists[1].add(g);
462: else if (g instanceof Polygon)
463: lists[2].add(g);
464: else if (g instanceof GeometryCollection) {
465: Geometry gc = (GeometryCollection) g;
466: for (int j = 0; j < gc.getNumGeometries(); j++) {
467: Geometry gp = gc.getGeometryN(j);
468: if (gp instanceof Point)
469: lists[0].add(gp);
470: else if (gp instanceof LineString)
471: lists[1].add(gp);
472: else if (gp instanceof Polygon)
473: lists[2].add(gp);
474: else
475: ;
476: }
477: }
478: }
479: return lists;
480: }
481:
482: private Geometry mergeLines(Geometry g) {
483: List linesList = new ArrayList();
484: LinearComponentExtracter lineFilter = new LinearComponentExtracter(
485: linesList);
486: g.apply(lineFilter);
487: LineMerger merger = new LineMerger();
488: merger.add(linesList);
489: return fact.buildGeometry(merger.getMergedLineStrings());
490: }
491:
492: private Geometry extractPoint(Collection lines) {
493: int minPts = Integer.MAX_VALUE;
494: Geometry point = null;
495: // extract first point from first non-empty geometry
496: for (Iterator i = lines.iterator(); i.hasNext();) {
497: Geometry g = (Geometry) i.next();
498: if (!g.isEmpty()) {
499: Coordinate p = g.getCoordinate();
500: point = g.getFactory().createPoint(p);
501: }
502: }
503: return point;
504: }
505:
506: private List getFieldsFromLayerWithoutGeometry(Layer lyr) {
507: List fields = new ArrayList();
508: FeatureSchema schema = lyr.getFeatureCollectionWrapper()
509: .getFeatureSchema();
510: for (int i = 0; i < schema.getAttributeCount(); i++) {
511: if (schema.getAttributeType(i) != AttributeType.GEOMETRY) {
512: fields.add(schema.getAttributeName(i));
513: }
514: }
515: return fields;
516: }
517:
518: private List getFieldsFromLayerWithoutGeometry() {
519: return getFieldsFromLayerWithoutGeometry(dialog.getLayer(LAYER));
520: }
521:
522: }
|