de.codesourcery.eve.skills.ui.components.impl.planning.CostStatementComponent.java Source code

Java tutorial

Introduction

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

Source

package de.codesourcery.eve.skills.ui.components.impl.planning;

/**
 * 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.
 */

import java.awt.Color;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.event.ActionEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.Resource;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.DefaultCellEditor;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.text.JTextComponent;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;

import de.codesourcery.eve.skills.datamodel.IStaticDataModel;
import de.codesourcery.eve.skills.datamodel.ItemWithQuantity;
import de.codesourcery.eve.skills.datamodel.ManufacturingJobRequest;
import de.codesourcery.eve.skills.datamodel.PriceInfo;
import de.codesourcery.eve.skills.datamodel.PriceInfo.Source;
import de.codesourcery.eve.skills.datamodel.PriceInfo.Type;
import de.codesourcery.eve.skills.db.datamodel.InventoryType;
import de.codesourcery.eve.skills.db.datamodel.Region;
import de.codesourcery.eve.skills.market.IMarketDataProvider;
import de.codesourcery.eve.skills.market.IMarketDataProvider.IPriceInfoChangeListener;
import de.codesourcery.eve.skills.production.OreRefiningData;
import de.codesourcery.eve.skills.production.ShoppingListManager;
import de.codesourcery.eve.skills.ui.components.AbstractComponent;
import de.codesourcery.eve.skills.ui.components.ComponentWrapper;
import de.codesourcery.eve.skills.ui.components.impl.ReverseRefiningComponent;
import de.codesourcery.eve.skills.ui.components.impl.planning.CostPosition.Kind;
import de.codesourcery.eve.skills.ui.components.impl.planning.treenodes.BlueprintNode;
import de.codesourcery.eve.skills.ui.components.impl.planning.treenodes.ManufacturingJobNode;
import de.codesourcery.eve.skills.ui.components.impl.planning.treenodes.RequiredMaterialNode;
import de.codesourcery.eve.skills.ui.config.IRegionQueryCallback;
import de.codesourcery.eve.skills.ui.model.ITreeNode;
import de.codesourcery.eve.skills.ui.spreadsheet.CellAttributes;
import de.codesourcery.eve.skills.ui.spreadsheet.IHighlightingFunction;
import de.codesourcery.eve.skills.ui.spreadsheet.ITableCell;
import de.codesourcery.eve.skills.ui.spreadsheet.ITableFactory;
import de.codesourcery.eve.skills.ui.spreadsheet.SimpleCell;
import de.codesourcery.eve.skills.ui.spreadsheet.SpreadSheetCellRenderer;
import de.codesourcery.eve.skills.ui.spreadsheet.SpreadSheetTable;
import de.codesourcery.eve.skills.ui.spreadsheet.SpreadSheetTableModel;
import de.codesourcery.eve.skills.ui.spreadsheet.TableRow;
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.VerticalGroup;
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.EveDate;
import de.codesourcery.eve.skills.utils.ISKAmount;

/**
 * Component that renders a spreadsheet-like
 * (sorry, JTable just doesn't cut it here...)
 * table displaying the costs associated with
 * production of a given {@link ManufacturingJobRequest}.
 * 
 * @author tobias.gierke@code-sourcery.de
 */
public class CostStatementComponent extends AbstractComponent {
    private static final Logger log = Logger.getLogger(CostStatementComponent.class);

    private static final IHighlightingFunction unknownCostHighlightingFunction = new IHighlightingFunction() {
        @Override
        public boolean requiresHighlighting(ITableCell cell) {
            if (!(cell instanceof CostPositionCell)) {
                return false;
            }
            final CostPositionCell costPositionCell = (CostPositionCell) cell;
            return costPositionCell.getCostPosition().getPricePerUnit().equals(ISKAmount.ZERO_ISK)
                    || costPositionCell.getCostPosition().hasUnknownCost();
        }
    };

    // region used for price updates
    private Region defaultRegion = null;

    private volatile ManufacturingJobNode jobRequest;
    private volatile CostStatement statement;

    private final SpreadSheetTable table = new SpreadSheetTable();
    private TreeNodeCostCalculator costCalculator;

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

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

    @Resource(name = "shoppinglist-manager")
    private ShoppingListManager shoppingListManager;

    private final IPriceInfoChangeListener priceChangeListener = new IPriceInfoChangeListener() {

        @Override
        public void priceChanged(IMarketDataProvider caller, Region region, Set<InventoryType> type) {
            System.out.println("priceChanged(): Prices changed ... IMPLEMENT COST STATEMENT UPDATE");
            refresh();
        }
    };

    public void setCostCalculator(TreeNodeCostCalculator costCalculator) {
        if (costCalculator == null) {
            throw new IllegalArgumentException("costCalculator cannot be NULL");
        }

        this.costCalculator = costCalculator;
    }

    @Override
    protected void onAttachHook(IComponentCallback callback) {
        marketDataProvider.addChangeListener(priceChangeListener);
    }

    @Override
    protected void onDetachHook() {
        marketDataProvider.removeChangeListener(priceChangeListener);
    }

    @Override
    protected void disposeHook() {
        marketDataProvider.removeChangeListener(priceChangeListener);
    }

    protected int getAsIntValue(JTextComponent comp) {
        return Integer.parseInt(comp.getText());
    }

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

        new SpreadSheetCellRenderer().attachTo(table);

        table.setFillsViewportHeight(true);
        table.setBorder(BorderFactory.createLineBorder(Color.BLACK));

        final VerticalGroup vGroup = new VerticalGroup(new Cell(new JScrollPane(table)));

        new GridLayoutBuilder().add(vGroup).addTo(result);

        final PopupMenuBuilder builder = new PopupMenuBuilder();

        builder.addItem("Calculate required ore amounts", new AbstractAction() {

            private List<ItemWithQuantity> items;

            @Override
            public void actionPerformed(ActionEvent e) {
                calculateRequiredOres(items);
            }

            @Override
            public boolean isEnabled() {
                if (jobRequest != null && statement != null && table.getModel().getRowCount() > 0) {
                    items = getAllMineralsFromCostStatement();
                    return !items.isEmpty();
                }
                return false;
            }
        });

        builder.addItem("Create shopping list...", new AbstractAction() {

            @Override
            public void actionPerformed(ActionEvent e) {
                createNewShoppingList();
            }

        });

        builder.addItem("Put on clipboard (text)", new AbstractAction() {

            @Override
            public void actionPerformed(ActionEvent e) {
                putOnClipboard();
            }
        });

        builder.addItem("Show resource status", new AbstractAction() {

            @Override
            public void actionPerformed(ActionEvent e) {
                ManufacturingJobRequest request = jobRequest.getManufacturingJobRequest();

                final ResourceStatusComponent comp = new ResourceStatusComponent("Production of "
                        + request.getQuantity() + " x " + request.getBlueprint().getProductType().getName());

                comp.setData(request.getCharacter(), getRequiredMaterialsFromCostStatement());

                comp.setModal(true);
                ComponentWrapper.wrapComponent("Resource status", comp).setVisible(true);
            }
        });

        builder.attach(table);

        refresh();
        return result;
    }

    private void createNewShoppingList() {
        final ManufacturingJobRequest job = jobRequest.getManufacturingJobRequest();

        final List<ItemWithQuantity> items = new ArrayList<ItemWithQuantity>();

        for (CostPosition p : statement.getCostPositions()) {

            final ITreeNode n = p.getTreeNode();

            if (n instanceof RequiredMaterialNode) {
                items.add(new ItemWithQuantity(((RequiredMaterialNode) n).getRequiredMaterial().getType(),
                        p.getQuantity()));
            } else if (n instanceof BlueprintNode) {
                final BlueprintNode node = (BlueprintNode) n;

                if (((BlueprintNode) n).isRequiresOriginal()) {
                    items.add(new ItemWithQuantity(node.getBlueprint().getType().getBlueprintType(),
                            p.getQuantity()));
                }
            }
        }

        final ShoppingListEditorComponent comp = new ShoppingListEditorComponent(
                "Production of " + job.getQuantity() + " x " + job.getBlueprint().getProductType().getName(), "",
                items);

        comp.setModal(true);
        ComponentWrapper.wrapComponent(comp).setVisible(true);
        if (!comp.wasCancelled() && !comp.getShoppingList().isEmpty()) {
            shoppingListManager.addShoppingList(comp.getShoppingList());
        }
    }

    protected void calculateRequiredOres(List<ItemWithQuantity> items) {

        final ReverseRefiningComponent component = new ReverseRefiningComponent();

        component.setSelectedCharacter(jobRequest.getManufacturingJobRequest().getCharacter());
        component.setRequiredMinerals(items);

        ComponentWrapper.wrapComponent(component).setVisible(true);
    }

    private List<ItemWithQuantity> getRequiredMaterialsFromCostStatement() {
        final List<ItemWithQuantity> items = new ArrayList<ItemWithQuantity>();

        for (CostPosition p : statement.getCostPositions()) {
            if (p.getKind() != CostPosition.Kind.FIXED_COSTS) {
                continue;
            }

            if (p.getTreeNode() == null || !(p.getTreeNode() instanceof RequiredMaterialNode)) {
                continue;
            }
            final RequiredMaterialNode node = (RequiredMaterialNode) p.getTreeNode();

            final InventoryType material = node.getRequiredMaterial().getType();
            items.add(new ItemWithQuantity(material, p.getQuantity()));
        }
        return items;
    }

    private List<ItemWithQuantity> getAllMineralsFromCostStatement() {
        final List<ItemWithQuantity> items = new ArrayList<ItemWithQuantity>();

        System.out.println("---------------- " + statement + " ------------");

        for (CostPosition p : statement.getCostPositions()) {
            if (p.getTreeNode() == null || !(p.getTreeNode() instanceof RequiredMaterialNode)) {
                continue;
            }
            final RequiredMaterialNode node = (RequiredMaterialNode) p.getTreeNode();

            final InventoryType material = node.getRequiredMaterial().getType();
            if (!OreRefiningData.isMineral(material)) {
                continue;
            }
            final ItemWithQuantity mineral = new ItemWithQuantity(material, p.getQuantity());
            items.add(mineral);
        }
        return items;
    }

    public void setManufacturingJobRequest(final ManufacturingJobNode node) {
        this.jobRequest = node;
        refresh();
    }

    public void refresh() {
        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                if (jobRequest != null) {
                    System.out.println("\n==== BEFORE refresh() =====\n" + statement);
                    populateProductionCostTable();
                    System.out.println("\n==== AFTER refresh() =====\n" + statement);
                }
            }
        });
    }

    protected static ITableCell cell(CostPosition pos, ISKAmount amount) {
        return new CostPositionCell(pos, toString(amount));
    }

    protected interface ICellEditingCallback {
        public void setValue(ITableCell cell, Object value);
    }

    protected static ITableCell editableCell(CostPosition pos, ISKAmount amount,
            final ICellEditingCallback callback) {
        return new CostPositionCell(pos, toString(amount)) {
            @Override
            public void setValue(Object value) {
                callback.setValue(this, value);
            }

            @Override
            public boolean isEditable() {
                return true;
            }
        };
    }

    protected static ITableCell cell(ISKAmount amount) {
        return cell(toString(amount));
    }

    protected static ITableCell cell(String text) {
        return new SimpleCell(text);
    }

    protected static ITableCell emptyCell() {
        return new SimpleCell("");
    }

    private String toString(ITableCell cell) {
        return cell.getValue() != null ? cell.getValue().toString() : "";
    }

    protected void putOnClipboard() {

        // create text
        final SpreadSheetTableModel model = (SpreadSheetTableModel) this.table.getModel();

        int maxColumnCount = 1;

        final Map<Integer, Integer> maxColumnWidth = new HashMap<Integer, Integer>();

        for (TableRow r : model.getRows()) {
            if (r.getCellCount() > maxColumnCount) {
                maxColumnCount = r.getCellCount();
            }

            for (int i = 0; i < r.getCellCount(); i++) {

                Integer maxWidth = maxColumnWidth.get(i);
                if (maxWidth == null) {
                    maxWidth = new Integer(0);
                    maxColumnWidth.put(i, maxWidth);
                }
                final ITableCell cell = r.getCell(i);
                if (cell.getValue() != null) {
                    final int len = toString(cell).length();

                    if (len > maxWidth.intValue()) {
                        maxWidth = new Integer(len);
                        maxColumnWidth.put(i, maxWidth);
                    }
                }
            }
        }

        final StringBuffer text = new StringBuffer();

        for (TableRow r : model.getRows()) {
            for (int i = 0; i < r.getCellCount(); i++) {
                final int width = maxColumnWidth.get(i);
                final ITableCell cell = r.getCell(i);
                if (i == 0) {
                    text.append(StringUtils.rightPad(toString(cell), width)).append(" | ");
                } else {
                    text.append(StringUtils.leftPad(toString(cell), width)).append(" | ");
                }
            }
            text.append("\n");
        }

        // put on clipboard
        final Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();

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

    protected Region getDefaultRegion() {
        if (defaultRegion == null) {
            IRegionQueryCallback callback = RegionSelectionDialog.createCallback(null, dataModel);
            defaultRegion = callback.getRegion("Please selected the region for which to set buy prices");
        }
        return defaultRegion;
    }

    protected void populateProductionCostTable() {
        if (jobRequest == null || costCalculator == null) {
            System.out.println("populateProductionCostTable(): Cannot update - dependencies not set.");
            return;
        }

        final SpreadSheetTableModel tableModel = new SpreadSheetTableModel(new ITableFactory() {

            @Override
            public ITableCell createEmptyCell(int row, int column) {
                return new SimpleCell();
            }

            @Override
            public TableRow createRow(SpreadSheetTableModel tableModel) {
                return new TableRow(tableModel);
            }

        });

        final ProductionCostStatementGenerator calc = new ProductionCostStatementGenerator(jobRequest,
                costCalculator, dataModel);

        statement = calc.createCostStatement();

        tableModel.addRow(rightAligned(bold(cell("Product:"))),
                rightAligned(bold(cell(jobRequest.getProduct().getName()))));

        tableModel.addRow(rightAligned(bold(cell("Produced quantity:"))),
                rightAligned(bold(cell("" + jobRequest.getQuantity()))));

        tableModel.addEmptyRow();

        tableModel.addRow(centered(bold(cell("Cost type"))), centered(bold(cell("Quantity"))),
                centered(bold(cell("Description"))), centered(bold(cell("Price per unit"))),
                centered(bold(cell("Total costs"))));

        // sort by type & description
        final List<CostPosition> sorted = new ArrayList<CostPosition>(statement.getCostPositions());

        Collections.sort(sorted, new Comparator<CostPosition>() {
            @Override
            public int compare(CostPosition o1, CostPosition o2) {

                if (o1.getKind() != o2.getKind()) {
                    if (o1.getKind() == CostPosition.Kind.FIXED_COSTS) {
                        return -1;
                    } else if (o1.getKind() == CostPosition.Kind.VARIABLE_COSTS) {
                        return 0;
                    }
                    return 1;
                }
                return o1.getDescription().compareTo(o2.getDescription());
            }
        });

        // 
        ISKAmount totalFixedCost = ISKAmount.ZERO_ISK;
        ISKAmount totalVariableCost = ISKAmount.ZERO_ISK;
        ISKAmount totalOneTimeCost = ISKAmount.ZERO_ISK;
        for (final CostPosition p : sorted) {
            final ISKAmount amount = getTotalAmount(p);

            final ICellEditingCallback cellEditCb = new ICellEditingCallback() {

                @Override
                public void setValue(ITableCell cell, Object value) {
                    final CostPositionCell cp = (CostPositionCell) cell;
                    final InventoryType itemType = cp.getCostPosition().getItemType();
                    if (itemType != null && value instanceof String && StringUtils.isNotBlank((String) value)) {
                        try {
                            final long parsed = AmountHelper.parseISKAmount((String) value);

                            final PriceInfo priceInfo = new PriceInfo(Type.BUY, itemType, Source.USER_PROVIDED);
                            priceInfo.setRegion(getDefaultRegion());
                            priceInfo.setMinPrice(parsed);
                            priceInfo.setAveragePrice(parsed);
                            priceInfo.setMaxPrice(parsed);
                            priceInfo.setTimestamp(new EveDate(getSystemClock()));

                            marketDataProvider.store(priceInfo);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
            };
            tableModel.addRow(centered(cell(classify(p))), rightAligned(cell("" + p.getQuantity())),
                    leftAligned(cell(p.getDescription())),
                    rightAligned(highlightUnknownCost(editableCell(p, p.getPricePerUnit(), cellEditCb))),
                    rightAligned(highlightUnknownCost(cell(p, amount))));

            if (p.getKind() == Kind.ONE_TIME_COSTS) {
                totalOneTimeCost = amount.addTo(totalOneTimeCost);
            } else if (p.getKind() == Kind.FIXED_COSTS) {
                totalFixedCost = amount.addTo(totalFixedCost);
            } else if (p.getKind() == Kind.VARIABLE_COSTS) {
                totalVariableCost = amount.addTo(totalVariableCost);
            } else {
                throw new RuntimeException("Internal error, unhandled cost position kind " + p.getKind());
            }
        }

        tableModel.addEmptyRow();

        tableModel.addRow(emptyCell(), emptyCell(), emptyCell(), leftAligned(bold(cell("Total (fixed)"))),
                bold(rightAligned(cell(totalFixedCost))));

        tableModel.addRow(emptyCell(), emptyCell(), emptyCell(), leftAligned(bold(cell("Total (variable)"))),
                bold(rightAligned(cell(totalVariableCost))));

        tableModel.addRow(emptyCell(), emptyCell(), emptyCell(), leftAligned(bold(cell("Total (one-time)"))),
                bold(rightAligned(cell(totalOneTimeCost))));

        tableModel.addEmptyRow();

        tableModel.addRow(emptyCell(), emptyCell(), emptyCell(),
                leftAligned(bold(cell("Total (fixed + variable)"))),
                bold(rightAligned(cell(totalVariableCost.addTo(totalFixedCost)))));

        tableModel.addEmptyRow();

        final ISKAmount pricePerUnit = new ISKAmount(totalVariableCost.addTo(totalFixedCost).toDouble()
                / jobRequest.getManufacturingJobRequest().getQuantity());

        tableModel.addRow(emptyCell(), emptyCell(), emptyCell(), leftAligned(bold(cell("Price per unit"))),
                bold(rightAligned(cell(pricePerUnit))));

        table.setModel(tableModel);
        final JTextField tf = new JTextField();
        table.setDefaultEditor(ITableCell.class, new DefaultCellEditor(tf));
    }

    protected static ITableCell centered(ITableCell cell) {
        return align(cell, CellAttributes.Alignment.CENTERED);
    }

    protected static ITableCell leftAligned(ITableCell cell) {
        return align(cell, CellAttributes.Alignment.LEFT);
    }

    protected static ITableCell rightAligned(ITableCell cell) {
        return align(cell, CellAttributes.Alignment.RIGHT);
    }

    protected static ITableCell align(ITableCell cell, CellAttributes.Alignment alignment) {
        cell.getAttributes().setAttribute(CellAttributes.ALIGNMENT, alignment);
        return cell;
    }

    protected static ITableCell highlightUnknownCost(ITableCell cell) {
        if (cell instanceof CostPositionCell) {
            cell.getAttributes().setAttribute(CellAttributes.HIGHLIGHTING_FUNCTION,
                    unknownCostHighlightingFunction);
            return cell;
        }
        throw new IllegalArgumentException("Invalid cell class " + cell.getClass().getName());
    }

    protected static ITableCell bold(ITableCell cell) {
        cell.getAttributes().setAttribute(CellAttributes.RENDER_BOLD, Boolean.TRUE);
        return cell;
    }

    private String classify(CostPosition p) {
        switch (p.getKind()) {
        case FIXED_COSTS:
            return "fixed";
        case VARIABLE_COSTS:
            return "variable";
        case ONE_TIME_COSTS:
            return "one-time";
        default:
            throw new IllegalArgumentException("Don't know how to render cost position with kind " + p.getKind());
        }
    }

    private ISKAmount getTotalAmount(CostPosition p) {
        if (p.getType() == CostPosition.Type.TOTAL) {
            return p.getPricePerUnit();
        }
        return p.getPricePerUnit().multiplyBy(p.getQuantity());
    }

}