/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* 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.optaplanner.examples.investment.swingui;
import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.util.List;
import java.util.Map;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSpinner;
import javax.swing.JTabbedPane;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingConstants;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.optaplanner.examples.common.swingui.SolutionPanel;
import org.optaplanner.examples.common.swingui.timetable.TimeTablePanel;
import org.optaplanner.examples.investment.domain.AssetClass;
import org.optaplanner.examples.investment.domain.AssetClassAllocation;
import org.optaplanner.examples.investment.domain.InvestmentParametrization;
import org.optaplanner.examples.investment.domain.InvestmentSolution;
import org.optaplanner.examples.investment.domain.Region;
import org.optaplanner.examples.investment.domain.Sector;
import org.optaplanner.examples.investment.domain.util.InvestmentNumericUtil;
import org.optaplanner.swing.impl.SwingUtils;
import org.optaplanner.swing.impl.TangoColorFactory;
import static org.optaplanner.examples.common.swingui.timetable.TimeTablePanel.HeaderColumnKey.*;
import static org.optaplanner.examples.common.swingui.timetable.TimeTablePanel.HeaderRowKey.*;
public class InvestmentPanel extends SolutionPanel<InvestmentSolution> {
public static final String LOGO_PATH = "/org/optaplanner/examples/investment/swingui/investmentLogo.png";
private final TimeTablePanel<AssetClass, AssetClass> assetClassPanel;
private final TimeTablePanel<Void, Region> regionPanel;
private final TimeTablePanel<Void, Sector> sectorPanel;
private JSpinner standardDeviationMaximumField;
private boolean ignoreChangeEvents = false;
public InvestmentPanel() {
setLayout(new BorderLayout());
add(createTableHeader(), BorderLayout.NORTH);
JTabbedPane tabbedPane = new JTabbedPane();
assetClassPanel = new TimeTablePanel<>();
tabbedPane.add("Asset classes", new JScrollPane(assetClassPanel));
regionPanel = new TimeTablePanel<>();
tabbedPane.add("Regions", new JScrollPane(regionPanel));
sectorPanel = new TimeTablePanel<>();
tabbedPane.add("Sectors", new JScrollPane(sectorPanel));
add(tabbedPane, BorderLayout.CENTER);
setPreferredSize(PREFERRED_SCROLLABLE_VIEWPORT_SIZE);
}
private JPanel createTableHeader() {
JPanel headerPanel = new JPanel(new FlowLayout());
headerPanel.add(new JLabel("Standard deviation maximum"));
standardDeviationMaximumField = new JSpinner(new SpinnerNumberModel(1.0, 0.0, 10.0, 0.001));
standardDeviationMaximumField.setEditor(new JSpinner.NumberEditor(standardDeviationMaximumField,
InvestmentNumericUtil.MILLIS_PERCENT_PATTERN));
headerPanel.add(standardDeviationMaximumField);
standardDeviationMaximumField.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
if (ignoreChangeEvents) {
return;
}
long standardDeviationMillisMaximum = (long) (((Number) standardDeviationMaximumField.getValue()).doubleValue() * 1000.0);
changeStandardDeviationMillisMaximum(standardDeviationMillisMaximum);
}
});
return headerPanel;
}
@Override
public boolean isWrapInScrollPane() {
return false;
}
@Override
public void resetPanel(InvestmentSolution solution) {
ignoreChangeEvents = true;
assetClassPanel.reset();
regionPanel.reset();
sectorPanel.reset();
InvestmentParametrization parametrization = solution.getParametrization();
standardDeviationMaximumField.setValue((double) parametrization.getStandardDeviationMillisMaximum() / 1000.0);
defineGrid(solution);
fillCells(solution);
ignoreChangeEvents = false;
repaint(); // Hack to force a repaint of TimeTableLayout during "refresh screen while solving"
}
private void defineGrid(InvestmentSolution solution) {
JButton footprint = SwingUtils.makeSmallButton(new JButton("99999999"));
int footprintWidth = footprint.getPreferredSize().width;
assetClassPanel.defineColumnHeaderByKey(HEADER_COLUMN);
assetClassPanel.defineColumnHeaderByKey(HEADER_COLUMN_EXTRA_PROPERTY_1);
assetClassPanel.defineColumnHeaderByKey(HEADER_COLUMN_EXTRA_PROPERTY_2);
assetClassPanel.defineColumnHeaderByKey(HEADER_COLUMN_EXTRA_PROPERTY_3);
assetClassPanel.defineColumnHeaderByKey(HEADER_COLUMN_EXTRA_PROPERTY_4);
assetClassPanel.defineColumnHeaderByKey(HEADER_COLUMN_EXTRA_PROPERTY_5);
for (AssetClass assetClass : solution.getAssetClassList()) {
assetClassPanel.defineColumnHeader(assetClass, footprintWidth);
}
assetClassPanel.defineRowHeaderByKey(HEADER_ROW_GROUP1);
assetClassPanel.defineRowHeaderByKey(HEADER_ROW);
for (AssetClass assetClass : solution.getAssetClassList()) {
assetClassPanel.defineRowHeader(assetClass);
}
assetClassPanel.defineRowHeaderByKey(TRAILING_HEADER_ROW); // Total
regionPanel.defineColumnHeaderByKey(HEADER_COLUMN);
regionPanel.defineColumnHeaderByKey(HEADER_COLUMN_EXTRA_PROPERTY_1);
regionPanel.defineColumnHeaderByKey(HEADER_COLUMN_EXTRA_PROPERTY_2);
regionPanel.defineRowHeaderByKey(HEADER_ROW);
for (Region region : solution.getRegionList()) {
regionPanel.defineRowHeader(region);
}
sectorPanel.defineColumnHeaderByKey(HEADER_COLUMN);
sectorPanel.defineColumnHeaderByKey(HEADER_COLUMN_EXTRA_PROPERTY_1);
sectorPanel.defineColumnHeaderByKey(HEADER_COLUMN_EXTRA_PROPERTY_2);
sectorPanel.defineRowHeaderByKey(HEADER_ROW);
for (Sector sector : solution.getSectorList()) {
sectorPanel.defineRowHeader(sector);
}
}
private void fillCells(InvestmentSolution solution) {
List<AssetClass> assetClassList = solution.getAssetClassList();
assetClassPanel.addCornerHeader(HEADER_COLUMN, HEADER_ROW, createTableHeader(new JLabel("Asset class"), null));
assetClassPanel.addCornerHeader(HEADER_COLUMN_EXTRA_PROPERTY_1, HEADER_ROW,
createTableHeader(new JLabel("Region"), null));
assetClassPanel.addCornerHeader(HEADER_COLUMN_EXTRA_PROPERTY_2, HEADER_ROW,
createTableHeader(new JLabel("Sector"), null));
assetClassPanel.addCornerHeader(HEADER_COLUMN_EXTRA_PROPERTY_3, HEADER_ROW,
createTableHeader(new JLabel("Expected return"), null));
assetClassPanel.addCornerHeader(HEADER_COLUMN_EXTRA_PROPERTY_4, HEADER_ROW,
createTableHeader(new JLabel("Standard deviation risk"), null));
JLabel quantityHeaderLabel = new JLabel("Quantity");
quantityHeaderLabel.setForeground(TangoColorFactory.ORANGE_3);
assetClassPanel.addCornerHeader(HEADER_COLUMN_EXTRA_PROPERTY_5, HEADER_ROW,
createTableHeader(quantityHeaderLabel, null));
assetClassPanel.addColumnHeader(assetClassList.get(0), HEADER_ROW_GROUP1,
assetClassList.get(assetClassList.size() - 1), HEADER_ROW_GROUP1,
createTableHeader(new JLabel("Correlation"), null));
for (AssetClass assetClass : assetClassList) {
assetClassPanel.addColumnHeader(assetClass, HEADER_ROW,
createTableHeader(new JLabel(assetClass.getName(), SwingConstants.CENTER),
"Expected return: " + assetClass.getExpectedReturnLabel()
+ " - Standard deviation risk: " + assetClass.getStandardDeviationRiskLabel()));
}
for (AssetClass assetClass : assetClassList) {
assetClassPanel.addRowHeader(HEADER_COLUMN, assetClass,
createTableHeader(new JLabel(assetClass.getName(), SwingConstants.LEFT),
"Expected return: " + assetClass.getExpectedReturnLabel()
+ " - Standard deviation risk: " + assetClass.getStandardDeviationRiskLabel()));
}
for (AssetClass a : assetClassList) {
for (AssetClass b : assetClassList) {
assetClassPanel.addCell(a, b, new JLabel(a.getCorrelationLabel(b), SwingConstants.RIGHT));
}
}
assetClassPanel.addCornerHeader(HEADER_COLUMN, TRAILING_HEADER_ROW,
createTableHeader(new JLabel("Total"), null));
long quantityTotalMillis = 0L;
for (AssetClassAllocation allocation : solution.getAssetClassAllocationList()) {
if (allocation.getQuantityMillis() != null) {
quantityTotalMillis += allocation.getQuantityMillis();
}
assetClassPanel.addRowHeader(HEADER_COLUMN_EXTRA_PROPERTY_1, allocation.getAssetClass(),
new JLabel(allocation.getAssetClass().getRegion().getName()));
assetClassPanel.addRowHeader(HEADER_COLUMN_EXTRA_PROPERTY_2, allocation.getAssetClass(),
new JLabel(allocation.getAssetClass().getSector().getName()));
assetClassPanel.addRowHeader(HEADER_COLUMN_EXTRA_PROPERTY_3, allocation.getAssetClass(),
new JLabel(allocation.getAssetClass().getExpectedReturnLabel(), SwingConstants.RIGHT));
assetClassPanel.addRowHeader(HEADER_COLUMN_EXTRA_PROPERTY_4, allocation.getAssetClass(),
new JLabel(allocation.getAssetClass().getStandardDeviationRiskLabel(), SwingConstants.RIGHT));
JLabel quantityLabel = new JLabel(allocation.getQuantityLabel(), SwingConstants.RIGHT);
quantityLabel.setForeground(TangoColorFactory.ORANGE_3);
assetClassPanel.addRowHeader(HEADER_COLUMN_EXTRA_PROPERTY_5, allocation.getAssetClass(),
quantityLabel);
}
JLabel expectedReturnLabel = new JLabel(InvestmentNumericUtil.formatMicrosAsPercentage(solution.calculateExpectedReturnMicros()), SwingConstants.RIGHT);
assetClassPanel.addCornerHeader(HEADER_COLUMN_EXTRA_PROPERTY_3, TRAILING_HEADER_ROW,
expectedReturnLabel);
long standardDeviationMicros = solution.calculateStandardDeviationMicros();
JLabel standardDeviationLabel = new JLabel(InvestmentNumericUtil.formatMicrosAsPercentage(standardDeviationMicros), SwingConstants.RIGHT);
if (standardDeviationMicros > solution.getParametrization().getStandardDeviationMillisMaximum() * 1000L) {
standardDeviationLabel.setForeground(TangoColorFactory.SCARLET_3);
}
assetClassPanel.addCornerHeader(HEADER_COLUMN_EXTRA_PROPERTY_4, TRAILING_HEADER_ROW,
standardDeviationLabel);
JLabel quantityTotalLabel = new JLabel(InvestmentNumericUtil.formatMillisAsPercentage(quantityTotalMillis), SwingConstants.RIGHT);
quantityTotalLabel.setForeground(TangoColorFactory.ORANGE_3);
assetClassPanel.addCornerHeader(HEADER_COLUMN_EXTRA_PROPERTY_5, TRAILING_HEADER_ROW, quantityTotalLabel);
regionPanel.addCornerHeader(HEADER_COLUMN, HEADER_ROW, createTableHeader(new JLabel("Region"), null));
regionPanel.addCornerHeader(HEADER_COLUMN_EXTRA_PROPERTY_1, HEADER_ROW, createTableHeader(new JLabel("Quantity total"), null));
regionPanel.addCornerHeader(HEADER_COLUMN_EXTRA_PROPERTY_2, HEADER_ROW, createTableHeader(new JLabel("Quantity maximum"), null));
Map<Region, Long> regionTotalMap = solution.calculateRegionQuantityMillisTotalMap();
for (final Region region : solution.getRegionList()) {
regionPanel.addRowHeader(HEADER_COLUMN, region, new JLabel(region.getName()));
long total = regionTotalMap.get(region);
JLabel totalLabel = new JLabel(InvestmentNumericUtil.formatMillisAsPercentage(total), SwingConstants.RIGHT);
if (total > region.getQuantityMillisMaximum()) {
totalLabel.setForeground(TangoColorFactory.SCARLET_3);
}
regionPanel.addRowHeader(HEADER_COLUMN_EXTRA_PROPERTY_1, region,
totalLabel);
final JSpinner maximumField = new JSpinner(new SpinnerNumberModel(
(double) region.getQuantityMillisMaximum() / 1000.0, 0.0, 1.0, 0.010));
maximumField.setEditor(new JSpinner.NumberEditor(maximumField,
InvestmentNumericUtil.MILLIS_PERCENT_PATTERN));
regionPanel.addRowHeader(HEADER_COLUMN_EXTRA_PROPERTY_2, region, maximumField);
maximumField.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
if (ignoreChangeEvents) {
return;
}
long quantityMillisMaximum = (long) (((Number) maximumField.getValue()).doubleValue() * 1000.0);
changeRegionQuantityMillisMaximum(region, quantityMillisMaximum);
}
});
}
sectorPanel.addCornerHeader(HEADER_COLUMN, HEADER_ROW, createTableHeader(new JLabel("Sector"), null));
sectorPanel.addCornerHeader(HEADER_COLUMN_EXTRA_PROPERTY_1, HEADER_ROW, createTableHeader(new JLabel("Quantity total"), null));
sectorPanel.addCornerHeader(HEADER_COLUMN_EXTRA_PROPERTY_2, HEADER_ROW, createTableHeader(new JLabel("Quantity maximum"), null));
Map<Sector, Long> sectorTotalMap = solution.calculateSectorQuantityMillisTotalMap();
for (final Sector sector : solution.getSectorList()) {
sectorPanel.addRowHeader(HEADER_COLUMN, sector, new JLabel(sector.getName()));
long total = sectorTotalMap.get(sector);
JLabel totalLabel = new JLabel(InvestmentNumericUtil.formatMillisAsPercentage(total), SwingConstants.RIGHT);
if (total > sector.getQuantityMillisMaximum()) {
totalLabel.setForeground(TangoColorFactory.SCARLET_3);
}
sectorPanel.addRowHeader(HEADER_COLUMN_EXTRA_PROPERTY_1, sector,
totalLabel);
final JSpinner maximumField = new JSpinner(new SpinnerNumberModel(
(double) sector.getQuantityMillisMaximum() / 1000.0, 0.0, 1.0, 0.010));
maximumField.setEditor(new JSpinner.NumberEditor(maximumField,
InvestmentNumericUtil.MILLIS_PERCENT_PATTERN));
sectorPanel.addRowHeader(HEADER_COLUMN_EXTRA_PROPERTY_2, sector, maximumField);
maximumField.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
if (ignoreChangeEvents) {
return;
}
long quantityMillisMaximum = (long) (((Number) maximumField.getValue()).doubleValue() * 1000.0);
changeSectorQuantityMillisMaximum(sector, quantityMillisMaximum);
}
});
}
}
private JPanel createTableHeader(JLabel label, String toolTip) {
if (toolTip != null) {
label.setToolTipText(toolTip);
}
JPanel headerPanel = new JPanel(new BorderLayout());
headerPanel.add(label, BorderLayout.NORTH);
headerPanel.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(TangoColorFactory.ALUMINIUM_5),
BorderFactory.createEmptyBorder(2, 2, 2, 2)));
return headerPanel;
}
private void changeStandardDeviationMillisMaximum(final long standardDeviationMillisMaximum) {
doProblemFactChange(scoreDirector -> {
InvestmentSolution solution = scoreDirector.getWorkingSolution();
InvestmentParametrization parametrization = solution.getParametrization();
scoreDirector.beforeProblemPropertyChanged(parametrization);
parametrization.setStandardDeviationMillisMaximum(standardDeviationMillisMaximum);
scoreDirector.afterProblemPropertyChanged(parametrization);
}, true);
}
private void changeRegionQuantityMillisMaximum(final Region region, final long quantityMillisMaximum) {
doProblemFactChange(scoreDirector -> {
InvestmentSolution solution = scoreDirector.getWorkingSolution();
for (Region workingRegion : solution.getRegionList()) {
if (region.getId().equals(workingRegion.getId())) {
scoreDirector.beforeProblemPropertyChanged(workingRegion);
workingRegion.setQuantityMillisMaximum(quantityMillisMaximum);
scoreDirector.afterProblemPropertyChanged(workingRegion);
break;
}
}
}, true);
}
private void changeSectorQuantityMillisMaximum(final Sector sector, final long quantityMillisMaximum) {
doProblemFactChange(scoreDirector -> {
InvestmentSolution solution = scoreDirector.getWorkingSolution();
for (Sector workingSector : solution.getSectorList()) {
if (sector.getId().equals(workingSector.getId())) {
scoreDirector.beforeProblemPropertyChanged(workingSector);
workingSector.setQuantityMillisMaximum(quantityMillisMaximum);
scoreDirector.afterProblemPropertyChanged(workingSector);
break;
}
}
}, true);
}
}