From 6c6dadbd944798776a35f4ef38c8aa6ae0d2ae69 Mon Sep 17 00:00:00 2001 From: Elias Kuiter Date: Tue, 19 Nov 2019 19:56:26 +0100 Subject: [PATCH] Export feature models to FeatureIDE-compatible formats --- client/src/components/commands.ts | 32 ++++++--- .../components/featureDiagramView/export.ts | 66 ++++++++++++++++--- .../components/overlays/CommandPalette.tsx | 6 +- .../src/components/overlays/ExportDialog.tsx | 10 +-- client/src/i18n.tsx | 6 ++ client/src/store/actions.ts | 4 +- client/src/store/reducer.ts | 8 +++ client/src/types.ts | 13 +++- .../spldev/varied/CollaborativeSession.java | 1 + 9 files changed, 117 insertions(+), 29 deletions(-) diff --git a/client/src/components/commands.ts b/client/src/components/commands.ts index f479acf5..a84b2158 100644 --- a/client/src/components/commands.ts +++ b/client/src/components/commands.ts @@ -5,10 +5,10 @@ */ import i18n from '../i18n'; -import {FeatureDiagramLayoutType, OverlayType, FormatType, Message} from '../types'; +import {FeatureDiagramLayoutType, OverlayType, Message, ClientFormatType, ServerFormatType} from '../types'; import {ContextualMenuItemType} from 'office-ui-fabric-react/lib/ContextualMenu'; import {getShortcutText} from '../shortcuts'; -import {canExport} from './featureDiagramView/export'; +import {canExport, doExport} from './featureDiagramView/export'; import {OnShowOverlayFunction, OnCollapseFeaturesFunction, OnExpandFeaturesFunction, OnSetFeatureDiagramLayoutFunction, OnFitToScreenFunction, OnDeselectAllFeaturesFunction, OnCollapseFeaturesBelowFunction, OnExpandFeaturesBelowFunction, OnSetSelectMultipleFeaturesFunction, OnSelectAllFeaturesFunction, OnCollapseAllFeaturesFunction, OnExpandAllFeaturesFunction, OnRemoveFeatureFunction, OnUndoFunction, OnRedoFunction, OnCreateFeatureBelowFunction, OnCreateFeatureAboveFunction, OnRemoveFeatureSubtreeFunction, OnSetFeatureAbstractFunction, OnSetFeatureHiddenFunction, OnSetFeatureOptionalFunction, OnSetFeatureAndFunction, OnSetFeatureOrFunction, OnSetFeatureAlternativeFunction, OnSetSettingFunction} from '../store/types'; import FeatureModel from '../modeling/FeatureModel'; import {Feature} from '../modeling/types'; @@ -17,8 +17,17 @@ import {preconditions} from '../modeling/preconditions'; import logger from '../helpers/logger'; import {forceFlushMessageQueues} from '../server/messageQueue'; -const exportFormatItem = (featureDiagramLayout: FeatureDiagramLayoutType, - onShowOverlay: OnShowOverlayFunction, format: FormatType) => +const exportServerFormatItem = (featureDiagramLayout: FeatureDiagramLayoutType, format: ServerFormatType) => + canExport(featureDiagramLayout, format) + ? [{ + key: format, + text: i18n.t('commands.featureDiagram', format), + onClick: () => doExport(featureDiagramLayout, ServerFormatType[format], {}) + }] + : []; + +const exportClientFormatItem = (featureDiagramLayout: FeatureDiagramLayoutType, + onShowOverlay: OnShowOverlayFunction, format: ClientFormatType) => canExport(featureDiagramLayout, format) ? [{ key: format, @@ -105,11 +114,18 @@ const commands = { iconProps: {iconName: 'CloudDownload'}, subMenuProps: { items: [ - ...exportFormatItem(featureDiagramLayout, onShowOverlay, FormatType.png), - ...exportFormatItem(featureDiagramLayout, onShowOverlay, FormatType.jpg), + ...exportServerFormatItem(featureDiagramLayout, ServerFormatType.XmlFeatureModelFormat), + ...exportServerFormatItem(featureDiagramLayout, ServerFormatType.SXFMFormat), + ...exportServerFormatItem(featureDiagramLayout, ServerFormatType.ConquererFMWriter), + ...exportServerFormatItem(featureDiagramLayout, ServerFormatType.DIMACSFormat), + ...exportServerFormatItem(featureDiagramLayout, ServerFormatType.CNFFormat), + ...exportServerFormatItem(featureDiagramLayout, ServerFormatType.GuidslFormat), + makeDivider(), + ...exportClientFormatItem(featureDiagramLayout, onShowOverlay, ClientFormatType.png), + ...exportClientFormatItem(featureDiagramLayout, onShowOverlay, ClientFormatType.jpg), makeDivider(), - ...exportFormatItem(featureDiagramLayout, onShowOverlay, FormatType.svg), - ...exportFormatItem(featureDiagramLayout, onShowOverlay, FormatType.pdf) + ...exportClientFormatItem(featureDiagramLayout, onShowOverlay, ClientFormatType.svg), + ...exportClientFormatItem(featureDiagramLayout, onShowOverlay, ClientFormatType.pdf) ] } }), diff --git a/client/src/components/featureDiagramView/export.ts b/client/src/components/featureDiagramView/export.ts index 6fccde05..5376cde8 100644 --- a/client/src/components/featureDiagramView/export.ts +++ b/client/src/components/featureDiagramView/export.ts @@ -5,8 +5,11 @@ import FeatureModel from '../../modeling/FeatureModel'; import {saveAs} from 'file-saver'; import {importSvg2PdfJs, importJspdfYworks, importCanvg} from '../../imports'; -import {FeatureDiagramLayoutType, FormatType, FormatOptions} from '../../types'; +import {FeatureDiagramLayoutType, FormatType, FormatOptions, ArtifactPath, ClientFormatType, ServerFormatType} from '../../types'; import logger from '../../helpers/logger'; +import actions from '../../store/actions'; +import {State} from '../../store/types'; +import {getCurrentArtifactPath} from '../../router'; type BlobPromise = Promise; const BlobPromise = Promise; // see https://github.com/Microsoft/TypeScript/issues/12776 @@ -74,31 +77,74 @@ async function exportPdf({}, fileName: string): BlobPromise { return null; } +const exportServer = (format: ServerFormatType) => async (): BlobPromise => { + const store = (window as any).app && (window as any).app.store; + if (!store) + throw 'store not accessible, can not export'; + const state: State = store.getState(); + const artifactPath: ArtifactPath | undefined = getCurrentArtifactPath(state.collaborativeSessions); + if (!artifactPath) + throw 'no current artifact path'; + store.dispatch(actions.server.exportArtifact({artifactPath, format})); + return null; +}; + const exportMap: { [x in FeatureDiagramLayoutType]: { [x in FormatType]: (options: FormatOptions, fileName: string) => BlobPromise } } = { [FeatureDiagramLayoutType.verticalTree]: { - [FormatType.svg]: exportSvg, - [FormatType.png]: exportPng, - [FormatType.jpg]: exportJpg, - [FormatType.pdf]: exportPdf + [ClientFormatType.svg]: exportSvg, + [ClientFormatType.png]: exportPng, + [ClientFormatType.jpg]: exportJpg, + [ClientFormatType.pdf]: exportPdf, + [ServerFormatType.XmlFeatureModelFormat]: exportServer(ServerFormatType.XmlFeatureModelFormat), + [ServerFormatType.DIMACSFormat]: exportServer(ServerFormatType.DIMACSFormat), + [ServerFormatType.SXFMFormat]: exportServer(ServerFormatType.SXFMFormat), + [ServerFormatType.GuidslFormat]: exportServer(ServerFormatType.GuidslFormat), + [ServerFormatType.ConquererFMWriter]: exportServer(ServerFormatType.ConquererFMWriter), + [ServerFormatType.CNFFormat]: exportServer(ServerFormatType.CNFFormat) }, [FeatureDiagramLayoutType.horizontalTree]: { - [FormatType.svg]: exportSvg, - [FormatType.png]: exportPng, - [FormatType.jpg]: exportJpg, - [FormatType.pdf]: exportPdf + [ClientFormatType.svg]: exportSvg, + [ClientFormatType.png]: exportPng, + [ClientFormatType.jpg]: exportJpg, + [ClientFormatType.pdf]: exportPdf, + [ServerFormatType.XmlFeatureModelFormat]: exportServer(ServerFormatType.XmlFeatureModelFormat), + [ServerFormatType.DIMACSFormat]: exportServer(ServerFormatType.DIMACSFormat), + [ServerFormatType.SXFMFormat]: exportServer(ServerFormatType.SXFMFormat), + [ServerFormatType.GuidslFormat]: exportServer(ServerFormatType.GuidslFormat), + [ServerFormatType.ConquererFMWriter]: exportServer(ServerFormatType.ConquererFMWriter), + [ServerFormatType.CNFFormat]: exportServer(ServerFormatType.CNFFormat) } }; +const extensionMap: { + [x in FormatType]: string +} = { + [ClientFormatType.svg]: 'svg', + [ClientFormatType.png]: 'png', + [ClientFormatType.jpg]: 'jpg', + [ClientFormatType.pdf]: 'pdf', + [ServerFormatType.XmlFeatureModelFormat]: 'xml', + [ServerFormatType.DIMACSFormat]: 'dimacs', + [ServerFormatType.SXFMFormat]: 'xml', + [ServerFormatType.GuidslFormat]: 'm', + [ServerFormatType.ConquererFMWriter]: 'xml', + [ServerFormatType.CNFFormat]: 'txt' +}; + +export function getExportFileName(format: FormatType) { + return `featureModel-${new Date().toLocaleDateString()}.${extensionMap[format]}`; +} + export function canExport(featureDiagramLayout: FeatureDiagramLayoutType, format: FormatType): boolean { return !!exportMap[featureDiagramLayout][format]; } export function doExport(featureDiagramLayout: FeatureDiagramLayoutType, format: FormatType, options: FormatOptions): void { - const fileName = `featureModel-${new Date().toLocaleDateString()}.${format}`, + const fileName = getExportFileName(format), promise: BlobPromise = exportMap[featureDiagramLayout][format](options, fileName); promise.then(blob => blob && saveAs(blob, fileName)); } \ No newline at end of file diff --git a/client/src/components/overlays/CommandPalette.tsx b/client/src/components/overlays/CommandPalette.tsx index e4e80297..70c10961 100644 --- a/client/src/components/overlays/CommandPalette.tsx +++ b/client/src/components/overlays/CommandPalette.tsx @@ -2,7 +2,7 @@ import React from 'react'; import i18n from '../../i18n'; import {OnShowOverlayFunction, OnUndoFunction, OnRedoFunction, OnSetFeatureDiagramLayoutFunction, OnFitToScreenFunction, OnCreateFeatureAboveFunction, OnCreateFeatureBelowFunction, OnCollapseFeaturesFunction, OnCollapseFeaturesBelowFunction, OnExpandFeaturesFunction, OnExpandFeaturesBelowFunction, OnRemoveFeatureFunction, OnRemoveFeatureSubtreeFunction, OnSetFeatureAbstractFunction, OnSetFeatureHiddenFunction, OnSetFeatureOptionalFunction, OnSetFeatureAndFunction, OnSetFeatureOrFunction, OnSetFeatureAlternativeFunction, OnExpandAllFeaturesFunction, OnCollapseAllFeaturesFunction, OnLeaveRequestFunction, CollaborativeSession, OnSetSettingFunction, OnMoveFeatureSubtreeFunction, OnCreateConstraintFunction, OnSetConstraintFunction, OnRemoveConstraintFunction, OnRemoveArtifactFunction, OnResetFunction, OnSetVotingStrategyFunction} from '../../store/types'; import {getShortcutText} from '../../shortcuts'; -import {OverlayType, Omit, FeatureDiagramLayoutType, FormatType, isArtifactPathEqual, ArtifactPath, VotingStrategy} from '../../types'; +import {OverlayType, Omit, FeatureDiagramLayoutType, ClientFormatType, isArtifactPathEqual, ArtifactPath, VotingStrategy} from '../../types'; import Palette, {PaletteItem, PaletteAction, getKey} from '../../helpers/Palette'; import {canExport} from '../featureDiagramView/export'; import FeatureModel, {Constraint, paletteConstraintRenderer} from '../../modeling/FeatureModel'; @@ -296,11 +296,11 @@ export default class extends React.Component { action: this.actionWithArguments( [{ title: i18n.t('commandPalette.format'), - items: () => Object.values(FormatType).map(format => + items: () => Object.values(ClientFormatType).map(format => ({text: i18n.t('commandPalette.featureDiagram', format), key: format})) }], formatString => { - const format = FormatType[formatString]; + const format = ClientFormatType[formatString]; if (canExport(this.props.featureDiagramLayout!, format)) this.props.onShowOverlay({overlay: OverlayType.exportDialog, overlayProps: {format}}); }) diff --git a/client/src/components/overlays/ExportDialog.tsx b/client/src/components/overlays/ExportDialog.tsx index e2f4a9cb..83c897d8 100644 --- a/client/src/components/overlays/ExportDialog.tsx +++ b/client/src/components/overlays/ExportDialog.tsx @@ -11,7 +11,7 @@ import SpinButton from '../../helpers/SpinButton'; import {Settings} from '../../store/settings'; import {doExport} from '../featureDiagramView/export'; import {FeatureDiagramLayoutType} from '../../types'; -import {FormatType} from '../../types'; +import {FormatType, ClientFormatType} from '../../types'; import {OnSetSettingFunction} from '../../store/types'; interface Props { @@ -66,9 +66,9 @@ export default class extends React.Component { hidden={!this.props.isOpen} onDismiss={this.props.onDismiss} dialogContentProps={{title: i18n.t('overlays.exportDialog', this.props.format!, 'title')}}> - {this.props.format === FormatType.svg && this.renderFontComboBox()} - {this.props.format === FormatType.png && this.renderZoomSpinButton()} - {this.props.format === FormatType.jpg && + {this.props.format === ClientFormatType.svg && this.renderFontComboBox()} + {this.props.format === ClientFormatType.png && this.renderZoomSpinButton()} + {this.props.format === ClientFormatType.jpg && {this.renderZoomSpinButton()} { value={this.state.quality} min={10} max={100} suffix=" %"/> } - {this.props.format === FormatType.pdf && this.renderFontComboBox()} + {this.props.format === ClientFormatType.pdf && this.renderFontComboBox()} diff --git a/client/src/i18n.tsx b/client/src/i18n.tsx index 8b556b63..c1531f3d 100644 --- a/client/src/i18n.tsx +++ b/client/src/i18n.tsx @@ -65,6 +65,12 @@ const translationMap = { share: 'Share…', featureDiagram: { export: 'Export as', + XmlFeatureModelFormat: 'XML (FeatureIDE)', + DIMACSFormat: 'DIMACS', + SXFMFormat: 'XML (SXFM)', + GuidslFormat: 'Guidsl', + ConquererFMWriter: 'XML (SPL Conqueror)', + CNFFormat: 'CNF', svg: 'SVG…', png: 'PNG…', jpg: 'JPEG…', diff --git a/client/src/store/actions.ts b/client/src/store/actions.ts index df3b6ffa..b5b9ac0c 100644 --- a/client/src/store/actions.ts +++ b/client/src/store/actions.ts @@ -4,7 +4,7 @@ */ import {createStandardAction, ActionType, action} from 'typesafe-actions'; -import {Message, MessageType, FeatureDiagramLayoutType, OverlayType, OverlayProps, ArtifactPath} from '../types'; +import {Message, MessageType, FeatureDiagramLayoutType, OverlayType, OverlayProps, ArtifactPath, ServerFormatType} from '../types'; import {Dispatch, AnyAction, Action as ReduxAction} from 'redux'; import {ThunkAction} from 'redux-thunk'; import {State} from './types'; @@ -80,7 +80,7 @@ const actions = { ({type: MessageType.ADD_ARTIFACT, artifactPath, source})), removeArtifact: createMessageAction(({artifactPath}: {artifactPath: ArtifactPath}) => ({type: MessageType.REMOVE_ARTIFACT, artifactPath})), - exportArtifact: createMessageAction(({artifactPath, format}: {artifactPath: ArtifactPath, format: string}) => + exportArtifact: createMessageAction(({artifactPath, format}: {artifactPath: ArtifactPath, format: ServerFormatType}) => ({type: MessageType.EXPORT_ARTIFACT, artifactPath, format})), joinRequest: createMessageAction(({artifactPath}: {artifactPath: ArtifactPath}) => ({type: MessageType.JOIN_REQUEST, artifactPath})), leaveRequest: createMessageAction(({artifactPath}: {artifactPath: ArtifactPath}) => ({type: MessageType.LEAVE_REQUEST, artifactPath})), diff --git a/client/src/store/reducer.ts b/client/src/store/reducer.ts index 0135dd29..147d281d 100644 --- a/client/src/store/reducer.ts +++ b/client/src/store/reducer.ts @@ -22,6 +22,8 @@ import {KernelFeatureModel, isKernelConflictDescriptor} from '../modeling/types' import {getCurrentArtifactPath, redirectToArtifactPath} from '../router'; import {enqueueOutgoingMessage, flushOutgoingMessageQueue} from '../server/messageQueue'; import deferred from '../helpers/deferred'; +import {saveAs} from 'file-saver'; +import {getExportFileName} from '../components/featureDiagramView/export'; function getNewState(state: State, ...args: any[]): State { if (args.length % 2 === 1) @@ -178,6 +180,12 @@ function serverReceiveReducer(state: State, action: Action): State { state.artifactPaths, action.payload.artifactPath!, artifactPath => artifactPath, isArtifactPathEqual)); + case MessageType.EXPORT_ARTIFACT: + saveAs( + new Blob([action.payload.data], {type: 'text/plain'}), + getExportFileName(action.payload.format)); + return state; + case MessageType.INITIALIZE: if (!state.myself) throw new Error('no site ID assigned to self'); diff --git a/client/src/types.ts b/client/src/types.ts index 0851fee6..b4040e6f 100644 --- a/client/src/types.ts +++ b/client/src/types.ts @@ -53,13 +53,24 @@ export function isFloatingFeatureOverlay(type: OverlayType): boolean { return type === OverlayType.featureCallout || type === OverlayType.featureContextualMenu; } -export enum FormatType { +export enum ClientFormatType { svg = 'svg', png = 'png', jpg = 'jpg', pdf = 'pdf' }; +export enum ServerFormatType { + XmlFeatureModelFormat = 'XmlFeatureModelFormat', + DIMACSFormat = 'DIMACSFormat', + SXFMFormat = 'SXFMFormat', + GuidslFormat = 'GuidslFormat', + ConquererFMWriter = 'ConquererFMWriter', + CNFFormat = 'CNFFormat' +}; + +export type FormatType = ClientFormatType | ServerFormatType; + export enum VotingStrategy { reject = 'reject', firstVote = 'firstVote', diff --git a/server/src/main/java/de/ovgu/spldev/varied/CollaborativeSession.java b/server/src/main/java/de/ovgu/spldev/varied/CollaborativeSession.java index c2c78545..15a0e139 100644 --- a/server/src/main/java/de/ovgu/spldev/varied/CollaborativeSession.java +++ b/server/src/main/java/de/ovgu/spldev/varied/CollaborativeSession.java @@ -147,6 +147,7 @@ protected boolean _onMessage(Collaborator collaborator, Message.IDecodable messa Api.ExportArtifact exportArtifactMessage = (Api.ExportArtifact) message; exportArtifactMessage.data = FeatureModelUtils.serializeFeatureModel(toFeatureModel(), exportArtifactMessage.format); collaborator.send(exportArtifactMessage); + return true; } return false;