Skip to content

Commit

Permalink
Export feature models to FeatureIDE-compatible formats
Browse files Browse the repository at this point in the history
  • Loading branch information
ekuiter committed Nov 19, 2019
1 parent 6be8374 commit 6c6dadb
Show file tree
Hide file tree
Showing 9 changed files with 117 additions and 29 deletions.
32 changes: 24 additions & 8 deletions client/src/components/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand Down Expand Up @@ -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)
]
}
}),
Expand Down
66 changes: 56 additions & 10 deletions client/src/components/featureDiagramView/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Blob | null>;
const BlobPromise = Promise; // see https://github.com/Microsoft/TypeScript/issues/12776
Expand Down Expand Up @@ -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));
}
6 changes: 3 additions & 3 deletions client/src/components/overlays/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -296,11 +296,11 @@ export default class extends React.Component<Props, State> {
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}});
})
Expand Down
10 changes: 5 additions & 5 deletions client/src/components/overlays/ExportDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -66,9 +66,9 @@ export default class extends React.Component<Props, State> {
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 &&
<React.Fragment>
{this.renderZoomSpinButton()}
<SpinButton
Expand All @@ -78,7 +78,7 @@ export default class extends React.Component<Props, State> {
value={this.state.quality}
min={10} max={100} suffix=" %"/>
</React.Fragment>}
{this.props.format === FormatType.pdf && this.renderFontComboBox()}
{this.props.format === ClientFormatType.pdf && this.renderFontComboBox()}
<DialogFooter>
<PrimaryButton onClick={this.onSubmit} text={i18n.t('overlays.exportDialog.export')}/>
</DialogFooter>
Expand Down
6 changes: 6 additions & 0 deletions client/src/i18n.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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…',
Expand Down
4 changes: 2 additions & 2 deletions client/src/store/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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})),
Expand Down
8 changes: 8 additions & 0 deletions client/src/store/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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');
Expand Down
13 changes: 12 additions & 1 deletion client/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit 6c6dadb

Please sign in to comment.