1 |
/******************************************************************************* |
2 |
* Copyright (c) 2009 Martin O. J. Schmitz. |
3 |
* |
4 |
* This file is part of the SCHMITZM library - a collection of utility |
5 |
* classes based on Java 1.6, focusing (not only) on Java Swing |
6 |
* and the Geotools library. |
7 |
* |
8 |
* The SCHMITZM project is hosted at: |
9 |
* http://wald.intevation.org/projects/schmitzm/ |
10 |
* |
11 |
* This program is free software; you can redistribute it and/or |
12 |
* modify it under the terms of the GNU Lesser General Public License |
13 |
* as published by the Free Software Foundation; either version 3 |
14 |
* of the License, or (at your option) any later version. |
15 |
* |
16 |
* This program is distributed in the hope that it will be useful, |
17 |
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
18 |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
19 |
* GNU General Public License for more details. |
20 |
* |
21 |
* You should have received a copy of the GNU Lesser General Public License (license.txt) |
22 |
* along with this program; if not, write to the Free Software |
23 |
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
24 |
* or try this link: http://www.gnu.org/licenses/lgpl.html |
25 |
* |
26 |
* Contributors: |
27 |
* Martin O. J. Schmitz - initial API and implementation |
28 |
* Stefan A. Krüger - additional utility classes |
29 |
******************************************************************************/ |
30 |
/** |
31 |
Copyright 2008 Stefan Alfons Krüger |
32 |
|
33 |
atlas-framework - This file is part of the Atlas Framework |
34 |
|
35 |
This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. |
36 |
This library 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 Lesser General Public License for more details. |
37 |
You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110, USA |
38 |
|
39 |
Diese Bibliothek ist freie Software; Sie dürfen sie unter den Bedingungen der GNU Lesser General Public License, wie von der Free Software Foundation veröffentlicht, weiterverteilen und/oder modifizieren; entweder gemäß Version 2.1 der Lizenz oder (nach Ihrer Option) jeder späteren Version. |
40 |
Diese Bibliothek wird in der Hoffnung weiterverbreitet, daß sie nützlich sein wird, jedoch OHNE IRGENDEINE GARANTIE, auch ohne die implizierte Garantie der MARKTREIFE oder der VERWENDBARKEIT FÜR EINEN BESTIMMTEN ZWECK. Mehr Details finden Sie in der GNU Lesser General Public License. |
41 |
Sie sollten eine Kopie der GNU Lesser General Public License zusammen mit dieser Bibliothek erhalten haben; falls nicht, schreiben Sie an die Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110, USA. |
42 |
**/ |
43 |
package skrueger.geotools.selection; |
44 |
|
45 |
import java.awt.RenderingHints; |
46 |
import java.beans.PropertyChangeEvent; |
47 |
import java.beans.PropertyChangeListener; |
48 |
import java.io.File; |
49 |
import java.util.HashMap; |
50 |
import java.util.HashSet; |
51 |
import java.util.Iterator; |
52 |
import java.util.Set; |
53 |
import java.util.Vector; |
54 |
|
55 |
import javax.swing.JTable; |
56 |
import javax.swing.ListSelectionModel; |
57 |
import javax.swing.event.ListSelectionListener; |
58 |
|
59 |
import org.geotools.feature.FeatureCollection; |
60 |
import org.geotools.feature.FeatureIterator; |
61 |
import org.geotools.filter.FidFilterImpl; |
62 |
import org.geotools.map.MapLayer; |
63 |
import org.geotools.renderer.GTRenderer; |
64 |
import org.geotools.renderer.label.LabelCacheImpl; |
65 |
import org.geotools.renderer.lite.StreamingRenderer; |
66 |
import org.geotools.styling.FeatureTypeStyle; |
67 |
import org.geotools.styling.Style; |
68 |
import org.geotools.styling.visitor.DuplicatingStyleVisitor; |
69 |
import org.opengis.feature.simple.SimpleFeature; |
70 |
import org.opengis.feature.simple.SimpleFeatureType; |
71 |
import org.opengis.filter.Filter; |
72 |
import org.opengis.filter.Not; |
73 |
import org.opengis.filter.identity.FeatureId; |
74 |
|
75 |
import schmitzm.geotools.FilterUtil; |
76 |
import schmitzm.geotools.GTUtil; |
77 |
import schmitzm.geotools.gui.SelectableXMapPane; |
78 |
import schmitzm.geotools.map.event.FeatureSelectedEvent; |
79 |
import schmitzm.geotools.map.event.JMapPaneListener; |
80 |
import schmitzm.geotools.map.event.MapPaneEvent; |
81 |
import schmitzm.geotools.styling.StylingUtil; |
82 |
import skrueger.geotools.MapPaneToolBar; |
83 |
import skrueger.geotools.StyledFeaturesInterface; |
84 |
|
85 |
/** |
86 |
* This class keeps the selection of a (feature) {@link JTable} synchronized |
87 |
* with the {@link StyledLayerSelectionModel} of a layer. This is done by |
88 |
* implementing: |
89 |
* <ul> |
90 |
* <li>a {@link PropertyChangeListener} which listens to the |
91 |
* {@link StyledLayerSelectionModel} and accordingly changes the {@link JTable} |
92 |
* selection</li> |
93 |
* <li>a {@link ListSelectionListener} which listens to the {@link JTable} and |
94 |
* accordingly changes the {@link StyledLayerSelectionModel} selection</li> |
95 |
* </ul> |
96 |
* After creating, the instance of this synchronizer must be added as listener |
97 |
* to both, the {@link StyledLayerSelectionModel} and the table's |
98 |
* {@link ListSelectionModel}. |
99 |
* |
100 |
* @author <a href="mailto:[email protected]">Martin Schmitz</a> |
101 |
* (University of Bonn/Germany) |
102 |
*/ |
103 |
public class FeatureMapLayerSelectionSynchronizer extends |
104 |
StyledLayerSelectionModelSynchronizer<StyledFeatureLayerSelectionModel> |
105 |
implements JMapPaneListener { |
106 |
|
107 |
/** |
108 |
* This constant is set as the {@link FeatureTypeStyle#getName()} attribute |
109 |
* in the {@link FeatureTypeStyle}s that only exist to present the selected |
110 |
* features |
111 |
**/ |
112 |
public static final String SELECTION_STYLING_FTS_NAME = "SELECTION"; |
113 |
|
114 |
/** |
115 |
* Holds the {@link MapLayer} to keep synchronized with the layer selection |
116 |
* model. |
117 |
*/ |
118 |
protected final MapLayer mapLayer; |
119 |
protected final StyledFeaturesInterface<?> styledLayer; |
120 |
protected final SelectableXMapPane mapPane; |
121 |
private final MapPaneToolBar toolBar; |
122 |
|
123 |
/** |
124 |
* Creates a new synchronizer |
125 |
* |
126 |
* @param layerSelModel |
127 |
* layer selection model to keep synchronized with the |
128 |
* {@link MapLayer} |
129 |
* |
130 |
* @param mapLayer |
131 |
* {@link MapLayer} to keep synchronized with. |
132 |
*/ |
133 |
public FeatureMapLayerSelectionSynchronizer( |
134 |
StyledFeatureLayerSelectionModel layerSelModel, |
135 |
StyledFeaturesInterface<?> styledLayer, MapLayer mapLayer, |
136 |
SelectableXMapPane mapPane, MapPaneToolBar toolBar) { |
137 |
|
138 |
super(layerSelModel); |
139 |
this.styledLayer = styledLayer; |
140 |
|
141 |
this.mapLayer = mapLayer; |
142 |
this.mapPane = mapPane; |
143 |
this.toolBar = toolBar; |
144 |
} |
145 |
|
146 |
/** |
147 |
* Called by {@link StyledLayerSelectionModel} when a the selection on other |
148 |
* selection components (map, chart, ...) has changed. When calling this |
149 |
* |
150 |
* method changes the {@link MapLayer} selection according to the |
151 |
* {@link StyledLayerSelectionModel} selection. |
152 |
* |
153 |
* @param evt |
154 |
* an event |
155 |
*/ |
156 |
@Override |
157 |
public void propertyChange(PropertyChangeEvent evt) { |
158 |
|
159 |
if (!isEnabled()) |
160 |
return; |
161 |
|
162 |
if (!(evt instanceof StyledLayerSelectionEvent)) |
163 |
return; |
164 |
StyledLayerSelectionEvent selEvt = (StyledLayerSelectionEvent) evt; |
165 |
// Only react on own layer and ignore events invoked by |
166 |
// this component itself |
167 |
if (selEvt.getEmitter() != layerSelModel || selectionChangeCausedByMe) |
168 |
return; |
169 |
// Apply new selection on table (except this event is one of |
170 |
// some more events) |
171 |
Vector<String> newSelection = layerSelModel.getSelection(); |
172 |
if (newSelection == null) |
173 |
return; |
174 |
|
175 |
// Avoid event circles in valueChanged(..) |
176 |
selectionChangeCausedByMe = true; |
177 |
|
178 |
changeLayerStyle(newSelection); |
179 |
|
180 |
// Danger of event circles in valueChanged(..) banned |
181 |
selectionChangeCausedByMe = false; |
182 |
} |
183 |
|
184 |
/** |
185 |
* Changes the Style of the {@link MapLayer} to reflect changes of the |
186 |
* selection. |
187 |
* |
188 |
* @param newSelection |
189 |
* A {@link Vector} of SimpleFeature-IDs that are selected. |
190 |
*/ |
191 |
private void changeLayerStyle(final Vector<String> newSelection) { |
192 |
try { |
193 |
|
194 |
Style originalStyle = mapLayer.getStyle(); |
195 |
|
196 |
if (newSelection.isEmpty()) { |
197 |
|
198 |
// Check if the Style contains a SELECTION FTS |
199 |
|
200 |
FeatureTypeStyle[] clone = originalStyle.featureTypeStyles() |
201 |
.toArray(new FeatureTypeStyle[] {}).clone(); |
202 |
|
203 |
for (FeatureTypeStyle fts : clone) { |
204 |
if (fts.getName() != null |
205 |
&& fts.getName().equals(SELECTION_STYLING_FTS_NAME)) { |
206 |
originalStyle.featureTypeStyles().remove(fts); |
207 |
|
208 |
mapLayer.setStyle(originalStyle); |
209 |
// replaceRenderer(); |
210 |
// // mapPane.refresh(); |
211 |
|
212 |
return; |
213 |
} |
214 |
} |
215 |
|
216 |
} else { |
217 |
|
218 |
LOGGER.debug("SELECTION .. change style"); |
219 |
|
220 |
// Saving a repaint if the selection didn't change |
221 |
if (!selectionChanged(newSelection, originalStyle)) { |
222 |
return; |
223 |
} |
224 |
|
225 |
// We take Style from the MapLayer that is displayed at the |
226 |
// moment. We do not use the styledLayer.getStyle, because in |
227 |
// the atlas, this always return the default style, but |
228 |
// additional styles might be selected. |
229 |
// Taking the style from the mapLayer indicated, that we have to |
230 |
// remove any selection rules first. |
231 |
|
232 |
FeatureTypeStyle selectionFTStyle = StylingUtil |
233 |
.createSelectionStyle(styledLayer.getGeoObject()); |
234 |
|
235 |
selectionFTStyle.setName(SELECTION_STYLING_FTS_NAME); |
236 |
|
237 |
/** |
238 |
* |
239 |
* Add a Filter to the selectionMapStyle, so that it is only |
240 |
* used on objects that are selected. <br/> |
241 |
* To optimize rendering speed, the filter checks whether more |
242 |
* than half of the features are selected. If so, the filter is |
243 |
* inverted to be shorter. |
244 |
* |
245 |
*/ |
246 |
final FeatureCollection<SimpleFeatureType, SimpleFeature> featureCollectionFiltered = styledLayer |
247 |
.getFeatureCollectionFiltered(); |
248 |
if (newSelection.size() > featureCollectionFiltered.size() / 2) { |
249 |
FeatureIterator<SimpleFeature> iterator = featureCollectionFiltered |
250 |
.features(); |
251 |
Set<FeatureId> antiFids = new HashSet<FeatureId>(); |
252 |
try { |
253 |
while (iterator.hasNext()) { |
254 |
SimpleFeature next = iterator.next(); |
255 |
if (!newSelection.contains(next.getID())) |
256 |
antiFids.add(FilterUtil.FILTER_FAC2 |
257 |
.featureId(next.getID())); |
258 |
} |
259 |
|
260 |
selectionFTStyle.rules().get(0).setFilter( |
261 |
FilterUtil.FILTER_FAC2 |
262 |
.not(FilterUtil.FILTER_FAC2 |
263 |
.id(antiFids))); |
264 |
|
265 |
} finally { |
266 |
featureCollectionFiltered.close(iterator); |
267 |
} |
268 |
} else { |
269 |
Set<FeatureId> fids = new HashSet<FeatureId>(); |
270 |
for (String fid : newSelection) { |
271 |
fids.add(FilterUtil.FILTER_FAC2.featureId(fid)); |
272 |
} |
273 |
|
274 |
selectionFTStyle.rules().get(0).setFilter( |
275 |
FilterUtil.FILTER_FAC2.id(fids)); |
276 |
} |
277 |
|
278 |
// Maybe there has already been another selection |
279 |
// FeatureTypeStyle... Let's replace it... |
280 |
boolean foundAndReplaced = false; |
281 |
for (FeatureTypeStyle fts : originalStyle.featureTypeStyles()) { |
282 |
if (fts.getName() != null |
283 |
&& fts.getName().equals(SELECTION_STYLING_FTS_NAME)) { |
284 |
foundAndReplaced = true; |
285 |
fts.rules().clear(); |
286 |
fts.rules().addAll(selectionFTStyle.rules()); |
287 |
break; |
288 |
} |
289 |
} |
290 |
if (!foundAndReplaced) { |
291 |
originalStyle.featureTypeStyles().add(selectionFTStyle); |
292 |
} |
293 |
|
294 |
// Refresh the map |
295 |
// mapPane.refresh(); |
296 |
|
297 |
DuplicatingStyleVisitor dsv = new DuplicatingStyleVisitor(); |
298 |
dsv.visit(originalStyle); |
299 |
Style newStyle = (Style) dsv.getCopy(); |
300 |
|
301 |
// SK-Debug |
302 |
try { |
303 |
// |
304 |
StylingUtil.saveStyleToSLD(newStyle, new File( |
305 |
"/home/stefan/Desktop/selection.sld")); |
306 |
} catch (Exception e) { |
307 |
} |
308 |
|
309 |
// DuplicatingStyleVisitor dsv = new DuplicatingStyleVisitor(); |
310 |
// dsv.visit(originalStyle); |
311 |
// Style newStyle = originalStyle; |
312 |
|
313 |
// Style newStyle = StylingUtil.STYLE_BUILDER.createStyle(); |
314 |
// newStyle.featureTypeStyles().addAll(originalStyle.featureTypeStyles()); |
315 |
mapLayer.setStyle(newStyle); |
316 |
|
317 |
replaceRenderer(); |
318 |
} |
319 |
|
320 |
} catch (Exception e) { |
321 |
LOGGER.error("Error while trying to create a selection style", e); |
322 |
} |
323 |
} |
324 |
|
325 |
/** |
326 |
* Analyses whether the selection has changed in comparison to the selection |
327 |
* stored in the mapLayer.Style |
328 |
* |
329 |
* @param newSelection |
330 |
* @param originalStyle |
331 |
* @return |
332 |
*/ |
333 |
private boolean selectionChanged(Vector<String> newSelection, |
334 |
Style originalStyle) { |
335 |
|
336 |
boolean SELECTION_STYLING_foundInMapStyle = false; |
337 |
|
338 |
/** |
339 |
* testing, whether the selection really changed. If not, we can save |
340 |
* one rendering! |
341 |
*/ |
342 |
for (FeatureTypeStyle fts : originalStyle.featureTypeStyles()) { |
343 |
|
344 |
if (fts.getName() != null |
345 |
&& fts.getName().equals(SELECTION_STYLING_FTS_NAME)) { |
346 |
|
347 |
SELECTION_STYLING_foundInMapStyle = true; |
348 |
|
349 |
Filter filter = fts.rules().get(0).getFilter(); |
350 |
if (filter instanceof Not) { |
351 |
FidFilterImpl antiOrigFidsFilter = (FidFilterImpl) ((Not) filter) |
352 |
.getFilter(); |
353 |
|
354 |
// Check one way |
355 |
final Set<String> antiFids = antiOrigFidsFilter |
356 |
.getFidsSet(); |
357 |
|
358 |
if (antiFids.isEmpty() && !newSelection.isEmpty()) |
359 |
return true; |
360 |
|
361 |
for (String fid : antiFids) { |
362 |
if (newSelection.contains(fid)) { |
363 |
return true; |
364 |
} |
365 |
} |
366 |
|
367 |
// Check the other way |
368 |
for (String fid : newSelection) { |
369 |
if (antiFids.contains(fid)) { |
370 |
return true; |
371 |
} |
372 |
} |
373 |
|
374 |
if (antiFids.size() + newSelection.size() != styledLayer |
375 |
.getFeatureCollectionFiltered().size()) |
376 |
return true; |
377 |
|
378 |
} else { |
379 |
FidFilterImpl origFidsFilter = (FidFilterImpl) filter; |
380 |
|
381 |
// Check one way |
382 |
final Set<String> fids = origFidsFilter.getFidsSet(); |
383 |
|
384 |
if (fids.isEmpty() && newSelection.isEmpty()) |
385 |
return false; |
386 |
if (fids.size() != newSelection.size()) |
387 |
return true; |
388 |
|
389 |
for (String fid : fids) { |
390 |
if (!newSelection.contains(fid)) { |
391 |
return true; |
392 |
} |
393 |
} |
394 |
|
395 |
// Check the other way |
396 |
for (String fid : newSelection) { |
397 |
if (!fids.contains(fid)) { |
398 |
return true; |
399 |
} |
400 |
} |
401 |
|
402 |
} |
403 |
|
404 |
break; |
405 |
} |
406 |
} |
407 |
|
408 |
if (!SELECTION_STYLING_foundInMapStyle && !newSelection.isEmpty()) |
409 |
return true; |
410 |
|
411 |
return false; |
412 |
} |
413 |
|
414 |
/** |
415 |
* Replaces the local renderer |
416 |
*/ |
417 |
private void replaceRenderer() { |
418 |
} |
419 |
|
420 |
/** |
421 |
* Used to synchronize {@link FeatureSelectedEvent}s with the |
422 |
* {@link StyledFeatureLayerSelectionModel} |
423 |
*/ |
424 |
@Override |
425 |
public void performMapPaneEvent(MapPaneEvent e) { |
426 |
|
427 |
// Ignore event if it is caused by us or the synchronizer is disabled. |
428 |
if (!isEnabled() || selectionChangeCausedByMe) |
429 |
return; |
430 |
|
431 |
if (!(e instanceof FeatureSelectedEvent)) { |
432 |
// LOGGER.debug("Ignoring event " + e); |
433 |
return; |
434 |
} |
435 |
|
436 |
/** |
437 |
* Only listen to FeatureSelectedEvents if an appropriate tool is |
438 |
* selected. |
439 |
*/ |
440 |
final int selectedTool = toolBar.getSelectedTool(); |
441 |
if (selectedTool != MapPaneToolBar.TOOL_SELECTION_ADD |
442 |
&& selectedTool != MapPaneToolBar.TOOL_SELECTION_REMOVE |
443 |
&& selectedTool != MapPaneToolBar.TOOL_SELECTION_SET) { |
444 |
return; |
445 |
} |
446 |
|
447 |
// only listen to events directly coming from JMapPane |
448 |
// selection (ignore selections from FilterDialog) |
449 |
if (e.getSourceObject() != this.mapPane) |
450 |
return; |
451 |
|
452 |
/** |
453 |
* Checking, that the FeatureSelectedEvent actually contains features |
454 |
* from this layer |
455 |
*/ |
456 |
FeatureSelectedEvent fse = (FeatureSelectedEvent) e; |
457 |
final String sourceID = fse.getSourceLayer().getTitle(); |
458 |
final String syncForID = mapLayer.getTitle(); |
459 |
if (sourceID != null && syncForID != null |
460 |
&& !sourceID.equals(syncForID)) { |
461 |
LOGGER.debug("Ignoring a FeatureSelectedEvent from " + sourceID); |
462 |
return; |
463 |
} |
464 |
|
465 |
// LOGGER.debug("do event " + fse); |
466 |
|
467 |
// Avoid event circles in propertyChange(..) |
468 |
selectionChangeCausedByMe = true; |
469 |
|
470 |
final FeatureCollection<SimpleFeatureType, SimpleFeature> selectionResult = fse |
471 |
.getSelectionResult(); |
472 |
Iterator<SimpleFeature> fi = selectionResult.iterator(); |
473 |
try { |
474 |
|
475 |
// reset the selection of the DpLayerSelectionModel |
476 |
// layerSelModel.setValueIsAdjusting(true); |
477 |
|
478 |
if (selectedTool == MapPaneToolBar.TOOL_SELECTION_ADD) { |
479 |
layerSelModel.setValueIsAdjusting(true); |
480 |
|
481 |
for (int fIdx = 0; fi.hasNext(); fIdx++) { |
482 |
SimpleFeature f = fi.next(); |
483 |
layerSelModel.addSelection(f.getID()); |
484 |
} |
485 |
layerSelModel.setValueIsAdjusting(false); |
486 |
} else if (selectedTool == MapPaneToolBar.TOOL_SELECTION_SET) { |
487 |
layerSelModel.setValueIsAdjusting(true); |
488 |
layerSelModel.clearSelection(); |
489 |
|
490 |
for (int fIdx = 0; fi.hasNext(); fIdx++) { |
491 |
SimpleFeature f = fi.next(); |
492 |
layerSelModel.addSelection(f.getID()); |
493 |
} |
494 |
|
495 |
// LOGGER.debug("Setting selection to " + fi.()); |
496 |
|
497 |
layerSelModel.setValueIsAdjusting(false); |
498 |
} else if (selectedTool == MapPaneToolBar.TOOL_SELECTION_REMOVE) { |
499 |
layerSelModel.setValueIsAdjusting(true); |
500 |
for (int fIdx = 0; fi.hasNext(); fIdx++) { |
501 |
SimpleFeature f = fi.next(); |
502 |
layerSelModel.removeSelection(f.getID()); |
503 |
} |
504 |
layerSelModel.setValueIsAdjusting(false); |
505 |
} |
506 |
|
507 |
} finally { |
508 |
selectionResult.close(fi); |
509 |
} |
510 |
|
511 |
// Show selected features in the map, which is not automatically done by |
512 |
// the origin: FeatureSelectedEvent |
513 |
changeLayerStyle(layerSelModel.getSelection()); |
514 |
|
515 |
// Danger of event circles in propertyChange(..) banned |
516 |
selectionChangeCausedByMe = false; |
517 |
} |
518 |
|
519 |
} |