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