de.codesourcery.eve.skills.ui.components.impl.OreChartComponent.java Source code

Java tutorial

Introduction

Here is the source code for de.codesourcery.eve.skills.ui.components.impl.OreChartComponent.java

Source

/**
 * Copyright 2004-2009 Tobias Gierke <tobias.gierke@code-sourcery.de>
 *
 * 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 de.codesourcery.eve.skills.ui.components.impl;

import java.awt.Color;
import java.awt.Component;
import java.awt.GridBagLayout;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Vector;

import javax.annotation.Resource;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellRenderer;

import org.apache.commons.lang.StringUtils;

import de.codesourcery.eve.skills.datamodel.IStaticDataModel;
import de.codesourcery.eve.skills.datamodel.ItemWithQuantity;
import de.codesourcery.eve.skills.datamodel.PriceInfo;
import de.codesourcery.eve.skills.datamodel.PriceInfo.Source;
import de.codesourcery.eve.skills.db.datamodel.InventoryType;
import de.codesourcery.eve.skills.db.datamodel.Region;
import de.codesourcery.eve.skills.exceptions.PriceInfoUnavailableException;
import de.codesourcery.eve.skills.market.IMarketDataProvider;
import de.codesourcery.eve.skills.market.IPriceInfoStore;
import de.codesourcery.eve.skills.market.IPriceQueryCallback;
import de.codesourcery.eve.skills.market.MarketFilter;
import de.codesourcery.eve.skills.market.MarketFilterBuilder;
import de.codesourcery.eve.skills.market.PriceInfoQueryResult;
import de.codesourcery.eve.skills.production.OreRefiningData;
import de.codesourcery.eve.skills.production.OreRefiningData.OreVariant;
import de.codesourcery.eve.skills.production.OreRefiningData.RefiningOutcome;
import de.codesourcery.eve.skills.ui.components.AbstractComponent;
import de.codesourcery.eve.skills.ui.components.impl.TotalItemValueComponent.IDataProvider;
import de.codesourcery.eve.skills.ui.config.IAppConfigProvider;
import de.codesourcery.eve.skills.ui.config.IRegionQueryCallback;
import de.codesourcery.eve.skills.ui.model.AbstractTableModel;
import de.codesourcery.eve.skills.ui.model.TableColumnBuilder;
import de.codesourcery.eve.skills.ui.utils.GridLayoutBuilder;
import de.codesourcery.eve.skills.ui.utils.GridLayoutBuilder.Cell;
import de.codesourcery.eve.skills.ui.utils.GridLayoutBuilder.HorizontalGroup;
import de.codesourcery.eve.skills.ui.utils.GridLayoutBuilder.VerticalGroup;
import de.codesourcery.eve.skills.ui.utils.ImprovedSplitPane;
import de.codesourcery.eve.skills.ui.utils.PersistentDialogManager;
import de.codesourcery.eve.skills.ui.utils.PlainTextTransferable;
import de.codesourcery.eve.skills.ui.utils.PopupMenuBuilder;
import de.codesourcery.eve.skills.ui.utils.RegionSelectionDialog;
import de.codesourcery.eve.skills.util.AmountHelper;
import de.codesourcery.eve.skills.utils.DateHelper;
import de.codesourcery.eve.skills.utils.EveDate;
import de.codesourcery.eve.skills.utils.ISKAmount;
import de.codesourcery.eve.skills.utils.ISystemClock;

public class OreChartComponent extends AbstractComponent {
    private final JTable table = new JTable();

    @Resource(name = "static-datamodel")
    private IStaticDataModel dataModel;

    @Resource(name = "marketdata-provider")
    private IMarketDataProvider marketDataProvider;

    @Resource(name = "dialog-manager")
    private PersistentDialogManager dialogManager;

    @Resource(name = "appconfig-provider")
    private IAppConfigProvider appConfigProvider;

    private OreRefiningData refiningData;
    private MyTableModel tableModel;

    @Resource(name = "system-clock")
    private ISystemClock clock;

    private static final int FIRST_MINERAL_COLUMN = 1;

    private final JTable mineralPriceTable = new JTable();
    private final MineralPriceTableModel mineralPriceTableModel;

    private final PriceCache priceCache;

    private JTextField oreHoldSize = new JTextField("8500");
    private JComboBox<String> oreChooser = new JComboBox<>();
    private JComboBox<OreVariant> oreVariantChooser = new JComboBox<>();

    private final IDataProvider<String> dataProvider = new IDataProvider<String>() {

        @Override
        public int getQuantity(String obj) {
            try {
                return Integer.parseInt(oreHoldSize.getText());
            } catch (Exception e) {
                oreHoldSize.setText("8500");
                return 8500;
            }
        }

        @Override
        public ISKAmount getPricePerUnit(String obj) {
            final String oreName = (String) oreChooser.getSelectedItem();
            final InventoryType oreType = refiningData.getVariantType(oreName, getSelectedOreVariant());
            final List<? extends ItemWithQuantity> refiningOutcome = refiningData.getRefiningOutcome(oreType);
            return getISKperM3(oreType, refiningOutcome);
        }
    };

    private TotalItemValueComponent<String> oreHoldValue = new TotalItemValueComponent<String>(
            "Cargo hold value (ISK)", dataProvider);

    private final class PriceCache {
        private final Map<Long, PriceInfo> priceInfoCache = new HashMap<Long, PriceInfo>();

        public void flush() {
            priceInfoCache.clear();
        }

        public PriceInfo getSellPrice(InventoryType t) {

            PriceInfo info = priceInfoCache.get(t.getId());
            if (info == null) {
                info = fetchSellPrice(t);
                priceInfoCache.put(t.getId(), info);
            }
            return info;
        }

        private PriceInfo fetchSellPrice(InventoryType t) {
            final MarketFilter filter = new MarketFilterBuilder(PriceInfo.Type.SELL, getDefaultRegion()).end();

            try {
                final PriceInfoQueryResult result = marketDataProvider.getPriceInfo(filter,
                        IPriceQueryCallback.NOP_INSTANCE, t);
                return result.sellPrice();
            } catch (PriceInfoUnavailableException e) {
                throw new RuntimeException(e);
            }
        }

        public void updatePrice(PriceInfo info, long amount) {

            info.setAveragePrice(amount);
            info.setTimestamp(new EveDate(clock));
            info.setSource(Source.USER_PROVIDED);

            marketDataProvider.store(info);
            this.priceInfoCache.remove(info.getItemType().getId());
            mineralPriceTableModel.refresh();
            tableModel.refresh();
        }
    }

    protected Region getDefaultRegion() {
        final IRegionQueryCallback callback = RegionSelectionDialog.createCallback(null, dataModel);
        return appConfigProvider.getAppConfig().getDefaultRegion(callback);
    }

    private final class TableRow {

        public final InventoryType oreType;
        public final String oreName;
        public final List<? extends ItemWithQuantity> data;

        public TableRow(InventoryType oreType, String oreName, List<? extends ItemWithQuantity> data) {
            this.oreType = oreType;
            this.oreName = oreName;
            this.data = data;
        }

        public ItemWithQuantity getYieldForMineral(int columnIndex) {

            final String mineralName = OreRefiningData.getMineralNames().get(columnIndex - FIRST_MINERAL_COLUMN);

            for (ItemWithQuantity it : data) {
                if (it.getType().getName().equals(mineralName)) {
                    return it;
                }
            }
            return new ItemWithQuantity(dataModel.getInventoryTypeByName(mineralName), 0);
        }
    }

    private static final class CsvRow implements Comparable<CsvRow> {
        private final String oreName;
        private final ISKAmount iskPerM3;

        public CsvRow(String oreName, ISKAmount iskPerM3) {
            this.oreName = oreName;
            this.iskPerM3 = iskPerM3;
        }

        @Override
        public int compareTo(CsvRow o) {
            return this.iskPerM3.compareTo(o.iskPerM3);
        }
    }

    protected void saveSummaryToClipboard() {

        final Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();

        final StringBuffer buffer = new StringBuffer();

        buffer.append("Current date: " + DateHelper.format(new Date()) + "\n");
        buffer.append("Region: " + getDefaultRegion().getName() + "\n");
        buffer.append("\n");

        for (int i = 0; i < mineralPriceTableModel.getRowCount(); i++) {
            final MineralPrice minPrice = mineralPriceTableModel.getRow(i);
            buffer.append(StringUtils.rightPad(minPrice.itemName(), 13)).append(" : ");
            buffer.append(StringUtils.leftPad(AmountHelper.formatISKAmount(minPrice.getSellPrice()), 10) + " ISK");
            buffer.append("\n");
        }

        buffer.append("\n");

        final List<CsvRow> data = new ArrayList<CsvRow>();

        for (int i = 0; i < tableModel.getRowCount(); i++) {

            final TableRow row = tableModel.getRow(i);
            final ISKAmount amount = tableModel.getISKperM3(row);

            data.add(new CsvRow(row.oreName, amount));
        }

        Collections.sort(data);

        for (CsvRow r : data) {
            buffer.append(StringUtils.rightPad(r.oreName, 13)).append(" : ");
            buffer.append(StringUtils.leftPad(AmountHelper.formatISKAmount(r.iskPerM3), 10) + " ISK / m3");
            buffer.append("\n");
        }

        clipboard.setContents(new PlainTextTransferable(buffer.toString()), null);
    }

    public OreChartComponent() {
        super();
        this.priceCache = new PriceCache();
        this.mineralPriceTableModel = new MineralPriceTableModel();
        mineralPriceTable.setModel(mineralPriceTableModel);
        registerChildren(oreHoldValue);
    }

    private OreVariant getSelectedOreVariant() {
        OreVariant result = (OreVariant) oreVariantChooser.getSelectedItem();
        return result == null ? OreVariant.BASIC : result;
    }

    @Override
    protected void onAttachHook(IComponentCallback callback) {
        if (refiningData == null) {
            refiningData = new OreRefiningData(dataModel);
        }
        if (tableModel == null) {
            tableModel = new MyTableModel();
        }
        tableModel.refresh();
        mineralPriceTableModel.refresh();
    }

    protected void refresh() {
        tableModel.refresh();
        mineralPriceTableModel.refresh();
        oreHoldValue.setItems(Arrays.asList("dummyValue"));
    }

    private final class MyTableModel extends AbstractTableModel<TableRow> {

        private List<TableRow> rows = new ArrayList<TableRow>();

        private final List<String> mineralNames;
        private final int lastMineralColumn;

        public MyTableModel() {
            super(new TableColumnBuilder().add("Ore").addAll(OreRefiningData.getMineralNames(), Integer.class)
                    .add("ISK / m3", ISKAmount.class));

            this.mineralNames = OreRefiningData.getMineralNames();
            lastMineralColumn = FIRST_MINERAL_COLUMN + this.mineralNames.size() - 1;
        }

        public void refresh() {
            rows.clear();

            final OreVariant oreVariant = getSelectedOreVariant();
            for (String ore : refiningData.getOreNames(oreVariant)) {
                final RefiningOutcome outcome = refiningData.getRawOutcome(ore);

                rows.add(new TableRow(outcome.getType(oreVariant), ore, refiningData.getRefiningOutcome(ore)));
            }
            modelDataChanged();
        }

        public boolean isHighestMineralYield(int modelRow, int modelColumn) {

            if (modelColumn >= FIRST_MINERAL_COLUMN && modelColumn <= lastMineralColumn) {
                int maxYield = 0;
                int maxRow = 0;
                for (int i = 0; i < rows.size(); i++) {
                    final int yield = rows.get(i).getYieldForMineral(modelColumn).getQuantity();

                    if (yield > maxYield) {
                        maxYield = yield;
                        maxRow = i;
                    }
                }
                return modelRow == maxRow;
            }

            return false;
        }

        @Override
        protected Object getColumnValueAt(int modelRow, int modelColumn) {
            final TableRow r = getRow(modelRow);

            if (modelColumn == 0) {
                return r.oreName;
            } else if (modelColumn >= FIRST_MINERAL_COLUMN && modelColumn <= lastMineralColumn) {
                return r.getYieldForMineral(modelColumn).getQuantity();
            } else if (modelColumn == lastMineralColumn + 1) {
                return getISKperM3(r);
            } else {
                throw new IllegalArgumentException("Unhandled column " + modelColumn);
            }
        }

        public ISKAmount getISKperM3(TableRow r) {
            return OreChartComponent.this.getISKperM3(r.oreType, r.data);
        }

        @Override
        public TableRow getRow(int modelRow) {
            return rows.get(modelRow);
        }

        @Override
        public int getRowCount() {
            return rows.size();
        }
    }

    protected ISKAmount getISKperM3(InventoryType oreType, List<? extends ItemWithQuantity> outcome) {
        final double unitsPerM3 = 1.0d / oreType.getVolume();
        final double batchesPerM3 = unitsPerM3 / oreType.getPortionSize();

        final long avgSellPrive = calculateBatchSellPrice(outcome);

        return new ISKAmount(avgSellPrive).multiplyBy(batchesPerM3);
    }

    protected long calculateBatchSellPrice(List<? extends ItemWithQuantity> items) {
        long batchValue = 0;
        for (ItemWithQuantity x : items) {
            long avgMineralSellPrive = priceCache.getSellPrice(x.getType()).getAveragePrice();

            batchValue += (avgMineralSellPrive * x.getQuantity());
        }
        return batchValue;
    }

    private final TableCellRenderer tableRenderer = new DefaultTableCellRenderer() {

        @Override
        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
                boolean hasFocus, int row, int column) {
            super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);

            if (column < FIRST_MINERAL_COLUMN) {
                setHorizontalAlignment(JLabel.LEADING);
            } else {
                setHorizontalAlignment(JLabel.TRAILING);
            }

            if (!isSelected && tableModel.isHighestMineralYield(table.convertRowIndexToModel(row),
                    table.convertColumnIndexToModel(column))) {
                setBackground(Color.GREEN);
            } else {
                if (isSelected) {
                    setBackground(table.getSelectionBackground());
                } else {
                    setBackground(table.getBackground());
                }
            }

            if (value instanceof ISKAmount) {
                setText(AmountHelper.formatISKAmount((ISKAmount) value) + " ISK");
            }
            return this;
        }
    };

    @Override
    protected JPanel createPanel() {
        final JPanel result = new JPanel();

        table.setFillsViewportHeight(true);
        table.setModel(this.tableModel);
        table.setRowSorter(tableModel.getRowSorter());
        table.setDefaultRenderer(String.class, tableRenderer);
        table.setDefaultRenderer(ISKAmount.class, tableRenderer);
        table.setDefaultRenderer(Integer.class, tableRenderer);

        final PopupMenuBuilder builder = new PopupMenuBuilder();
        builder.addItem("Save summary to clipboard", new Runnable() {

            @Override
            public void run() {
                saveSummaryToClipboard();
            }
        });

        builder.attach(table);

        mineralPriceTable.setFillsViewportHeight(true);

        final ImprovedSplitPane splitPane = new ImprovedSplitPane(JSplitPane.VERTICAL_SPLIT,
                new JScrollPane(mineralPriceTable), new JScrollPane(table));

        splitPane.setDividerLocation(0.3);

        // setup ore hold panel
        final JPanel oreHoldPanel = new JPanel();
        oreHoldPanel.setLayout(new GridBagLayout());

        oreHoldSize.setColumns(10);

        new GridLayoutBuilder().add(new HorizontalGroup(
                new VerticalGroup(
                        new HorizontalGroup(new Cell("ohLabel", new JLabel("Ore hold size (m3):")),
                                new Cell("ohSize", oreHoldSize)),
                        new HorizontalGroup(new Cell("oreTypeLabel", new JLabel("Ore type:")),
                                new Cell("oreChooser", oreChooser)),
                        new HorizontalGroup(new Cell("oreVariantLabel", new JLabel("Ore variant:")),
                                new Cell("oreVariantChooser", oreVariantChooser))),
                new Cell("oreHoldValue", oreHoldValue.getPanel()))).enableDebugMode().addTo(oreHoldPanel);

        final List<String> oreNames = refiningData.getOreNames(OreVariant.BASIC);
        Collections.sort(oreNames);

        oreChooser.setModel(new DefaultComboBoxModel<>(new Vector<String>(oreNames)));
        oreChooser.setSelectedItem(oreNames.get(0));

        oreVariantChooser.setModel(new DefaultComboBoxModel<>(OreVariant.values()));
        oreVariantChooser.setSelectedItem(OreVariant.BASIC);

        final ActionListener refreshListener = new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                refresh();
            }
        };

        oreHoldSize.addActionListener(refreshListener);
        oreChooser.addActionListener(refreshListener);
        oreVariantChooser.addActionListener(refreshListener);

        new GridLayoutBuilder().add(new VerticalGroup(new Cell(oreHoldPanel).noResize(), new Cell(splitPane)))
                .addTo(result);

        refresh();

        return result;
    }

    private final class MineralPrice {

        public final PriceInfo price;

        public MineralPrice(PriceInfo price) {
            this.price = price;
        }

        public String itemName() {
            return price.getItemType().getName();
        }

        public ISKAmount getSellPrice() {
            return new ISKAmount(price.getAveragePrice());
        }
    }

    private final class MineralPriceTableModel extends AbstractTableModel<MineralPrice> {

        private final List<MineralPrice> prices = new ArrayList<MineralPrice>();

        public MineralPriceTableModel() {
            super(new TableColumnBuilder().add("Mineral").add("Price"));
        }

        public void refresh() {

            prices.clear();
            for (String mineralName : OreRefiningData.getMineralNames()) {
                final InventoryType type = dataModel.getInventoryTypeByName(mineralName);
                prices.add(new MineralPrice(priceCache.getSellPrice(type)));
            }
            modelDataChanged();
        }

        @Override
        public boolean isCellEditable(int rowIndex, int columnIndex) {
            return columnIndex == 1;
        }

        @Override
        protected Object getColumnValueAt(int modelRowIndex, int col) {
            final MineralPrice p = getRow(modelRowIndex);

            if (col == 0) {
                return p.itemName();
            } else if (col == 1) {
                return AmountHelper.formatISKAmount(p.getSellPrice());
            } else {
                throw new IllegalArgumentException("Invalid column index " + col);
            }
        }

        @Override
        public void setValueAt(Object value, int rowIndex, int columnIndex) {
            if (columnIndex == 1) {

                final String label = "You are about to change the average sell price " + " of "
                        + getRow(rowIndex).itemName() + " in region '" + getDefaultRegion().getName() + "'";

                if (dialogManager.showPermanentWarningDialog("warn_ore_chart_price_editing", "Warning", label)) {
                    long iskAmount = AmountHelper.parseISKAmount((String) value);
                    if (iskAmount >= 0) {
                        priceCache.updatePrice(getRow(rowIndex).price, iskAmount);
                    }
                }
            }
        }

        @Override
        public MineralPrice getRow(int modelRow) {
            return prices.get(modelRow);
        }

        @Override
        public int getRowCount() {
            return prices.size();
        }

    }
}