/******************************************************************************* * 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.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.SubMonitor; 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.charts.ontology.ChartResource; 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.exception.DatabaseException; import org.simantics.db.layer0.SelectionHints; import org.simantics.db.layer0.request.ProjectModels; import org.simantics.db.request.Read; 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.strings.AlphanumComparator; import org.simantics.utils.ui.ISelectionUtils; import org.simantics.utils.ui.SWTUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author Antti Villberg */ public class CSVExportPage extends WizardPage { private static final Logger LOGGER = LoggerFactory.getLogger(CSVExportPage.class); private static class Model { private static final Comparator COMP = (o1,o2) -> AlphanumComparator.CASE_INSENSITIVE_COMPARATOR.compare( o1.getName(), o2.getName()); public final NamedResource model; public List sortedSubs = new ArrayList<>(); public Map subs = new HashMap<>(); public Map chartItemsToSubs = new HashMap<>(); public Set initiallySelectedSubscriptions = Collections.emptySet(); public Model(NamedResource model) { this.model = model; } public void initialize(ReadGraph graph, Collection inputSelection) throws DatabaseException { Layer0 L0 = Layer0.getInstance(graph); ModelingResources MOD = ModelingResources.getInstance(graph); ChartResource CHART = ChartResource.getInstance(graph); SubscriptionItemLabelRule rule = new SubscriptionItemLabelRule(); Resource m = model.getResource(); String name = graph.getPossibleRelatedValue(m, L0.HasName, Bindings.STRING); if (name == null) return; name = NameLabelUtil.modalName(graph, m); for (Resource item : ModelingUtils.searchByTypeShallow(graph, m, MOD.Subscription_Item)) { String subscriptionLabel = null; Resource subscription = graph.getPossibleObject(item, L0.PartOf); if (subscription != null) subscriptionLabel = graph.getPossibleRelatedValue(subscription, L0.HasLabel, Bindings.STRING); String label = rule.getLabel(graph, item).get(ColumnKeys.SINGLE); if (label == null) continue; if (subscriptionLabel != null) label = subscriptionLabel + "/" + label; subs.put(item, new NamedResource(label, item)); } for (Resource cItem : ModelingUtils.searchByTypeShallow(graph, m, CHART.Chart_Item)) { Resource sItem = graph.getPossibleObject(cItem, CHART.Chart_Item_HasSubscriptionItem); if (sItem != null && subs.containsKey(sItem)) chartItemsToSubs.put(cItem, sItem); } sortedSubs = subs.values().stream().sorted(COMP).collect(Collectors.toList()); initiallySelectedSubscriptions = initiallySelectedSubscriptions(graph, inputSelection); } private Set initiallySelectedSubscriptions(ReadGraph graph, Collection inputSelection) throws DatabaseException { if (inputSelection == null) return Collections.emptySet(); HashSet result = new HashSet<>(); for (Resource i : inputSelection) { for (NamedResource nr : sortedSubs) if (graph.syncRequest(new IsParent(i, nr.getResource()))) result.add(nr); for (Map.Entry cs : chartItemsToSubs.entrySet()) if (graph.syncRequest(new IsParent(i, cs.getKey()))) result.add( subs.get( cs.getValue() ) ); } return result; } } CSVExportPlan exportModel; CCombo model; Table item; Button selectAllItems; SelectionAdapter selectAllItemsListener; CCombo exportLocation; CCombo decimalSeparator; CCombo columnSeparator; CCombo sampling; Group resampling; Text timeStep; Text startTime; Text timeStamps; CCombo samplingMode; Text singlePrecision; Text doublePrecision; Button overwrite; Collection initialSelection; List models = Collections.emptyList(); 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(); } }); model.addSelectionListener(s); initialSelection = ISelectionUtils.getPossibleKeys(exportModel.selection, SelectionHints.KEY_MAIN, Resource.class); initializeWidgetsFromPreferences(); initializeData(); scroller.setMinSize(container.computeSize(SWT.DEFAULT, SWT.DEFAULT)); setControl(scroller); validatePage(); } private void initializeWidgetsFromPreferences() { // 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); this.decimalSeparator.select(DecimalSeparator.fromPreference(decimalSeparator).ordinal()); this.columnSeparator.select(ColumnSeparator.fromPreference(columnSeparator).ordinal()); this.sampling.select(resample ? 1 : 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); } private void initializeData() { try { getContainer().run(true, true, monitor -> { try { initializeModelData(monitor); SWTUtils.asyncExec(model, () -> { if (!model.isDisposed()) initializeModelAndItemSelection(); }); } catch (DatabaseException e) { throw new InvocationTargetException(e); } finally { monitor.done(); } }); } catch (InvocationTargetException e) { setErrorMessage(e.getMessage()); LOGGER.error("Failed to initialized model data for wizard.", e.getCause()); } catch (InterruptedException e) { setErrorMessage(e.getMessage()); LOGGER.error("Interrupted wizard model data initialization.", e); } } private void initializeModelData(IProgressMonitor monitor) throws DatabaseException { models = exportModel.sessionContext.getSession().syncRequest( (Read>) graph -> readModelData(monitor, graph)); } private List readModelData(IProgressMonitor monitor, ReadGraph graph) throws DatabaseException { List result = new ArrayList<>(); Layer0 L0 = Layer0.getInstance(graph); Collection models = graph.syncRequest(new ProjectModels(Simantics.getProjectResource())); SubMonitor mon = SubMonitor.convert(monitor, "Reading model subscriptions", models.size()); for (Resource model : models) { String name = graph.getPossibleRelatedValue(model, L0.HasName, Bindings.STRING); if (name != null) { name = NameLabelUtil.modalName(graph, model); mon.subTask(name); Model m = new Model(new NamedResource(name, model)); m.initialize(graph, initialSelection); result.add(m); } mon.worked(1); } return result; } private void initializeModelAndItemSelection() { boolean initialSelectionDone = false; for (int i = 0; i < models.size(); i++) { Model m = models.get(i); model.add(m.model.getName()); if (!initialSelectionDone) { boolean hasInitialSelection = m.sortedSubs.stream().anyMatch(m.initiallySelectedSubscriptions::contains); if (hasInitialSelection) { initializeItemSelectionForModel(m); initialSelectionDone = true; } } } } private void initializeItemSelectionForModel(Model m) { int i = models.indexOf(m); model.select(i); item.removeAll(); exportModel.items.clear(); int index = 0; int firstIndex = -1; item.setRedraw(false); for (NamedResource nr : m.sortedSubs) { TableItem ti = new TableItem(item, SWT.NONE); ti.setText(nr.getName()); ti.setData(nr); if (m.initiallySelectedSubscriptions.contains(nr)) { exportModel.items.add(nr.getResource()); ti.setChecked(true); if (firstIndex == -1) firstIndex = index; } index++; } item.setTopIndex(Math.max(0, firstIndex)); item.setData(m); item.setRedraw(true); int checked = countCheckedItems(item); updateSelectAll(checked > 0, checked < item.getItemCount(), false); } private 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; } 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; } } Model getModel(String name) { for (Model m : models) if (m.model.getName().equals(name)) return m; return null; } private void setText(Group g, String text) { if (!g.getText().equals(text)) g.setText(text); } void validatePage() { boolean resample = sampling.getText().equals("Resampled"); if (resample) { setText(resampling, "Resampling settings"); timeStep.setEnabled(true); startTime.setEnabled(true); samplingMode.setEnabled(true); } else { setText(resampling, "Resampling settings (not used with recorded samples)"); timeStep.setEnabled(false); startTime.setEnabled(false); samplingMode.setEnabled(false); } String selectedModel = model.getText(); Model m = getModel(selectedModel); if (m != null) { Model existing = (Model) item.getData(); if (!m.equals(existing)) { item.setRedraw(false); item.removeAll(); for (NamedResource sub : m.sortedSubs) { TableItem ti = new TableItem(item, SWT.NONE); ti.setText(sub.getName()); ti.setData(sub); ti.setChecked(m.initiallySelectedSubscriptions.contains(sub)); } item.setData(m); item.setRedraw(true); } exportModel.items = java.util.Arrays.stream(item.getItems()) .filter(TableItem::getChecked) .map(ti -> ((NamedResource) ti.getData()).getResource()) .collect(Collectors.toSet()); } 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.items.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.items.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 for " + exportModel.items.size() + " items."); setPageComplete(true); } }