4b77b90a2845cbf9ac7e40d287c0fee04f6ec404
[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.FileOutputStream;
14 import java.io.IOException;
15 import java.util.ArrayList;
16 import java.util.Arrays;
17 import java.util.Collection;
18 import java.util.Collections;
19 import java.util.HashMap;
20 import java.util.HashSet;
21 import java.util.List;
22 import java.util.Map;
23 import java.util.Properties;
24 import java.util.Set;
25 import java.util.UUID;
26
27 import javax.annotation.PostConstruct;
28 import javax.annotation.PreDestroy;
29 import javax.inject.Inject;
30
31 import org.apache.commons.collections4.map.LinkedMap;
32 import org.eclipse.core.runtime.IProgressMonitor;
33 import org.eclipse.e4.ui.di.Focus;
34 import org.eclipse.e4.ui.di.Persist;
35 import org.eclipse.e4.ui.model.application.ui.MDirtyable;
36 import org.eclipse.e4.ui.model.application.ui.basic.MPart;
37 import org.eclipse.e4.ui.workbench.modeling.ESelectionService;
38 import org.eclipse.jface.layout.GridDataFactory;
39 import org.eclipse.jface.viewers.ArrayContentProvider;
40 import org.eclipse.jface.viewers.ComboViewer;
41 import org.eclipse.jface.viewers.LabelProvider;
42 import org.eclipse.jface.viewers.StructuredSelection;
43 import org.eclipse.jface.window.Window;
44 import org.eclipse.nebula.widgets.nattable.NatTable;
45 import org.eclipse.nebula.widgets.nattable.config.AbstractRegistryConfiguration;
46 import org.eclipse.nebula.widgets.nattable.config.CellConfigAttributes;
47 import org.eclipse.nebula.widgets.nattable.config.ConfigRegistry;
48 import org.eclipse.nebula.widgets.nattable.config.DefaultNatTableStyleConfiguration;
49 import org.eclipse.nebula.widgets.nattable.config.IConfigRegistry;
50 import org.eclipse.nebula.widgets.nattable.config.IEditableRule;
51 import org.eclipse.nebula.widgets.nattable.data.IDataProvider;
52 import org.eclipse.nebula.widgets.nattable.data.ListDataProvider;
53 import org.eclipse.nebula.widgets.nattable.edit.EditConfigAttributes;
54 import org.eclipse.nebula.widgets.nattable.edit.editor.IComboBoxDataProvider;
55 import org.eclipse.nebula.widgets.nattable.export.command.ExportCommand;
56 import org.eclipse.nebula.widgets.nattable.export.command.ExportCommandHandler;
57 import org.eclipse.nebula.widgets.nattable.extension.glazedlists.GlazedListsEventLayer;
58 import org.eclipse.nebula.widgets.nattable.extension.glazedlists.tree.GlazedListTreeData;
59 import org.eclipse.nebula.widgets.nattable.extension.glazedlists.tree.GlazedListTreeRowModel;
60 import org.eclipse.nebula.widgets.nattable.grid.GridRegion;
61 import org.eclipse.nebula.widgets.nattable.grid.data.DefaultColumnHeaderDataProvider;
62 import org.eclipse.nebula.widgets.nattable.grid.data.DefaultCornerDataProvider;
63 import org.eclipse.nebula.widgets.nattable.grid.data.DefaultRowHeaderDataProvider;
64 import org.eclipse.nebula.widgets.nattable.grid.data.FixedSummaryRowHeaderLayer;
65 import org.eclipse.nebula.widgets.nattable.grid.layer.ColumnHeaderLayer;
66 import org.eclipse.nebula.widgets.nattable.grid.layer.CornerLayer;
67 import org.eclipse.nebula.widgets.nattable.grid.layer.DefaultRowHeaderDataLayer;
68 import org.eclipse.nebula.widgets.nattable.grid.layer.GridLayer;
69 import org.eclipse.nebula.widgets.nattable.hideshow.ColumnHideShowLayer;
70 import org.eclipse.nebula.widgets.nattable.hideshow.RowHideShowLayer;
71 import org.eclipse.nebula.widgets.nattable.layer.CompositeLayer;
72 import org.eclipse.nebula.widgets.nattable.layer.DataLayer;
73 import org.eclipse.nebula.widgets.nattable.layer.ILayer;
74 import org.eclipse.nebula.widgets.nattable.layer.ILayerListener;
75 import org.eclipse.nebula.widgets.nattable.layer.cell.ColumnOverrideLabelAccumulator;
76 import org.eclipse.nebula.widgets.nattable.layer.cell.ILayerCell;
77 import org.eclipse.nebula.widgets.nattable.layer.event.ILayerEvent;
78 import org.eclipse.nebula.widgets.nattable.persistence.PersistenceHelper;
79 import org.eclipse.nebula.widgets.nattable.persistence.command.DisplayPersistenceDialogCommand;
80 import org.eclipse.nebula.widgets.nattable.persistence.command.DisplayPersistenceDialogCommandHandler;
81 import org.eclipse.nebula.widgets.nattable.persistence.command.IStateChangedListener;
82 import org.eclipse.nebula.widgets.nattable.persistence.command.StateChangeEvent;
83 import org.eclipse.nebula.widgets.nattable.persistence.gui.PersistenceDialog;
84 import org.eclipse.nebula.widgets.nattable.reorder.ColumnReorderLayer;
85 import org.eclipse.nebula.widgets.nattable.reorder.RowReorderLayer;
86 import org.eclipse.nebula.widgets.nattable.selection.SelectionLayer;
87 import org.eclipse.nebula.widgets.nattable.selection.event.CellSelectionEvent;
88 import org.eclipse.nebula.widgets.nattable.style.DisplayMode;
89 import org.eclipse.nebula.widgets.nattable.summaryrow.FixedSummaryRowLayer;
90 import org.eclipse.nebula.widgets.nattable.summaryrow.SummaryRowLayer;
91 import org.eclipse.nebula.widgets.nattable.tree.ITreeRowModel;
92 import org.eclipse.nebula.widgets.nattable.tree.TreeLayer;
93 import org.eclipse.nebula.widgets.nattable.tree.command.TreeCollapseAllCommand;
94 import org.eclipse.nebula.widgets.nattable.tree.command.TreeExpandAllCommand;
95 import org.eclipse.nebula.widgets.nattable.ui.menu.AbstractHeaderMenuConfiguration;
96 import org.eclipse.nebula.widgets.nattable.ui.menu.PopupMenuBuilder;
97 import org.eclipse.nebula.widgets.nattable.viewport.ViewportLayer;
98 import org.eclipse.swt.SWT;
99 import org.eclipse.swt.events.SelectionAdapter;
100 import org.eclipse.swt.events.SelectionEvent;
101 import org.eclipse.swt.layout.GridData;
102 import org.eclipse.swt.layout.GridLayout;
103 import org.eclipse.swt.layout.RowLayout;
104 import org.eclipse.swt.widgets.Button;
105 import org.eclipse.swt.widgets.Composite;
106 import org.eclipse.swt.widgets.Label;
107
108 import ca.odell.glazedlists.EventList;
109 import ca.odell.glazedlists.GlazedLists;
110 import ca.odell.glazedlists.SortedList;
111 import ca.odell.glazedlists.TreeList;
112 import eu.etaxonomy.cdm.api.conversation.ConversationHolder;
113 import eu.etaxonomy.cdm.api.conversation.IConversationEnabled;
114 import eu.etaxonomy.cdm.api.service.IWorkingSetService;
115 import eu.etaxonomy.cdm.model.common.TermVocabulary;
116 import eu.etaxonomy.cdm.model.description.DescriptionBase;
117 import eu.etaxonomy.cdm.model.description.DescriptionElementBase;
118 import eu.etaxonomy.cdm.model.description.Feature;
119 import eu.etaxonomy.cdm.model.description.FeatureTree;
120 import eu.etaxonomy.cdm.model.description.MeasurementUnit;
121 import eu.etaxonomy.cdm.model.description.SpecimenDescription;
122 import eu.etaxonomy.cdm.model.description.State;
123 import eu.etaxonomy.cdm.model.description.WorkingSet;
124 import eu.etaxonomy.cdm.model.occurrence.SpecimenOrObservationBase;
125 import eu.etaxonomy.cdm.persistence.hibernate.CdmDataChangeMap;
126 import eu.etaxonomy.taxeditor.editor.workingSet.matrix.categorical.CategoricalDataCellEditor;
127 import eu.etaxonomy.taxeditor.editor.workingSet.matrix.categorical.CategoricalDataDisplayConverter;
128 import eu.etaxonomy.taxeditor.editor.workingSet.matrix.quantitative.QuantitativeDataCellEditor;
129 import eu.etaxonomy.taxeditor.editor.workingSet.matrix.quantitative.QuantitativeDataDisplayConverter;
130 import eu.etaxonomy.taxeditor.editor.workingSet.matrix.supplementalInfo.SupplementalInfoDisplayConverter;
131 import eu.etaxonomy.taxeditor.model.DescriptionHelper;
132 import eu.etaxonomy.taxeditor.model.IDirtyMarkable;
133 import eu.etaxonomy.taxeditor.model.IPartContentHasDetails;
134 import eu.etaxonomy.taxeditor.model.ImageResources;
135 import eu.etaxonomy.taxeditor.model.MessagingUtils;
136 import eu.etaxonomy.taxeditor.session.ICdmEntitySession;
137 import eu.etaxonomy.taxeditor.session.ICdmEntitySessionEnabled;
138 import eu.etaxonomy.taxeditor.store.CdmStore;
139 import eu.etaxonomy.taxeditor.workbench.WorkbenchUtility;
140 import eu.etaxonomy.taxeditor.workbench.part.IE4SavablePart;
141
142 /**
143 * @author pplitzner
144 * @since Nov 26, 2017
145 *
146 */
147 public class CharacterMatrix implements IE4SavablePart, IPartContentHasDetails, IConversationEnabled, IDirtyMarkable,
148 ICdmEntitySessionEnabled{
149
150 private static final List<String> WS_PROPERTY_PATH = Arrays.asList(new String[] {
151 "descriptions", //$NON-NLS-1$
152 "descriptions.descriptionElements", //$NON-NLS-1$
153 "descriptions.descriptionElements.inDescription", //$NON-NLS-1$
154 "descriptions.descriptionElements.inDescription.descriptionElements", //$NON-NLS-1$
155 "descriptions.descriptionElements.feature", //$NON-NLS-1$
156 });
157
158 private static final String CHARACTER_MATRIX_STATE_PROPERTIES = "characterMatrixState.properties";
159
160 private static final int LEADING_COLUMN_COUNT = 4;
161 private static final String TAXON_COLUMN = "taxon_column";
162 private static final String COLLECTOR_COLUMN = "collector_column";
163 private static final String IDENTIFIER_COLUMN = "identifier_column";
164 private static final String COUNTRY_COLUMN = "country_column";
165
166 private WorkingSet workingSet;
167
168 private Composite parent;
169
170 private ConversationHolder conversation;
171
172 private ICdmEntitySession cdmEntitySession;
173
174 @Inject
175 private ESelectionService selService;
176
177 @Inject
178 private MDirtyable dirty;
179
180 @Inject
181 private MPart thisPart;
182
183 private NatTable natTable;
184
185 private Map<Integer, Feature> indexToFeatureMap = new HashMap<>();
186
187 private LinkedMap<String, String> propertyToLabelMap = new LinkedMap<>();
188
189 private Properties natTableState;
190
191 private EventList<Object> descriptions;
192
193 private Collection<SpecimenOrObservationBase> specimenCache = null;
194
195 private ListDataProvider<Object> bodyDataProvider;
196
197 @PostConstruct
198 public void create(Composite parent) {
199 if(CdmStore.isActive() && conversation==null){
200 conversation = CdmStore.createConversation();
201 }
202 if(cdmEntitySession == null){
203 cdmEntitySession = CdmStore.getCurrentSessionManager().newSession(this, true);
204 }
205 else{
206 return;
207 }
208 parent.setLayout(new GridLayout());
209 this.parent = parent;
210 }
211
212
213 public void init(UUID workingSetUuid) {
214 this.workingSet = CdmStore.getService(IWorkingSetService.class).load(workingSetUuid, WS_PROPERTY_PATH);
215 if(workingSet.getDescriptiveSystem()==null){
216 MessagingUtils.informationDialog("Editor could not be opened", "The working set has no feature tree selected.");
217 return;
218 }
219 thisPart.setLabel(workingSet.getLabel());
220
221 Composite toolbarComposite = new Composite(parent, SWT.NONE);
222 toolbarComposite.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
223 toolbarComposite.setLayout(new GridLayout(6, false));
224
225 //get features/columns stored in working set
226 FeatureTree tree = workingSet.getDescriptiveSystem();
227 List<Feature> features = new ArrayList<>(tree.getDistinctFeatures());
228 Collections.sort(features);
229
230 descriptions = GlazedLists.eventList(getDescriptions(workingSet));
231 // use the SortedList constructor with 'null' for the Comparator
232 // because the Comparator will be set by configuration
233 SortedList<Object> sortedList = new SortedList<>(descriptions, null);
234 // wrap the SortedList with the TreeList
235 TreeList treeList = new TreeList(sortedList, new DescriptionTreeFormat(workingSet.getMaxRank()), TreeList.NODES_START_EXPANDED);
236
237 ConfigRegistry configRegistry = new ConfigRegistry();
238
239 /**
240 * data provider
241 */
242 SpecimenColumnPropertyAccessor columnPropertyAccessor = new SpecimenColumnPropertyAccessor(this);
243 bodyDataProvider = new ListDataProvider<Object>(treeList, columnPropertyAccessor);
244
245 /**
246 * BODY layer
247 *
248 *
249
250 CompositeLayer
251 - (top) SummaryRowLayer
252 - (bottom) ViewportLayer
253
254 ^
255 ViewportLayer
256
257 ^
258 TreeLayer
259
260 ^
261 SelectionLayer
262
263 ^
264 RowHideShowLayer
265
266 ^
267 ColumnHideShowLayer
268
269 ^
270 ColumnReorderLayer
271
272 ^
273 RowReorderLayer
274
275 ^
276 DataLayer
277
278 *
279
280 */
281 DataLayer bodyDataLayer = new DataLayer(bodyDataProvider);
282
283 //register labels for columns
284 ColumnOverrideLabelAccumulator bodyColumnLabelAccumulator =new ColumnOverrideLabelAccumulator(bodyDataLayer);
285 bodyDataLayer.setConfigLabelAccumulator(bodyColumnLabelAccumulator);
286 propertyToLabelMap.put(TAXON_COLUMN, "Taxon");
287 bodyColumnLabelAccumulator.registerColumnOverrides(0, TAXON_COLUMN);
288 propertyToLabelMap.put(COLLECTOR_COLUMN, "Collector + No");
289 bodyColumnLabelAccumulator.registerColumnOverrides(1, COLLECTOR_COLUMN);
290 propertyToLabelMap.put(IDENTIFIER_COLUMN, "Identifier");
291 bodyColumnLabelAccumulator.registerColumnOverrides(2, IDENTIFIER_COLUMN);
292 propertyToLabelMap.put(COUNTRY_COLUMN, "Country");
293 bodyColumnLabelAccumulator.registerColumnOverrides(3, COUNTRY_COLUMN);
294 for(int i=0;i<features.size();i++){
295 Feature feature = features.get(i);
296 initLabels(bodyColumnLabelAccumulator, i, feature);
297 }
298
299 // layer for event handling of GlazedLists and PropertyChanges
300 GlazedListsEventLayer eventLayer = new GlazedListsEventLayer<>(bodyDataLayer, treeList);
301 GlazedListTreeData treeData = new GlazedListTreeData<>(treeList);
302 ITreeRowModel treeRowModel = new GlazedListTreeRowModel<>(treeData);
303
304 RowReorderLayer rowReorderLayer = new RowReorderLayer(eventLayer);
305 ColumnReorderLayer columnReorderLayer = new ColumnReorderLayer(rowReorderLayer);
306 ColumnHideShowLayer columnHideShowLayer = new ColumnHideShowLayer(columnReorderLayer);
307 RowHideShowLayer rowHideShowLayer = new RowHideShowLayer(columnHideShowLayer);
308 SelectionLayer selectionLayer = new SelectionLayer(rowHideShowLayer);
309 TreeLayer treeLayer = new TreeLayer(selectionLayer, treeRowModel);
310 ViewportLayer viewportLayer = new ViewportLayer(treeLayer);
311
312 // create a standalone FixedSummaryRowLayer
313 // since the summary row should be fixed at the top of the body
314 // region the horizontal dependency of the FixedSummaryRowLayer
315 // is the ViewportLayer
316 FixedSummaryRowLayer summaryRowLayer =
317 new FixedSummaryRowLayer(bodyDataLayer, viewportLayer, configRegistry, false);
318 //register labels with summary prefix for summary layer
319 ColumnOverrideLabelAccumulator summaryColumnLabelAccumulator =new ColumnOverrideLabelAccumulator(bodyDataLayer);
320 summaryRowLayer.setConfigLabelAccumulator(summaryColumnLabelAccumulator);
321 for(int i=0;i<features.size();i++){
322 Feature feature = features.get(i);
323 summaryColumnLabelAccumulator.registerColumnOverrides(
324 i+LEADING_COLUMN_COUNT,
325 SummaryRowLayer.DEFAULT_SUMMARY_COLUMN_CONFIG_LABEL_PREFIX+MatrixUtility.getProperty(feature));
326 }
327 // because the horizontal dependency is the ViewportLayer
328 // we need to set the composite dependency to false
329 summaryRowLayer.setHorizontalCompositeDependency(false);
330
331 CompositeLayer composite = new CompositeLayer(1, 2);
332 composite.setChildLayer("SUMMARY", summaryRowLayer, 0, 0);
333 composite.setChildLayer(GridRegion.BODY, viewportLayer, 0, 1);
334
335
336 /**
337 * column header layer
338 */
339 IDataProvider columnHeaderDataProvider = new DefaultColumnHeaderDataProvider(
340 propertyToLabelMap.values().toArray(new String[] {}), propertyToLabelMap);
341 DataLayer columnHeaderDataLayer = new DataLayer(columnHeaderDataProvider);
342 ColumnHeaderLayer columnHeaderLayer = new ColumnHeaderLayer(columnHeaderDataLayer, viewportLayer, selectionLayer);
343
344 // add the SortHeaderLayer to the column header layer stack
345 // as we use GlazedLists, we use the GlazedListsSortModel which
346 // delegates the sorting to the SortedList
347 //TODO currently disabled sorting because of tree list
348 // final SortHeaderLayer<SpecimenDescription> sortHeaderLayer = new SortHeaderLayer<>(
349 // columnHeaderLayer,
350 // new GlazedListsSortModel<>(
351 // sortedList,
352 // columnPropertyAccessor,
353 // configRegistry,
354 // columnHeaderDataLayer));
355
356
357 /**
358 * row header layer
359 */
360 IDataProvider rowHeaderDataProvider = new DefaultRowHeaderDataProvider(bodyDataProvider);
361 DefaultRowHeaderDataLayer rowHeaderDataLayer = new DefaultRowHeaderDataLayer(rowHeaderDataProvider);
362 FixedSummaryRowHeaderLayer fixedSummaryRowHeaderLayer = new FixedSummaryRowHeaderLayer(rowHeaderDataLayer,
363 composite, selectionLayer);
364 fixedSummaryRowHeaderLayer.setSummaryRowLabel("\u2211");
365
366
367 /**
368 * corner layer
369 */
370 ILayer cornerLayer = new CornerLayer(
371 new DataLayer(new DefaultCornerDataProvider(columnHeaderDataProvider, rowHeaderDataProvider)),
372 fixedSummaryRowHeaderLayer, columnHeaderLayer);
373
374
375 /**
376 * GRID layer (composition of all other layers)
377 */
378 GridLayer gridLayer = new GridLayer(composite, columnHeaderLayer, fixedSummaryRowHeaderLayer, cornerLayer);
379
380
381
382 natTable = new NatTable(parent, gridLayer, false);
383
384
385 /**
386 * CONFIGURATION
387 */
388 natTable.setConfigRegistry(configRegistry);
389
390
391 //add default configuration because autoconfigure is set to false in constructor
392 natTable.addConfiguration(new DefaultNatTableStyleConfiguration());
393
394 //FIXME: this is for DEBUG ONLY
395 // natTable.addConfiguration(new DebugMenuConfiguration(natTable));
396
397 // override the default sort configuration and change the mouse bindings
398 // to sort on a single click
399 //currently removed sorting because of using the TreeConfiguration
400 // natTable.addConfiguration(new SingleClickSortConfiguration());
401
402
403 // add the header menu configuration for adding the column header menu
404 // with hide/show actions
405 natTable.addConfiguration(new AbstractHeaderMenuConfiguration(natTable) {
406
407 @Override
408 protected PopupMenuBuilder createColumnHeaderMenu(NatTable natTable) {
409 return super.createColumnHeaderMenu(natTable)
410 .withHideColumnMenuItem()
411 .withShowAllColumnsMenuItem();
412 }
413
414 @Override
415 protected PopupMenuBuilder createRowHeaderMenu(NatTable natTable) {
416 return super.createRowHeaderMenu(natTable)
417 .withHideRowMenuItem()
418 .withShowAllRowsMenuItem();
419 }
420
421 @Override
422 protected PopupMenuBuilder createCornerMenu(NatTable natTable) {
423 return super.createCornerMenu(natTable)
424 .withShowAllColumnsMenuItem()
425 .withShowAllRowsMenuItem();
426 }
427 });
428
429 // add custom configuration for data conversion and add column labels to viewport layer
430 viewportLayer.addConfiguration(new AbstractRegistryConfiguration() {
431 @Override
432 public void configureRegistry(IConfigRegistry configRegistry) {
433 //add display converter for string representation
434 configRegistry.registerConfigAttribute(
435 CellConfigAttributes.DISPLAY_CONVERTER,
436 new SupplementalInfoDisplayConverter(),
437 DisplayMode.NORMAL,
438 TAXON_COLUMN);
439 configRegistry.registerConfigAttribute(
440 CellConfigAttributes.DISPLAY_CONVERTER,
441 new SupplementalInfoDisplayConverter(),
442 DisplayMode.NORMAL,
443 COLLECTOR_COLUMN);
444 configRegistry.registerConfigAttribute(
445 CellConfigAttributes.DISPLAY_CONVERTER,
446 new SupplementalInfoDisplayConverter(),
447 DisplayMode.NORMAL,
448 IDENTIFIER_COLUMN);
449 configRegistry.registerConfigAttribute(
450 CellConfigAttributes.DISPLAY_CONVERTER,
451 new SupplementalInfoDisplayConverter(),
452 DisplayMode.NORMAL,
453 COUNTRY_COLUMN);
454 features.forEach(feature->registerColumnConfiguration(feature, configRegistry));
455 }
456
457 });
458
459 //register aggregation configuration for each feature
460 features.forEach(feature->summaryRowLayer.addConfiguration(new AggregationConfiguration(bodyDataProvider, feature)));
461
462 // add the ExportCommandHandler to the ViewportLayer in order to make
463 // exporting work
464 viewportLayer.registerCommandHandler(new ExportCommandHandler(viewportLayer));
465
466 //propagate single cell selection
467 natTable.addLayerListener(new ILayerListener() {
468 @Override
469 public void handleLayerEvent(ILayerEvent event) {
470 if(event instanceof CellSelectionEvent){
471 CellSelectionEvent cellSelectionEvent = (CellSelectionEvent)event;
472 Collection<ILayerCell> selectedCells = cellSelectionEvent.getSelectionLayer().getSelectedCells();
473 StructuredSelection selection = new StructuredSelection();
474 if(selectedCells.size()==1){
475 ILayerCell cell = selectedCells.iterator().next();
476 Object dataValue = cell.getDataValue();
477 if(dataValue!=null){
478 selection = new StructuredSelection(dataValue);
479 }
480 }
481 selService.setSelection(selection);
482 }
483 }
484 });
485
486 natTable.configure();
487
488 GridDataFactory.fillDefaults().grab(true, true).applyTo(natTable);
489
490 /**
491 * Info label
492 */
493 Label wsLabel = new Label(toolbarComposite, SWT.NONE);
494 wsLabel.setText(workingSet.getLabel());
495 wsLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
496
497
498 /**
499 * Expand/Collapse button
500 */
501 Button collapseAllButton = new Button(toolbarComposite, SWT.PUSH);
502 collapseAllButton.setImage(ImageResources.getImage(ImageResources.COLLAPSE_ALL));
503 collapseAllButton.setToolTipText("Collapse all");
504 collapseAllButton.addSelectionListener(new SelectionAdapter() {
505 @Override
506 public void widgetSelected(SelectionEvent e) {
507 natTable.doCommand(new TreeCollapseAllCommand());
508 }
509 });
510 Button expandAllButton = new Button(toolbarComposite, SWT.PUSH);
511 expandAllButton.setImage(ImageResources.getImage(ImageResources.EXPAND_ALL));
512 expandAllButton.setToolTipText("Expand all");
513 expandAllButton.addSelectionListener(new SelectionAdapter() {
514 @Override
515 public void widgetSelected(SelectionEvent e) {
516 natTable.doCommand(new TreeExpandAllCommand());
517 }
518 });
519
520 /**
521 * Table state persistence
522 */
523 natTableState = new Properties();
524 //load persisted state
525 File statePropertiesFile = getStatePropertiesFile();
526 FileInputStream inputStream;
527 try {
528 inputStream = new FileInputStream(statePropertiesFile);
529 natTableState.load(inputStream);
530 } catch (IOException e) {
531 MessagingUtils.info("No initial state properties file found for character matrix");
532 }
533
534 DisplayPersistenceDialogCommandHandler handler =
535 new DisplayPersistenceDialogCommandHandler(natTableState, natTable);
536 gridLayer.registerCommandHandler(handler);
537 // create a combobox for showing the available view states
538 ComboViewer comboStates= new ComboViewer(toolbarComposite, SWT.DROP_DOWN) ;
539 Collection<String> availableStates = PersistenceHelper.getAvailableStates(natTableState);
540 comboStates.setLabelProvider(new LabelProvider(){
541 @Override
542 public String getText(Object element) {
543 if(element instanceof String && ((String) element).isEmpty()){
544 return "-default-";
545 }
546 return super.getText(element);
547 }
548 });
549 comboStates.setContentProvider(new ArrayContentProvider());
550 comboStates.addSelectionChangedListener(e->
551 {
552 int index = comboStates.getCombo().getSelectionIndex();
553 if (index >= 0) {
554 String selected = comboStates.getCombo().getItem(index);
555 // load the state
556 natTable.loadState(selected, natTableState);
557 natTableState.setProperty(PersistenceDialog.ACTIVE_VIEW_CONFIGURATION_KEY, selected);
558 }
559 });
560 comboStates.setInput(availableStates);
561 if(comboStates.getCombo().getItemCount()>0){
562 comboStates.getCombo().select(0);
563 }
564
565 // add listener to update the combo on view state management changes
566 handler.addStateChangeListener(new IStateChangedListener() {
567 @Override
568 public void handleStateChange(StateChangeEvent event) {
569 comboStates.setInput(PersistenceHelper.getAvailableStates(natTableState));
570 selectStateItem(comboStates, event.getViewConfigName());
571 }
572 });
573
574 // add button to show dialog
575 Button btnManageState = new Button(toolbarComposite, SWT.PUSH);
576 btnManageState.setImage(ImageResources.getImage(ImageResources.SETTINGS));
577 btnManageState.setToolTipText("View configuration");
578 btnManageState.addSelectionListener(new SelectionAdapter() {
579 @Override
580 public void widgetSelected(SelectionEvent e) {
581 natTable.doCommand(new DisplayPersistenceDialogCommand(natTable));
582 selectStateItem(comboStates, natTableState.get(PersistenceDialog.ACTIVE_VIEW_CONFIGURATION_KEY).toString());
583 }
584 });
585
586 /**
587 * excel export
588 */
589 Button btnExcelExport = new Button(toolbarComposite, SWT.PUSH);
590 btnExcelExport.setToolTipText("Export to Excel");
591 btnExcelExport.setImage(ImageResources.getImage(ImageResources.EXPORT));
592 btnExcelExport.addSelectionListener(new SelectionAdapter() {
593 @Override
594 public void widgetSelected(SelectionEvent e) {
595 natTable.doCommand(
596 new ExportCommand(
597 natTable.getConfigRegistry(),
598 natTable.getShell()));
599 }
600 });
601
602 /**
603 * bottom button toolbar
604 */
605 Composite buttonPanel = new Composite(parent, SWT.NONE);
606 buttonPanel.setLayout(new RowLayout());
607 GridDataFactory.fillDefaults().grab(true, false).applyTo(buttonPanel);
608
609 /**
610 * Add description button
611 */
612 Button btnAddDescription = new Button(buttonPanel, SWT.PUSH);
613 btnAddDescription.setImage(ImageResources.getImage(ImageResources.ADD_ICON));
614 btnAddDescription.addSelectionListener(new SelectionAdapter() {
615 @Override
616 public void widgetSelected(SelectionEvent e) {
617 SpecimenSelectionDialog dialog = new SpecimenSelectionDialog(natTable.getShell(), CharacterMatrix.this);
618 if(dialog.open()==Window.OK){
619 Collection<SpecimenOrObservationBase> specimens = dialog.getSpecimen();
620 boolean hasAdded = false;
621 for (SpecimenOrObservationBase specimen : specimens) {
622 SpecimenDescription description = getDescriptionForWorkingSet(specimen);
623 if(!workingSet.getDescriptions().contains(description)){
624 CharacterMatrix.this.descriptions.add(new RowWrapper(description));
625 workingSet.addDescription(description);
626 hasAdded = true;
627 }
628 }
629 if(hasAdded){
630 setDirty();
631 }
632 }
633 }
634 });
635
636 parent.layout();
637 }
638
639 private void selectStateItem(ComboViewer comboStates, String stateName){
640 String[] items = comboStates.getCombo().getItems();
641 for(int i=0;i<items.length;i++){
642 if(items[i].equals(stateName)){
643 comboStates.getCombo().select(i);
644 break;
645 }
646 }
647 }
648
649 private SpecimenDescription getDescriptionForWorkingSet(SpecimenOrObservationBase specimen){
650 Set<Feature> wsFeatures = workingSet.getDescriptiveSystem().getDistinctFeatures();
651 List<DescriptionElementBase> matchingDescriptionElements = new ArrayList<>();
652
653 for (SpecimenDescription specimenDescription : (Set<SpecimenDescription>) specimen.getDescriptions()) {
654 Set<Feature> specimenDescriptionFeatures = new HashSet<>();
655 //gather specimen description features and check for match with WS features
656 for (DescriptionElementBase specimenDescriptionElement : specimenDescription.getElements()) {
657 Feature feature = specimenDescriptionElement.getFeature();
658 specimenDescriptionFeatures.add(feature);
659 if(wsFeatures.contains(feature)){
660 matchingDescriptionElements.add(specimenDescriptionElement);
661 }
662 }
663 //if description with the exact same features is found return the description
664 if(specimenDescriptionFeatures.equals(wsFeatures)){
665 return specimenDescription;
666 }
667 }
668 //Create new specimen description if no match was found
669 setDirty();
670 SpecimenDescription newDesription = SpecimenDescription.NewInstance(specimen);
671 newDesription.setTitleCache("WorkingSet "+workingSet.getLabel()+": "+newDesription.generateTitle(), true);
672
673 //check for equals description element (same feature and same values)
674 Map<Feature, List<DescriptionElementBase>> featureToElementMap = new HashMap<>();
675 for(DescriptionElementBase element:matchingDescriptionElements){
676 List<DescriptionElementBase> list = featureToElementMap.get(element.getFeature());
677 if(list==null){
678 list = new ArrayList<>();
679 }
680 list.add(element);
681 featureToElementMap.put(element.getFeature(), list);
682 }
683 Set<DescriptionElementBase> descriptionElementsToClone = new HashSet<>();
684 for(Feature feature:featureToElementMap.keySet()){
685 List<DescriptionElementBase> elements = featureToElementMap.get(feature);
686 //no duplicate description elements found for this feature
687 if(elements.size()==1){
688 descriptionElementsToClone.add(elements.get(0));
689 }
690 //duplicates found -> check if all are equal
691 else{
692 DescriptionElementBase match = null;
693 for (DescriptionElementBase descriptionElementBase : elements) {
694 if(match==null){
695 match = descriptionElementBase;
696 }
697 else if(!new DescriptionElementCompareWrapper(match).equals(new DescriptionElementCompareWrapper(descriptionElementBase))){
698 match = null;
699 MessagingUtils.informationDialog("Multiple data found",
700 String.format("Multiple description elements with different values "
701 + "found for feature '%s'.\nData will not be copied to new specimen description.", feature.getLabel()));
702 break;
703 }
704 }
705 if(match!=null){
706 descriptionElementsToClone.add(match);
707 }
708 }
709 }
710 //clone matching descriptionElements
711 for (DescriptionElementBase descriptionElementBase : descriptionElementsToClone) {
712 DescriptionElementBase clone;
713 try {
714 clone = descriptionElementBase.clone(newDesription);
715 clone.getSources().forEach(source -> source.setOriginalNameString(DescriptionHelper.getLabel(descriptionElementBase)));
716 } catch (CloneNotSupportedException e) {
717 MessagingUtils.error(CharacterMatrix.class, e);
718 }
719 }
720 return newDesription;
721
722 }
723
724 private void initLabels(final ColumnOverrideLabelAccumulator columnLabelAccumulator,
725 int index, Feature feature) {
726
727 columnLabelAccumulator.registerColumnOverrides(index+LEADING_COLUMN_COUNT, MatrixUtility.getProperty(feature));
728 indexToFeatureMap.put(index+LEADING_COLUMN_COUNT, feature);
729
730 String featureLabel = feature.getLabel();
731 String property = featureLabel;
732 //show unit for quantitative data
733 if(feature.isSupportsQuantitativeData()){
734 Set<MeasurementUnit> recommendedMeasurementUnits = feature.getRecommendedMeasurementUnits();
735 if(recommendedMeasurementUnits.size()!=1){
736 MessagingUtils.warningDialog("Column initialization problem", CharacterMatrix.class,
737 String.format("Only one unit is allowed for quantitative data: %s", feature.getLabel()));
738 return;
739 }
740 MeasurementUnit unit = recommendedMeasurementUnits.iterator().next();
741 featureLabel += " ["+unit.getLabel()+"]";
742 }
743 propertyToLabelMap.put(property, featureLabel);
744 }
745
746 private void registerColumnConfiguration(Feature feature, IConfigRegistry configRegistry) {
747 //make cell editable
748 configRegistry.registerConfigAttribute(
749 EditConfigAttributes.CELL_EDITABLE_RULE,
750 IEditableRule.ALWAYS_EDITABLE,
751 DisplayMode.EDIT,
752 MatrixUtility.getProperty(feature)
753 );
754 if(feature.isSupportsQuantitativeData()){
755 //add display converter for string representation
756 configRegistry.registerConfigAttribute(
757 CellConfigAttributes.DISPLAY_CONVERTER,
758 new QuantitativeDataDisplayConverter(),
759 DisplayMode.NORMAL,
760 MatrixUtility.getProperty(feature));
761 //register quantitative editor
762 configRegistry.registerConfigAttribute(
763 EditConfigAttributes.CELL_EDITOR,
764 new QuantitativeDataCellEditor(feature, this),
765 DisplayMode.EDIT,
766 MatrixUtility.getProperty(feature));
767 }
768 else if(feature.isSupportsCategoricalData()){
769 //add display converter for string representation
770 configRegistry.registerConfigAttribute(
771 CellConfigAttributes.DISPLAY_CONVERTER,
772 new CategoricalDataDisplayConverter(),
773 DisplayMode.NORMAL,
774 MatrixUtility.getProperty(feature));
775
776 //add combo box cell editor
777 CategoricalDataCellEditor comboBoxCellEditor = new CategoricalDataCellEditor(new IComboBoxDataProvider() {
778
779 @Override
780 public List<?> getValues(int columnIndex, int rowIndex) {
781 List<State> states = new ArrayList<>();
782 Feature feature = indexToFeatureMap.get(columnIndex);
783 if(feature.isSupportsCategoricalData()){
784 Set<TermVocabulary<State>> stateVocs = feature.getSupportedCategoricalEnumerations();
785 for (TermVocabulary<State> voc : stateVocs) {
786 states.addAll(voc.getTerms());
787 }
788 }
789 return states;
790 }
791 }, 5, this, feature);
792 //register editor
793 configRegistry.registerConfigAttribute(EditConfigAttributes.CELL_EDITOR,
794 comboBoxCellEditor,
795 DisplayMode.EDIT,
796 MatrixUtility.getProperty(feature));
797
798 }
799
800 }
801
802 private List<RowWrapper> getDescriptions(WorkingSet workingSet) {
803 List<RowWrapper> rowWrappers = new ArrayList<>();
804 Set<DescriptionBase> wsDescriptions = workingSet.getDescriptions();
805 for (DescriptionBase descriptionBase : wsDescriptions) {
806 if(descriptionBase instanceof SpecimenDescription){
807 rowWrappers.add(new RowWrapper((SpecimenDescription) descriptionBase));
808 }
809 }
810 return rowWrappers;
811 }
812
813 public Map<Integer, Feature> getIndexToFeatureMap() {
814 return indexToFeatureMap;
815 }
816
817 public LinkedMap<String, String> getPropertyToLabelMap() {
818 return propertyToLabelMap;
819 }
820
821 public void setDirty() {
822 this.dirty.setDirty(true);
823 }
824
825 public NatTable getNatTable() {
826 return natTable;
827 }
828
829 public WorkingSet getWorkingSet() {
830 return workingSet;
831 }
832
833 public Collection<SpecimenOrObservationBase> getSpecimenCache() {
834 return specimenCache;
835 }
836
837 public void setSpecimenCache(Collection<SpecimenOrObservationBase> specimenCache) {
838 this.specimenCache = specimenCache;
839 }
840
841 /**
842 * @return the bodyDataProvider
843 */
844 public ListDataProvider<Object> getBodyDataProvider() {
845 return bodyDataProvider;
846 }
847
848 @Persist
849 @Override
850 public void save(IProgressMonitor monitor) {
851 CdmStore.getService(IWorkingSetService.class).merge(workingSet, true);
852 conversation.commit();
853 dirty.setDirty(false);
854 }
855
856 @Focus
857 public void setFocus(){
858 if(conversation!=null){
859 conversation.bind();
860 }
861 if(cdmEntitySession != null) {
862 cdmEntitySession.bind();
863 }
864 }
865
866 @PreDestroy
867 public void dispose(){
868 if (conversation != null) {
869 conversation.close();
870 conversation = null;
871 }
872 if(cdmEntitySession != null) {
873 cdmEntitySession.dispose();
874 cdmEntitySession = null;
875 }
876 dirty.setDirty(false);
877 if(natTableState!=null){
878 try (FileOutputStream tableStateStream =
879 new FileOutputStream(getStatePropertiesFile())) {
880 natTableState.store(tableStateStream, null);
881 } catch (IOException ioe) {
882 ioe.printStackTrace();
883 }
884 }
885 }
886
887 private File getStatePropertiesFile() {
888 return new File(WorkbenchUtility.getBaseLocation(), CHARACTER_MATRIX_STATE_PROPERTIES);
889 }
890
891 /**
892 * {@inheritDoc}
893 */
894 @Override
895 public void update(CdmDataChangeMap arg0) {
896 }
897
898 /**
899 * {@inheritDoc}
900 */
901 @Override
902 public ConversationHolder getConversationHolder() {
903 return conversation;
904 }
905
906 /**
907 * {@inheritDoc}
908 */
909 @Override
910 public void changed(Object element) {
911 setDirty();
912 natTable.refresh();
913 }
914
915 /**
916 * {@inheritDoc}
917 */
918 @Override
919 public void forceDirty() {
920 setDirty();
921 }
922
923
924 /**
925 * {@inheritDoc}
926 */
927 @Override
928 public ICdmEntitySession getCdmEntitySession() {
929 return cdmEntitySession;
930 }
931
932
933 /**
934 * {@inheritDoc}
935 */
936 @Override
937 public Collection<WorkingSet> getRootEntities() {
938 return Collections.singleton(this.workingSet);
939 }
940
941
942 /**
943 * {@inheritDoc}
944 */
945 @Override
946 public Map<Object, List<String>> getPropertyPathsMap() {
947 Map<Object, List<String>> propertyMap = new HashMap<>();
948 propertyMap.put(SpecimenOrObservationBase.class,WS_PROPERTY_PATH);
949 return propertyMap;
950 }
951
952 }