Project

General

Profile

Download (40.1 KB) Statistics
| Branch: | Tag: | Revision:
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.GlazedListsSortModel;
59
import org.eclipse.nebula.widgets.nattable.extension.glazedlists.tree.GlazedListTreeData;
60
import org.eclipse.nebula.widgets.nattable.extension.glazedlists.tree.GlazedListTreeRowModel;
61
import org.eclipse.nebula.widgets.nattable.grid.GridRegion;
62
import org.eclipse.nebula.widgets.nattable.grid.data.DefaultColumnHeaderDataProvider;
63
import org.eclipse.nebula.widgets.nattable.grid.data.DefaultCornerDataProvider;
64
import org.eclipse.nebula.widgets.nattable.grid.data.DefaultRowHeaderDataProvider;
65
import org.eclipse.nebula.widgets.nattable.grid.data.FixedSummaryRowHeaderLayer;
66
import org.eclipse.nebula.widgets.nattable.grid.layer.ColumnHeaderLayer;
67
import org.eclipse.nebula.widgets.nattable.grid.layer.CornerLayer;
68
import org.eclipse.nebula.widgets.nattable.grid.layer.DefaultRowHeaderDataLayer;
69
import org.eclipse.nebula.widgets.nattable.grid.layer.GridLayer;
70
import org.eclipse.nebula.widgets.nattable.hideshow.ColumnHideShowLayer;
71
import org.eclipse.nebula.widgets.nattable.hideshow.RowHideShowLayer;
72
import org.eclipse.nebula.widgets.nattable.layer.CompositeLayer;
73
import org.eclipse.nebula.widgets.nattable.layer.DataLayer;
74
import org.eclipse.nebula.widgets.nattable.layer.ILayer;
75
import org.eclipse.nebula.widgets.nattable.layer.ILayerListener;
76
import org.eclipse.nebula.widgets.nattable.layer.cell.ColumnOverrideLabelAccumulator;
77
import org.eclipse.nebula.widgets.nattable.layer.cell.ILayerCell;
78
import org.eclipse.nebula.widgets.nattable.layer.event.ILayerEvent;
79
import org.eclipse.nebula.widgets.nattable.persistence.PersistenceHelper;
80
import org.eclipse.nebula.widgets.nattable.persistence.command.DisplayPersistenceDialogCommand;
81
import org.eclipse.nebula.widgets.nattable.persistence.command.DisplayPersistenceDialogCommandHandler;
82
import org.eclipse.nebula.widgets.nattable.persistence.command.IStateChangedListener;
83
import org.eclipse.nebula.widgets.nattable.persistence.command.StateChangeEvent;
84
import org.eclipse.nebula.widgets.nattable.persistence.gui.PersistenceDialog;
85
import org.eclipse.nebula.widgets.nattable.reorder.ColumnReorderLayer;
86
import org.eclipse.nebula.widgets.nattable.reorder.RowReorderLayer;
87
import org.eclipse.nebula.widgets.nattable.selection.SelectionLayer;
88
import org.eclipse.nebula.widgets.nattable.selection.event.CellSelectionEvent;
89
import org.eclipse.nebula.widgets.nattable.sort.SortHeaderLayer;
90
import org.eclipse.nebula.widgets.nattable.sort.config.SingleClickSortConfiguration;
91
import org.eclipse.nebula.widgets.nattable.style.DisplayMode;
92
import org.eclipse.nebula.widgets.nattable.summaryrow.FixedSummaryRowLayer;
93
import org.eclipse.nebula.widgets.nattable.summaryrow.ISummaryProvider;
94
import org.eclipse.nebula.widgets.nattable.summaryrow.SummaryRowConfigAttributes;
95
import org.eclipse.nebula.widgets.nattable.summaryrow.SummaryRowLayer;
96
import org.eclipse.nebula.widgets.nattable.tree.ITreeRowModel;
97
import org.eclipse.nebula.widgets.nattable.tree.TreeLayer;
98
import org.eclipse.nebula.widgets.nattable.tree.command.TreeCollapseAllCommand;
99
import org.eclipse.nebula.widgets.nattable.tree.command.TreeExpandAllCommand;
100
import org.eclipse.nebula.widgets.nattable.ui.menu.AbstractHeaderMenuConfiguration;
101
import org.eclipse.nebula.widgets.nattable.ui.menu.PopupMenuBuilder;
102
import org.eclipse.nebula.widgets.nattable.viewport.ViewportLayer;
103
import org.eclipse.swt.SWT;
104
import org.eclipse.swt.events.SelectionAdapter;
105
import org.eclipse.swt.events.SelectionEvent;
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.api.conversation.ConversationHolder;
118
import eu.etaxonomy.cdm.api.conversation.IConversationEnabled;
119
import eu.etaxonomy.cdm.api.service.IWorkingSetService;
120
import eu.etaxonomy.cdm.model.common.TermVocabulary;
121
import eu.etaxonomy.cdm.model.description.DescriptionBase;
122
import eu.etaxonomy.cdm.model.description.DescriptionElementBase;
123
import eu.etaxonomy.cdm.model.description.Feature;
124
import eu.etaxonomy.cdm.model.description.FeatureTree;
125
import eu.etaxonomy.cdm.model.description.MeasurementUnit;
126
import eu.etaxonomy.cdm.model.description.SpecimenDescription;
127
import eu.etaxonomy.cdm.model.description.State;
128
import eu.etaxonomy.cdm.model.description.WorkingSet;
129
import eu.etaxonomy.cdm.model.occurrence.SpecimenOrObservationBase;
130
import eu.etaxonomy.cdm.persistence.hibernate.CdmDataChangeMap;
131
import eu.etaxonomy.taxeditor.editor.workingSet.matrix.categorical.CategoricalDataCellEditor;
132
import eu.etaxonomy.taxeditor.editor.workingSet.matrix.categorical.CategoricalDataDisplayConverter;
133
import eu.etaxonomy.taxeditor.editor.workingSet.matrix.quantitative.QuantitativeDataCellEditor;
134
import eu.etaxonomy.taxeditor.editor.workingSet.matrix.quantitative.QuantitativeDataDisplayConverter;
135
import eu.etaxonomy.taxeditor.editor.workingSet.matrix.supplementalInfo.SupplementalInfoDisplayConverter;
136
import eu.etaxonomy.taxeditor.model.DescriptionHelper;
137
import eu.etaxonomy.taxeditor.model.IDirtyMarkable;
138
import eu.etaxonomy.taxeditor.model.IPartContentHasDetails;
139
import eu.etaxonomy.taxeditor.model.ImageResources;
140
import eu.etaxonomy.taxeditor.model.MessagingUtils;
141
import eu.etaxonomy.taxeditor.session.ICdmEntitySession;
142
import eu.etaxonomy.taxeditor.session.ICdmEntitySessionEnabled;
143
import eu.etaxonomy.taxeditor.store.CdmStore;
144
import eu.etaxonomy.taxeditor.workbench.WorkbenchUtility;
145
import eu.etaxonomy.taxeditor.workbench.part.IE4SavablePart;
146

    
147
/**
148
 * @author pplitzner
149
 * @since Nov 26, 2017
150
 *
151
 */
152
public class CharacterMatrix implements IE4SavablePart, IPartContentHasDetails, IConversationEnabled, IDirtyMarkable,
153
ICdmEntitySessionEnabled{
154

    
155
    private static final List<String> WS_PROPERTY_PATH = Arrays.asList(new String[] {
156
            "descriptions", //$NON-NLS-1$
157
            "descriptions.descriptionElements", //$NON-NLS-1$
158
            "descriptions.descriptionElements.inDescription", //$NON-NLS-1$
159
            "descriptions.descriptionElements.inDescription.descriptionElements", //$NON-NLS-1$
160
            "descriptions.descriptionElements.feature", //$NON-NLS-1$
161
    });
162

    
163
    private static final String CHARACTER_MATRIX_STATE_PROPERTIES = "characterMatrixState.properties";
164

    
165
    private static final int LEADING_COLUMN_COUNT = 4;
166
    private static final String TAXON_COLUMN = "taxon_column";
167
    private static final String COLLECTOR_COLUMN = "collector_column";
168
    private static final String IDENTIFIER_COLUMN = "identifier_column";
169
    private static final String COUNTRY_COLUMN = "country_column";
170

    
171
    private WorkingSet workingSet;
172

    
173
    private Composite parent;
174

    
175
    private ConversationHolder conversation;
176

    
177
    private ICdmEntitySession cdmEntitySession;
178

    
179
    @Inject
180
    private ESelectionService selService;
181

    
182
    @Inject
183
    private MDirtyable dirty;
184

    
185
    @Inject
186
    private MPart thisPart;
187

    
188
    private NatTable natTable;
189

    
190
    private Map<Integer, Feature> indexToFeatureMap = new HashMap<>();
191

    
192
    private LinkedMap<String, String> propertyToLabelMap = new LinkedMap<>();
193

    
194
    private Properties natTableState;
195

    
196
    private EventList<Object> descriptions;
197

    
198
    private Collection<SpecimenOrObservationBase> specimenCache = null;
199

    
200
    private ListDataProvider<Object> bodyDataProvider;
201

    
202
    @PostConstruct
203
    public void create(Composite parent) {
204
        if(CdmStore.isActive() && conversation==null){
205
            conversation = CdmStore.createConversation();
206
        }
207
        if(cdmEntitySession == null){
208
            cdmEntitySession = CdmStore.getCurrentSessionManager().newSession(this, true);
209
        }
210
        else{
211
            return;
212
        }
213
        parent.setLayout(new GridLayout());
214
        this.parent = parent;
215
    }
216

    
217

    
218
    public void init(UUID workingSetUuid) {
219
        this.workingSet = CdmStore.getService(IWorkingSetService.class).load(workingSetUuid, WS_PROPERTY_PATH);
220
        if(workingSet.getDescriptiveSystem()==null){
221
            MessagingUtils.informationDialog("Editor could not be opened", "The working set has no feature tree selected.");
222
            return;
223
        }
224
        thisPart.setLabel(workingSet.getLabel());
225

    
226
        Composite toolbarComposite = new Composite(parent, SWT.NONE);
227
        toolbarComposite.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
228
        toolbarComposite.setLayout(new GridLayout(6, false));
229

    
230
        //get features/columns stored in working set
231
        FeatureTree tree = workingSet.getDescriptiveSystem();
232
        List<Feature> features = new ArrayList<>(tree.getDistinctFeatures());
233
        Collections.sort(features);
234

    
235
        descriptions = GlazedLists.eventList(getDescriptions(workingSet));
236
        // use the SortedList constructor with 'null' for the Comparator
237
        // because the Comparator will be set by configuration
238
        SortedList<Object> sortedList = new SortedList<>(descriptions, null);
239
        // wrap the SortedList with the TreeList
240
        TreeList treeList = new TreeList(sortedList, new DescriptionTreeFormat(), TreeList.NODES_START_EXPANDED);
241

    
242
        ConfigRegistry configRegistry = new ConfigRegistry();
243

    
244
        /**
245
         * data provider
246
         */
247
        SpecimenColumnPropertyAccessor columnPropertyAccessor = new SpecimenColumnPropertyAccessor(this);
248
        bodyDataProvider = new ListDataProvider<Object>(treeList, columnPropertyAccessor);
249

    
250
        /**
251
         * BODY layer
252
         *
253
         *
254

    
255
        CompositeLayer
256
         - (top) SummaryRowLayer
257
         - (bottom) ViewportLayer
258

    
259
             ^
260
        ViewportLayer
261

    
262
             ^
263
        SelectionLayer
264

    
265
             ^
266
        RowHideShowLayer
267

    
268
             ^
269
        ColumnHideShowLayer
270

    
271
             ^
272
        ColumnReorderLayer
273

    
274
             ^
275
        RowReorderLayer
276

    
277
             ^
278
        DataLayer
279

    
280
         *
281

    
282
         */
283
        DataLayer bodyDataLayer = new DataLayer(bodyDataProvider);
284

    
285
        //register labels for columns
286
        ColumnOverrideLabelAccumulator bodyColumnLabelAccumulator =new ColumnOverrideLabelAccumulator(bodyDataLayer);
287
        bodyDataLayer.setConfigLabelAccumulator(bodyColumnLabelAccumulator);
288
        propertyToLabelMap.put(TAXON_COLUMN, "Taxon");
289
        bodyColumnLabelAccumulator.registerColumnOverrides(0, TAXON_COLUMN);
290
        propertyToLabelMap.put(COLLECTOR_COLUMN, "Collector + No");
291
        bodyColumnLabelAccumulator.registerColumnOverrides(1, COLLECTOR_COLUMN);
292
        propertyToLabelMap.put(IDENTIFIER_COLUMN, "Identifier");
293
        bodyColumnLabelAccumulator.registerColumnOverrides(2, IDENTIFIER_COLUMN);
294
        propertyToLabelMap.put(COUNTRY_COLUMN, "Country");
295
        bodyColumnLabelAccumulator.registerColumnOverrides(3, COUNTRY_COLUMN);
296
        for(int i=0;i<features.size();i++){
297
            Feature feature = features.get(i);
298
            initLabels(bodyColumnLabelAccumulator, i, feature);
299
        }
300

    
301
        // layer for event handling of GlazedLists and PropertyChanges
302
        GlazedListsEventLayer eventLayer = new GlazedListsEventLayer<>(bodyDataLayer, treeList);
303
        GlazedListTreeData treeData = new GlazedListTreeData<>(treeList);
304
        ITreeRowModel treeRowModel = new GlazedListTreeRowModel<>(treeData);
305

    
306
        RowReorderLayer rowReorderLayer = new RowReorderLayer(eventLayer);
307
        ColumnReorderLayer columnReorderLayer = new ColumnReorderLayer(rowReorderLayer);
308
        ColumnHideShowLayer columnHideShowLayer = new ColumnHideShowLayer(columnReorderLayer);
309
        RowHideShowLayer rowHideShowLayer = new RowHideShowLayer(columnHideShowLayer);
310
        SelectionLayer selectionLayer = new SelectionLayer(rowHideShowLayer);
311
        TreeLayer treeLayer = new TreeLayer(selectionLayer, treeRowModel);
312
        ViewportLayer viewportLayer = new ViewportLayer(treeLayer);
313

    
314
        configRegistry.registerConfigAttribute(
315
                SummaryRowConfigAttributes.SUMMARY_PROVIDER,
316
                new ISummaryProvider() {
317

    
318
                    @Override
319
                    public Object summarize(int columnIndex) {
320
                        // TODO Auto-generated method stub
321
                        return null;
322
                    }
323
                },
324
                DisplayMode.NORMAL,
325
                COUNTRY_COLUMN);
326

    
327

    
328
        // create a standalone FixedSummaryRowLayer
329
        // since the summary row should be fixed at the top of the body
330
        // region the horizontal dependency of the FixedSummaryRowLayer
331
        // is the ViewportLayer
332
        FixedSummaryRowLayer summaryRowLayer =
333
                new FixedSummaryRowLayer(bodyDataLayer, viewportLayer, configRegistry, false);
334
        //register labels with summary prefix for summary layer
335
        ColumnOverrideLabelAccumulator summaryColumnLabelAccumulator =new ColumnOverrideLabelAccumulator(bodyDataLayer);
336
        summaryRowLayer.setConfigLabelAccumulator(summaryColumnLabelAccumulator);
337
        for(int i=0;i<features.size();i++){
338
            Feature feature = features.get(i);
339
            summaryColumnLabelAccumulator.registerColumnOverrides(
340
                    i+LEADING_COLUMN_COUNT,
341
                    SummaryRowLayer.DEFAULT_SUMMARY_COLUMN_CONFIG_LABEL_PREFIX+MatrixUtility.getProperty(feature));
342
        }
343
        // because the horizontal dependency is the ViewportLayer
344
        // we need to set the composite dependency to false
345
        summaryRowLayer.setHorizontalCompositeDependency(false);
346

    
347
        CompositeLayer composite = new CompositeLayer(1, 2);
348
        composite.setChildLayer("SUMMARY", summaryRowLayer, 0, 0);
349
        composite.setChildLayer(GridRegion.BODY, viewportLayer, 0, 1);
350

    
351

    
352

    
353

    
354
        /**
355
         * column header layer
356
         */
357
        IDataProvider columnHeaderDataProvider = new DefaultColumnHeaderDataProvider(
358
                propertyToLabelMap.values().toArray(new String[] {}), propertyToLabelMap);
359
        DataLayer columnHeaderDataLayer = new DataLayer(columnHeaderDataProvider);
360
        ColumnHeaderLayer columnHeaderLayer = new ColumnHeaderLayer(columnHeaderDataLayer, viewportLayer, selectionLayer);
361

    
362
        // add the SortHeaderLayer to the column header layer stack
363
        // as we use GlazedLists, we use the GlazedListsSortModel which
364
        // delegates the sorting to the SortedList
365
        final SortHeaderLayer<SpecimenDescription> sortHeaderLayer = new SortHeaderLayer<>(
366
                columnHeaderLayer,
367
                new GlazedListsSortModel<>(
368
                        sortedList,
369
                        columnPropertyAccessor,
370
                        configRegistry,
371
                        columnHeaderDataLayer));
372

    
373

    
374
        /**
375
         * row header layer
376
         */
377
        IDataProvider rowHeaderDataProvider = new DefaultRowHeaderDataProvider(bodyDataProvider);
378
        DefaultRowHeaderDataLayer rowHeaderDataLayer = new DefaultRowHeaderDataLayer(rowHeaderDataProvider);
379
        FixedSummaryRowHeaderLayer fixedSummaryRowHeaderLayer = new FixedSummaryRowHeaderLayer(rowHeaderDataLayer,
380
                composite, selectionLayer);
381
        fixedSummaryRowHeaderLayer.setSummaryRowLabel("\u2211");
382

    
383

    
384
        /**
385
         * corner layer
386
         */
387
        ILayer cornerLayer = new CornerLayer(
388
                new DataLayer(new DefaultCornerDataProvider(columnHeaderDataProvider, rowHeaderDataProvider)),
389
                fixedSummaryRowHeaderLayer, sortHeaderLayer);
390

    
391

    
392
        /**
393
         * GRID layer (composition of all other layers)
394
         */
395
        GridLayer gridLayer = new GridLayer(composite, sortHeaderLayer, fixedSummaryRowHeaderLayer, cornerLayer);
396

    
397

    
398

    
399
        natTable = new NatTable(parent, gridLayer, false);
400

    
401

    
402
        /**
403
         * CONFIGURATION
404
         */
405
        natTable.setConfigRegistry(configRegistry);
406

    
407

    
408
        //add default configuration because autoconfigure is set to false in constructor
409
        natTable.addConfiguration(new DefaultNatTableStyleConfiguration());
410

    
411
        //FIXME: this is for DEBUG ONLY
412
        //        natTable.addConfiguration(new DebugMenuConfiguration(natTable));
413

    
414
        // override the default sort configuration and change the mouse bindings
415
        // to sort on a single click
416
        natTable.addConfiguration(new SingleClickSortConfiguration());
417

    
418

    
419
        // add the header menu configuration for adding the column header menu
420
        // with hide/show actions
421
        natTable.addConfiguration(new AbstractHeaderMenuConfiguration(natTable) {
422

    
423
            @Override
424
            protected PopupMenuBuilder createColumnHeaderMenu(NatTable natTable) {
425
                return super.createColumnHeaderMenu(natTable)
426
                        .withHideColumnMenuItem()
427
                        .withShowAllColumnsMenuItem();
428
            }
429

    
430
            @Override
431
            protected PopupMenuBuilder createRowHeaderMenu(NatTable natTable) {
432
                return super.createRowHeaderMenu(natTable)
433
                        .withHideRowMenuItem()
434
                        .withShowAllRowsMenuItem();
435
            }
436

    
437
            @Override
438
            protected PopupMenuBuilder createCornerMenu(NatTable natTable) {
439
                return super.createCornerMenu(natTable)
440
                        .withShowAllColumnsMenuItem()
441
                        .withShowAllRowsMenuItem();
442
            }
443
        });
444

    
445
        // add custom configuration for data conversion and add column labels to viewport layer
446
        viewportLayer.addConfiguration(new AbstractRegistryConfiguration() {
447
            @Override
448
            public void configureRegistry(IConfigRegistry configRegistry) {
449
                //add display converter for string representation
450
                configRegistry.registerConfigAttribute(
451
                        CellConfigAttributes.DISPLAY_CONVERTER,
452
                        new SupplementalInfoDisplayConverter(),
453
                        DisplayMode.NORMAL,
454
                        TAXON_COLUMN);
455
                configRegistry.registerConfigAttribute(
456
                        CellConfigAttributes.DISPLAY_CONVERTER,
457
                        new SupplementalInfoDisplayConverter(),
458
                        DisplayMode.NORMAL,
459
                        COLLECTOR_COLUMN);
460
                configRegistry.registerConfigAttribute(
461
                        CellConfigAttributes.DISPLAY_CONVERTER,
462
                        new SupplementalInfoDisplayConverter(),
463
                        DisplayMode.NORMAL,
464
                        IDENTIFIER_COLUMN);
465
                configRegistry.registerConfigAttribute(
466
                        CellConfigAttributes.DISPLAY_CONVERTER,
467
                        new SupplementalInfoDisplayConverter(),
468
                        DisplayMode.NORMAL,
469
                        COUNTRY_COLUMN);
470
                features.forEach(feature->registerColumnConfiguration(feature, configRegistry));
471
            }
472

    
473
        });
474

    
475
        //register aggregation configuration for each feature
476
        features.forEach(feature->summaryRowLayer.addConfiguration(new AggregationConfiguration(bodyDataProvider, feature)));
477

    
478
        // add the ExportCommandHandler to the ViewportLayer in order to make
479
        // exporting work
480
        viewportLayer.registerCommandHandler(new ExportCommandHandler(viewportLayer));
481

    
482
        //propagate single cell selection
483
        natTable.addLayerListener(new ILayerListener() {
484
            @Override
485
            public void handleLayerEvent(ILayerEvent event) {
486
                if(event instanceof CellSelectionEvent){
487
                    CellSelectionEvent cellSelectionEvent = (CellSelectionEvent)event;
488
                    Collection<ILayerCell> selectedCells = cellSelectionEvent.getSelectionLayer().getSelectedCells();
489
                    StructuredSelection selection = new StructuredSelection();
490
                    if(selectedCells.size()==1){
491
                        ILayerCell cell = selectedCells.iterator().next();
492
                        Object dataValue = cell.getDataValue();
493
                        if(dataValue!=null){
494
                            selection = new StructuredSelection(dataValue);
495
                        }
496
                    }
497
                    selService.setSelection(selection);
498
                }
499
            }
500
        });
501

    
502
        natTable.configure();
503

    
504
        GridDataFactory.fillDefaults().grab(true, true).applyTo(natTable);
505

    
506
        /**
507
         * Info label
508
         */
509
        Label wsLabel = new Label(toolbarComposite, SWT.NONE);
510
        wsLabel.setText(workingSet.getLabel());
511
        wsLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
512

    
513

    
514
        /**
515
         * Expand/Collapse button
516
         */
517
        Button collapseAllButton = new Button(toolbarComposite, SWT.PUSH);
518
        collapseAllButton.setImage(ImageResources.getImage(ImageResources.COLLAPSE_ALL));
519
        collapseAllButton.setToolTipText("Collapse all");
520
        collapseAllButton.addSelectionListener(new SelectionAdapter() {
521
            @Override
522
            public void widgetSelected(SelectionEvent e) {
523
                natTable.doCommand(new TreeCollapseAllCommand());
524
            }
525
        });
526
        Button expandAllButton = new Button(toolbarComposite, SWT.PUSH);
527
        expandAllButton.setImage(ImageResources.getImage(ImageResources.EXPAND_ALL));
528
        expandAllButton.setToolTipText("Expand all");
529
        expandAllButton.addSelectionListener(new SelectionAdapter() {
530
            @Override
531
            public void widgetSelected(SelectionEvent e) {
532
                natTable.doCommand(new TreeExpandAllCommand());
533
            }
534
        });
535

    
536
        /**
537
         * Table state persistence
538
         */
539
        natTableState = new Properties();
540
        //load persisted state
541
        File statePropertiesFile = getStatePropertiesFile();
542
        FileInputStream inputStream;
543
        try {
544
            inputStream = new FileInputStream(statePropertiesFile);
545
            natTableState.load(inputStream);
546
        } catch (IOException e) {
547
            MessagingUtils.info("No initial state properties file found for character matrix");
548
        }
549

    
550
        DisplayPersistenceDialogCommandHandler handler =
551
                new DisplayPersistenceDialogCommandHandler(natTableState, natTable);
552
        gridLayer.registerCommandHandler(handler);
553
        // create a combobox for showing the available view states
554
        ComboViewer comboStates= new ComboViewer(toolbarComposite, SWT.DROP_DOWN) ;
555
        Collection<String> availableStates = PersistenceHelper.getAvailableStates(natTableState);
556
        comboStates.setLabelProvider(new LabelProvider(){
557
            @Override
558
            public String getText(Object element) {
559
                if(element instanceof String && ((String) element).isEmpty()){
560
                    return "-default-";
561
                }
562
                return super.getText(element);
563
            }
564
        });
565
        comboStates.setContentProvider(new ArrayContentProvider());
566
        comboStates.addSelectionChangedListener(e->
567
        {
568
            int index = comboStates.getCombo().getSelectionIndex();
569
            if (index >= 0) {
570
                String selected = comboStates.getCombo().getItem(index);
571
                // load the state
572
                natTable.loadState(selected, natTableState);
573
                natTableState.setProperty(PersistenceDialog.ACTIVE_VIEW_CONFIGURATION_KEY, selected);
574
            }
575
        });
576
        comboStates.setInput(availableStates);
577
        if(comboStates.getCombo().getItemCount()>0){
578
            comboStates.getCombo().select(0);
579
        }
580

    
581
        // add listener to update the combo on view state management changes
582
        handler.addStateChangeListener(new IStateChangedListener() {
583
            @Override
584
            public void handleStateChange(StateChangeEvent event) {
585
                comboStates.setInput(PersistenceHelper.getAvailableStates(natTableState));
586
                selectStateItem(comboStates, event.getViewConfigName());
587
            }
588
        });
589

    
590
        // add button to show dialog
591
        Button btnManageState = new Button(toolbarComposite, SWT.PUSH);
592
        btnManageState.setImage(ImageResources.getImage(ImageResources.SETTINGS));
593
        btnManageState.setToolTipText("View configuration");
594
        btnManageState.addSelectionListener(new SelectionAdapter() {
595
            @Override
596
            public void widgetSelected(SelectionEvent e) {
597
                natTable.doCommand(new DisplayPersistenceDialogCommand(natTable));
598
                selectStateItem(comboStates, natTableState.get(PersistenceDialog.ACTIVE_VIEW_CONFIGURATION_KEY).toString());
599
            }
600
        });
601

    
602
        /**
603
         * excel export
604
         */
605
        Button btnExcelExport = new Button(toolbarComposite, SWT.PUSH);
606
        btnExcelExport.setToolTipText("Export to Excel");
607
        btnExcelExport.setImage(ImageResources.getImage(ImageResources.EXPORT));
608
        btnExcelExport.addSelectionListener(new SelectionAdapter() {
609
            @Override
610
            public void widgetSelected(SelectionEvent e) {
611
                natTable.doCommand(
612
                        new ExportCommand(
613
                                natTable.getConfigRegistry(),
614
                                natTable.getShell()));
615
            }
616
        });
617

    
618
        /**
619
         * bottom button toolbar
620
         */
621
        Composite buttonPanel = new Composite(parent, SWT.NONE);
622
        buttonPanel.setLayout(new RowLayout());
623
        GridDataFactory.fillDefaults().grab(true, false).applyTo(buttonPanel);
624

    
625
        /**
626
         * Add description button
627
         */
628
        Button btnAddDescription = new Button(buttonPanel, SWT.PUSH);
629
        btnAddDescription.setImage(ImageResources.getImage(ImageResources.ADD_ICON));
630
        btnAddDescription.addSelectionListener(new SelectionAdapter() {
631
            @Override
632
            public void widgetSelected(SelectionEvent e) {
633
                SpecimenSelectionDialog dialog = new SpecimenSelectionDialog(natTable.getShell(), CharacterMatrix.this);
634
                if(dialog.open()==Window.OK){
635
                    Collection<SpecimenOrObservationBase> specimens = dialog.getSpecimen();
636
                    boolean hasAdded = false;
637
                    for (SpecimenOrObservationBase specimen : specimens) {
638
                        SpecimenDescription description = getDescriptionForWorkingSet(specimen);
639
                        if(!workingSet.getDescriptions().contains(description)){
640
                            CharacterMatrix.this.descriptions.add(new RowWrapper(description));
641
                            workingSet.addDescription(description);
642
                            hasAdded = true;
643
                        }
644
                    }
645
                    if(hasAdded){
646
                        setDirty();
647
                    }
648
                }
649
            }
650
        });
651

    
652
        parent.layout();
653
    }
654

    
655
    private void selectStateItem(ComboViewer comboStates, String stateName){
656
        String[] items = comboStates.getCombo().getItems();
657
        for(int i=0;i<items.length;i++){
658
            if(items[i].equals(stateName)){
659
                comboStates.getCombo().select(i);
660
                break;
661
            }
662
        }
663
    }
664

    
665
    private SpecimenDescription getDescriptionForWorkingSet(SpecimenOrObservationBase specimen){
666
        Set<Feature> wsFeatures = workingSet.getDescriptiveSystem().getDistinctFeatures();
667
        List<DescriptionElementBase> matchingDescriptionElements = new ArrayList<>();
668

    
669
        for (SpecimenDescription specimenDescription : (Set<SpecimenDescription>) specimen.getDescriptions()) {
670
            Set<Feature> specimenDescriptionFeatures = new HashSet<>();
671
            //gather specimen description features and check for match with WS features
672
            for (DescriptionElementBase specimenDescriptionElement : specimenDescription.getElements()) {
673
                Feature feature = specimenDescriptionElement.getFeature();
674
                specimenDescriptionFeatures.add(feature);
675
                if(wsFeatures.contains(feature)){
676
                    matchingDescriptionElements.add(specimenDescriptionElement);
677
                }
678
            }
679
            //if description with the exact same features is found return the description
680
            if(specimenDescriptionFeatures.equals(wsFeatures)){
681
                return specimenDescription;
682
            }
683
        }
684
        //Create new specimen description if no match was found
685
        setDirty();
686
        SpecimenDescription newDesription = SpecimenDescription.NewInstance(specimen);
687
        newDesription.setTitleCache("WorkingSet "+workingSet.getLabel()+": "+newDesription.generateTitle(), true);
688

    
689
        //check for equals description element (same feature and same values)
690
        Map<Feature, List<DescriptionElementBase>> featureToElementMap = new HashMap<>();
691
        for(DescriptionElementBase element:matchingDescriptionElements){
692
            List<DescriptionElementBase> list = featureToElementMap.get(element.getFeature());
693
            if(list==null){
694
                list = new ArrayList<>();
695
            }
696
            list.add(element);
697
            featureToElementMap.put(element.getFeature(), list);
698
        }
699
        Set<DescriptionElementBase> descriptionElementsToClone = new HashSet<>();
700
        for(Feature feature:featureToElementMap.keySet()){
701
            List<DescriptionElementBase> elements = featureToElementMap.get(feature);
702
            //no duplicate description elements found for this feature
703
            if(elements.size()==1){
704
                descriptionElementsToClone.add(elements.get(0));
705
            }
706
            //duplicates found -> check if all are equal
707
            else{
708
                DescriptionElementBase match = null;
709
                for (DescriptionElementBase descriptionElementBase : elements) {
710
                    if(match==null){
711
                        match = descriptionElementBase;
712
                    }
713
                    else if(!new DescriptionElementCompareWrapper(match).equals(new DescriptionElementCompareWrapper(descriptionElementBase))){
714
                        match = null;
715
                        MessagingUtils.informationDialog("Multiple data found",
716
                                String.format("Multiple description elements with different values "
717
                                        + "found for feature '%s'.\nData will not be copied to new specimen description.", feature.getLabel()));
718
                        break;
719
                    }
720
                }
721
                if(match!=null){
722
                    descriptionElementsToClone.add(match);
723
                }
724
            }
725
        }
726
        //clone matching descriptionElements
727
        for (DescriptionElementBase descriptionElementBase : descriptionElementsToClone) {
728
            DescriptionElementBase clone;
729
            try {
730
                clone = descriptionElementBase.clone(newDesription);
731
                clone.getSources().forEach(source -> source.setOriginalNameString(DescriptionHelper.getLabel(descriptionElementBase)));
732
            } catch (CloneNotSupportedException e) {
733
                MessagingUtils.error(CharacterMatrix.class, e);
734
            }
735
        }
736
        return newDesription;
737

    
738
    }
739

    
740
    private void initLabels(final ColumnOverrideLabelAccumulator columnLabelAccumulator,
741
            int index, Feature feature) {
742

    
743
        columnLabelAccumulator.registerColumnOverrides(index+LEADING_COLUMN_COUNT, MatrixUtility.getProperty(feature));
744
        indexToFeatureMap.put(index+LEADING_COLUMN_COUNT, feature);
745

    
746
        String featureLabel = feature.getLabel();
747
        String property = featureLabel;
748
        //show unit for quantitative data
749
        if(feature.isSupportsQuantitativeData()){
750
            Set<MeasurementUnit> recommendedMeasurementUnits = feature.getRecommendedMeasurementUnits();
751
            if(recommendedMeasurementUnits.size()!=1){
752
                MessagingUtils.warningDialog("Column initialization problem", CharacterMatrix.class,
753
                        String.format("Only one unit is allowed for quantitative data: %s", feature.getLabel()));
754
                return;
755
            }
756
            MeasurementUnit unit = recommendedMeasurementUnits.iterator().next();
757
            featureLabel += " ["+unit.getLabel()+"]";
758
        }
759
        propertyToLabelMap.put(property, featureLabel);
760
    }
761

    
762
    private void registerColumnConfiguration(Feature feature, IConfigRegistry configRegistry) {
763
        //make cell editable
764
        configRegistry.registerConfigAttribute(
765
                EditConfigAttributes.CELL_EDITABLE_RULE,
766
                IEditableRule.ALWAYS_EDITABLE,
767
                DisplayMode.EDIT,
768
                MatrixUtility.getProperty(feature)
769
                );
770
        if(feature.isSupportsQuantitativeData()){
771
            //add display converter for string representation
772
            configRegistry.registerConfigAttribute(
773
                    CellConfigAttributes.DISPLAY_CONVERTER,
774
                    new QuantitativeDataDisplayConverter(),
775
                    DisplayMode.NORMAL,
776
                    MatrixUtility.getProperty(feature));
777
            //register quantitative editor
778
            configRegistry.registerConfigAttribute(
779
                    EditConfigAttributes.CELL_EDITOR,
780
                    new QuantitativeDataCellEditor(feature, this),
781
                    DisplayMode.EDIT,
782
                    MatrixUtility.getProperty(feature));
783
        }
784
        else if(feature.isSupportsCategoricalData()){
785
            //add display converter for string representation
786
            configRegistry.registerConfigAttribute(
787
                    CellConfigAttributes.DISPLAY_CONVERTER,
788
                    new CategoricalDataDisplayConverter(),
789
                    DisplayMode.NORMAL,
790
                    MatrixUtility.getProperty(feature));
791

    
792
            //add combo box cell editor
793
            CategoricalDataCellEditor comboBoxCellEditor = new CategoricalDataCellEditor(new IComboBoxDataProvider() {
794

    
795
                @Override
796
                public List<?> getValues(int columnIndex, int rowIndex) {
797
                    List<State> states = new ArrayList<>();
798
                    Feature feature = indexToFeatureMap.get(columnIndex);
799
                    if(feature.isSupportsCategoricalData()){
800
                        Set<TermVocabulary<State>> stateVocs = feature.getSupportedCategoricalEnumerations();
801
                        for (TermVocabulary<State> voc : stateVocs) {
802
                            states.addAll(voc.getTerms());
803
                        }
804
                    }
805
                    return states;
806
                }
807
            }, 5, this, feature);
808
            //register editor
809
            configRegistry.registerConfigAttribute(EditConfigAttributes.CELL_EDITOR,
810
                    comboBoxCellEditor,
811
                    DisplayMode.EDIT,
812
                    MatrixUtility.getProperty(feature));
813

    
814
        }
815

    
816
    }
817

    
818
    private List<RowWrapper> getDescriptions(WorkingSet workingSet) {
819
        List<RowWrapper> rowWrappers = new ArrayList<>();
820
        Set<DescriptionBase> wsDescriptions = workingSet.getDescriptions();
821
        for (DescriptionBase descriptionBase : wsDescriptions) {
822
            if(descriptionBase instanceof SpecimenDescription){
823
                rowWrappers.add(new RowWrapper((SpecimenDescription) descriptionBase));
824
            }
825
        }
826
        return rowWrappers;
827
    }
828

    
829
    public Map<Integer, Feature> getIndexToFeatureMap() {
830
        return indexToFeatureMap;
831
    }
832

    
833
    public LinkedMap<String, String> getPropertyToLabelMap() {
834
        return propertyToLabelMap;
835
    }
836

    
837
    public void setDirty() {
838
        this.dirty.setDirty(true);
839
    }
840

    
841
    public NatTable getNatTable() {
842
        return natTable;
843
    }
844

    
845
    public WorkingSet getWorkingSet() {
846
        return workingSet;
847
    }
848

    
849
    public Collection<SpecimenOrObservationBase> getSpecimenCache() {
850
        return specimenCache;
851
    }
852

    
853
    public void setSpecimenCache(Collection<SpecimenOrObservationBase> specimenCache) {
854
        this.specimenCache = specimenCache;
855
    }
856

    
857
    /**
858
     * @return the bodyDataProvider
859
     */
860
    public ListDataProvider<Object> getBodyDataProvider() {
861
        return bodyDataProvider;
862
    }
863

    
864
    @Persist
865
    @Override
866
    public void save(IProgressMonitor monitor) {
867
        CdmStore.getService(IWorkingSetService.class).merge(workingSet, true);
868
        conversation.commit();
869
        dirty.setDirty(false);
870
    }
871

    
872
    @Focus
873
    public void setFocus(){
874
        if(conversation!=null){
875
            conversation.bind();
876
        }
877
        if(cdmEntitySession != null) {
878
            cdmEntitySession.bind();
879
        }
880
    }
881

    
882
    @PreDestroy
883
    public void dispose(){
884
        if (conversation != null) {
885
            conversation.close();
886
            conversation = null;
887
        }
888
        if(cdmEntitySession != null) {
889
            cdmEntitySession.dispose();
890
            cdmEntitySession = null;
891
        }
892
        dirty.setDirty(false);
893
        if(natTableState!=null){
894
            try (FileOutputStream tableStateStream =
895
                    new FileOutputStream(getStatePropertiesFile())) {
896
                natTableState.store(tableStateStream, null);
897
            } catch (IOException ioe) {
898
                ioe.printStackTrace();
899
            }
900
        }
901
    }
902

    
903
    private File getStatePropertiesFile() {
904
        return new File(WorkbenchUtility.getBaseLocation(), CHARACTER_MATRIX_STATE_PROPERTIES);
905
    }
906

    
907
    /**
908
     * {@inheritDoc}
909
     */
910
    @Override
911
    public void update(CdmDataChangeMap arg0) {
912
    }
913

    
914
    /**
915
     * {@inheritDoc}
916
     */
917
    @Override
918
    public ConversationHolder getConversationHolder() {
919
        return conversation;
920
    }
921

    
922
    /**
923
     * {@inheritDoc}
924
     */
925
    @Override
926
    public void changed(Object element) {
927
        setDirty();
928
        natTable.refresh();
929
    }
930

    
931
    /**
932
     * {@inheritDoc}
933
     */
934
    @Override
935
    public void forceDirty() {
936
        setDirty();
937
    }
938

    
939

    
940
    /**
941
     * {@inheritDoc}
942
     */
943
    @Override
944
    public ICdmEntitySession getCdmEntitySession() {
945
        return cdmEntitySession;
946
    }
947

    
948

    
949
    /**
950
     * {@inheritDoc}
951
     */
952
    @Override
953
    public Collection<WorkingSet> getRootEntities() {
954
        return Collections.singleton(this.workingSet);
955
    }
956

    
957

    
958
    /**
959
     * {@inheritDoc}
960
     */
961
    @Override
962
    public Map<Object, List<String>> getPropertyPathsMap() {
963
        Map<Object, List<String>> propertyMap = new HashMap<>();
964
        propertyMap.put(SpecimenOrObservationBase.class,WS_PROPERTY_PATH);
965
        return propertyMap;
966
    }
967

    
968
}
(2-2/10)