/**
* Copyright (C) 2001-2017 by RapidMiner and the contributors
*
* Complete list of developers available at our web site:
*
* http://rapidminer.com
*
* This program is free software: you can redistribute it and/or modify it under the terms of the
* GNU Affero General Public License as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License along with this program.
* If not, see http://www.gnu.org/licenses/.
*/
package com.rapidminer.gui.viewer.metadata;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.logging.Level;
import javax.swing.SwingWorker;
import com.rapidminer.example.ExampleSet;
import com.rapidminer.example.Statistics;
import com.rapidminer.gui.tools.UpdateQueue;
import com.rapidminer.gui.viewer.metadata.model.AbstractAttributeStatisticsModel;
import com.rapidminer.gui.viewer.metadata.model.MetaDataStatisticsModel;
import com.rapidminer.gui.viewer.metadata.model.MetaDataStatisticsModel.SortingDirection;
import com.rapidminer.gui.viewer.metadata.model.MetaDataStatisticsModel.SortingType;
import com.rapidminer.tools.LogService;
import com.rapidminer.tools.Ontology;
/**
* This class is the controller for the {@link MetaDataStatisticsViewer}. It makes changes to the
* {@link MetaDataStatisticsModel} when the view tells it the user changes something.
*
* @author Marco Boeck
*
*/
public class MetaDataStatisticsController {
/**
* the barrier which is used to update the stats of all {@link AttributeStatisticsPanel}s once
* the {@link ExampleSet} statistics have been calculated
*/
private CyclicBarrier barrier;
/** the model backing this */
private MetaDataStatisticsModel model;
/** the {@link UpdateQueue} used to sort */
private UpdateQueue sortingQueue;
/** the {@link SwingWorker} to recalculate statistics */
private SwingWorker<Void, Void> worker;
/**
* needed to restore the initial order; faster way as opposed to sorting (which is not easily
* possible because role information is not available)
*/
private List<AbstractAttributeStatisticsModel> backupInitialOrderList;
/**
* Creates a new {@link MetaDataStatisticsController} instance. Does not store
*
* @param exampleSet
* the {@link ExampleSet} for which the meta data statistics should be created. No
* reference to it is stored to prevent memory leaks.
* @param view
* @param model
*/
public MetaDataStatisticsController(MetaDataStatisticsViewer view, MetaDataStatisticsModel model) {
if (model.getExampleSetOrNull() == null) {
throw new IllegalArgumentException("model exampleSet must not be null at construction time!");
}
this.model = model;
backupInitialOrderList = new ArrayList<>();
barrier = createUpdateBarrier();
// start up sorting queue (for future sorting)
sortingQueue = new UpdateQueue("Attribute Sorting");
sortingQueue.start();
// init sorting to none
resetSorting();
calculateStatistics(model.getExampleSetOrNull());
}
/**
* Call to let the controller know that GUI or calculations are done. Once both are done (aka
* this method has been called twice after initialization), the GUI will be notified to display
* everything.
*/
public void waitAtBarrier() {
try {
// GUI is done, tell barrier so once GUI and calculations are done the GUI can be
// updated
barrier.await();
} catch (InterruptedException e) {
LogService.getRoot().log(Level.INFO, "com.rapidminer.gui.meta_data_view.calc_interrupted");
} catch (BrokenBarrierException e) {
LogService.getRoot().log(Level.INFO, "com.rapidminer.gui.meta_data_view.calc_sync_broken");
}
}
/**
* Changes the current page index to the first page (if possible).
*/
public void setCurrentPageIndexToFirstPage() {
if (model.getCurrentPageIndex() > 0) {
model.setCurrentPageIndex(0);
model.firePaginationChangedEvent();
}
}
/**
* Decrements the current page index (if possible).
*/
public void decrementCurrentPageIndex() {
if (model.getCurrentPageIndex() > 0) {
model.setCurrentPageIndex(model.getCurrentPageIndex() - 1);
model.firePaginationChangedEvent();
}
}
/**
* Increments the current page index (if possible).
*/
public void incrementCurrentPageIndex() {
if (model.getCurrentPageIndex() < model.getNumberOfPages() - 1) {
model.setCurrentPageIndex(model.getCurrentPageIndex() + 1);
model.firePaginationChangedEvent();
}
}
/**
* Changes the current page index to the last page (if possible).
*/
public void setCurrentPageIndexToLastPage() {
if (model.getCurrentPageIndex() < model.getNumberOfPages() - 1) {
model.setCurrentPageIndex(model.getNumberOfPages() - 1);
model.firePaginationChangedEvent();
}
}
/**
* Changes the current page to the human index page (which obviously starts at 1 rather than 0).
*
* @param humanPageIndex
*/
public void jumpToHumanPageIndex(int humanPageIndex) {
if (model.getCurrentPageIndex() != humanPageIndex - 1) {
model.setCurrentPageIndex(humanPageIndex - 1);
model.firePaginationChangedEvent();
}
}
/**
* Call when the attribute name sorting should be cycled.
*/
public void cycleAttributeNameSorting() {
SortingDirection direction = model.getSortingDirection(SortingType.NAME);
switch (direction) {
case UNDEFINED:
setSorting(SortingType.NAME, SortingDirection.DESCENDING);
break;
case DESCENDING:
setSorting(SortingType.NAME, SortingDirection.ASCENDING);
break;
case ASCENDING:
setSorting(SortingType.NAME, SortingDirection.UNDEFINED);
break;
default:
setSorting(SortingType.NAME, SortingDirection.UNDEFINED);
}
}
/**
* Call when the attribute type sorting should be cycled.
*/
public void cycleAttributeTypeSorting() {
SortingDirection direction = model.getSortingDirection(SortingType.TYPE);
switch (direction) {
case UNDEFINED:
setSorting(SortingType.TYPE, SortingDirection.DESCENDING);
break;
case DESCENDING:
setSorting(SortingType.TYPE, SortingDirection.ASCENDING);
break;
case ASCENDING:
setSorting(SortingType.TYPE, SortingDirection.UNDEFINED);
break;
default:
setSorting(SortingType.TYPE, SortingDirection.UNDEFINED);
}
}
/**
* Call when the attribute missings sorting should be cycled.
*/
public void cycleAttributeMissingSorting() {
SortingDirection direction = model.getSortingDirection(SortingType.MISSING);
switch (direction) {
case UNDEFINED:
setSorting(SortingType.MISSING, SortingDirection.DESCENDING);
break;
case DESCENDING:
setSorting(SortingType.MISSING, SortingDirection.ASCENDING);
break;
case ASCENDING:
setSorting(SortingType.MISSING, SortingDirection.UNDEFINED);
break;
default:
setSorting(SortingType.MISSING, SortingDirection.UNDEFINED);
}
}
/**
* Sets the {@link List} of ordered {@link AbstractAttributeStatisticsModel}s.
*
* @param list
*/
public void setAttributeStatisticsModels(List<AbstractAttributeStatisticsModel> orderedModelList) {
model.setOrderedModelList(new ArrayList<>(orderedModelList));
this.backupInitialOrderList = new ArrayList<>(orderedModelList);
for (AbstractAttributeStatisticsModel statModel : orderedModelList) {
model.setAttributeStatisticsModelVisible(statModel, true);
}
}
/**
* Set the attribute name filter.
*
* @param filterNameString
*/
public void setFilterNameString(String filterNameString) {
model.setFilterNameString(filterNameString);
applyFilters();
}
/**
* Sets whether only attributes with missing values should be shown.
*
* @param showOnlyMissingsAttributes
*/
public void setShowOnlyMissingsAttributes(boolean showOnlyMissingsAttributes) {
model.setShowOnlyMissingsAttributes(showOnlyMissingsAttributes);
applyFilters();
}
/**
* Sets whether special attributes should be shown.
*
* @param showSpecialAttributes
*/
public void setShowSpecialAttributes(boolean showSpecialAttributes) {
model.setShowSpecialAttributes(showSpecialAttributes);
applyFilters();
}
/**
* Sets whether regular attributes should be shown.
*
* @param showRegularAttributes
*/
public void setShowRegularAttributes(boolean showRegularAttributes) {
model.setShowRegularAttributes(showRegularAttributes);
applyFilters();
}
/**
* Sets the visibility of {@link Ontology#VALUE_TYPE}s.
*
* @param valueType
* @param visible
*/
public void setAttributeTypeVisibility(int valueType, boolean visible) {
model.setAttributeTypeVisibility(valueType, visible);
applyFilters();
}
/**
* Returns the ordered {@link List} of {@link AbstractAttributeStatisticsModel}s of the given
* page index. Only returns models which are visible and only returns max {@value #PAGE_SIZE}
* models. If pageIndex * {@value #PAGE_SIZE} > {@link #getTotalSize()} returns an empty list.
*
* @return
*/
public List<AbstractAttributeStatisticsModel> getPagedAndVisibleAttributeStatisticsModels() {
List<AbstractAttributeStatisticsModel> resultList = new LinkedList<>();
// this would be the starting index of no other stat models where hidden before this index
int i = model.getCurrentPageIndex() * MetaDataStatisticsModel.PAGE_SIZE;
// but we need to know how many are hidden before this index
int hiddenCount = 0;
for (int j = Math.min(i, model.getTotalSize() - 1); j >= 0; j--) {
if (!model.isAttributeStatisticsModelsVisible(model.getOrderedAttributeStatisticsModels().get(j))) {
hiddenCount++;
}
}
// now add hidden count to starting index, because that many more elements are shown on the
// page(s) before
i = i + hiddenCount;
int count = 1;
while (i < model.getTotalSize() && count <= MetaDataStatisticsModel.PAGE_SIZE) {
AbstractAttributeStatisticsModel statMmodel = model.getOrderedAttributeStatisticsModels().get(i++);
if (model.isAttributeStatisticsModelsVisible(statMmodel)) {
resultList.add(statMmodel);
count++;
}
}
return resultList;
}
/**
* Stops the statistics recalculation and the sorting queue.
*/
void stop() {
worker.cancel(true);
sortingQueue.shutdown();
}
/**
* Sets the desired {@link SortingDirection} for the given {@link SortingType}.
*
* @param type
* @param direction
*/
private void setSorting(SortingType type, SortingDirection direction) {
// make sure we are allowed to do so
if (!model.isSortingAndFilteringAllowed()) {
return;
}
// we only allow one type of sorting at the same time, so reset everything beforehand
resetSorting();
model.setSortingDirection(type, direction);
applySorting();
}
/**
* Applies the current filters.
*/
private void applyFilters() {
// make sure we are allowed to do so
if (!model.isSortingAndFilteringAllowed()) {
return;
}
// count this for performance reasons
int visibleCount = model.getTotalSize();
// reset filters on empty filter string
if ("".equals(model.getFilterNameString())) {
for (AbstractAttributeStatisticsModel statModel : backupInitialOrderList) {
model.setAttributeStatisticsModelVisible(statModel, true);
}
} else {
// apply filter on non empty string
for (AbstractAttributeStatisticsModel statModel : model.getOrderedAttributeStatisticsModels()) {
String attName = statModel.getAttribute().getName();
boolean show = attName.toLowerCase().contains(model.getFilterNameString().toLowerCase());
if (!show) {
visibleCount--;
}
model.setAttributeStatisticsModelVisible(statModel, show);
}
}
// apply attribute filters
for (AbstractAttributeStatisticsModel statModel : model.getOrderedAttributeStatisticsModels()) {
// if special attributes should not be visible and the attribute is special, hide it
if (!model.isShowSpecialAttributes() && statModel.isSpecialAtt()) {
if (model.setAttributeStatisticsModelVisible(statModel, false)) {
visibleCount--;
}
}
// if regular attributes should not be visible and the attribute is regular, hide it
if (!model.isShowRegularAttributes() && !statModel.isSpecialAtt()) {
if (model.setAttributeStatisticsModelVisible(statModel, false)) {
visibleCount--;
}
}
// if attribute has no missing values and only missing values should be visible, hide it
ExampleSet exSet = model.getExampleSetOrNull();
if (exSet != null) {
if (model.isShowOnlyMissingsAttributes()
&& exSet.getStatistics(statModel.getAttribute(), Statistics.UNKNOWN) <= 0) {
if (model.setAttributeStatisticsModelVisible(statModel, false)) {
visibleCount--;
}
}
}
// if the attribute value type should not be visible, hide it
boolean showValueType = true;
for (String valueTypeString : Ontology.VALUE_TYPE_NAMES) {
int valueType = Ontology.ATTRIBUTE_VALUE_TYPE.mapName(valueTypeString);
if (Ontology.ATTRIBUTE_VALUE_TYPE.isA(statModel.getAttribute().getValueType(), valueType)) {
showValueType &= model.getAttributeTypeVisibility(valueType);
if (!showValueType) {
break;
}
}
}
if (!showValueType) {
if (model.setAttributeStatisticsModelVisible(statModel, false)) {
visibleCount--;
}
}
}
model.setVisibleCount(visibleCount);
// update pagination system to current data
if (model.getCurrentPageIndex() >= model.getNumberOfPages()) {
// not firing this here because we fire filter changed directly after and they overlap
// here
model.setCurrentPageIndex(0);
}
model.fireFilterChangedEvent();
}
/**
* Applies the current sorting.
*/
private void applySorting() {
sortingQueue.execute(new Runnable() {
@Override
public void run() {
if (model.getSortingDirection(SortingType.NAME) != SortingDirection.UNDEFINED) {
sortByName(model.getOrderedAttributeStatisticsModels(), model.getSortingDirection(SortingType.NAME));
}
if (model.getSortingDirection(SortingType.TYPE) != SortingDirection.UNDEFINED) {
sortByType(model.getOrderedAttributeStatisticsModels(), model.getSortingDirection(SortingType.TYPE));
}
if (model.getSortingDirection(SortingType.MISSING) != SortingDirection.UNDEFINED) {
sortByMissing(model.getOrderedAttributeStatisticsModels(),
model.getSortingDirection(SortingType.MISSING));
}
model.fireOrderChangedEvent();
}
});
}
/**
* Sorts the {@link AbstractAttributeStatisticsModel}s via their attribute names.
*
* @param direction
*/
private void sortByName(List<AbstractAttributeStatisticsModel> list, final SortingDirection direction) {
sort(list, new Comparator<AbstractAttributeStatisticsModel>() {
@Override
public int compare(AbstractAttributeStatisticsModel o1, AbstractAttributeStatisticsModel o2) {
int sortResult = o1.getAttribute().getName().compareTo(o2.getAttribute().getName());
switch (direction) {
case ASCENDING:
return -1 * sortResult;
case DESCENDING:
return sortResult;
case UNDEFINED:
return 0;
default:
return sortResult;
}
}
});
}
/**
* Sorts the {@link AbstractAttributeStatisticsModel}s via their attribute types.
*
* @param direction
*/
private void sortByType(List<AbstractAttributeStatisticsModel> list, final SortingDirection direction) {
sort(list, new Comparator<AbstractAttributeStatisticsModel>() {
@Override
public int compare(AbstractAttributeStatisticsModel o1, AbstractAttributeStatisticsModel o2) {
int sortResult = Ontology.ATTRIBUTE_VALUE_TYPE.mapIndex(o1.getAttribute().getValueType())
.compareTo(Ontology.ATTRIBUTE_VALUE_TYPE.mapIndex(o2.getAttribute().getValueType()));
switch (direction) {
case ASCENDING:
return -1 * sortResult;
case DESCENDING:
return sortResult;
case UNDEFINED:
return 0;
default:
return sortResult;
}
}
});
}
/**
* Sorts the {@link AbstractAttributeStatisticsModel}s via their attribute types.
*
* @param direction
*/
private void sortByMissing(List<AbstractAttributeStatisticsModel> list, final SortingDirection direction) {
sort(list, new Comparator<AbstractAttributeStatisticsModel>() {
@Override
public int compare(AbstractAttributeStatisticsModel o1, AbstractAttributeStatisticsModel o2) {
ExampleSet exSet = model.getExampleSetOrNull();
if (exSet == null) {
return 0;
}
Double missing1 = exSet.getStatistics(o1.getAttribute(), Statistics.UNKNOWN);
Double missing2 = exSet.getStatistics(o2.getAttribute(), Statistics.UNKNOWN);
if (missing1 == null || missing2 == null) {
return 0;
}
int sortResult = missing1.compareTo(missing2);
switch (direction) {
case ASCENDING:
return -1 * sortResult;
case DESCENDING:
return sortResult;
case UNDEFINED:
return 0;
default:
return sortResult;
}
}
});
}
/**
* Resets the current sorting to none and restores the initial order of the
* {@link AbstractAttributeStatisticsModel}s.
*/
private void resetSorting() {
for (SortingType type : SortingType.values()) {
model.setSortingDirection(type, SortingDirection.UNDEFINED);
}
restoreInitialStatModelOrder();
}
/**
* Restores the initial order of the attribute stat models (special at the top, regular below).
* This is done because we no longer may have access to attribute roles after construction time.
*/
private void restoreInitialStatModelOrder() {
model.setOrderedModelList(backupInitialOrderList);
}
/**
* Creates a {@link CyclicBarrier} which will update the statistics part of all
* {@link AttributeStatisticsPanel}s.
*
* @return
*/
private CyclicBarrier createUpdateBarrier() {
return new CyclicBarrier(2, new Runnable() {
@Override
public void run() {
// once both barriers are broken
ExampleSet exampleSet = model.getExampleSetOrNull();
if (exampleSet == null) {
throw new IllegalArgumentException("model exampleSet must not be null at construction time!");
}
// update stats on all attribute stat models
for (AbstractAttributeStatisticsModel statModel : model.getOrderedAttributeStatisticsModels()) {
statModel.updateStatistics(exampleSet);
}
// allow sorting and filtering
model.setAllowSortingAndFiltering();
// signal that everything is done
model.fireInitDoneEvent();
}
});
}
/**
* Calculates the statistics of the given {@link ExampleSet} in a {@link SwingWorker}. Once the
* statistics are calculated, will update the stats on all {@link AttributeStatisticsPanel}s.
*
* @param exampleSet
*/
private void calculateStatistics(final ExampleSet exampleSet) {
worker = new SwingWorker<Void, Void>() {
@Override
protected Void doInBackground() throws Exception {
exampleSet.recalculateAllAttributeStatistics();
waitAtBarrier();
return null;
}
};
worker.execute();
}
/**
* Sorts the given {@link List} of {@link AbstractAttributeStatisticsModel}s with the given
* {@link Comparator}.
*
* @param listOfStatModels
* @param comp
*/
private static void sort(List<AbstractAttributeStatisticsModel> listOfStatModels,
Comparator<AbstractAttributeStatisticsModel> comp) {
Collections.sort(listOfStatModels, comp);
}
}