/******************************************************************************* * Copyright (c) 2012 Association for Decentralized Information Management in * Industry THTH ry. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * VTT Technical Research Centre of Finland - initial API and implementation *******************************************************************************/ package org.simantics.charts.ui; import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.core.runtime.preferences.InstanceScope; import org.eclipse.jface.layout.GridDataFactory; import org.eclipse.jface.layout.GridLayoutFactory; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.wizard.WizardPage; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.CCombo; import org.eclipse.swt.custom.ScrolledComposite; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.FileDialog; import org.eclipse.swt.widgets.Group; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableItem; import org.eclipse.swt.widgets.Text; import org.eclipse.ui.preferences.ScopedPreferenceStore; import org.simantics.NameLabelUtil; import org.simantics.Simantics; import org.simantics.browsing.ui.common.ColumnKeys; import org.simantics.databoard.Bindings; import org.simantics.databoard.parser.StringEscapeUtils; import org.simantics.db.ReadGraph; import org.simantics.db.Resource; import org.simantics.db.common.NamedResource; import org.simantics.db.common.request.IsParent; import org.simantics.db.common.request.UniqueRead; import org.simantics.db.exception.DatabaseException; import org.simantics.db.layer0.SelectionHints; import org.simantics.history.csv.ColumnSeparator; import org.simantics.history.csv.DecimalSeparator; import org.simantics.history.csv.ExportInterpolation; import org.simantics.layer0.Layer0; import org.simantics.modeling.ModelingResources; import org.simantics.modeling.ModelingUtils; import org.simantics.modeling.preferences.CSVPreferences; import org.simantics.modeling.ui.modelBrowser2.label.SubscriptionItemLabelRule; import org.simantics.utils.datastructures.Arrays; import org.simantics.utils.datastructures.Pair; import org.simantics.utils.strings.AlphanumComparator; import org.simantics.utils.ui.ISelectionUtils; /** * @author Antti Villberg */ public class CSVExportPage extends WizardPage { CSVExportPlan exportModel; CCombo model; Table item; Button selectAllItems; SelectionAdapter selectAllItemsListener; CCombo exportLocation; CCombo decimalSeparator; CCombo columnSeparator; CCombo sampling; Text timeStep; Text startTime; Text timeStamps; CCombo samplingMode; Text singlePrecision; Text doublePrecision; Group resampling; Collection initialSelection; List>> models = new ArrayList<>(); private Button overwrite; ModifyListener m = (e) -> validatePage(); SelectionListener s = new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { validatePage(); } }; protected CSVExportPage(CSVExportPlan model) { super("Export CSV Data", "Define Export Properties", null); this.exportModel = model; } @Override public void createControl(Composite parent) { ScrolledComposite scroller = new ScrolledComposite(parent, SWT.V_SCROLL); scroller.setExpandHorizontal(true); scroller.setExpandVertical(true); Composite container = new Composite(scroller, SWT.NONE); scroller.setContent(container); GridLayoutFactory.swtDefaults().spacing(20, 10).numColumns(3).applyTo(container); new Label(container, SWT.NONE).setText("Select a model:"); model = new CCombo(container, SWT.BORDER); { model.setEditable(false); model.setText(""); model.setToolTipText("Selects the Model To Export From"); GridDataFactory.fillDefaults().grab(true, false).span(2, 1).applyTo(model); } new Label(container, SWT.NONE).setText("Exported items:"); item = new Table(container, SWT.BORDER | SWT.MULTI | SWT.FULL_SELECTION | SWT.CHECK); { item.setToolTipText("Selects the Subscription Items"); GridDataFactory.fillDefaults().grab(true, true).span(2, 1).hint(SWT.DEFAULT, 105).applyTo(item); } item.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { if (e.detail == SWT.CHECK) { TableItem[] selected = item.getSelection(); TableItem it = (TableItem) e.item; boolean checkedWasSelected = Arrays.contains(selected, it); if (checkedWasSelected) { boolean check = it.getChecked(); for (TableItem i : selected) i.setChecked(check); } int checked = countCheckedItems(item); int totalItems = item.getItemCount(); updateSelectAll(checked > 0, checked < totalItems, false); validatePage(); } } }); new Label(container, 0); selectAllItems = new Button(container, SWT.CHECK); { selectAllItems.setText("&Select All"); selectAllItems.setToolTipText("Select/Deselect All Listed Subscription Items"); GridDataFactory.fillDefaults().grab(true, false).span(2, 1).applyTo(selectAllItems); } selectAllItemsListener = new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { boolean select = selectAllItems.getSelection(); updateSelectAll(select, false, false); item.setRedraw(false); for (TableItem it : item.getItems()) it.setChecked(select); item.setRedraw(true); validatePage(); } }; selectAllItems.addSelectionListener(selectAllItemsListener); new Label(container, SWT.NONE).setText("&Output file:"); exportLocation = new CCombo(container, SWT.BORDER); { exportLocation.setText(""); GridDataFactory.fillDefaults().grab(true, false).span(1, 1).applyTo(exportLocation); exportLocation.addModifyListener(m); } Button browseFileButton = new Button(container, SWT.PUSH); { browseFileButton.setText("Browse..."); browseFileButton.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false)); browseFileButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { FileDialog dialog = new FileDialog(getShell(), SWT.SAVE); dialog.setText("Choose Output File"); dialog.setFilterPath(new File(exportLocation.getText()).getParent()); dialog.setFilterExtensions(new String[] { "*.csv" }); dialog.setFilterNames(new String[] { "Comma separated values (*.csv)" }); dialog.setOverwrite(false); String file = dialog.open(); if (file == null) return; exportLocation.setText(file); validatePage(); } }); } Label horizRule = new Label(container, SWT.BORDER); GridDataFactory.fillDefaults().hint(SWT.DEFAULT, 0).grab(true, false).span(3, 1).applyTo(horizRule); new Label(container, SWT.NONE).setText("&Decimal separator:"); decimalSeparator = new CCombo(container, SWT.READ_ONLY | SWT.BORDER); for(DecimalSeparator s : DecimalSeparator.values()) decimalSeparator.add(s.label); decimalSeparator.select(0); decimalSeparator.addSelectionListener(s); GridDataFactory.fillDefaults().grab(true, false).span(2, 1).applyTo(decimalSeparator); new Label(container, SWT.NONE).setText("&Column separator:"); columnSeparator = new CCombo(container, SWT.READ_ONLY | SWT.BORDER); for(ColumnSeparator s : ColumnSeparator.values()) columnSeparator.add(s.label); columnSeparator.select(0); columnSeparator.addSelectionListener(s); GridDataFactory.fillDefaults().grab(true, false).span(2, 1).applyTo(columnSeparator); new Label(container, SWT.NONE).setText("Sampling:"); sampling = new CCombo(container, SWT.READ_ONLY | SWT.BORDER); sampling.add("Recorded samples"); sampling.add("Resampled"); sampling.select(0); sampling.addSelectionListener(s); GridDataFactory.fillDefaults().grab(true, false).span(2, 1).applyTo(sampling); resampling = new Group(container, SWT.NONE); resampling.setText("Resampling settings (not used with recorded samples)"); GridDataFactory.fillDefaults().grab(true, false).span(3, 1).applyTo(resampling); GridLayoutFactory.swtDefaults().numColumns(3).applyTo(resampling); new Label(resampling, SWT.NONE).setText("&Start time:"); startTime = new Text(resampling, SWT.BORDER); startTime.addModifyListener(m); GridDataFactory.fillDefaults().grab(true, false).applyTo(startTime); new Label(resampling, SWT.NONE).setText(" seconds"); new Label(resampling, SWT.NONE).setText("&Time step:"); timeStep = new Text(resampling, SWT.BORDER); timeStep.addModifyListener(m); GridDataFactory.fillDefaults().grab(true, false).applyTo(timeStep); new Label(resampling, SWT.NONE).setText(" seconds"); new Label(resampling, SWT.NONE).setText("Sampling mode:"); samplingMode = new CCombo(resampling, SWT.READ_ONLY | SWT.BORDER); samplingMode.add("Linear interpolation"); samplingMode.add("Previous sample"); samplingMode.select(0); samplingMode.addSelectionListener(s); GridDataFactory.fillDefaults().grab(true, false).span(2,1).applyTo(samplingMode); Group digits = new Group(container, SWT.NONE); digits.setText("Significant digits"); GridDataFactory.fillDefaults().grab(true, false).span(3, 1).applyTo(digits); GridLayoutFactory.swtDefaults().numColumns(2).applyTo(digits); new Label(digits, SWT.NONE).setText("&Time stamps:"); timeStamps = new Text(digits, SWT.BORDER); timeStamps.addModifyListener(m); GridDataFactory.fillDefaults().grab(true, false).applyTo(timeStamps); new Label(digits, SWT.NONE).setText("&Single precision floating point:"); singlePrecision = new Text(digits, SWT.BORDER); singlePrecision.addModifyListener(m); GridDataFactory.fillDefaults().grab(true, false).applyTo(singlePrecision); new Label(digits, SWT.NONE).setText("&Double precision floating point:"); doublePrecision = new Text(digits, SWT.BORDER); doublePrecision.addModifyListener(m); GridDataFactory.fillDefaults().grab(true, false).applyTo(doublePrecision); overwrite = new Button(container, SWT.CHECK); overwrite.setText("&Overwrite existing files without warning"); overwrite.setSelection(exportModel.overwrite); GridDataFactory.fillDefaults().grab(true, false).span(3, 1).applyTo(overwrite); overwrite.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { validatePage(); } }); try { initializeData(); } catch (DatabaseException e) { e.printStackTrace(); } model.addSelectionListener(s); scroller.setMinSize(container.computeSize(SWT.DEFAULT, SWT.DEFAULT)); setControl(scroller); validatePage(); } void updateSelectAll(boolean checked, boolean gray, boolean notify) { if (checked) { selectAllItems.setText("Select None"); } else { selectAllItems.setText("Select All"); } if (!notify) selectAllItems.removeSelectionListener(selectAllItemsListener); selectAllItems.setGrayed(checked && gray); selectAllItems.setSelection(checked); if (!notify) selectAllItems.addSelectionListener(selectAllItemsListener); } protected int countCheckedItems(Table table) { int ret = 0; for (TableItem item : table.getItems()) ret += item.getChecked() ? 1 : 0; return ret; } private void initializeData() throws DatabaseException { // Write preferences to formatter IPreferenceStore csvnode = new ScopedPreferenceStore( InstanceScope.INSTANCE, CSVPreferences.P_NODE ); Double startTime = csvnode.getDouble(CSVPreferences.P_CSV_START_TIME); Double timeStep = csvnode.getDouble(CSVPreferences.P_CSV_TIME_STEP); String decimalSeparator = csvnode.getString(CSVPreferences.P_CSV_DECIMAL_SEPARATOR); String columnSeparator = StringEscapeUtils.unescape( csvnode.getString(CSVPreferences.P_CSV_COLUMN_SEPARATOR) ); Boolean resample = csvnode.getBoolean(CSVPreferences.P_CSV_RESAMPLE); String samplingModePreference = csvnode.getString(CSVPreferences.P_CSV_SAMPLING_MODE); int timeDigits = csvnode.getInt(CSVPreferences.P_CSV_TIME_DIGITS); int floatDigits = csvnode.getInt(CSVPreferences.P_CSV_FLOAT_DIGITS); int doubleDigits = csvnode.getInt(CSVPreferences.P_CSV_DOUBLE_DIGITS); initialSelection = ISelectionUtils.getPossibleKeys(exportModel.selection, SelectionHints.KEY_MAIN, Resource.class); models = exportModel.sessionContext.getSession().syncRequest(new UniqueRead>>>() { @Override public List>> perform(ReadGraph graph) throws DatabaseException { Layer0 L0 = Layer0.getInstance(graph); ModelingResources MOD = ModelingResources.getInstance(graph); List>> result = new ArrayList<>(); for(Resource model : graph.syncRequest(new org.simantics.db.layer0.request.ProjectModels(Simantics.getProjectResource()))) { String name = graph.getPossibleRelatedValue(model, L0.HasName, Bindings.STRING); if(name == null) continue; name = NameLabelUtil.modalName(graph, model); List subs = new ArrayList<>(); for(Resource item : ModelingUtils.searchByType(graph, model, MOD.Subscription_Item)) { String subscriptionLabel = null; Resource subscription = graph.getPossibleObject(item, L0.PartOf); if(subscription != null) { subscriptionLabel = graph.getPossibleRelatedValue(subscription, L0.HasLabel, Bindings.STRING); } SubscriptionItemLabelRule rule = new SubscriptionItemLabelRule(); Map map = rule.getLabel(graph, item); String label = map.get(ColumnKeys.SINGLE); if(label == null) continue; if(subscriptionLabel != null) label = subscriptionLabel + "/" + label; subs.add(new NamedResource(label, item)); } subs.sort((o1,o2) -> AlphanumComparator.CASE_INSENSITIVE_COMPARATOR.compare(o1.getName(), o2.getName())); result.add(new Pair<>(new NamedResource(name, model), subs)); } return result; } }); Set selected = exportModel.sessionContext.getSession().syncRequest(new UniqueRead>() { @Override public Set perform(ReadGraph graph) throws DatabaseException { if(initialSelection == null) return Collections.emptySet(); HashSet result = new HashSet<>(); for(Pair> p : models) { for(NamedResource nr : p.second) { for(Resource i : initialSelection) { if(graph.syncRequest(new IsParent(i, nr.getResource()))) result.add(nr); } } } return result; } }); for(int i=0;i> p = models.get(i); model.add(p.first.getName()); boolean hasInitial = false; for(NamedResource nr : p.second) { if(selected.contains(nr)) hasInitial = true; } if(hasInitial) { model.select(i); item.removeAll(); exportModel.models.clear(); int index = 0; int firstIndex = 0; for(NamedResource nr : p.second) { TableItem ti = new TableItem(item, SWT.NONE); ti.setText(nr.getName()); ti.setData(nr); if(selected.contains(nr)) { exportModel.models.add(nr); ti.setChecked(true); if(firstIndex == 0) firstIndex = index; } index++; } item.setTopIndex(firstIndex); item.setData(p.first); } } this.decimalSeparator.select(DecimalSeparator.fromPreference(decimalSeparator).ordinal()); this.columnSeparator.select(ColumnSeparator.fromPreference(columnSeparator).ordinal()); if(resample) { this.sampling.select(1); } else { this.sampling.select(0); } this.samplingMode.select(ExportInterpolation.fromPreference(samplingModePreference).index()); this.startTime.setText("" + startTime); this.timeStep.setText("" + timeStep); this.timeStamps.setText("" + timeDigits); this.singlePrecision.setText("" + floatDigits); this.doublePrecision.setText("" + doubleDigits); for (String path : exportModel.recentLocations) { exportLocation.add(path); } if (exportLocation.getItemCount() > 0) exportLocation.select(0); } Integer validInteger(String s) { try { return Integer.parseInt(s); } catch (NumberFormatException e) { return null; } } Double validDouble(String s) { try { return Double.parseDouble(s); } catch (NumberFormatException e) { return null; } } Pair> getModel(String name) { for(Pair> data : models) { if(data.first.getName().equals(name)) return data; } return null; } void validatePage() { boolean resample = sampling.getText().equals("Resampled"); if(resample) { resampling.setText("Resampling settings"); timeStep.setEnabled(true); startTime.setEnabled(true); samplingMode.setEnabled(true); } else { resampling.setText("Resampling settings (not used with recorded samples)"); timeStep.setEnabled(false); startTime.setEnabled(false); samplingMode.setEnabled(false); } String selectedModel = model.getText(); Pair> p = getModel(selectedModel); if(p != null) { HashSet checked = new HashSet<>(); NamedResource existing = (NamedResource)item.getData(); if(!p.first.equals(existing)) { item.removeAll(); for(NamedResource nr : p.second) { TableItem ti = new TableItem(item, SWT.NONE); ti.setText(nr.getName()); ti.setData(nr); } item.setData(p.first); } for(TableItem ti : item.getItems()) { if(ti.getChecked()) checked.add((NamedResource)ti.getData()); } exportModel.models = checked; } Double validStartTime = validDouble(startTime.getText()); Double validStepSize = validDouble(timeStep.getText()); if(resample) { if(validStartTime == null) { setErrorMessage("Start time must be a number."); setPageComplete(false); return; } if(validStepSize == null) { setErrorMessage("Step size must be a number."); setPageComplete(false); return; } if(validStepSize <= 0) { setErrorMessage("Step size must be greater than 0."); setPageComplete(false); return; } } else { if(exportModel.models.size() > 1) { setErrorMessage("Recorded samples can only be exported for a single subscription item."); setPageComplete(false); return; } } if(item.getItemCount() == 0) { setErrorMessage("No subscription items in selected model."); setPageComplete(false); return; } if (exportModel.models.isEmpty()) { setErrorMessage("No items selected for export."); setPageComplete(false); return; } String exportLoc = exportLocation.getText(); if (exportLoc.isEmpty()) { setErrorMessage("Select output file."); setPageComplete(false); return; } File file = new File(exportLoc); if (file.isDirectory()) { setErrorMessage("The output file is a directory."); setPageComplete(false); return; } File parent = file.getParentFile(); if (parent == null || !parent.isDirectory()) { setErrorMessage("The output directory does not exist."); setPageComplete(false); return; } exportModel.columnSeparator = ColumnSeparator.fromIndex(columnSeparator.getSelectionIndex()); exportModel.decimalSeparator = DecimalSeparator.fromIndex(decimalSeparator.getSelectionIndex()); if (exportModel.columnSeparator.preference.equals(exportModel.decimalSeparator.preference)) { setErrorMessage("Decimal and column separator cannot be the same character."); setPageComplete(false); return; } Integer validTimeDigits = validInteger(timeStamps.getText()); if(validTimeDigits == null) { setErrorMessage("Time stamps needs to be an integer number."); setPageComplete(false); return; } Integer validSinglePrecision = validInteger(singlePrecision.getText()); if(validSinglePrecision == null) { setErrorMessage("Single precision needs to be an integer number."); setPageComplete(false); return; } Integer validDoublePrecision = validInteger(doublePrecision.getText()); if(validDoublePrecision == null) { setErrorMessage("Double precision needs to be an integer number."); setPageComplete(false); return; } exportModel.exportLocation = file; exportModel.overwrite = overwrite.getSelection(); exportModel.startTime = validStartTime; exportModel.timeStep = validStepSize; exportModel.resample = sampling.getSelectionIndex() == 1; exportModel.samplingMode = ExportInterpolation.fromIndex(samplingMode.getSelectionIndex()); exportModel.timeDigits = validTimeDigits; exportModel.floatDigits = validSinglePrecision; exportModel.doubleDigits = validDoublePrecision; setErrorMessage(null); setMessage("Press Finish to export subscription data."); setPageComplete(true); } }