ref #7095 Add remove description button
[taxeditor.git] / eu.etaxonomy.taxeditor.editor / src / main / java / eu / etaxonomy / taxeditor / editor / workingSet / matrix / CharacterMatrix.java
1 /**
2 * Copyright (C) 2017 EDIT
3 * European Distributed Institute of Taxonomy
4 * http://www.e-taxonomy.eu
5 *
6 * The contents of this file are subject to the Mozilla Public License Version 1.1
7 * See LICENSE.TXT at the top of this package for the full license terms.
8 */
9 package eu.etaxonomy.taxeditor.editor.workingSet.matrix;
10
11 import java.io.File;
12 import java.io.FileInputStream;
13 import java.io.IOException;
14 import java.util.ArrayList;
15 import java.util.Arrays;
16 import java.util.Collection;
17 import java.util.Collections;
18 import java.util.HashMap;
19 import java.util.HashSet;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.Properties;
23 import java.util.Set;
24
25 import org.apache.commons.collections4.map.LinkedMap;
26 import org.eclipse.jface.layout.GridDataFactory;
27 import org.eclipse.jface.viewers.ArrayContentProvider;
28 import org.eclipse.jface.viewers.ComboViewer;
29 import org.eclipse.jface.viewers.LabelProvider;
30 import org.eclipse.jface.viewers.StructuredSelection;
31 import org.eclipse.jface.window.Window;
32 import org.eclipse.nebula.widgets.nattable.NatTable;
33 import org.eclipse.nebula.widgets.nattable.command.VisualRefreshCommand;
34 import org.eclipse.nebula.widgets.nattable.config.AbstractRegistryConfiguration;
35 import org.eclipse.nebula.widgets.nattable.config.CellConfigAttributes;
36 import org.eclipse.nebula.widgets.nattable.config.ConfigRegistry;
37 import org.eclipse.nebula.widgets.nattable.config.DefaultNatTableStyleConfiguration;
38 import org.eclipse.nebula.widgets.nattable.config.IConfigRegistry;
39 import org.eclipse.nebula.widgets.nattable.config.IEditableRule;
40 import org.eclipse.nebula.widgets.nattable.coordinate.PositionCoordinate;
41 import org.eclipse.nebula.widgets.nattable.data.IDataProvider;
42 import org.eclipse.nebula.widgets.nattable.data.ListDataProvider;
43 import org.eclipse.nebula.widgets.nattable.edit.EditConfigAttributes;
44 import org.eclipse.nebula.widgets.nattable.export.command.ExportCommand;
45 import org.eclipse.nebula.widgets.nattable.export.command.ExportCommandHandler;
46 import org.eclipse.nebula.widgets.nattable.extension.glazedlists.GlazedListsEventLayer;
47 import org.eclipse.nebula.widgets.nattable.extension.glazedlists.GlazedListsSortModel;
48 import org.eclipse.nebula.widgets.nattable.extension.glazedlists.tree.GlazedListTreeData;
49 import org.eclipse.nebula.widgets.nattable.extension.glazedlists.tree.GlazedListTreeRowModel;
50 import org.eclipse.nebula.widgets.nattable.freeze.CompositeFreezeLayer;
51 import org.eclipse.nebula.widgets.nattable.freeze.FreezeHelper;
52 import org.eclipse.nebula.widgets.nattable.freeze.FreezeLayer;
53 import org.eclipse.nebula.widgets.nattable.grid.GridRegion;
54 import org.eclipse.nebula.widgets.nattable.grid.command.ClientAreaResizeCommand;
55 import org.eclipse.nebula.widgets.nattable.grid.data.DefaultColumnHeaderDataProvider;
56 import org.eclipse.nebula.widgets.nattable.grid.data.DefaultCornerDataProvider;
57 import org.eclipse.nebula.widgets.nattable.grid.data.DefaultRowHeaderDataProvider;
58 import org.eclipse.nebula.widgets.nattable.grid.data.FixedSummaryRowHeaderLayer;
59 import org.eclipse.nebula.widgets.nattable.grid.layer.ColumnHeaderLayer;
60 import org.eclipse.nebula.widgets.nattable.grid.layer.CornerLayer;
61 import org.eclipse.nebula.widgets.nattable.grid.layer.DefaultRowHeaderDataLayer;
62 import org.eclipse.nebula.widgets.nattable.grid.layer.GridLayer;
63 import org.eclipse.nebula.widgets.nattable.grid.layer.config.DefaultRowStyleConfiguration;
64 import org.eclipse.nebula.widgets.nattable.layer.AbstractLayer;
65 import org.eclipse.nebula.widgets.nattable.layer.CompositeLayer;
66 import org.eclipse.nebula.widgets.nattable.layer.DataLayer;
67 import org.eclipse.nebula.widgets.nattable.layer.ILayer;
68 import org.eclipse.nebula.widgets.nattable.layer.ILayerListener;
69 import org.eclipse.nebula.widgets.nattable.layer.cell.ColumnOverrideLabelAccumulator;
70 import org.eclipse.nebula.widgets.nattable.layer.cell.ILayerCell;
71 import org.eclipse.nebula.widgets.nattable.layer.config.DefaultColumnHeaderStyleConfiguration;
72 import org.eclipse.nebula.widgets.nattable.layer.config.DefaultRowHeaderStyleConfiguration;
73 import org.eclipse.nebula.widgets.nattable.layer.event.ILayerEvent;
74 import org.eclipse.nebula.widgets.nattable.layer.stack.DefaultBodyLayerStack;
75 import org.eclipse.nebula.widgets.nattable.persistence.PersistenceHelper;
76 import org.eclipse.nebula.widgets.nattable.persistence.command.DisplayPersistenceDialogCommand;
77 import org.eclipse.nebula.widgets.nattable.persistence.command.DisplayPersistenceDialogCommandHandler;
78 import org.eclipse.nebula.widgets.nattable.persistence.command.IStateChangedListener;
79 import org.eclipse.nebula.widgets.nattable.persistence.command.StateChangeEvent;
80 import org.eclipse.nebula.widgets.nattable.persistence.gui.PersistenceDialog;
81 import org.eclipse.nebula.widgets.nattable.selection.SelectionLayer;
82 import org.eclipse.nebula.widgets.nattable.selection.config.DefaultSelectionStyleConfiguration;
83 import org.eclipse.nebula.widgets.nattable.selection.event.CellSelectionEvent;
84 import org.eclipse.nebula.widgets.nattable.sort.SortHeaderLayer;
85 import org.eclipse.nebula.widgets.nattable.sort.config.SingleClickSortConfiguration;
86 import org.eclipse.nebula.widgets.nattable.style.CellStyleAttributes;
87 import org.eclipse.nebula.widgets.nattable.style.DisplayMode;
88 import org.eclipse.nebula.widgets.nattable.style.HorizontalAlignmentEnum;
89 import org.eclipse.nebula.widgets.nattable.style.Style;
90 import org.eclipse.nebula.widgets.nattable.style.VerticalAlignmentEnum;
91 import org.eclipse.nebula.widgets.nattable.summaryrow.FixedSummaryRowLayer;
92 import org.eclipse.nebula.widgets.nattable.summaryrow.SummaryRowLayer;
93 import org.eclipse.nebula.widgets.nattable.tree.ITreeRowModel;
94 import org.eclipse.nebula.widgets.nattable.tree.TreeLayer;
95 import org.eclipse.nebula.widgets.nattable.tree.command.TreeCollapseAllCommand;
96 import org.eclipse.nebula.widgets.nattable.tree.command.TreeExpandAllCommand;
97 import org.eclipse.nebula.widgets.nattable.ui.menu.AbstractHeaderMenuConfiguration;
98 import org.eclipse.nebula.widgets.nattable.ui.menu.PopupMenuBuilder;
99 import org.eclipse.nebula.widgets.nattable.util.GUIHelper;
100 import org.eclipse.nebula.widgets.nattable.viewport.ViewportLayer;
101 import org.eclipse.swt.SWT;
102 import org.eclipse.swt.events.SelectionAdapter;
103 import org.eclipse.swt.events.SelectionEvent;
104 import org.eclipse.swt.graphics.Color;
105 import org.eclipse.swt.graphics.FontData;
106 import org.eclipse.swt.layout.GridData;
107 import org.eclipse.swt.layout.GridLayout;
108 import org.eclipse.swt.layout.RowLayout;
109 import org.eclipse.swt.widgets.Button;
110 import org.eclipse.swt.widgets.Composite;
111 import org.eclipse.swt.widgets.Label;
112
113 import ca.odell.glazedlists.EventList;
114 import ca.odell.glazedlists.GlazedLists;
115 import ca.odell.glazedlists.SortedList;
116 import ca.odell.glazedlists.TreeList;
117 import eu.etaxonomy.cdm.model.description.DescriptionBase;
118 import eu.etaxonomy.cdm.model.description.DescriptionElementBase;
119 import eu.etaxonomy.cdm.model.description.Feature;
120 import eu.etaxonomy.cdm.model.description.FeatureTree;
121 import eu.etaxonomy.cdm.model.description.MeasurementUnit;
122 import eu.etaxonomy.cdm.model.description.SpecimenDescription;
123 import eu.etaxonomy.cdm.model.description.State;
124 import eu.etaxonomy.cdm.model.description.WorkingSet;
125 import eu.etaxonomy.cdm.model.occurrence.SpecimenOrObservationBase;
126 import eu.etaxonomy.taxeditor.editor.l10n.Messages;
127 import eu.etaxonomy.taxeditor.editor.workingSet.matrix.categorical.CategoricalDataCellEditor;
128 import eu.etaxonomy.taxeditor.editor.workingSet.matrix.categorical.CategoricalDataDisplayConverter;
129 import eu.etaxonomy.taxeditor.editor.workingSet.matrix.quantitative.QuantitativeDataCellEditor;
130 import eu.etaxonomy.taxeditor.editor.workingSet.matrix.quantitative.QuantitativeDataDisplayConverter;
131 import eu.etaxonomy.taxeditor.editor.workingSet.matrix.supplementalInfo.SupplementalInfoDisplayConverter;
132 import eu.etaxonomy.taxeditor.model.ColorResources;
133 import eu.etaxonomy.taxeditor.model.DescriptionHelper;
134 import eu.etaxonomy.taxeditor.model.ImageResources;
135 import eu.etaxonomy.taxeditor.model.MessagingUtils;
136 import eu.etaxonomy.taxeditor.preference.Resources;
137 import eu.etaxonomy.taxeditor.workbench.WorkbenchUtility;
138
139 /**
140 * Character matrix editor for editing specimen/taxon descriptions in a table
141 * @author pplitzner
142 * @since Nov 26, 2017
143 *
144 */
145 public class CharacterMatrix extends Composite {
146
147 private static final List<String> WS_PROPERTY_PATH = Arrays.asList(new String[] {
148 "descriptions", //$NON-NLS-1$
149 "descriptions.descriptionElements", //$NON-NLS-1$
150 "descriptions.descriptionElements.inDescription", //$NON-NLS-1$
151 "descriptions.descriptionElements.inDescription.descriptionElements", //$NON-NLS-1$
152 "descriptions.descriptionElements.feature", //$NON-NLS-1$
153 });
154
155 private static final String CHARACTER_MATRIX_STATE_PROPERTIES = "characterMatrixState.properties"; //$NON-NLS-1$
156
157 private static final int LEADING_COLUMN_COUNT = 4;
158 private static final String TAXON_COLUMN = "taxon_column"; //$NON-NLS-1$
159 private static final String COLLECTOR_COLUMN = "collector_column"; //$NON-NLS-1$
160 private static final String IDENTIFIER_COLUMN = "identifier_column"; //$NON-NLS-1$
161 private static final String COUNTRY_COLUMN = "country_column"; //$NON-NLS-1$
162
163 private WorkingSet workingSet;
164
165 private NatTable natTable;
166
167 private Map<Integer, Feature> indexToFeatureMap = new HashMap<>();
168
169 private Map<Feature, List<State>> categoricalFeatureToStateMap = new HashMap<>();
170
171 private LinkedMap<String, String> propertyToLabelMap = new LinkedMap<>();
172
173 private Properties natTableState;
174
175 private EventList<Object> descriptions;
176
177 private Collection<SpecimenOrObservationBase> specimenCache = null;
178
179 private ListDataProvider<Object> bodyDataProvider;
180
181 private FreezeLayer freezeLayer;
182
183 private ViewportLayer viewportLayer;
184
185 private Label wsLabel;
186
187 private List<Feature> features;
188
189 private DisplayPersistenceDialogCommandHandler displayPersistenceDialogCommandHandler;
190
191 private CharacterMatrixPart part;
192
193 private AbstractLayer topMostLayer;
194
195 private FixedSummaryRowLayer summaryRowLayer;
196
197 private ConfigRegistry configRegistry;
198
199 private DefaultBodyLayerStack bodyLayer;
200
201
202 public CharacterMatrix(Composite parent, CharacterMatrixPart part) {
203 super(parent, SWT.NONE);
204 this.part = part;
205 this.setLayout(new GridLayout());
206
207 createToolBar();
208
209 natTable = new NatTable(this, false);
210
211 applyStyles();
212
213 createBottomToolbar();
214
215 }
216
217 private void createToolBar(){
218 Composite toolbarComposite = new Composite(this, SWT.NONE);
219 toolbarComposite.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
220 toolbarComposite.setLayout(new GridLayout(9, false));
221
222 wsLabel = new Label(toolbarComposite, SWT.NONE);
223
224 Button btnToggleTree = new Button(toolbarComposite, SWT.PUSH);
225 Button btnToggleFlat = new Button(toolbarComposite, SWT.PUSH);
226 Button btnCollapseAll = new Button(toolbarComposite, SWT.PUSH);
227 Button btnExpandAll = new Button(toolbarComposite, SWT.PUSH);
228 Button btnFreezeSuppInfo = new Button(toolbarComposite, SWT.TOGGLE);
229 ComboViewer comboStates = new ComboViewer(toolbarComposite, SWT.DROP_DOWN);
230 Button btnManageState = new Button(toolbarComposite, SWT.PUSH);
231 Button btnExcelExport = new Button(toolbarComposite, SWT.PUSH);
232
233 /**
234 * Toogle tree button
235 */
236 btnToggleTree.setImage(ImageResources.getImage(ImageResources.HIERARCHICAL));
237 btnToggleTree.setToolTipText(Messages.CharacterMatrix_SHOW_HIERARCHY);
238 btnToggleTree.setSelection(true);
239 btnToggleTree.setEnabled(false);
240 btnToggleTree.addSelectionListener(new SelectionAdapter() {
241 @Override
242 public void widgetSelected(SelectionEvent e) {
243 toggleTreeFlat(true, btnToggleFlat, btnToggleTree, btnCollapseAll, btnExpandAll, btnFreezeSuppInfo);
244 }
245 });
246
247 /**
248 * Toogle flat button
249 */
250 btnToggleFlat.setImage(ImageResources.getImage(ImageResources.FLAT));
251 btnToggleFlat.setToolTipText(Messages.CharacterMatrix_SHOW_FLAT_LIST);
252 btnToggleFlat.addSelectionListener(new SelectionAdapter() {
253 @Override
254 public void widgetSelected(SelectionEvent e) {
255 toggleTreeFlat(false, btnToggleFlat, btnToggleTree, btnCollapseAll, btnExpandAll, btnFreezeSuppInfo);
256 }
257 });
258
259 /**
260 *
261 * Collapse button
262 */
263 btnCollapseAll.setImage(ImageResources.getImage(ImageResources.COLLAPSE_ALL));
264 btnCollapseAll.setToolTipText(Messages.CharacterMatrix_COLLAPSE);
265 btnCollapseAll.addSelectionListener(new SelectionAdapter() {
266 @Override
267 public void widgetSelected(SelectionEvent e) {
268 natTable.doCommand(new TreeCollapseAllCommand());
269 }
270 });
271
272 /**
273 * Expand button
274 */
275 btnExpandAll.setImage(ImageResources.getImage(ImageResources.EXPAND_ALL));
276 btnExpandAll.setToolTipText(Messages.CharacterMatrix_EXPAND);
277 btnExpandAll.addSelectionListener(new SelectionAdapter() {
278 @Override
279 public void widgetSelected(SelectionEvent e) {
280 natTable.doCommand(new TreeExpandAllCommand());
281 }
282 });
283
284 /**
285 * Freeze supplemental info button
286 */
287 btnFreezeSuppInfo.setImage(ImageResources.getImage(ImageResources.LOCK_ICON));
288 btnFreezeSuppInfo.setToolTipText(Messages.CharacterMatrix_LOCK_COLUMNS);
289 btnFreezeSuppInfo.setSelection(true);
290 btnFreezeSuppInfo.addSelectionListener(new SelectionAdapter() {
291 @Override
292 public void widgetSelected(SelectionEvent e) {
293 boolean isSelected = btnFreezeSuppInfo.getSelection();
294 freezeSupplementalColumns(isSelected);
295 btnFreezeSuppInfo.setImage(isSelected?
296 ImageResources.getImage(ImageResources.LOCK_ICON):
297 ImageResources.getImage(ImageResources.LOCK_OPEN_ICON));
298 }
299 });
300
301 /**
302 * Table state persistence
303 */
304 natTableState = new Properties();
305 //load persisted state
306 File statePropertiesFile = getStatePropertiesFile();
307 FileInputStream inputStream;
308 try {
309 inputStream = new FileInputStream(statePropertiesFile);
310 natTableState.load(inputStream);
311 } catch (IOException e) {
312 MessagingUtils.info("No initial state properties file found for character matrix"); //$NON-NLS-1$
313 }
314
315 // create a combobox for showing the available view states
316 Collection<String> availableStates = PersistenceHelper.getAvailableStates(natTableState);
317 comboStates.setLabelProvider(new LabelProvider(){
318 @Override
319 public String getText(Object element) {
320 if(element instanceof String && ((String) element).isEmpty()){
321 return Messages.CharacterMatrix_DEFAULT;
322 }
323 return super.getText(element);
324 }
325 });
326 comboStates.setContentProvider(new ArrayContentProvider());
327 comboStates.addSelectionChangedListener(e->
328 {
329 int index = comboStates.getCombo().getSelectionIndex();
330 if (index >= 0) {
331 String selected = comboStates.getCombo().getItem(index);
332 // load the state
333 natTable.loadState(selected, natTableState);
334 natTableState.setProperty(PersistenceDialog.ACTIVE_VIEW_CONFIGURATION_KEY, selected);
335 }
336 });
337 comboStates.setInput(availableStates);
338 if(comboStates.getCombo().getItemCount()>0){
339 comboStates.getCombo().select(0);
340 }
341
342 displayPersistenceDialogCommandHandler = new DisplayPersistenceDialogCommandHandler(natTableState, natTable);
343 // add listener to update the combo on view state management changes
344 displayPersistenceDialogCommandHandler.addStateChangeListener(new IStateChangedListener() {
345 @Override
346 public void handleStateChange(StateChangeEvent event) {
347 comboStates.setInput(PersistenceHelper.getAvailableStates(natTableState));
348 selectStateItem(comboStates, event.getViewConfigName());
349 }
350 });
351
352 // add button to show dialog
353 btnManageState.setImage(ImageResources.getImage(ImageResources.SETTINGS));
354 btnManageState.setToolTipText(Messages.CharacterMatrix_VIEW_CONFIG);
355 btnManageState.addSelectionListener(new SelectionAdapter() {
356 @Override
357 public void widgetSelected(SelectionEvent e) {
358 natTable.doCommand(new DisplayPersistenceDialogCommand(natTable));
359 selectStateItem(comboStates, natTableState.get(PersistenceDialog.ACTIVE_VIEW_CONFIGURATION_KEY).toString());
360 }
361 });
362
363 /**
364 * excel export
365 */
366 btnExcelExport.setToolTipText(Messages.CharacterMatrix_EXPORT);
367 btnExcelExport.setImage(ImageResources.getImage(ImageResources.EXPORT));
368 btnExcelExport.addSelectionListener(new SelectionAdapter() {
369 @Override
370 public void widgetSelected(SelectionEvent e) {
371 natTable.doCommand(
372 new ExportCommand(
373 natTable.getConfigRegistry(),
374 natTable.getShell()));
375 }
376 });
377 }
378
379 private void createBottomToolbar() {
380 Composite buttonPanel = new Composite(this, SWT.NONE);
381
382 buttonPanel.setLayout(new RowLayout());
383 GridDataFactory.fillDefaults().grab(true, false).applyTo(buttonPanel);
384
385 /**
386 * Add description button
387 */
388 Button btnAddDescription = new Button(buttonPanel, SWT.PUSH);
389 btnAddDescription.setImage(ImageResources.getImage(ImageResources.ADD_ICON_GREEN));
390 btnAddDescription.addSelectionListener(new SelectionAdapter() {
391 @Override
392 public void widgetSelected(SelectionEvent e) {
393 SpecimenSelectionDialog dialog = new SpecimenSelectionDialog(natTable.getShell(), CharacterMatrix.this);
394 if(dialog.open()==Window.OK){
395 Collection<SpecimenOrObservationBase> specimens = dialog.getSpecimen();
396 boolean hasAdded = false;
397 for (SpecimenOrObservationBase specimen : specimens) {
398 SpecimenDescription description = getDescriptionForWorkingSet(specimen);
399 if(!workingSet.getDescriptions().contains(description)){
400 CharacterMatrix.this.descriptions.add(new RowWrapper(description));
401 workingSet.addDescription(description);
402 hasAdded = true;
403 }
404 }
405 if(hasAdded){
406 setDirty();
407 }
408 }
409 }
410 });
411 /**
412 * Remove description button
413 */
414 Button btnRemoveDescription = new Button(buttonPanel, SWT.PUSH);
415 btnRemoveDescription.setImage(ImageResources.getImage(ImageResources.ACTIVE_DELETE_ICON));
416 btnRemoveDescription.addSelectionListener(new SelectionAdapter() {
417 @Override
418 public void widgetSelected(SelectionEvent e) {
419 int[] fullySelectedRowPositions = bodyLayer.getSelectionLayer().getFullySelectedRowPositions();
420 for (int i : fullySelectedRowPositions) {
421 Object rowObject = bodyDataProvider.getRowObject(i);
422 if(rowObject instanceof RowWrapper){
423 CharacterMatrix.this.descriptions.remove(rowObject);
424 workingSet.removeDescription(((RowWrapper) rowObject).getSpecimenDescription());
425 setDirty();
426 }
427 }
428 }
429 });
430 }
431
432 private void applyStyles(){
433 // NOTE: Getting the colors and fonts from the GUIHelper ensures that
434 // they are disposed properly (required by SWT)
435 DefaultNatTableStyleConfiguration natTableConfiguration = new DefaultNatTableStyleConfiguration();
436 natTableConfiguration.bgColor = GUIHelper.getColor(249, 172, 7);
437 natTableConfiguration.fgColor = GUIHelper.getColor(30, 76, 19);
438 natTableConfiguration.hAlign = HorizontalAlignmentEnum.LEFT;
439 natTableConfiguration.vAlign = VerticalAlignmentEnum.TOP;
440 // natTableConfiguration.borderStyle = new BorderStyle(1, GUIHelper.getColor(249, 172, 7), LineStyleEnum.SOLID);
441
442 // Setup even odd row colors - row colors override the NatTable default
443 // colors
444 DefaultRowStyleConfiguration rowStyleConfiguration = new DefaultRowStyleConfiguration();
445 rowStyleConfiguration.oddRowBgColor = ColorResources.getColor(Resources.COLOR_LIST_ODD);
446 rowStyleConfiguration.evenRowBgColor = ColorResources.getColor(Resources.COLOR_LIST_EVEN);
447
448 // Setup selection styling
449 DefaultSelectionStyleConfiguration selectionStyle = new DefaultSelectionStyleConfiguration();
450 // selectionStyle.selectionFont = GUIHelper.getFont(new FontData("Verdana", 8, SWT.NORMAL));
451 // selectionStyle.selectionBgColor = GUIHelper.getColor(217, 232, 251);
452 // selectionStyle.selectionFgColor = GUIHelper.COLOR_BLACK;
453 // selectionStyle.anchorBorderStyle = new BorderStyle(1, GUIHelper.COLOR_DARK_GRAY, LineStyleEnum.SOLID);
454 // selectionStyle.anchorBgColor = GUIHelper.getColor(65, 113, 43);
455 selectionStyle.selectedHeaderBgColor = GUIHelper.getColor(156, 209, 103);
456
457 // Add all style configurations to NatTable
458 natTable.addConfiguration(natTableConfiguration);
459 natTable.addConfiguration(rowStyleConfiguration);
460 natTable.addConfiguration(selectionStyle);
461
462 // Column/Row header style and custom painters
463 DefaultRowHeaderStyleConfiguration rowHeaderConfig = new DefaultRowHeaderStyleConfiguration();
464 Color rowColumnColor = GUIHelper.getColor(230, 255, 255);
465 rowHeaderConfig.bgColor = rowColumnColor;
466 natTable.addConfiguration(rowHeaderConfig);
467 DefaultColumnHeaderStyleConfiguration columnHeaderStyle = new DefaultColumnHeaderStyleConfiguration();
468 columnHeaderStyle.bgColor = rowColumnColor;
469 columnHeaderStyle.font = GUIHelper.getFont(new FontData("Verdana", 9, SWT.BOLD)); //$NON-NLS-1$
470 natTable.addConfiguration(columnHeaderStyle);
471
472 }
473
474 private void toggleTreeFlat(boolean isTree, Button btnToggleFlat, Button btnToggleTree, Button btnCollapseAll, Button btnExpandAll, Button btnFreezeSuppInfo) {
475 createLayers(isTree);
476 btnToggleFlat.setEnabled(isTree);
477 btnToggleTree.setEnabled(!isTree);
478 btnCollapseAll.setEnabled(isTree);
479 btnExpandAll.setEnabled(isTree);
480 natTable.doCommand(new ClientAreaResizeCommand(natTable));
481 natTable.doCommand(new VisualRefreshCommand());
482 freezeSupplementalColumns(btnFreezeSuppInfo.getSelection());
483 }
484
485 public void createTable(boolean treeView){
486 /**
487 * layers
488 */
489 createLayers(treeView);
490
491 /**
492 * configuration
493 */
494 configureNatTable(treeView, configRegistry, topMostLayer, summaryRowLayer);
495
496 /**
497 * handlers and listeners
498 */
499 registerHandlersAndListeners(topMostLayer);
500
501 GridDataFactory.fillDefaults().grab(true, true).applyTo(natTable);
502
503 wsLabel.setText(workingSet.getLabel());
504 wsLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
505 wsLabel.getParent().layout();
506
507 freezeSupplementalColumns(true);
508
509 this.layout();
510 }
511
512 public void initWorkingSet(WorkingSet workingSet){
513 this.workingSet = workingSet;
514 //get features/columns stored in working set
515 FeatureTree tree = workingSet.getDescriptiveSystem();
516 features = new ArrayList<>(tree.getDistinctFeatures());
517 Collections.sort(features);
518 //init state data for categorical features
519 features.forEach(feature->
520 {
521 if(feature.isSupportsCategoricalData()){
522 List<State> supportedStates = new ArrayList<>();
523 feature.getSupportedCategoricalEnumerations().forEach(voc->supportedStates.addAll(voc.getTerms()));
524 categoricalFeatureToStateMap.put(feature, supportedStates);
525 }
526 });
527 descriptions = GlazedLists.eventList(getDescriptions(workingSet));
528
529 }
530
531 private void createLayers(boolean treeView) {
532 // use the SortedList constructor with 'null' for the Comparator
533 // because the Comparator will be set by configuration
534 SortedList<Object> sortedList = new SortedList<>(descriptions, new MatrixRowComparator());
535 // wrap the SortedList with the TreeList
536 TreeList<Object> treeList = new TreeList(sortedList, new DescriptionTreeFormat(workingSet), TreeList.NODES_START_EXPANDED);
537 /**
538 * data provider
539 */
540 SpecimenColumnPropertyAccessor columnPropertyAccessor = new SpecimenColumnPropertyAccessor(this);
541 bodyDataProvider = treeView?new ListDataProvider<>(treeList, columnPropertyAccessor):new ListDataProvider<>(sortedList, columnPropertyAccessor);
542
543 configRegistry = new ConfigRegistry();
544
545
546 /**
547 * BODY layer
548 *
549 *
550
551 CompositeLayer
552 - (top) SummaryRowLayer
553 - (bottom) ViewportLayer
554
555 ^
556 ViewportLayer
557
558 ^
559 TreeLayer (default visible)
560
561 ^
562 CompositeFreezeLayer
563 - viewportLayer
564 - selectionLayer
565 - freezeLayer
566
567 ^
568 FreezeLayer
569
570 ^
571 SelectionLayer
572
573 ^
574 ColumnHideShowLayer
575
576 ^
577 ColumnReorderLayer
578
579 ^
580 DataLayer
581
582 *
583
584 */
585 DataLayer bodyDataLayer = new DataLayer(bodyDataProvider);
586
587 //register labels for columns
588 ColumnOverrideLabelAccumulator bodyColumnLabelAccumulator =new ColumnOverrideLabelAccumulator(bodyDataLayer);
589 bodyDataLayer.setConfigLabelAccumulator(bodyColumnLabelAccumulator);
590 propertyToLabelMap.put(TAXON_COLUMN, Messages.CharacterMatrix_TAXON);
591 bodyColumnLabelAccumulator.registerColumnOverrides(0, TAXON_COLUMN);
592 propertyToLabelMap.put(COLLECTOR_COLUMN, Messages.CharacterMatrix_COLLECTOR_NO);
593 bodyColumnLabelAccumulator.registerColumnOverrides(1, COLLECTOR_COLUMN);
594 propertyToLabelMap.put(IDENTIFIER_COLUMN, Messages.CharacterMatrix_IDENTIFIER);
595 bodyColumnLabelAccumulator.registerColumnOverrides(2, IDENTIFIER_COLUMN);
596 propertyToLabelMap.put(COUNTRY_COLUMN, Messages.CharacterMatrix_COUNTRY);
597 bodyColumnLabelAccumulator.registerColumnOverrides(3, COUNTRY_COLUMN);
598 for(int i=0;i<features.size();i++){
599 Feature feature = features.get(i);
600 initLabels(bodyColumnLabelAccumulator, i, feature);
601 }
602
603 // layer for event handling of GlazedLists and PropertyChanges
604 GlazedListsEventLayer eventLayer = new GlazedListsEventLayer<>(bodyDataLayer, treeList);
605 GlazedListTreeData treeData = new GlazedListTreeData<>(treeList);
606 ITreeRowModel treeRowModel = new GlazedListTreeRowModel<>(treeData);
607
608 bodyLayer = new DefaultBodyLayerStack(
609 eventLayer);
610 viewportLayer = bodyLayer.getViewportLayer();
611 final SelectionLayer selectionLayer = bodyLayer.getSelectionLayer();
612 freezeLayer = new FreezeLayer(selectionLayer);
613 final CompositeFreezeLayer compositeFreezeLayer = new CompositeFreezeLayer(
614 freezeLayer, bodyLayer.getViewportLayer(), selectionLayer);
615 TreeLayer treeLayer = new TreeLayer(compositeFreezeLayer, treeRowModel);
616
617 topMostLayer = treeView?treeLayer:compositeFreezeLayer;
618
619 summaryRowLayer = new FixedSummaryRowLayer(bodyDataLayer, topMostLayer, configRegistry, false);
620 //regoster labels with summary prefix for summary layer
621 ColumnOverrideLabelAccumulator summaryColumnLabelAccumulator =new ColumnOverrideLabelAccumulator(bodyDataLayer);
622 summaryRowLayer.setConfigLabelAccumulator(summaryColumnLabelAccumulator);
623 for(int i=0;i<features.size();i++){
624 Feature feature = features.get(i);
625 summaryColumnLabelAccumulator.registerColumnOverrides(
626 i+LEADING_COLUMN_COUNT,
627 SummaryRowLayer.DEFAULT_SUMMARY_COLUMN_CONFIG_LABEL_PREFIX+MatrixUtility.getProperty(feature));
628 }
629 // because the horizontal dependency is the ViewportLayer
630 // we need to set the composite dependency to false
631 summaryRowLayer.setHorizontalCompositeDependency(false);
632
633 CompositeLayer composite = new CompositeLayer(1, 2);
634 composite.setChildLayer("SUMMARY", summaryRowLayer, 0, 0); //$NON-NLS-1$
635 composite.setChildLayer(GridRegion.BODY, topMostLayer, 0, 1);
636
637
638 /**
639 * column header layer
640 */
641 IDataProvider columnHeaderDataProvider = new DefaultColumnHeaderDataProvider(
642 propertyToLabelMap.values().toArray(new String[] {}), propertyToLabelMap);
643 DataLayer columnHeaderDataLayer = new DataLayer(columnHeaderDataProvider);
644 ColumnHeaderLayer columnHeaderLayer = new ColumnHeaderLayer(columnHeaderDataLayer, topMostLayer, selectionLayer);
645
646 // add the SortHeaderLayer to the column header layer stack
647 // as we use GlazedLists, we use the GlazedListsSortModel which
648 // delegates the sorting to the SortedList
649 final SortHeaderLayer<SpecimenDescription> sortHeaderLayer = new SortHeaderLayer<>(
650 columnHeaderLayer,
651 new GlazedListsSortModel<>(
652 sortedList,
653 columnPropertyAccessor,
654 configRegistry,
655 columnHeaderDataLayer));
656
657
658 /**
659 * row header layer
660 */
661 IDataProvider rowHeaderDataProvider = new DefaultRowHeaderDataProvider(bodyDataProvider);
662 DefaultRowHeaderDataLayer rowHeaderDataLayer = new DefaultRowHeaderDataLayer(rowHeaderDataProvider);
663 FixedSummaryRowHeaderLayer fixedSummaryRowHeaderLayer = new FixedSummaryRowHeaderLayer(rowHeaderDataLayer,
664 composite, selectionLayer);
665 fixedSummaryRowHeaderLayer.setSummaryRowLabel("\u2211"); //$NON-NLS-1$
666
667
668 /**
669 * corner layer
670 */
671 ILayer cornerLayer = new CornerLayer(
672 new DataLayer(new DefaultCornerDataProvider(columnHeaderDataProvider, rowHeaderDataProvider)),
673 fixedSummaryRowHeaderLayer, sortHeaderLayer);
674
675
676 /**
677 * GRID layer (composition of all other layers)
678 */
679 GridLayer gridLayer = new GridLayer(composite, sortHeaderLayer, fixedSummaryRowHeaderLayer, cornerLayer);
680
681 natTable.setLayer(gridLayer);
682
683 }
684
685 private void registerHandlersAndListeners(AbstractLayer topMostLayer) {
686 // add the ExportCommandHandler to the ViewportLayer in order to make
687 // exporting work
688 topMostLayer.registerCommandHandler(new ExportCommandHandler(topMostLayer));
689
690 //propagate single cell selection
691 natTable.addLayerListener(new ILayerListener() {
692 @Override
693 public void handleLayerEvent(ILayerEvent event) {
694 if(event instanceof CellSelectionEvent){
695 CellSelectionEvent cellSelectionEvent = (CellSelectionEvent)event;
696 int columnPosition = cellSelectionEvent.getColumnPosition();
697 if(columnPosition>LEADING_COLUMN_COUNT){
698 Collection<ILayerCell> selectedCells = cellSelectionEvent.getSelectionLayer().getSelectedCells();
699 StructuredSelection selection = new StructuredSelection();
700 if(selectedCells.size()==1){
701 ILayerCell cell = selectedCells.iterator().next();
702 Object dataValue = cell.getDataValue();
703 if(dataValue!=null){
704 selection = new StructuredSelection(dataValue);
705 }
706 }
707 part.getSelectionService().setSelection(selection);
708 }
709 }
710 }
711 });
712
713 //register handler for view configuration menu
714 natTable.registerCommandHandler(displayPersistenceDialogCommandHandler);
715 }
716
717 private void configureNatTable(boolean treeView, ConfigRegistry configRegistry, AbstractLayer topMostLayer,
718 FixedSummaryRowLayer summaryRowLayer) {
719 /**
720 * CONFIGURATION
721 */
722 natTable.setConfigRegistry(configRegistry);
723
724 //add default configuration because autoconfigure is set to false in constructor
725 natTable.addConfiguration(new DefaultNatTableStyleConfiguration());
726
727 //FIXME: this is for DEBUG ONLY
728 // natTable.addConfiguration(new DebugMenuConfiguration(natTable));
729
730 // override the default sort configuration and change the mouse bindings
731 // to sort on a single click
732 if(!treeView){
733 natTable.addConfiguration(new SingleClickSortConfiguration());
734 }
735
736 // add the header menu configuration for adding the column header menu
737 // with hide/show actions
738 natTable.addConfiguration(new AbstractHeaderMenuConfiguration(natTable) {
739
740 @Override
741 protected PopupMenuBuilder createColumnHeaderMenu(NatTable natTable) {
742 return super.createColumnHeaderMenu(natTable)
743 .withHideColumnMenuItem()
744 .withShowAllColumnsMenuItem();
745 }
746
747 });
748
749 Style cellStyle = new Style();
750 cellStyle.setAttributeValue(CellStyleAttributes.HORIZONTAL_ALIGNMENT, HorizontalAlignmentEnum.LEFT);
751
752 // add custom configuration for data conversion and add column labels to viewport layer
753 topMostLayer.addConfiguration(new AbstractRegistryConfiguration() {
754 @Override
755 public void configureRegistry(IConfigRegistry configRegistry) {
756 //add display converter for string representation
757 configRegistry.registerConfigAttribute(
758 CellConfigAttributes.DISPLAY_CONVERTER,
759 new SupplementalInfoDisplayConverter(),
760 DisplayMode.NORMAL,
761 TAXON_COLUMN);
762 configRegistry.registerConfigAttribute(
763 CellConfigAttributes.CELL_STYLE,
764 cellStyle,
765 DisplayMode.NORMAL,
766 TAXON_COLUMN);
767 configRegistry.registerConfigAttribute(
768 CellConfigAttributes.DISPLAY_CONVERTER,
769 new SupplementalInfoDisplayConverter(),
770 DisplayMode.NORMAL,
771 COLLECTOR_COLUMN);
772 configRegistry.registerConfigAttribute(
773 CellConfigAttributes.CELL_STYLE,
774 cellStyle,
775 DisplayMode.NORMAL,
776 COLLECTOR_COLUMN);
777 configRegistry.registerConfigAttribute(
778 CellConfigAttributes.DISPLAY_CONVERTER,
779 new SupplementalInfoDisplayConverter(),
780 DisplayMode.NORMAL,
781 IDENTIFIER_COLUMN);
782 configRegistry.registerConfigAttribute(
783 CellConfigAttributes.CELL_STYLE,
784 cellStyle,
785 DisplayMode.NORMAL,
786 IDENTIFIER_COLUMN);
787 configRegistry.registerConfigAttribute(
788 CellConfigAttributes.DISPLAY_CONVERTER,
789 new SupplementalInfoDisplayConverter(),
790 DisplayMode.NORMAL,
791 COUNTRY_COLUMN);
792 configRegistry.registerConfigAttribute(
793 CellConfigAttributes.CELL_STYLE,
794 cellStyle,
795 DisplayMode.NORMAL,
796 COUNTRY_COLUMN);
797 features.forEach(feature->registerColumnConfiguration(feature, configRegistry));
798 }
799
800 });
801
802 //register aggregation configuration for each feature
803 features.forEach(feature->summaryRowLayer.addConfiguration(new AggregationConfiguration(bodyDataProvider, feature)));
804
805 natTable.configure();
806 }
807
808 private void freezeSupplementalColumns(boolean freeze){
809 if(freeze){
810 FreezeHelper.freeze(freezeLayer, viewportLayer,
811 new PositionCoordinate(viewportLayer, 0, 0),
812 new PositionCoordinate(viewportLayer, LEADING_COLUMN_COUNT-1, -1));
813 }
814 else{
815 FreezeHelper.unfreeze(freezeLayer, viewportLayer);
816 }
817 }
818
819 private void selectStateItem(ComboViewer comboStates, String stateName){
820 String[] items = comboStates.getCombo().getItems();
821 for(int i=0;i<items.length;i++){
822 if(items[i].equals(stateName)){
823 comboStates.getCombo().select(i);
824 break;
825 }
826 }
827 }
828
829 private SpecimenDescription getDescriptionForWorkingSet(SpecimenOrObservationBase specimen){
830 Set<Feature> wsFeatures = workingSet.getDescriptiveSystem().getDistinctFeatures();
831 List<DescriptionElementBase> matchingDescriptionElements = new ArrayList<>();
832
833 for (SpecimenDescription specimenDescription : (Set<SpecimenDescription>) specimen.getDescriptions()) {
834 Set<Feature> specimenDescriptionFeatures = new HashSet<>();
835 //gather specimen description features and check for match with WS features
836 for (DescriptionElementBase specimenDescriptionElement : specimenDescription.getElements()) {
837 Feature feature = specimenDescriptionElement.getFeature();
838 specimenDescriptionFeatures.add(feature);
839 if(wsFeatures.contains(feature)){
840 matchingDescriptionElements.add(specimenDescriptionElement);
841 }
842 }
843 //if description with the exact same features is found return the description
844 if(specimenDescriptionFeatures.equals(wsFeatures)){
845 return specimenDescription;
846 }
847 }
848 //Create new specimen description if no match was found
849 setDirty();
850 SpecimenDescription newDesription = SpecimenDescription.NewInstance(specimen);
851 newDesription.setTitleCache(Messages.CharacterMatrix_WORKING_SET+workingSet.getLabel()+": "+newDesription.generateTitle(), true); //$NON-NLS-2$
852
853 //check for equals description element (same feature and same values)
854 Map<Feature, List<DescriptionElementBase>> featureToElementMap = new HashMap<>();
855 for(DescriptionElementBase element:matchingDescriptionElements){
856 List<DescriptionElementBase> list = featureToElementMap.get(element.getFeature());
857 if(list==null){
858 list = new ArrayList<>();
859 }
860 list.add(element);
861 featureToElementMap.put(element.getFeature(), list);
862 }
863 Set<DescriptionElementBase> descriptionElementsToClone = new HashSet<>();
864 for(Feature feature:featureToElementMap.keySet()){
865 List<DescriptionElementBase> elements = featureToElementMap.get(feature);
866 //no duplicate description elements found for this feature
867 if(elements.size()==1){
868 descriptionElementsToClone.add(elements.get(0));
869 }
870 //duplicates found -> check if all are equal
871 else{
872 DescriptionElementBase match = null;
873 for (DescriptionElementBase descriptionElementBase : elements) {
874 if(match==null){
875 match = descriptionElementBase;
876 }
877 else if(!new DescriptionElementCompareWrapper(match).equals(new DescriptionElementCompareWrapper(descriptionElementBase))){
878 match = null;
879 MessagingUtils.informationDialog(Messages.CharacterMatrix_MULTIPLE_DATA,
880 String.format(Messages.CharacterMatrix_MULTIPLE_DATA_MESSAGE, feature.getLabel()));
881 break;
882 }
883 }
884 if(match!=null){
885 descriptionElementsToClone.add(match);
886 }
887 }
888 }
889 //clone matching descriptionElements
890 for (DescriptionElementBase descriptionElementBase : descriptionElementsToClone) {
891 DescriptionElementBase clone;
892 try {
893 clone = descriptionElementBase.clone(newDesription);
894 clone.getSources().forEach(source -> source.setOriginalNameString(DescriptionHelper.getLabel(descriptionElementBase)));
895 } catch (CloneNotSupportedException e) {
896 MessagingUtils.error(CharacterMatrix.class, e);
897 }
898 }
899 return newDesription;
900
901 }
902
903 private void initLabels(final ColumnOverrideLabelAccumulator columnLabelAccumulator,
904 int index, Feature feature) {
905
906 columnLabelAccumulator.registerColumnOverrides(index+LEADING_COLUMN_COUNT, MatrixUtility.getProperty(feature));
907 indexToFeatureMap.put(index+LEADING_COLUMN_COUNT, feature);
908
909 String featureLabel = feature.getLabel();
910 String property = featureLabel;
911 //show unit for quantitative data
912 if(feature.isSupportsQuantitativeData()){
913 Set<MeasurementUnit> recommendedMeasurementUnits = feature.getRecommendedMeasurementUnits();
914 if(recommendedMeasurementUnits.size()>1){
915 MessagingUtils.warningDialog(Messages.CharacterMatrix_INIT_PROBLEM, CharacterMatrix.class,
916 String.format(Messages.CharacterMatrix_INIT_PROBLEM_MESSAGE, feature.getLabel()));
917 }
918 if(recommendedMeasurementUnits.size()==1){
919 MeasurementUnit unit = recommendedMeasurementUnits.iterator().next();
920 featureLabel += " ["+unit.getIdInVocabulary()+"]"; //$NON-NLS-1$ //$NON-NLS-2$
921 }
922 }
923 propertyToLabelMap.put(property, featureLabel);
924 }
925
926 private void registerColumnConfiguration(Feature feature, IConfigRegistry configRegistry) {
927 //make cell editable
928 configRegistry.registerConfigAttribute(
929 EditConfigAttributes.CELL_EDITABLE_RULE,
930 IEditableRule.ALWAYS_EDITABLE,
931 DisplayMode.EDIT,
932 MatrixUtility.getProperty(feature)
933 );
934 if(feature.isSupportsQuantitativeData()){
935 //add display converter for string representation
936 configRegistry.registerConfigAttribute(
937 CellConfigAttributes.DISPLAY_CONVERTER,
938 new QuantitativeDataDisplayConverter(),
939 DisplayMode.NORMAL,
940 MatrixUtility.getProperty(feature));
941 //register quantitative editor
942 configRegistry.registerConfigAttribute(
943 EditConfigAttributes.CELL_EDITOR,
944 new QuantitativeDataCellEditor(feature, this),
945 DisplayMode.EDIT,
946 MatrixUtility.getProperty(feature));
947 }
948 else if(feature.isSupportsCategoricalData()){
949 //add display converter for string representation
950 configRegistry.registerConfigAttribute(
951 CellConfigAttributes.DISPLAY_CONVERTER,
952 new CategoricalDataDisplayConverter(),
953 DisplayMode.NORMAL,
954 MatrixUtility.getProperty(feature));
955
956 //add combo box cell editor
957 //register editor
958 configRegistry.registerConfigAttribute(EditConfigAttributes.CELL_EDITOR,
959 new CategoricalDataCellEditor(getSupportedStatesForCategoricalFeature(feature), this, feature),
960 DisplayMode.EDIT,
961 MatrixUtility.getProperty(feature));
962
963 }
964
965 }
966
967 private List<RowWrapper> getDescriptions(WorkingSet workingSet) {
968 List<RowWrapper> rowWrappers = new ArrayList<>();
969 Set<DescriptionBase> wsDescriptions = workingSet.getDescriptions();
970 for (DescriptionBase descriptionBase : wsDescriptions) {
971 if(descriptionBase instanceof SpecimenDescription){
972 rowWrappers.add(new RowWrapper((SpecimenDescription) descriptionBase));
973 }
974 }
975 return rowWrappers;
976 }
977
978 public List<State> getSupportedStatesForCategoricalFeature(Feature feature){
979 return categoricalFeatureToStateMap.get(feature);
980 }
981
982 public Map<Integer, Feature> getIndexToFeatureMap() {
983 return indexToFeatureMap;
984 }
985
986 public LinkedMap<String, String> getPropertyToLabelMap() {
987 return propertyToLabelMap;
988 }
989
990 public void setDirty() {
991 part.setDirty();
992 }
993
994 public NatTable getNatTable() {
995 return natTable;
996 }
997
998 public WorkingSet getWorkingSet() {
999 return workingSet;
1000 }
1001
1002 public Collection<SpecimenOrObservationBase> getSpecimenCache() {
1003 return specimenCache;
1004 }
1005
1006 public void setSpecimenCache(Collection<SpecimenOrObservationBase> specimenCache) {
1007 this.specimenCache = specimenCache;
1008 }
1009
1010 public Properties getNatTableState() {
1011 return natTableState;
1012 }
1013
1014 public ListDataProvider<Object> getBodyDataProvider() {
1015 return bodyDataProvider;
1016 }
1017
1018 private File getStatePropertiesFile() {
1019 return new File(WorkbenchUtility.getBaseLocation(), CHARACTER_MATRIX_STATE_PROPERTIES);
1020 }
1021
1022 }