]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.charts/src/org/simantics/charts/ui/CSVExportPage.java
Improved subscription CSV export wizard page initial selection handling
[simantics/platform.git] / bundles / org.simantics.charts / src / org / simantics / charts / ui / CSVExportPage.java
1 /*******************************************************************************
2  * Copyright (c) 2012 Association for Decentralized Information Management in
3  * Industry THTH ry.
4  * All rights reserved. This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License v1.0
6  * which accompanies this distribution, and is available at
7  * http://www.eclipse.org/legal/epl-v10.html
8  *
9  * Contributors:
10  *     VTT Technical Research Centre of Finland - initial API and implementation
11  *******************************************************************************/
12 package org.simantics.charts.ui;
13
14 import java.io.File;
15 import java.lang.reflect.InvocationTargetException;
16 import java.util.ArrayList;
17 import java.util.Collection;
18 import java.util.Collections;
19 import java.util.Comparator;
20 import java.util.HashMap;
21 import java.util.HashSet;
22 import java.util.List;
23 import java.util.Map;
24 import java.util.Set;
25 import java.util.stream.Collectors;
26
27 import org.eclipse.core.runtime.IProgressMonitor;
28 import org.eclipse.core.runtime.SubMonitor;
29 import org.eclipse.core.runtime.preferences.InstanceScope;
30 import org.eclipse.jface.layout.GridDataFactory;
31 import org.eclipse.jface.layout.GridLayoutFactory;
32 import org.eclipse.jface.preference.IPreferenceStore;
33 import org.eclipse.jface.wizard.WizardPage;
34 import org.eclipse.swt.SWT;
35 import org.eclipse.swt.custom.CCombo;
36 import org.eclipse.swt.custom.ScrolledComposite;
37 import org.eclipse.swt.events.ModifyListener;
38 import org.eclipse.swt.events.SelectionAdapter;
39 import org.eclipse.swt.events.SelectionEvent;
40 import org.eclipse.swt.events.SelectionListener;
41 import org.eclipse.swt.layout.GridData;
42 import org.eclipse.swt.widgets.Button;
43 import org.eclipse.swt.widgets.Composite;
44 import org.eclipse.swt.widgets.FileDialog;
45 import org.eclipse.swt.widgets.Group;
46 import org.eclipse.swt.widgets.Label;
47 import org.eclipse.swt.widgets.Table;
48 import org.eclipse.swt.widgets.TableItem;
49 import org.eclipse.swt.widgets.Text;
50 import org.eclipse.ui.preferences.ScopedPreferenceStore;
51 import org.simantics.NameLabelUtil;
52 import org.simantics.Simantics;
53 import org.simantics.browsing.ui.common.ColumnKeys;
54 import org.simantics.charts.ontology.ChartResource;
55 import org.simantics.databoard.Bindings;
56 import org.simantics.databoard.parser.StringEscapeUtils;
57 import org.simantics.db.ReadGraph;
58 import org.simantics.db.Resource;
59 import org.simantics.db.common.NamedResource;
60 import org.simantics.db.common.request.IsParent;
61 import org.simantics.db.exception.DatabaseException;
62 import org.simantics.db.layer0.SelectionHints;
63 import org.simantics.db.layer0.request.ProjectModels;
64 import org.simantics.db.request.Read;
65 import org.simantics.history.csv.ColumnSeparator;
66 import org.simantics.history.csv.DecimalSeparator;
67 import org.simantics.history.csv.ExportInterpolation;
68 import org.simantics.layer0.Layer0;
69 import org.simantics.modeling.ModelingResources;
70 import org.simantics.modeling.ModelingUtils;
71 import org.simantics.modeling.preferences.CSVPreferences;
72 import org.simantics.modeling.ui.modelBrowser2.label.SubscriptionItemLabelRule;
73 import org.simantics.utils.datastructures.Arrays;
74 import org.simantics.utils.strings.AlphanumComparator;
75 import org.simantics.utils.ui.ISelectionUtils;
76 import org.simantics.utils.ui.SWTUtils;
77 import org.slf4j.Logger;
78 import org.slf4j.LoggerFactory;
79
80
81 /**
82  * @author Antti Villberg
83  */
84 public class CSVExportPage extends WizardPage {
85
86     private static final Logger LOGGER = LoggerFactory.getLogger(CSVExportPage.class);
87
88     private static class Model {
89         private static final Comparator<NamedResource> COMP = 
90                 (o1,o2) -> AlphanumComparator.CASE_INSENSITIVE_COMPARATOR.compare(
91                         o1.getName(), o2.getName());
92
93         public final NamedResource model;
94         public List<NamedResource> sortedSubs = new ArrayList<>();
95         public Map<Resource, NamedResource> subs = new HashMap<>();
96         public Map<Resource, Resource> chartItemsToSubs = new HashMap<>();
97         public Set<NamedResource> initiallySelectedSubscriptions = Collections.emptySet();
98
99         public Model(NamedResource model) {
100             this.model = model;
101         }
102
103         public void initialize(ReadGraph graph, Collection<Resource> inputSelection) throws DatabaseException {
104             Layer0 L0 = Layer0.getInstance(graph);
105             ModelingResources MOD = ModelingResources.getInstance(graph);
106             ChartResource CHART = ChartResource.getInstance(graph);
107             SubscriptionItemLabelRule rule = new SubscriptionItemLabelRule();
108
109             Resource m = model.getResource();
110             String name = graph.getPossibleRelatedValue(m, L0.HasName, Bindings.STRING);
111             if (name == null)
112                 return;
113
114             name = NameLabelUtil.modalName(graph, m);
115             for (Resource item : ModelingUtils.searchByTypeShallow(graph, m, MOD.Subscription_Item)) {
116                 String subscriptionLabel = null;
117                 Resource subscription = graph.getPossibleObject(item, L0.PartOf);
118                 if (subscription != null)
119                     subscriptionLabel = graph.getPossibleRelatedValue(subscription, L0.HasLabel, Bindings.STRING);
120                 String label = rule.getLabel(graph, item).get(ColumnKeys.SINGLE); 
121                 if (label == null)
122                     continue;
123                 if (subscriptionLabel != null)
124                     label = subscriptionLabel + "/" + label;
125                 subs.put(item, new NamedResource(label, item));
126             }
127             for (Resource cItem : ModelingUtils.searchByTypeShallow(graph, m, CHART.Chart_Item)) {
128                 Resource sItem = graph.getPossibleObject(cItem, CHART.Chart_Item_HasSubscriptionItem);
129                 if (sItem != null && subs.containsKey(sItem))
130                     chartItemsToSubs.put(cItem, sItem);
131             }
132
133             sortedSubs = subs.values().stream().sorted(COMP).collect(Collectors.toList());
134             initiallySelectedSubscriptions = initiallySelectedSubscriptions(graph, inputSelection);
135         }
136
137         private Set<NamedResource> initiallySelectedSubscriptions(ReadGraph graph, Collection<Resource> inputSelection) throws DatabaseException {
138             if (inputSelection == null)
139                 return Collections.emptySet();
140
141             HashSet<NamedResource> result = new HashSet<>();
142
143             for (Resource i : inputSelection) {
144                 for (NamedResource nr : sortedSubs)
145                     if (graph.syncRequest(new IsParent(i, nr.getResource())))
146                         result.add(nr);
147                 for (Map.Entry<Resource, Resource> cs : chartItemsToSubs.entrySet())
148                     if (graph.syncRequest(new IsParent(i, cs.getKey())))
149                         result.add( subs.get( cs.getValue() ) );
150             }
151
152             return result;
153         }
154     }
155
156     CSVExportPlan       exportModel;
157     CCombo              model;
158     Table               item;
159     Button              selectAllItems;
160     SelectionAdapter    selectAllItemsListener;
161     CCombo              exportLocation;
162
163     CCombo decimalSeparator;
164     CCombo columnSeparator;
165     CCombo sampling;
166     Group resampling;
167     Text timeStep;
168     Text startTime;
169     Text timeStamps;
170     CCombo samplingMode;
171     Text singlePrecision;
172     Text doublePrecision;
173     Button overwrite;
174
175     Collection<Resource> initialSelection;
176     List<Model> models = Collections.emptyList();
177
178     ModifyListener m = (e) -> validatePage();
179
180     SelectionListener s = new SelectionAdapter() {
181         @Override
182         public void widgetSelected(SelectionEvent e) {
183             validatePage();
184         }
185     };
186
187     protected CSVExportPage(CSVExportPlan model) {
188         super("Export CSV Data", "Define Export Properties", null);
189         this.exportModel = model;
190     }
191
192     @Override
193     public void createControl(Composite parent) {
194         ScrolledComposite scroller = new ScrolledComposite(parent, SWT.V_SCROLL);
195         scroller.setExpandHorizontal(true);
196         scroller.setExpandVertical(true);
197
198         Composite container = new Composite(scroller, SWT.NONE);
199         scroller.setContent(container);
200         GridLayoutFactory.swtDefaults().spacing(20, 10).numColumns(3).applyTo(container);
201         new Label(container, SWT.NONE).setText("Select a model:");
202         model = new CCombo(container, SWT.BORDER);
203         {
204             model.setEditable(false);
205             model.setText("");
206             model.setToolTipText("Selects the Model To Export From");
207             GridDataFactory.fillDefaults().grab(true, false).span(2, 1).applyTo(model);
208         }
209
210         new Label(container, SWT.NONE).setText("Exported items:");
211         item = new Table(container, SWT.BORDER | SWT.MULTI | SWT.FULL_SELECTION | SWT.CHECK);
212         {
213             item.setToolTipText("Selects the Subscription Items");
214             GridDataFactory.fillDefaults().grab(true, true).span(2, 1).hint(SWT.DEFAULT, 105).applyTo(item);
215         }
216         item.addSelectionListener(new SelectionAdapter() {
217             @Override
218             public void widgetSelected(SelectionEvent e) {
219                 if (e.detail == SWT.CHECK) {
220                     TableItem[] selected = item.getSelection();
221                     TableItem it = (TableItem) e.item;
222                     boolean checkedWasSelected = Arrays.contains(selected, it);
223                     if (checkedWasSelected) {
224                         boolean check = it.getChecked();
225                         for (TableItem i : selected)
226                             i.setChecked(check);
227                     }
228                     int checked = countCheckedItems(item);
229                     int totalItems = item.getItemCount();
230                     updateSelectAll(checked > 0, checked < totalItems, false);
231                     validatePage();
232                 }
233             }
234         });
235         new Label(container, 0);
236         selectAllItems = new Button(container, SWT.CHECK);
237         {
238             selectAllItems.setText("&Select All");
239             selectAllItems.setToolTipText("Select/Deselect All Listed Subscription Items");
240             GridDataFactory.fillDefaults().grab(true, false).span(2, 1).applyTo(selectAllItems);
241         }
242         selectAllItemsListener = new SelectionAdapter() {
243             @Override
244             public void widgetSelected(SelectionEvent e) {
245                 boolean select = selectAllItems.getSelection();
246                 updateSelectAll(select, false, false);
247                 item.setRedraw(false);
248                 for (TableItem it : item.getItems())
249                     it.setChecked(select);
250                 item.setRedraw(true);
251
252                 validatePage();
253             }
254         };
255         selectAllItems.addSelectionListener(selectAllItemsListener);
256
257         new Label(container, SWT.NONE).setText("&Output file:");
258         exportLocation = new CCombo(container, SWT.BORDER);
259         {
260             exportLocation.setText("");
261             GridDataFactory.fillDefaults().grab(true, false).span(1, 1).applyTo(exportLocation);
262             exportLocation.addModifyListener(m);
263         }
264         Button browseFileButton = new Button(container, SWT.PUSH);
265         {
266             browseFileButton.setText("Browse...");
267             browseFileButton.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false));
268             browseFileButton.addSelectionListener(new SelectionAdapter() {
269                 @Override
270                 public void widgetSelected(SelectionEvent e) {
271                     FileDialog dialog = new FileDialog(getShell(), SWT.SAVE);
272                     dialog.setText("Choose Output File");
273                     dialog.setFilterPath(new File(exportLocation.getText()).getParent());
274                     dialog.setFilterExtensions(new String[] { "*.csv" });
275                     dialog.setFilterNames(new String[] { "Comma separated values (*.csv)" });
276                     dialog.setOverwrite(false);
277                     String file = dialog.open();
278                     if (file == null)
279                         return;
280                     exportLocation.setText(file);
281                     validatePage();
282                 }
283             });
284         }
285
286         
287         Label horizRule = new Label(container, SWT.BORDER);
288         GridDataFactory.fillDefaults().hint(SWT.DEFAULT, 0).grab(true, false).span(3, 1).applyTo(horizRule);
289
290         new Label(container, SWT.NONE).setText("&Decimal separator:");
291         decimalSeparator = new CCombo(container, SWT.READ_ONLY | SWT.BORDER);
292         for(DecimalSeparator s : DecimalSeparator.values())
293             decimalSeparator.add(s.label);
294         decimalSeparator.select(0);
295         decimalSeparator.addSelectionListener(s);
296         GridDataFactory.fillDefaults().grab(true, false).span(2, 1).applyTo(decimalSeparator);
297
298         new Label(container, SWT.NONE).setText("&Column separator:");
299         columnSeparator = new CCombo(container, SWT.READ_ONLY | SWT.BORDER);
300         for(ColumnSeparator s : ColumnSeparator.values())
301             columnSeparator.add(s.label);
302         columnSeparator.select(0);
303         columnSeparator.addSelectionListener(s);
304         GridDataFactory.fillDefaults().grab(true, false).span(2, 1).applyTo(columnSeparator);
305
306         new Label(container, SWT.NONE).setText("Sampling:");
307         sampling = new CCombo(container, SWT.READ_ONLY | SWT.BORDER);
308         sampling.add("Recorded samples");
309         sampling.add("Resampled");
310         sampling.select(0);
311         sampling.addSelectionListener(s);
312         GridDataFactory.fillDefaults().grab(true, false).span(2, 1).applyTo(sampling);
313
314         resampling = new Group(container, SWT.NONE);
315                 resampling.setText("Resampling settings (not used with recorded samples)");
316         GridDataFactory.fillDefaults().grab(true, false).span(3, 1).applyTo(resampling);
317         GridLayoutFactory.swtDefaults().numColumns(3).applyTo(resampling);
318
319         new Label(resampling, SWT.NONE).setText("&Start time:");
320         startTime = new Text(resampling, SWT.BORDER);
321         startTime.addModifyListener(m);
322         GridDataFactory.fillDefaults().grab(true, false).applyTo(startTime);
323         new Label(resampling, SWT.NONE).setText(" seconds");
324
325         new Label(resampling, SWT.NONE).setText("&Time step:");
326         timeStep = new Text(resampling, SWT.BORDER);
327         timeStep.addModifyListener(m);
328         GridDataFactory.fillDefaults().grab(true, false).applyTo(timeStep);
329         new Label(resampling, SWT.NONE).setText(" seconds");
330         
331         new Label(resampling, SWT.NONE).setText("Sampling mode:");
332         samplingMode = new CCombo(resampling, SWT.READ_ONLY | SWT.BORDER);
333         samplingMode.add("Linear interpolation");
334         samplingMode.add("Previous sample");
335         samplingMode.select(0);
336         samplingMode.addSelectionListener(s);
337         GridDataFactory.fillDefaults().grab(true, false).span(2,1).applyTo(samplingMode);
338
339         Group digits = new Group(container, SWT.NONE);
340         digits.setText("Significant digits");
341         GridDataFactory.fillDefaults().grab(true, false).span(3, 1).applyTo(digits);
342         GridLayoutFactory.swtDefaults().numColumns(2).applyTo(digits);
343
344         new Label(digits, SWT.NONE).setText("&Time stamps:");
345         timeStamps = new Text(digits, SWT.BORDER);
346         timeStamps.addModifyListener(m);
347         GridDataFactory.fillDefaults().grab(true, false).applyTo(timeStamps);
348         
349         new Label(digits, SWT.NONE).setText("&Single precision floating point:");
350         singlePrecision = new Text(digits, SWT.BORDER);
351         singlePrecision.addModifyListener(m);
352         GridDataFactory.fillDefaults().grab(true, false).applyTo(singlePrecision);
353
354         new Label(digits, SWT.NONE).setText("&Double precision floating point:");
355         doublePrecision = new Text(digits, SWT.BORDER);
356         doublePrecision.addModifyListener(m);
357         GridDataFactory.fillDefaults().grab(true, false).applyTo(doublePrecision);
358
359         overwrite = new Button(container, SWT.CHECK);
360         overwrite.setText("&Overwrite existing files without warning");
361         overwrite.setSelection(exportModel.overwrite);
362         GridDataFactory.fillDefaults().grab(true, false).span(3, 1).applyTo(overwrite);
363         overwrite.addSelectionListener(new SelectionAdapter() {
364             @Override
365             public void widgetSelected(SelectionEvent e) {
366                 validatePage();
367             }
368         });
369
370         model.addSelectionListener(s);
371
372         initialSelection = ISelectionUtils.getPossibleKeys(exportModel.selection, SelectionHints.KEY_MAIN, Resource.class);
373         initializeWidgetsFromPreferences();
374         initializeData();
375
376         scroller.setMinSize(container.computeSize(SWT.DEFAULT, SWT.DEFAULT));
377         setControl(scroller);
378         validatePage();
379     }
380
381     private void initializeWidgetsFromPreferences() {
382         // Write preferences to formatter
383         IPreferenceStore csvnode = new ScopedPreferenceStore( InstanceScope.INSTANCE, CSVPreferences.P_NODE );
384
385         Double startTime =  csvnode.getDouble(CSVPreferences.P_CSV_START_TIME);
386         Double timeStep =  csvnode.getDouble(CSVPreferences.P_CSV_TIME_STEP);
387         String decimalSeparator = csvnode.getString(CSVPreferences.P_CSV_DECIMAL_SEPARATOR);
388         String columnSeparator = StringEscapeUtils.unescape( csvnode.getString(CSVPreferences.P_CSV_COLUMN_SEPARATOR) );
389         Boolean resample = csvnode.getBoolean(CSVPreferences.P_CSV_RESAMPLE);
390         String samplingModePreference = csvnode.getString(CSVPreferences.P_CSV_SAMPLING_MODE);
391         int timeDigits = csvnode.getInt(CSVPreferences.P_CSV_TIME_DIGITS);
392         int floatDigits = csvnode.getInt(CSVPreferences.P_CSV_FLOAT_DIGITS);
393         int doubleDigits = csvnode.getInt(CSVPreferences.P_CSV_DOUBLE_DIGITS);
394
395         this.decimalSeparator.select(DecimalSeparator.fromPreference(decimalSeparator).ordinal());
396         this.columnSeparator.select(ColumnSeparator.fromPreference(columnSeparator).ordinal());
397
398         this.sampling.select(resample ? 1 : 0);
399         this.samplingMode.select(ExportInterpolation.fromPreference(samplingModePreference).index());
400
401         this.startTime.setText("" + startTime);
402         this.timeStep.setText("" + timeStep);
403         this.timeStamps.setText("" + timeDigits);
404         this.singlePrecision.setText("" + floatDigits);
405         this.doublePrecision.setText("" + doubleDigits);
406
407         for (String path : exportModel.recentLocations)
408             exportLocation.add(path);
409         if (exportLocation.getItemCount() > 0)
410             exportLocation.select(0);
411     }
412
413     private void initializeData() {
414         try {
415             getContainer().run(true, true, monitor -> {
416                 try {
417                     initializeModelData(monitor);
418                     SWTUtils.asyncExec(model, () -> {
419                         if (!model.isDisposed())
420                             initializeModelAndItemSelection();
421                     });
422                 } catch (DatabaseException e) {
423                     throw new InvocationTargetException(e);
424                 } finally {
425                     monitor.done();
426                 }
427             });
428         } catch (InvocationTargetException e) {
429             setErrorMessage(e.getMessage());
430             LOGGER.error("Failed to initialized model data for wizard.", e.getCause());
431         } catch (InterruptedException e) {
432             setErrorMessage(e.getMessage());
433             LOGGER.error("Interrupted wizard model data initialization.", e);
434         }
435     }
436
437     private void initializeModelData(IProgressMonitor monitor) throws DatabaseException {
438         models = exportModel.sessionContext.getSession().syncRequest(
439                 (Read<List<Model>>) graph -> readModelData(monitor, graph));
440     }
441
442     private List<Model> readModelData(IProgressMonitor monitor, ReadGraph graph) throws DatabaseException {
443         List<Model> result = new ArrayList<>();
444         Layer0 L0 = Layer0.getInstance(graph);
445         Collection<Resource> models = graph.syncRequest(new ProjectModels(Simantics.getProjectResource()));
446         SubMonitor mon = SubMonitor.convert(monitor, "Reading model subscriptions", models.size());
447         for (Resource model : models) {
448             String name = graph.getPossibleRelatedValue(model, L0.HasName, Bindings.STRING);
449             if (name != null) {
450                 name = NameLabelUtil.modalName(graph, model);
451                 mon.subTask(name);
452                 Model m = new Model(new NamedResource(name, model));
453                 m.initialize(graph, initialSelection);
454                 result.add(m);
455             }
456             mon.worked(1);
457         }
458         return result;
459     }
460
461     private void initializeModelAndItemSelection() {
462         boolean initialSelectionDone = false;
463         for (int i = 0; i < models.size(); i++) {
464             Model m = models.get(i);
465             model.add(m.model.getName());
466             if (!initialSelectionDone) {
467                 boolean hasInitialSelection = m.sortedSubs.stream().anyMatch(m.initiallySelectedSubscriptions::contains);
468                 if (hasInitialSelection) {
469                     initializeItemSelectionForModel(m);
470                     initialSelectionDone = true;
471                 }
472             }
473         }
474     }
475
476     private void initializeItemSelectionForModel(Model m) {
477         int i = models.indexOf(m);
478         model.select(i);
479         item.removeAll();
480         exportModel.items.clear();
481         int index = 0;
482         int firstIndex = -1;
483         item.setRedraw(false);
484         for (NamedResource nr : m.sortedSubs) {
485             TableItem ti = new TableItem(item, SWT.NONE);
486             ti.setText(nr.getName());
487             ti.setData(nr);
488             if (m.initiallySelectedSubscriptions.contains(nr)) {
489                 exportModel.items.add(nr.getResource());
490                 ti.setChecked(true);
491                 if (firstIndex == -1)
492                     firstIndex = index;
493             }
494             index++;
495         }
496         item.setTopIndex(Math.max(0, firstIndex));
497         item.setData(m);
498         item.setRedraw(true);
499
500         int checked = countCheckedItems(item);
501         updateSelectAll(checked > 0, checked < item.getItemCount(), false);
502     }
503
504     private void updateSelectAll(boolean checked, boolean gray, boolean notify) {
505         if (checked) {
506             selectAllItems.setText("Select None");
507         } else {
508             selectAllItems.setText("Select All");
509         }
510         if (!notify)
511             selectAllItems.removeSelectionListener(selectAllItemsListener);
512         selectAllItems.setGrayed(checked && gray);
513         selectAllItems.setSelection(checked);
514         if (!notify)
515             selectAllItems.addSelectionListener(selectAllItemsListener);
516     }
517
518     protected int countCheckedItems(Table table) {
519         int ret = 0;
520         for (TableItem item : table.getItems())
521             ret += item.getChecked() ? 1 : 0;
522         return ret;
523     }
524
525     Integer validInteger(String s) {
526         try {
527             return Integer.parseInt(s);
528         } catch (NumberFormatException e) {
529             return null;
530         }
531     }
532
533     Double validDouble(String s) {
534         try {
535             return Double.parseDouble(s);
536         } catch (NumberFormatException e) {
537             return null;
538         }
539     }
540
541     Model getModel(String name) {
542         for (Model m : models)
543             if (m.model.getName().equals(name))
544                 return m;
545         return null;
546     }
547
548     private void setText(Group g, String text) {
549         if (!g.getText().equals(text))
550             g.setText(text);
551     }
552
553     void validatePage() {
554         boolean resample = sampling.getText().equals("Resampled"); 
555         if (resample) {
556             setText(resampling, "Resampling settings");
557             timeStep.setEnabled(true);
558             startTime.setEnabled(true);
559             samplingMode.setEnabled(true);
560         } else {
561             setText(resampling, "Resampling settings (not used with recorded samples)");
562             timeStep.setEnabled(false);
563             startTime.setEnabled(false);
564             samplingMode.setEnabled(false);
565         }
566
567         String selectedModel = model.getText();
568         Model m = getModel(selectedModel);
569         if (m != null) {
570             Model existing = (Model) item.getData();
571             if (!m.equals(existing)) {
572                 item.setRedraw(false);
573                 item.removeAll();
574                 for (NamedResource sub : m.sortedSubs) {
575                     TableItem ti = new TableItem(item, SWT.NONE);
576                     ti.setText(sub.getName());
577                     ti.setData(sub);
578                     ti.setChecked(m.initiallySelectedSubscriptions.contains(sub));
579                 }
580                 item.setData(m);
581                 item.setRedraw(true);
582             }
583
584             exportModel.items = java.util.Arrays.stream(item.getItems())
585                     .filter(TableItem::getChecked)
586                     .map(ti -> ((NamedResource) ti.getData()).getResource())
587                     .collect(Collectors.toSet());
588         }
589
590         Double validStartTime = validDouble(startTime.getText());
591         Double validStepSize = validDouble(timeStep.getText());
592
593         if (resample) {
594
595             if (validStartTime == null) {
596                 setErrorMessage("Start time must be a number.");
597                 setPageComplete(false);
598                 return;
599             }
600
601             if (validStepSize == null) {
602                 setErrorMessage("Step size must be a number.");
603                 setPageComplete(false);
604                 return;
605             }
606             if (validStepSize <= 0) {
607                 setErrorMessage("Step size must be greater than 0.");
608                 setPageComplete(false);
609                 return;
610             }
611
612         } else {
613
614             if (exportModel.items.size() > 1) {
615                 setErrorMessage("Recorded samples can only be exported for a single subscription item.");
616                 setPageComplete(false);
617                 return;
618             }
619
620         }
621
622         if (item.getItemCount() == 0) {
623             setErrorMessage("No subscription items in selected model.");
624             setPageComplete(false);
625             return;
626         }
627
628         if (exportModel.items.isEmpty()) {
629             setErrorMessage("No items selected for export.");
630             setPageComplete(false);
631             return;
632         }
633
634         String exportLoc = exportLocation.getText();
635         if (exportLoc.isEmpty()) {
636             setErrorMessage("Select output file.");
637             setPageComplete(false);
638             return;
639         }
640         File file = new File(exportLoc);
641         if (file.isDirectory()) {
642             setErrorMessage("The output file is a directory.");
643             setPageComplete(false);
644             return;
645         }
646         File parent = file.getParentFile();
647         if (parent == null || !parent.isDirectory()) {
648             setErrorMessage("The output directory does not exist.");
649             setPageComplete(false);
650             return;
651         }
652
653         exportModel.columnSeparator = ColumnSeparator.fromIndex(columnSeparator.getSelectionIndex());
654         exportModel.decimalSeparator = DecimalSeparator.fromIndex(decimalSeparator.getSelectionIndex());
655         if (exportModel.columnSeparator.preference.equals(exportModel.decimalSeparator.preference)) {
656             setErrorMessage("Decimal and column separator cannot be the same character.");
657             setPageComplete(false);
658             return;
659         }
660
661         Integer validTimeDigits = validInteger(timeStamps.getText());
662
663         if (validTimeDigits == null) {
664             setErrorMessage("Time stamps needs to be an integer number.");
665             setPageComplete(false);
666             return;
667         }
668
669         Integer validSinglePrecision = validInteger(singlePrecision.getText());
670         if (validSinglePrecision == null) {
671             setErrorMessage("Single precision needs to be an integer number.");
672             setPageComplete(false);
673             return;
674         }
675
676         Integer validDoublePrecision = validInteger(doublePrecision.getText());
677         if (validDoublePrecision == null) {
678             setErrorMessage("Double precision needs to be an integer number.");
679             setPageComplete(false);
680             return;
681         }
682
683         exportModel.exportLocation = file;
684         exportModel.overwrite = overwrite.getSelection();
685
686         exportModel.startTime = validStartTime;
687         exportModel.timeStep = validStepSize;
688
689         exportModel.resample = sampling.getSelectionIndex() == 1;
690         exportModel.samplingMode = ExportInterpolation.fromIndex(samplingMode.getSelectionIndex());
691
692         exportModel.timeDigits = validTimeDigits;
693         exportModel.floatDigits = validSinglePrecision;
694         exportModel.doubleDigits = validDoublePrecision;
695
696         setErrorMessage(null);
697         setMessage("Press Finish to export subscription data for " + exportModel.items.size() + " items.");
698         setPageComplete(true);
699     }
700
701 }