/*
* ARX: Powerful Data Anonymization
* Copyright 2012 - 2017 Fabian Prasser, Florian Kohlmayer and contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.deidentifier.arx.gui.view.impl.explore;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import org.deidentifier.arx.ARXLattice;
import org.deidentifier.arx.ARXLattice.ARXNode;
import org.deidentifier.arx.ARXLattice.Anonymity;
import org.deidentifier.arx.ARXResult;
import org.deidentifier.arx.gui.Controller;
import org.deidentifier.arx.gui.model.Model;
import org.deidentifier.arx.gui.model.ModelEvent;
import org.deidentifier.arx.gui.model.ModelEvent.ModelPart;
import org.deidentifier.arx.gui.model.ModelNodeFilter;
import org.deidentifier.arx.gui.resources.Resources;
import org.deidentifier.arx.gui.view.SWTUtil;
import org.deidentifier.arx.gui.view.def.IView;
import org.deidentifier.arx.metric.InformationLoss;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.nebula.widgets.nattable.util.GUIHelper;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CLabel;
import org.eclipse.swt.custom.StackLayout;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import de.linearbits.swt.tiles.DecoratorString;
/**
* This class provides an abstract base for views that display parts of the solution space
*
* @author Fabian Prasser
* @author Florian Kohlmayer
*
*/
public abstract class ViewSolutionSpace implements IView {
/** The controller. */
private final Controller controller;
/** Context menu. */
private Menu menu = null;
/** The selected node. */
private ARXNode selectedNode = null;
/** The model. */
private Model model = null;
/** The parent */
private Composite parent;
/** View component */
private Composite primary;
/** View component */
private Composite secondary;
/** View component */
private StackLayout layout;
/** View component */
private Composite base;
/** View component */
private CLabel label;
/** Tooltip decorator */
private DecoratorString<ARXNode> tooltipDecorator = null;
/** The optimum. */
private ARXNode optimum = null;
/** Color. */
private static final Color COLOR_GREEN = GUIHelper.getColor(50, 205, 50);
/** Color. */
private static final Color COLOR_LIGHT_GREEN = GUIHelper.getColor(150, 255, 150);
/** Color. */
private static final Color COLOR_RED = GUIHelper.getColor(255, 99, 71);
/** Color. */
private static final Color COLOR_LIGHT_RED = GUIHelper.getColor(255, 150, 150);
/** Color. */
private static final Color COLOR_BLUE = GUIHelper.getColor(0, 0, 255);
/** Color. */
private static final Color COLOR_YELLOW = GUIHelper.getColor(255, 215, 0);
/** Color. */
private static final Color COLOR_DARK_GRAY = GUIHelper.getColor(180, 180, 180);
/** Color. */
private static final Color COLOR_GRAY = GUIHelper.getColor(160, 160, 160);
/** Color. */
private static final Color COLOR_BLACK = GUIHelper.getColor(0, 0, 0);
/** Decorator */
private Gradient gradient;
/** Maximal length of a label in characters */
private static final int MAX_LABEL_LENGTH = 20;
/**
* Constructor
* @param parent
* @param controller
*/
public ViewSolutionSpace(final Composite parent, final Controller controller) {
// Listen
controller.addListener(ModelPart.SELECTED_NODE, this);
controller.addListener(ModelPart.FILTER, this);
controller.addListener(ModelPart.MODEL, this);
controller.addListener(ModelPart.RESULT, this);
controller.addListener(ModelPart.EXPAND, this);
// Store
this.parent = parent;
this.controller = controller;
// Initialize
initializeMenu();
initializeTooltip();
this.gradient = new Gradient(parent.getDisplay());
// Create stack
this.base = new Composite(parent, SWT.NONE);
this.base.setLayoutData(SWTUtil.createFillGridData());
this.layout = new StackLayout();
this.base.setLayout(layout);
// Create the primary composite
this.primary = new Composite(this.base, SWT.NONE);
this.primary.setLayout(SWTUtil.createGridLayout(1));
// Create the secondary composite
this.secondary = new Composite(this.base, SWT.NONE);
this.secondary.setLayout(SWTUtil.createGridLayout(1));
this.label = new CLabel(this.secondary, SWT.NONE);
this.label.setLayoutData(GridDataFactory.swtDefaults().align(SWT.CENTER, SWT.CENTER).grab(true, true).minSize(400, 200).create());
this.label.setImage(controller.getResources().getManagedImage("warning.png")); //$NON-NLS-1$
this.label.setText(""); //$NON-NLS-1$
this.label.setAlignment(SWT.LEFT);
// Show primary
this.showPrimaryComposite();
}
@Override
public void dispose() {
controller.removeListener(this);
gradient.dispose();
}
/**
* Resets the view.
*/
@Override
public void reset() {
this.optimum = null;
this.selectedNode = null;
}
@Override
public void update(final ModelEvent event) {
if (event.part == ModelPart.SELECTED_NODE) {
selectedNode = (ARXNode) event.data;
eventNodeSelected();
} else if (event.part == ModelPart.RESULT) {
ARXResult result = (ARXResult)event.data;
if (model != null && result != null && result.getGlobalOptimum() != null) {
optimum = result.getGlobalOptimum();
} else {
optimum = null;
}
if (model!=null && !isTooLarge(result, model.getNodeFilter(), model.getMaxNodesInViewer())) {
eventResultChanged(result);
}
} else if (event.part == ModelPart.MODEL) {
model = (Model) event.data;
if (model != null && model.getResult() != null &&
model.getResult().getGlobalOptimum() != null) {
optimum = model.getResult().getGlobalOptimum();
} else {
optimum = null;
}
if (model!=null && !isTooLarge(model.getResult(), model.getNodeFilter(), model.getMaxNodesInViewer())) {
eventModelChanged();
}
} else if (event.part == ModelPart.FILTER) {
if (model!=null && !isTooLarge(model.getResult(), (ModelNodeFilter) event.data, model.getMaxNodesInViewer())) {
eventFilterChanged(model.getResult(), (ModelNodeFilter) event.data);
}
} else if (event.part == ModelPart.EXPAND) {
if (model!=null && !isTooLarge(model.getResult(), model.getNodeFilter(), model.getMaxNodesInViewer())) {
eventFilterChanged(model.getResult(), model.getNodeFilter());
}
}
}
/**
* Creates the context menu.
*/
private void initializeMenu() {
menu = new Menu(parent.getShell());
MenuItem item1 = new MenuItem(menu, SWT.NONE);
item1.setText(Resources.getMessage("LatticeView.9")); //$NON-NLS-1$
item1.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(final SelectionEvent arg0) {
model.getClipboard().addToClipboard(selectedNode);
controller.update(new ModelEvent(ViewSolutionSpace.this, ModelPart.CLIPBOARD, selectedNode));
model.setSelectedNode(selectedNode);
controller.update(new ModelEvent(ViewSolutionSpace.this, ModelPart.SELECTED_NODE, selectedNode));
actionRedraw();
}
});
MenuItem item2 = new MenuItem(menu, SWT.NONE);
item2.setText(Resources.getMessage("LatticeView.10")); //$NON-NLS-1$
item2.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(final SelectionEvent arg0) {
controller.actionApplySelectedTransformation();
model.setSelectedNode(selectedNode);
controller.update(new ModelEvent(ViewSolutionSpace.this, ModelPart.SELECTED_NODE, selectedNode));
actionRedraw();
}
});
MenuItem item3 = new MenuItem(menu, SWT.NONE);
item3.setText(Resources.getMessage("LatticeView.11")); //$NON-NLS-1$
item3.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(final SelectionEvent arg0) {
model.setSelectedNode(selectedNode);
controller.update(new ModelEvent(ViewSolutionSpace.this, ModelPart.SELECTED_NODE, selectedNode));
controller.actionExpand(selectedNode);
controller.update(new ModelEvent(ViewSolutionSpace.this, ModelPart.EXPAND, selectedNode));
eventFilterChanged(model.getResult(), model.getNodeFilter());
}
});
}
private void initializeTooltip() {
this.tooltipDecorator = new DecoratorString<ARXNode>() {
@Override
public String decorate(ARXNode node) {
final StringBuffer b = new StringBuffer();
b.append(Resources.getMessage("LatticeView.1")); //$NON-NLS-1$
b.append(SWTUtil.getPrettyString(asRelativeValue(node.getLowestScore())));
b.append(" - "); //$NON-NLS-1$
b.append(SWTUtil.getPrettyString(asRelativeValue(node.getHighestScore())));
b.append(" [%]\n"); //$NON-NLS-1$
if (model.getOutputDefinition() != null) {
for (final String qi : node.getQuasiIdentifyingAttributes()) {
// Determine height of hierarchy
int height = model.getOutputDefinition().isHierarchyAvailable(qi) ?
model.getOutputDefinition().getHierarchy(qi)[0].length : 0;
b.append(" * "); //$NON-NLS-1$
b.append(qi);
b.append(": "); //$NON-NLS-1$
b.append(SWTUtil.getPrettyString(asRelativeValue(node.getGeneralization(qi), height - 1)));
b.append(" [%]\n"); //$NON-NLS-1$
}
}
b.setLength(b.length() - 1);
return b.toString();
}
};
}
/**
* Check whether the filtered part of the solution space is too large
* @param result
* @param filter
* @return
*/
private boolean isTooLarge(ARXResult result, ModelNodeFilter filter, int max) {
if(result == null) {
showPrimaryComposite();
return false;
}
int count = 0;
final ARXLattice l = result.getLattice();
for (final ARXNode[] level : l.getLevels()) {
for (final ARXNode node : level) {
if (filter.isAllowed(result.getLattice(), node)) {
count++;
}
}
}
if (count > max) {
showSecondaryComposite(count, max);
return true;
} else {
showPrimaryComposite();
return false;
}
}
/**gray.dispose();
* Action to redraw
*/
protected abstract void actionRedraw();
/**
* Action: select node
* @param node
*/
protected void actionSelectNode(ARXNode node) {
this.selectedNode = node;
getModel().setSelectedNode(node);
controller.update(new ModelEvent(this, ModelPart.SELECTED_NODE, node));
}
/**
* Action show menu
* @param x
* @param y
*/
protected void actionShowMenu(int x, int y){
menu.setLocation(x, y);
menu.setVisible(true);
}
/**
* Converts an score into a relative value in percent.
*
* @param infoLoss
* @return
*/
protected double asRelativeValue(final InformationLoss<?> infoLoss) {
if (model != null && model.getResult() != null && model.getResult().getLattice() != null &&
model.getResult().getLattice().getBottom() != null && model.getResult().getLattice().getTop() != null) {
return infoLoss.relativeTo(model.getResult().getLattice().getLowestScore(),
model.getResult().getLattice().getHighestScore()) * 100d;
} else {
return 0;
}
}
/**
* Converts a generalization to a relative value.
*
* @param generalization
* @param max
* @return
*/
protected double asRelativeValue(final int generalization, final int max) {
if (model != null && model.getResult() != null && model.getResult().getLattice() != null &&
model.getResult().getLattice().getBottom() != null && model.getResult().getLattice().getTop() != null) {
return ((double) generalization / (double) max) * 100d;
} else {
return 0;
}
}
/**
* Event: filter changed
* @param result
* @param filter
*/
protected abstract void eventFilterChanged(ARXResult result, ModelNodeFilter filter);
/**
* Event: model changed
*/
protected abstract void eventModelChanged();
/**
* Event: node selected
*/
protected abstract void eventNodeSelected();
/**
* Event: result changed
* @param result
*/
protected abstract void eventResultChanged(ARXResult result);
/**
* Returns the controller
* @return
*/
protected Controller getController() {
return this.controller;
}
/**
* Returns the inner color.
*
* @param node
* @return
*/
protected Color getInnerColor(final ARXNode node) {
if (node.getAnonymity() == Anonymity.ANONYMOUS) {
return node.equals(optimum) ? COLOR_YELLOW : COLOR_GREEN;
} else if (node.getAnonymity() == Anonymity.PROBABLY_ANONYMOUS) {
return COLOR_LIGHT_GREEN;
} else if (node.getAnonymity() == Anonymity.PROBABLY_NOT_ANONYMOUS) {
return COLOR_LIGHT_RED;
} else if (node.getAnonymity() == Anonymity.UNKNOWN) {
return COLOR_DARK_GRAY;
} else {
return COLOR_RED;
}
}
/**
* Returns the model
* @return
*/
protected Model getModel() {
return this.model;
}
/**
* Returns the outer color.
*
* @param node
* @return
*/
protected Color getOuterColor(final ARXNode node) {
return node.isChecked() ? COLOR_BLUE : COLOR_BLACK;
}
/**
* Returns the outer stroke width.
*
* @param node
* @param width
* @return
*/
protected int getOuterStrokeWidth(final ARXNode node, final int width) {
int result = node.isChecked() ? width / 100 : 1;
result = node.isChecked() ? result + 1 : result;
return result >=1 ? result < 1 ? 1 : result : 1;
}
/**
* Returns the primary composite
* @return
*/
protected Composite getPrimaryComposite() {
return this.primary;
}
/**
* Returns the selected node
* @return
*/
protected ARXNode getSelectedNode() {
return this.selectedNode;
}
/**
* Returns the tool tip decorator
* @return
*/
protected DecoratorString<ARXNode> getTooltipDecorator() {
return this.tooltipDecorator;
}
/**
* Returns the color according to a nodes utility
*
* @param node The node
* @return
*/
protected Color getUtilityColor(ARXNode node) {
if (node.getLowestScore().compareTo(node.getHighestScore()) != 0 &&
asRelativeValue(node.getLowestScore()) == 0d) {
return COLOR_GRAY;
} else {
return gradient.getColor(asRelativeValue(node.getLowestScore()) / 100d);
}
}
/**
* Shows the primary composite
*/
protected void showPrimaryComposite() {
this.layout.topControl = this.primary;
this.base.layout();
}
/**
* Shows the secondary composite
*/
protected void showSecondaryComposite(int num, int max) {
ByteArrayOutputStream os = new ByteArrayOutputStream();
PrintStream ps = new PrintStream(os);
ps.format(Resources.getMessage("LatticeView.7"), num, max); //$NON-NLS-1$
label.setText(os.toString());
this.layout.topControl = this.secondary;
this.base.layout();
}
/**
* Trims the label to a predefined length
* @param label
* @return
*/
protected String trimLabel(String label) {
if (label.length() > MAX_LABEL_LENGTH) {
label = label.replace(" ", "");
}
if (label.length() > MAX_LABEL_LENGTH) {
label = label.replace(",", "");
}
if (label.length() > MAX_LABEL_LENGTH) {
label = label.substring(0, MAX_LABEL_LENGTH -3) + "...";
}
return label;
}
}