/*
* 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.wizard;
import java.util.ArrayList;
import java.util.List;
import org.deidentifier.arx.DataType.DataTypeWithRatioScale;
import org.deidentifier.arx.gui.view.impl.wizard.HierarchyWizardModelGrouping.HierarchyWizardGroupingGroup;
import org.deidentifier.arx.gui.view.impl.wizard.HierarchyWizardModelGrouping.HierarchyWizardGroupingInterval;
import org.eclipse.nebula.widgets.nattable.util.GUIHelper;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
/**
* Renders the content.
*
* @author Fabian Prasser
* @param <T>
*/
public class HierarchyWizardEditorRenderer<T> {
/**
* Base class for rendering contexts.
*
* @author Fabian Prasser
* @param <T>
*/
public abstract static class RenderedComponent<T> {
/** Var. */
public Rectangle rectangle1;
/** Var. */
public Rectangle rectangle2;
/** Var. */
public int depth;
/** Var. */
public boolean enabled;
/** Var. */
public String label;
/** Var. */
public String bounds;
/** Var. */
public T min;
/** Var. */
public T max;
}
/**
* A rendering context for a group.
*
* @author Fabian Prasser
* @param <T>
*/
public static class RenderedGroup<T> extends RenderedComponent<T> {
/** Var. */
public HierarchyWizardGroupingGroup<T> group;
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
@SuppressWarnings("rawtypes")
RenderedGroup other = (RenderedGroup) obj;
if (group == null) {
if (other.group != null) return false;
} else if (!group.equals(other.group)) return false;
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((group == null) ? 0 : group.hashCode());
return result;
}
}
/**
* A rendering context for an interval.
*
* @author Fabian Prasser
* @param <T>
*/
public static class RenderedInterval<T> extends RenderedComponent<T> {
/** Var. */
public HierarchyWizardGroupingInterval<T> interval;
/** Var. */
public T offset;
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
@SuppressWarnings("rawtypes")
RenderedInterval other = (RenderedInterval) obj;
if (interval == null) {
if (other.interval != null) return false;
} else if (!interval.equals(other.interval)) return false;
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((interval == null) ? 0 : interval.hashCode());
return result;
}
}
/** Constants. */
public static final Font FONT = getFont();
/** Constants. */
public static final int OFFSET = 10;
/** Constants. */
public static final int INTERVAL_HEIGHT = 20;
/** Constants. */
public static final Color WIDGET_BACKGROUND = GUIHelper.COLOR_WHITE;
/** Constants. */
public static final Color DISABLED_FOREGROUND = GUIHelper.COLOR_GRAY;
/** Constants. */
public static final Color DISABLED_BACKGROUND = GUIHelper.getColor(230, 230, 230);
/** Constants. */
public static final Color NORMAL_FOREGROUND = GUIHelper.COLOR_BLACK;
/** Constants. */
public static final Color ALTERNATIVE_FOREGROUND = GUIHelper.COLOR_WHITE;
/** Constants. */
public static final Color NORMAL_BACKGROUND = GUIHelper.getColor(230, 230, 230);
/** Constants. */
public static final Color SELECTED_BACKGROUND = GUIHelper.COLOR_YELLOW;
/**
* Returns the font.
*
* @return
*/
private static Font getFont(){
FontData fontdata = GUIHelper.DEFAULT_FONT.getFontData()[0];
fontdata.setHeight(9);
return GUIHelper.getFont(fontdata);
}
/** Var. */
private final List<RenderedInterval<T>> intervals = new ArrayList<RenderedInterval<T>>();
/** Var. */
private final List<List<RenderedGroup<T>>> groups = new ArrayList<List<RenderedGroup<T>>>();
/** Var. */
private final List<RenderedInterval<T>> renderedIntervals = new ArrayList<RenderedInterval<T>>();
/** Var. */
private final List<List<RenderedGroup<T>>> renderedGroups = new ArrayList<List<RenderedGroup<T>>>();
/** Var. */
private final HierarchyWizardEditorLayout<T> layout;
/** Var. */
private final HierarchyWizardModelGrouping<T> model;
/**
* Creates a new instance.
*
* @param model
*/
public HierarchyWizardEditorRenderer(HierarchyWizardModelGrouping<T> model) {
this.model = model;
this.layout = new HierarchyWizardEditorLayout<T>(model);
}
/**
* Returns all components.
*
* @return
*/
public List<RenderedComponent<T>> getComponents(){
List<RenderedComponent<T>> result = new ArrayList<RenderedComponent<T>>();
if (model.isShowIntervals()) result.addAll(intervals);
for (List<RenderedGroup<T>> list : groups){
result.addAll(list);
}
return result;
}
/**
* Returns the required minimal size.
*
* @return
*/
public Point getMinSize() {
int minWidth = 0;
int minHeight = 0;
for (RenderedComponent<T> component : getComponents()) {
minWidth = Math.max(minWidth, component.rectangle2.x + component.rectangle2.width);
minHeight = Math.max(minHeight, component.rectangle2.y + component.rectangle2.height);
}
return new Point(minWidth + OFFSET, minHeight + OFFSET);
}
/**
* Mouse click.
*
* @param x
* @param y
* @return
*/
public boolean select(int x, int y) {
Object result = null;
for (RenderedComponent<T> component : getComponents()) {
if (component.enabled) {
if ((component.rectangle1 != null && component.rectangle1.contains(x, y)) ||
(component.rectangle2 != null && component.rectangle2.contains(x, y))) {
if (component instanceof RenderedInterval) {
result = ((RenderedInterval<T>)component).interval;
} else {
result = ((RenderedGroup<T>)component).group;
}
break;
}
}
}
if (result != model.getSelectedElement()) {
model.setSelectedElement(result);
return true;
} else {
model.setSelectedElement(result);
return false;
}
}
/**
* Updates the drawing context.
*/
public void update(){
// Init
int[] factors = layout.layout();
List<HierarchyWizardGroupingInterval<T>> modelIntervals = updateIntervals(factors);
updateGroups(factors, modelIntervals);
}
/**
* Update graphics layout.
*
* @param gc
*/
public void update(GC gc){
int intervalLabelWidth = 0;
int intervalBoundWidth = 0;
int intervalTotalWidth = 0;
if (model.isShowIntervals()) {
intervalLabelWidth = getRequiredLabelWidth(gc, intervals) + OFFSET;
intervalBoundWidth = getRequiredBoundWidth(gc, intervals) + OFFSET;
intervalTotalWidth = intervalLabelWidth + intervalBoundWidth;
}
List<Integer> fanoutLabelWidth = new ArrayList<Integer>();
List<Integer> fanoutBoundWidth = new ArrayList<Integer>();
List<Integer> fanoutTotalWidth = new ArrayList<Integer>();
for (List<RenderedGroup<T>> list : groups){
int label = getRequiredLabelWidth(gc, list) + OFFSET;
int bound = getRequiredBoundWidth(gc, list) + OFFSET;
fanoutLabelWidth.add(label);
fanoutBoundWidth.add(bound);
fanoutTotalWidth.add(label + bound);
}
int top = OFFSET;
if (model.isShowIntervals()) {
for (RenderedInterval<T> context : intervals){
context.rectangle1 = new Rectangle(OFFSET, top, intervalBoundWidth, INTERVAL_HEIGHT);
context.rectangle2 = new Rectangle(OFFSET + intervalBoundWidth, top, intervalLabelWidth, INTERVAL_HEIGHT);
top += INTERVAL_HEIGHT + OFFSET;
}
}
int left = OFFSET * 2 + intervalTotalWidth;
for (int i=0; i<groups.size(); i++){
top = OFFSET;
int offset = 0;
for (RenderedGroup<T> context : groups.get(i)) {
int height = INTERVAL_HEIGHT;
if (layout.isPretty()){
if (i==0){
height = INTERVAL_HEIGHT * context.group.size + OFFSET * (context.group.size - 1);
} else {
RenderedGroup<T> reference1 = groups.get(i-1).get(offset);
offset += context.group.size;
RenderedGroup<T> reference2 = groups.get(i-1).get(offset - 1);
height = reference2.rectangle1.y + reference2.rectangle1.height - reference1.rectangle1.y;
}
}
context.rectangle1 = new Rectangle(left, top, fanoutBoundWidth.get(i), height);
context.rectangle2 = new Rectangle(left + fanoutBoundWidth.get(i), top, fanoutLabelWidth.get(i), height);
top += height + OFFSET;
}
left += fanoutTotalWidth.get(i) + OFFSET;
}
renderedIntervals.clear();
renderedIntervals.addAll(intervals);
renderedGroups.clear();
renderedGroups.addAll(groups);
}
/**
*
*
* @param gc
* @param list
* @return
*/
@SuppressWarnings("unchecked")
private int getRequiredBoundWidth(GC gc, List<?> list){
gc.setFont(FONT);
int width = 0;
for (Object elem : list){
width = Math.max(width, gc.textExtent(((RenderedComponent<T>)elem).bounds).x);
}
return width;
}
/**
*
*
* @param gc
* @param list
* @return
*/
@SuppressWarnings("unchecked")
private int getRequiredLabelWidth(GC gc, List<?> list){
gc.setFont(FONT);
int width = 0;
for (Object elem : list){
width = Math.max(width, gc.textExtent(((RenderedComponent<T>)elem).label).x);
}
return width;
}
/**
*
*
* @param factors
* @param modelIntervals
*/
@SuppressWarnings("unchecked")
private void updateGroups(int[] factors, List<HierarchyWizardGroupingInterval<T>> modelIntervals) {
// Init
List<List<HierarchyWizardGroupingGroup<T>>> modelGroups = model.getModelGroups();
boolean showIntervals = model.isShowIntervals();
T width = null;
DataTypeWithRatioScale<T> dtype = null;
if (showIntervals) {
dtype = (DataTypeWithRatioScale<T>)model.getDataType();
width = dtype.subtract(modelIntervals.get(modelIntervals.size()-1).max, modelIntervals.get(0).min);
}
// Create groups
groups.clear();
int shift = showIntervals ? 1 : 0;
for (int i=0; i<modelGroups.size(); i++){
groups.add(new ArrayList<RenderedGroup<T>>());
int offset = 0;
if (layout.isPretty() && showIntervals && i>0) {
width = dtype.subtract(groups.get(i-1).get(groups.get(i-1).size()-1).max, groups.get(i-1).get(0).min);
}
for (int j=0; j < factors[i+shift]; j++) {
List<HierarchyWizardGroupingGroup<T>> list = modelGroups.get(i);
HierarchyWizardGroupingGroup<T> group = list.get(j % list.size());
RenderedGroup<T> element = new RenderedGroup<T>();
element.depth = i + 1;
element.enabled = j < list.size();
if (layout.isPretty() && showIntervals){
T min = null;
T max = null;
T scale1 = null;
T scale2 = null;
if (i==0) {
min = modelIntervals.get(offset % modelIntervals.size()).min;
if (offset >= modelIntervals.size()) {
int factor = offset / modelIntervals.size();
scale1 = dtype.multiply(width, factor);
}
offset += group.size;
max = modelIntervals.get((offset-1)% modelIntervals.size()).max;
if (offset >= modelIntervals.size()) {
int factor = (offset -1) / modelIntervals.size();
scale2 = dtype.multiply(width, factor);
}
} else {
min = groups.get(i-1).get(offset % groups.get(i-1).size()).min;
if (offset >= groups.get(i-1).size()) {
int factor = offset / groups.get(i-1).size();
scale1 = dtype.multiply(width, factor);
}
offset += group.size;
max = groups.get(i-1).get((offset-1) % groups.get(i-1).size()).max;
if (offset >= groups.get(i-1).size()) {
int factor = (offset -1) / groups.get(i-1).size();
scale2 = dtype.multiply(width, factor);
}
}
if (scale1 != null){
min = dtype.add(scale1, min);
}
if (scale2 != null){
max = dtype.add(scale2, max);
}
element.bounds = "["+dtype.format(min)+", "+dtype.format(max)+"["; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
String[] values = {dtype.format(min), dtype.format(max)};
element.label = group.function.aggregate(values);
element.min = min;
element.max = max;
} else {
element.bounds = String.valueOf(group.size);
element.label = group.function.toString();
}
element.group = group;
groups.get(i).add(element);
}
}
}
/**
*
*
* @param factors
* @return
*/
@SuppressWarnings("unchecked")
private List<HierarchyWizardGroupingInterval<T>> updateIntervals(int[] factors) {
// Init
List<HierarchyWizardGroupingInterval<T>> modelIntervals = model.getIntervals();
boolean showIntervals = model.isShowIntervals();
if (showIntervals) intervals.clear();
DataTypeWithRatioScale<T> dtype = null;
T width = null;
if (showIntervals) {
dtype = (DataTypeWithRatioScale<T>)model.getDataType();
width = dtype.subtract(modelIntervals.get(modelIntervals.size()-1).max, modelIntervals.get(0).min);
}
// Create intervals
if (showIntervals) {
for (int i=0; i < factors[0]; i++) {
HierarchyWizardGroupingInterval<T> interval = modelIntervals.get(i % modelIntervals.size());
RenderedInterval<T> element = new RenderedInterval<T>();
if (i<modelIntervals.size()) {
element.offset = null;
} else {
int factor = i / modelIntervals.size();
element.offset = dtype.multiply(width, factor);
}
element.depth = 0;
element.enabled = i < modelIntervals.size();
T min = interval.min;
T max = interval.max;
if (element.offset != null){
min = dtype.add(element.offset, min);
max = dtype.add(element.offset, max);
}
element.bounds = "["+dtype.format(min)+", "+dtype.format(max)+"["; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
String[] values = {dtype.format(min), dtype.format(max)};
element.label = interval.function.aggregate(values);
element.interval = interval;
intervals.add(element);
}
}
return modelIntervals;
}
}