use of qupath.lib.objects.PathObject in project qupath by qupath.
the class TMACommands method promptToDeleteTMAGridRowOrColumn.
private static boolean promptToDeleteTMAGridRowOrColumn(ImageData<?> imageData, TMARemoveType type) {
String typeString = type.toString();
String title = "Delete TMA " + typeString;
boolean removeRow = type == TMARemoveType.ROW;
if (imageData == null) {
Dialogs.showNoImageError(title);
return false;
}
if (imageData.getHierarchy().getTMAGrid() == null) {
Dialogs.showErrorMessage(title, "No image with dearrayed TMA cores selected!");
return false;
}
PathObjectHierarchy hierarchy = imageData.getHierarchy();
PathObject selected = hierarchy.getSelectionModel().getSelectedObject();
TMACoreObject selectedCore = null;
if (selected != null)
selectedCore = PathObjectTools.getAncestorTMACore(selected);
// Try to identify the row/column that we want
TMAGrid grid = hierarchy.getTMAGrid();
int row = -1;
int col = -1;
if (selectedCore != null) {
for (int y = 0; y < grid.getGridHeight(); y++) {
for (int x = 0; x < grid.getGridWidth(); x++) {
if (grid.getTMACore(y, x) == selectedCore) {
row = y;
col = x;
break;
}
}
}
}
// We need a selected core to know what to remove
if (row < 0 || col < 0) {
Dialogs.showErrorMessage(title, "Please select a TMA core to indicate which " + typeString + " to remove");
return false;
}
// Check we have enough rows/columns - if not, this is just a clear operation
if ((removeRow && grid.getGridHeight() <= 1) || (!removeRow && grid.getGridWidth() <= 1)) {
if (Dialogs.showConfirmDialog(title, "Are you sure you want to delete the entire TMA grid?"))
hierarchy.setTMAGrid(null);
return false;
}
// Confirm the removal - add 1 due to 'base 0' probably not being expected by most users...
int num = removeRow ? row : col;
if (!Dialogs.showConfirmDialog(title, "Are you sure you want to delete " + typeString + " " + (num + 1) + " from TMA grid?"))
return false;
// Create a new grid
List<TMACoreObject> coresNew = new ArrayList<>();
for (int r = 0; r < grid.getGridHeight(); r++) {
if (removeRow && row == r)
continue;
for (int c = 0; c < grid.getGridWidth(); c++) {
if (!removeRow && col == c)
continue;
coresNew.add(grid.getTMACore(r, c));
}
}
int newWidth = removeRow ? grid.getGridWidth() : grid.getGridWidth() - 1;
TMAGrid gridNew = DefaultTMAGrid.create(coresNew, newWidth);
hierarchy.setTMAGrid(gridNew);
hierarchy.getSelectionModel().clearSelection();
// Request new labels
promptToRelabelTMAGrid(imageData);
return true;
}
use of qupath.lib.objects.PathObject in project qupath by qupath.
the class Commands method promptToExportImageRegion.
/**
* Prompt to export the current image region selected in the viewer.
* @param viewer the viewer containing the image to export
* @param renderedImage if true, export the rendered (RGB) image rather than original pixel values
*/
public static void promptToExportImageRegion(QuPathViewer viewer, boolean renderedImage) {
if (viewer == null || viewer.getServer() == null) {
Dialogs.showErrorMessage("Export image region", "No viewer & image selected!");
return;
}
ImageServer<BufferedImage> server = viewer.getServer();
if (renderedImage)
server = RenderedImageServer.createRenderedServer(viewer);
PathObject pathObject = viewer.getSelectedObject();
ROI roi = pathObject == null ? null : pathObject.getROI();
double regionWidth = roi == null ? server.getWidth() : roi.getBoundsWidth();
double regionHeight = roi == null ? server.getHeight() : roi.getBoundsHeight();
// Create a dialog
GridPane pane = new GridPane();
int row = 0;
pane.add(new Label("Export format"), 0, row);
ComboBox<ImageWriter<BufferedImage>> comboImageType = new ComboBox<>();
Function<ImageWriter<BufferedImage>, String> fun = (ImageWriter<BufferedImage> writer) -> writer.getName();
comboImageType.setCellFactory(p -> GuiTools.createCustomListCell(fun));
comboImageType.setButtonCell(GuiTools.createCustomListCell(fun));
var writers = ImageWriterTools.getCompatibleWriters(server, null);
comboImageType.getItems().setAll(writers);
comboImageType.setTooltip(new Tooltip("Choose export image format"));
if (writers.contains(lastWriter))
comboImageType.getSelectionModel().select(lastWriter);
else
comboImageType.getSelectionModel().selectFirst();
comboImageType.setMaxWidth(Double.MAX_VALUE);
GridPane.setHgrow(comboImageType, Priority.ALWAYS);
pane.add(comboImageType, 1, row++);
TextArea textArea = new TextArea();
textArea.setPrefRowCount(2);
textArea.setEditable(false);
textArea.setWrapText(true);
// textArea.setPadding(new Insets(15, 0, 0, 0));
comboImageType.setOnAction(e -> textArea.setText(((ImageWriter<BufferedImage>) comboImageType.getValue()).getDetails()));
textArea.setText(((ImageWriter<BufferedImage>) comboImageType.getValue()).getDetails());
pane.add(textArea, 0, row++, 2, 1);
var label = new Label("Downsample factor");
pane.add(label, 0, row);
TextField tfDownsample = new TextField();
label.setLabelFor(tfDownsample);
pane.add(tfDownsample, 1, row++);
tfDownsample.setTooltip(new Tooltip("Amount to scale down image - choose 1 to export at full resolution (note: for large images this may not succeed for memory reasons)"));
ObservableDoubleValue downsample = Bindings.createDoubleBinding(() -> {
try {
return Double.parseDouble(tfDownsample.getText());
} catch (NumberFormatException e) {
return Double.NaN;
}
}, tfDownsample.textProperty());
// Define a sensible limit for non-pyramidal images
long maxPixels = 10000 * 10000;
Label labelSize = new Label();
labelSize.setMinWidth(400);
labelSize.setTextAlignment(TextAlignment.CENTER);
labelSize.setContentDisplay(ContentDisplay.CENTER);
labelSize.setAlignment(Pos.CENTER);
labelSize.setMaxWidth(Double.MAX_VALUE);
labelSize.setTooltip(new Tooltip("Estimated size of exported image"));
pane.add(labelSize, 0, row++, 2, 1);
labelSize.textProperty().bind(Bindings.createStringBinding(() -> {
if (!Double.isFinite(downsample.get())) {
labelSize.setStyle("-fx-text-fill: red;");
return "Invalid downsample value! Must be >= 1";
} else {
long w = (long) (regionWidth / downsample.get() + 0.5);
long h = (long) (regionHeight / downsample.get() + 0.5);
String warning = "";
var writer = comboImageType.getSelectionModel().getSelectedItem();
boolean supportsPyramid = writer == null ? false : writer.supportsPyramidal();
if (!supportsPyramid && w * h > maxPixels) {
labelSize.setStyle("-fx-text-fill: red;");
warning = " (too big!)";
} else if (w < 5 || h < 5) {
labelSize.setStyle("-fx-text-fill: red;");
warning = " (too small!)";
} else
labelSize.setStyle(null);
return String.format("Output image size: %d x %d pixels%s", w, h, warning);
}
}, downsample, comboImageType.getSelectionModel().selectedIndexProperty()));
tfDownsample.setText(Double.toString(exportDownsample.get()));
PaneTools.setMaxWidth(Double.MAX_VALUE, labelSize, textArea, tfDownsample, comboImageType);
PaneTools.setHGrowPriority(Priority.ALWAYS, labelSize, textArea, tfDownsample, comboImageType);
pane.setVgap(5);
pane.setHgap(5);
if (!Dialogs.showConfirmDialog("Export image region", pane))
return;
var writer = comboImageType.getSelectionModel().getSelectedItem();
boolean supportsPyramid = writer == null ? false : writer.supportsPyramidal();
int w = (int) (regionWidth / downsample.get() + 0.5);
int h = (int) (regionHeight / downsample.get() + 0.5);
if (!supportsPyramid && w * h > maxPixels) {
Dialogs.showErrorNotification("Export image region", "Requested export region too large - try selecting a smaller region, or applying a higher downsample factor");
return;
}
if (downsample.get() < 1 || !Double.isFinite(downsample.get())) {
Dialogs.showErrorMessage("Export image region", "Downsample factor must be >= 1!");
return;
}
exportDownsample.set(downsample.get());
// Now that we know the output, we can create a new server to ensure it is downsampled as the necessary resolution
if (renderedImage && downsample.get() != server.getDownsampleForResolution(0))
server = new RenderedImageServer.Builder(viewer).downsamples(downsample.get()).build();
// selectedImageType.set(comboImageType.getSelectionModel().getSelectedItem());
// Create RegionRequest
RegionRequest request = null;
if (pathObject != null && pathObject.hasROI())
request = RegionRequest.createInstance(server.getPath(), exportDownsample.get(), roi);
// Create a sensible default file name, and prompt for the actual name
String ext = writer.getDefaultExtension();
String writerName = writer.getName();
String defaultName = GeneralTools.getNameWithoutExtension(new File(ServerTools.getDisplayableImageName(server)));
if (roi != null) {
defaultName = String.format("%s (%s, x=%d, y=%d, w=%d, h=%d)", defaultName, GeneralTools.formatNumber(request.getDownsample(), 2), request.getX(), request.getY(), request.getWidth(), request.getHeight());
}
File fileOutput = Dialogs.promptToSaveFile("Export image region", null, defaultName, writerName, ext);
if (fileOutput == null)
return;
try {
if (request == null) {
if (exportDownsample.get() == 1.0)
writer.writeImage(server, fileOutput.getAbsolutePath());
else
writer.writeImage(ImageServers.pyramidalize(server, exportDownsample.get()), fileOutput.getAbsolutePath());
} else
writer.writeImage(server, request, fileOutput.getAbsolutePath());
lastWriter = writer;
} catch (IOException e) {
Dialogs.showErrorMessage("Export region", e);
}
}
use of qupath.lib.objects.PathObject in project qupath by qupath.
the class Commands method convertDetectionsToPoints.
/**
* Convert detection objects to point annotations based upon their ROI centroids.
* @param imageData the image data to process
* @param preferNucleus if true, use a nucleus ROI for cell objects (if available
*/
public static void convertDetectionsToPoints(ImageData<?> imageData, boolean preferNucleus) {
if (imageData == null) {
Dialogs.showNoImageError("Convert detections to points");
return;
}
PathObjectHierarchy hierarchy = imageData.getHierarchy();
Collection<PathObject> pathObjects = hierarchy.getDetectionObjects();
if (pathObjects.isEmpty()) {
Dialogs.showErrorMessage("Detections to points", "No detections found!");
return;
}
// Remove any detections that don't have a ROI - can't do much with them
Iterator<PathObject> iter = pathObjects.iterator();
while (iter.hasNext()) {
if (!iter.next().hasROI())
iter.remove();
}
if (pathObjects.isEmpty()) {
logger.warn("No detections found with ROIs!");
return;
}
// Check if existing objects should be deleted
String message = pathObjects.size() == 1 ? "Delete detection after converting to a point?" : String.format("Delete %d detections after converting to points?", pathObjects.size());
var button = Dialogs.showYesNoCancelDialog("Detections to points", message);
if (button == Dialogs.DialogButton.CANCEL)
return;
boolean deleteDetections = button == Dialogs.DialogButton.YES;
PathObjectTools.convertToPoints(hierarchy, pathObjects, preferNucleus, deleteDetections);
}
use of qupath.lib.objects.PathObject in project qupath by qupath.
the class QuPathGUI method setViewerPopupMenu.
private void setViewerPopupMenu(final QuPathViewerPlus viewer) {
final ContextMenu popup = new ContextMenu();
MenuItem miAddRow = new MenuItem("Add row");
miAddRow.setOnAction(e -> viewerManager.addRow(viewer));
MenuItem miAddColumn = new MenuItem("Add column");
miAddColumn.setOnAction(e -> viewerManager.addColumn(viewer));
MenuItem miRemoveRow = new MenuItem("Remove row");
miRemoveRow.setOnAction(e -> viewerManager.removeViewerRow(viewer));
MenuItem miRemoveColumn = new MenuItem("Remove column");
miRemoveColumn.setOnAction(e -> viewerManager.removeViewerColumn(viewer));
MenuItem miCloseViewer = new MenuItem("Close viewer");
miCloseViewer.setOnAction(e -> {
viewerManager.closeViewer(viewer);
// viewerManager.removeViewer(viewer);
});
MenuItem miResizeGrid = new MenuItem("Reset grid size");
miResizeGrid.setOnAction(e -> {
viewerManager.resetGridSize();
});
MenuItem miToggleSync = ActionTools.createCheckMenuItem(defaultActions.TOGGLE_SYNCHRONIZE_VIEWERS, null);
MenuItem miMatchResolutions = ActionTools.createMenuItem(defaultActions.MATCH_VIEWER_RESOLUTIONS);
Menu menuMultiview = MenuTools.createMenu("Multi-view", miToggleSync, miMatchResolutions, miCloseViewer, null, miResizeGrid, null, // null,
miAddRow, miAddColumn, null, miRemoveRow, miRemoveColumn);
Menu menuView = MenuTools.createMenu("Display", ActionTools.createCheckMenuItem(defaultActions.SHOW_ANALYSIS_PANE, null), defaultActions.BRIGHTNESS_CONTRAST, null, ActionTools.createAction(() -> Commands.setViewerDownsample(viewer, 0.25), "400%"), ActionTools.createAction(() -> Commands.setViewerDownsample(viewer, 1), "100%"), ActionTools.createAction(() -> Commands.setViewerDownsample(viewer, 2), "50%"), ActionTools.createAction(() -> Commands.setViewerDownsample(viewer, 10), "10%"), ActionTools.createAction(() -> Commands.setViewerDownsample(viewer, 100), "1%"));
ToggleGroup groupTools = new ToggleGroup();
Menu menuTools = MenuTools.createMenu("Set tool", ActionTools.createCheckMenuItem(defaultActions.MOVE_TOOL, groupTools), ActionTools.createCheckMenuItem(defaultActions.RECTANGLE_TOOL, groupTools), ActionTools.createCheckMenuItem(defaultActions.ELLIPSE_TOOL, groupTools), ActionTools.createCheckMenuItem(defaultActions.LINE_TOOL, groupTools), ActionTools.createCheckMenuItem(defaultActions.POLYGON_TOOL, groupTools), ActionTools.createCheckMenuItem(defaultActions.POLYLINE_TOOL, groupTools), ActionTools.createCheckMenuItem(defaultActions.BRUSH_TOOL, groupTools), ActionTools.createCheckMenuItem(defaultActions.POINTS_TOOL, groupTools), null, ActionTools.createCheckMenuItem(defaultActions.SELECTION_MODE));
// Handle awkward 'TMA core missing' option
CheckMenuItem miTMAValid = new CheckMenuItem("Set core valid");
miTMAValid.setOnAction(e -> setTMACoreMissing(viewer.getHierarchy(), false));
CheckMenuItem miTMAMissing = new CheckMenuItem("Set core missing");
miTMAMissing.setOnAction(e -> setTMACoreMissing(viewer.getHierarchy(), true));
Menu menuTMA = new Menu("TMA");
MenuTools.addMenuItems(menuTMA, miTMAValid, miTMAMissing, null, defaultActions.TMA_ADD_NOTE, null, MenuTools.createMenu("Add", createImageDataAction(imageData -> TMACommands.promptToAddRowBeforeSelected(imageData), "Add TMA row before"), createImageDataAction(imageData -> TMACommands.promptToAddRowAfterSelected(imageData), "Add TMA row after"), createImageDataAction(imageData -> TMACommands.promptToAddColumnBeforeSelected(imageData), "Add TMA column before"), createImageDataAction(imageData -> TMACommands.promptToAddColumnAfterSelected(imageData), "Add TMA column after")), MenuTools.createMenu("Remove", createImageDataAction(imageData -> TMACommands.promptToDeleteTMAGridRow(imageData), "Remove TMA row"), createImageDataAction(imageData -> TMACommands.promptToDeleteTMAGridColumn(imageData), "column")));
// Create an empty placeholder menu
Menu menuSetClass = MenuTools.createMenu("Set class");
// CheckMenuItem miTMAValid = new CheckMenuItem("Set core valid");
// miTMAValid.setOnAction(e -> setTMACoreMissing(viewer.getHierarchy(), false));
// CheckMenuItem miTMAMissing = new CheckMenuItem("Set core missing");
// miTMAMissing.setOnAction(e -> setTMACoreMissing(viewer.getHierarchy(), true));
Menu menuCells = MenuTools.createMenu("Cells", ActionTools.createCheckMenuItem(defaultActions.SHOW_CELL_BOUNDARIES_AND_NUCLEI, null), ActionTools.createCheckMenuItem(defaultActions.SHOW_CELL_NUCLEI, null), ActionTools.createCheckMenuItem(defaultActions.SHOW_CELL_BOUNDARIES, null), ActionTools.createCheckMenuItem(defaultActions.SHOW_CELL_CENTROIDS, null));
MenuItem miClearSelectedObjects = new MenuItem("Delete object");
miClearSelectedObjects.setOnAction(e -> {
PathObjectHierarchy hierarchy = viewer.getHierarchy();
if (hierarchy == null)
return;
if (hierarchy.getSelectionModel().singleSelection()) {
GuiTools.promptToRemoveSelectedObject(hierarchy.getSelectionModel().getSelectedObject(), hierarchy);
} else {
GuiTools.promptToClearAllSelectedObjects(viewer.getImageData());
}
});
// Create a standard annotations menu
Menu menuAnnotations = GuiTools.populateAnnotationsMenu(this, new Menu("Annotations"));
SeparatorMenuItem topSeparator = new SeparatorMenuItem();
popup.setOnShowing(e -> {
// Check if we have any cells
ImageData<?> imageData = viewer.getImageData();
if (imageData == null)
menuCells.setVisible(false);
else
menuCells.setVisible(!imageData.getHierarchy().getDetectionObjects().isEmpty());
// Check what to show for TMA cores or annotations
Collection<PathObject> selectedObjects = viewer.getAllSelectedObjects();
PathObject pathObject = viewer.getSelectedObject();
menuTMA.setVisible(false);
if (pathObject instanceof TMACoreObject) {
boolean isMissing = ((TMACoreObject) pathObject).isMissing();
miTMAValid.setSelected(!isMissing);
miTMAMissing.setSelected(isMissing);
menuTMA.setVisible(true);
}
// Add clear objects option if we have more than one non-TMA object
if (imageData == null || imageData.getHierarchy().getSelectionModel().noSelection() || imageData.getHierarchy().getSelectionModel().getSelectedObject() instanceof TMACoreObject)
miClearSelectedObjects.setVisible(false);
else {
if (imageData.getHierarchy().getSelectionModel().singleSelection()) {
miClearSelectedObjects.setText("Delete object");
miClearSelectedObjects.setVisible(true);
} else {
miClearSelectedObjects.setText("Delete objects");
miClearSelectedObjects.setVisible(true);
}
}
boolean hasAnnotations = pathObject instanceof PathAnnotationObject || (!selectedObjects.isEmpty() && selectedObjects.stream().allMatch(p -> p.isAnnotation()));
updateSetAnnotationPathClassMenu(menuSetClass, viewer);
menuAnnotations.setVisible(hasAnnotations);
topSeparator.setVisible(hasAnnotations || pathObject instanceof TMACoreObject);
// Occasionally, the newly-visible top part of a popup menu can have the wrong size?
popup.setWidth(popup.getPrefWidth());
});
// popup.add(menuClassify);
popup.getItems().addAll(miClearSelectedObjects, menuTMA, menuSetClass, menuAnnotations, topSeparator, menuMultiview, menuCells, menuView, menuTools);
popup.setAutoHide(true);
// Enable circle pop-up for quick classification on right-click
CirclePopupMenu circlePopup = new CirclePopupMenu(viewer.getView(), null);
viewer.getView().addEventHandler(MouseEvent.MOUSE_PRESSED, e -> {
if ((e.isPopupTrigger() || e.isSecondaryButtonDown()) && e.isShiftDown() && !getAvailablePathClasses().isEmpty()) {
circlePopup.setAnimationDuration(Duration.millis(200));
updateSetAnnotationPathClassMenu(circlePopup, viewer);
circlePopup.show(e.getScreenX(), e.getScreenY());
e.consume();
return;
} else if (circlePopup.isShown())
circlePopup.hide();
if (e.isPopupTrigger() || e.isSecondaryButtonDown()) {
popup.show(viewer.getView().getScene().getWindow(), e.getScreenX(), e.getScreenY());
e.consume();
}
});
// // It's necessary to make the Window the owner, since otherwise the context menu does not disappear when clicking elsewhere on the viewer
// viewer.getView().setOnContextMenuRequested(e -> {
// popup.show(viewer.getView().getScene().getWindow(), e.getScreenX(), e.getScreenY());
// // popup.show(viewer.getView(), e.getScreenX(), e.getScreenY());
// });
}
use of qupath.lib.objects.PathObject in project qupath by qupath.
the class QuPathGUI method setupViewer.
void setupViewer(final QuPathViewerPlus viewer) {
viewer.getView().setFocusTraversable(true);
// Update active viewer as required
viewer.getView().focusedProperty().addListener((e, f, nowFocussed) -> {
if (nowFocussed) {
viewerManager.setActiveViewer(viewer);
}
});
viewer.getView().addEventFilter(MouseEvent.MOUSE_PRESSED, e -> viewer.getView().requestFocus());
viewer.zoomToFitProperty().bind(zoomToFit);
// Create popup menu
setViewerPopupMenu(viewer);
viewer.getView().widthProperty().addListener((e, f, g) -> {
if (viewer.getZoomToFit())
updateMagnificationString();
});
viewer.getView().heightProperty().addListener((e, f, g) -> {
if (viewer.getZoomToFit())
updateMagnificationString();
});
// Enable drag and drop
dragAndDrop.setupTarget(viewer.getView());
// Listen to the scroll wheel
viewer.getView().setOnScroll(e -> {
if (viewer == viewerManager.getActiveViewer() || !viewerManager.getSynchronizeViewers()) {
double scrollUnits = e.getDeltaY() * PathPrefs.getScaledScrollSpeed();
// Use shift down to adjust opacity
if (e.isShortcutDown()) {
OverlayOptions options = viewer.getOverlayOptions();
options.setOpacity((float) (options.getOpacity() + scrollUnits * 0.001));
return;
}
// Avoid zooming at the end of a gesture when using touchscreens
if (e.isInertia())
return;
if (PathPrefs.invertScrollingProperty().get())
scrollUnits = -scrollUnits;
double newDownsampleFactor = viewer.getDownsampleFactor() * Math.pow(viewer.getDefaultZoomFactor(), scrollUnits);
newDownsampleFactor = Math.min(viewer.getMaxDownsample(), Math.max(newDownsampleFactor, viewer.getMinDownsample()));
viewer.setDownsampleFactor(newDownsampleFactor, e.getX(), e.getY());
}
});
viewer.getView().addEventFilter(RotateEvent.ANY, e -> {
if (!PathPrefs.useRotateGesturesProperty().get())
return;
// logger.debug("Rotating: " + e.getAngle());
viewer.setRotation(viewer.getRotation() + Math.toRadians(e.getAngle()));
e.consume();
});
viewer.getView().addEventFilter(ZoomEvent.ANY, e -> {
if (!PathPrefs.useZoomGesturesProperty().get())
return;
double zoomFactor = e.getZoomFactor();
if (Double.isNaN(zoomFactor))
return;
logger.debug("Zooming: " + e.getZoomFactor() + " (" + e.getTotalZoomFactor() + ")");
viewer.setDownsampleFactor(viewer.getDownsampleFactor() / zoomFactor, e.getX(), e.getY());
e.consume();
});
viewer.getView().addEventFilter(ScrollEvent.ANY, new ScrollEventPanningFilter(viewer));
viewer.getView().addEventHandler(KeyEvent.KEY_PRESSED, e -> {
PathObject pathObject = viewer.getSelectedObject();
if (!e.isConsumed() && pathObject != null) {
if (pathObject.isTMACore()) {
TMACoreObject core = (TMACoreObject) pathObject;
if (e.getCode() == KeyCode.ENTER) {
defaultActions.TMA_ADD_NOTE.handle(new ActionEvent(e.getSource(), e.getTarget()));
e.consume();
} else if (e.getCode() == KeyCode.BACK_SPACE) {
core.setMissing(!core.isMissing());
viewer.getHierarchy().fireObjectsChangedEvent(this, Collections.singleton(core));
e.consume();
}
} else if (pathObject.isAnnotation()) {
if (e.getCode() == KeyCode.ENTER) {
GuiTools.promptToSetActiveAnnotationProperties(viewer.getHierarchy());
e.consume();
}
}
}
});
}
Aggregations