commit 15bfe2e0f3403240266608b319aef599254ca7ac Author: iMoHax Date: Wed Jul 16 16:38:48 2014 +0400 Create repository diff --git a/Trader.iml b/Trader.iml new file mode 100644 index 0000000..d1d69ff --- /dev/null +++ b/Trader.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/client/ext-resources/all/1.xlsx b/client/ext-resources/all/1.xlsx new file mode 100644 index 0000000..300fbeb Binary files /dev/null and b/client/ext-resources/all/1.xlsx differ diff --git a/client/ext-resources/all/conf_/style.css b/client/ext-resources/all/conf_/style.css new file mode 100644 index 0000000..bacc33b --- /dev/null +++ b/client/ext-resources/all/conf_/style.css @@ -0,0 +1,11 @@ +.good .diff { + -fx-fill: green; + -fx-font-weight: bold; +} + +.bad .diff { + -fx-fill: blue; + -fx-font-weight: bold; +} + + diff --git a/client/ext-resources/all/world.xml b/client/ext-resources/all/world.xml new file mode 100644 index 0000000..43cb0d3 --- /dev/null +++ b/client/ext-resources/all/world.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/ext-resources/develop/log4j.properties b/client/ext-resources/develop/log4j.properties new file mode 100644 index 0000000..5286f74 --- /dev/null +++ b/client/ext-resources/develop/log4j.properties @@ -0,0 +1,5 @@ +log4j.rootLogger=DEBUG, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%p: %d{dd.MM.yyyy HH:mm:ss} (%F:%L) - %m%n diff --git a/client/ext-resources/release/log4j.properties b/client/ext-resources/release/log4j.properties new file mode 100644 index 0000000..e6036cb --- /dev/null +++ b/client/ext-resources/release/log4j.properties @@ -0,0 +1,12 @@ +log4j.rootLogger=INFO, FILE + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%p: %d{dd.MM.yyyy HH:mm:ss} (%F:%L) - %m%n + +log4j.appender.FILE=org.apache.log4j.DailyRollingFileAppender +log4j.appender.FILE.File=traider.log +log4j.appender.FILE.DatePattern='.'yyyy-MM-dd'.log' +log4j.appender.FILE.threshold=INFO +log4j.appender.FILE.layout=org.apache.log4j.PatternLayout +log4j.appender.FILE.layout.ConversionPattern=%p: %d{dd.MM.yyyy HH:mm:ss} - %m%n \ No newline at end of file diff --git a/client/pom.xml b/client/pom.xml new file mode 100644 index 0000000..cd0a59b --- /dev/null +++ b/client/pom.xml @@ -0,0 +1,106 @@ + + + 4.0.0 + + + + trader + Trader + 1.0 + ../pom.xml + + client + + + Open Software + + + + + develop + + true + + + ext-resources/develop + + + + + release + + ext-resources/release + + + + + jar + + + + ${project.groupId} + core + ${project.version} + + + ${project.groupId} + utils + ${project.version} + + + log4j + log4j + + + org.slf4j + slf4j-log4j12 + + + org.controlsfx + controlsfx + + + + + src/main/java + src/test/java + src/main/resourcesfalse + src/test/resourcesfalse + + + + com.zenjava + javafx-maven-plugin + + + maven-resources-plugin + 2.6 + + + copy-resources + package + + copy-resources + + + ${project.build.directory}/resources + + + ext-resources/all + false + + + ${project.ext-resources} + false + + + + + + + + + + \ No newline at end of file diff --git a/client/src/main/java/ru/trader/Main.java b/client/src/main/java/ru/trader/Main.java new file mode 100644 index 0000000..db7e001 --- /dev/null +++ b/client/src/main/java/ru/trader/Main.java @@ -0,0 +1,77 @@ +package ru.trader; + +import javafx.application.Application; +import javafx.scene.Scene; +import javafx.stage.Stage; +import org.apache.log4j.PropertyConfigurator; +import org.controlsfx.control.action.Action; +import org.controlsfx.dialog.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.trader.controllers.Screeners; + +import javax.xml.stream.XMLStreamException; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URL; + +public class Main extends Application { + private final static Logger LOG = LoggerFactory.getLogger(Main.class); + + @Override + public void start(Stage primaryStage) throws Exception { + loadResources(); + primaryStage.setTitle("Trader"); + primaryStage.setScene(new Scene(Screeners.newScreeners(Main.class.getResource("/view/main.fxml"),getUrl("style.css").toExternalForm()))); + primaryStage.setOnCloseRequest((we)->{ + try { + if (World.getMarket().isChange()){ + Action res = Screeners.showConfirm("Изменения не были сохранены, сохранить?"); + if (res == Dialog.Actions.YES) World.save(); + else if (res == Dialog.Actions.CANCEL) we.consume(); + } + Screeners.closeAll(); + } catch (FileNotFoundException | UnsupportedEncodingException | XMLStreamException e) { + LOG.error("Ошибка при сохранении",e); + Screeners.showException(e); + } + }); + primaryStage.show(); + } + + + @Override + public void stop() throws Exception { + super.stop(); + } + + public static void main(String[] args) { + PropertyConfigurator.configure("log4j.properties"); + Thread.setDefaultUncaughtExceptionHandler((t, e) -> { + System.err.print("Exception in thread \"" + t.getName() + "\" "); + e.printStackTrace(System.err); + LOG.error("", e); + Screeners.showException(e); + }); + launch(args); + } + + + private static void loadResources() throws IOException { + Screeners.loadItemDescStage(getUrl(("itemDesc.fxml"))); + Screeners.loadVEditorStage(getUrl(("vEditor.fxml"))); + Screeners.loadAddOfferStage(getUrl(("oEditor.fxml"))); + Screeners.loadOrdersStage(getUrl(("orders.fxml"))); + } + + private static URL getUrl(String filename) throws MalformedURLException { + File file = new File("conf"+File.separator+filename); + if (file.exists()) return file.toURI().toURL(); + return Main.class.getResource("/view/"+filename); + } + + +} diff --git a/client/src/main/java/ru/trader/World.java b/client/src/main/java/ru/trader/World.java new file mode 100644 index 0000000..74c05f1 --- /dev/null +++ b/client/src/main/java/ru/trader/World.java @@ -0,0 +1,47 @@ +package ru.trader; + +import org.xml.sax.SAXException; +import ru.trader.core.Market; +import ru.trader.core.SimpleMarket; +import ru.trader.model.ModelFabrica; +import ru.trader.store.Store; +import ru.trader.store.XSSFImporter; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.stream.XMLStreamException; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.UnsupportedEncodingException; + +public class World { + private static Market world; + private static final String STORE_FILE="world.xml"; + + static { + try { + File file = new File(STORE_FILE); + if (file.exists()) world = Store.loadFromFile(file); + else world = new SimpleMarket(); + } catch (ParserConfigurationException | SAXException | IOException e) { + throw new RuntimeException(e); + } + } + + public static void save() throws FileNotFoundException, UnsupportedEncodingException, XMLStreamException { + Store.saveToFile(world, new File("world.xml")); + world.setChange(false); + } + + public static void imp(File file) throws IOException, SAXException { + XSSFImporter xssfImporter = new XSSFImporter(file); + world = xssfImporter.doImport(); + ModelFabrica.clear(); + world.setChange(true); + } + + public static Market getMarket() { + return world; + } + +} diff --git a/client/src/main/java/ru/trader/controllers/ItemDescController.java b/client/src/main/java/ru/trader/controllers/ItemDescController.java new file mode 100644 index 0000000..c705d67 --- /dev/null +++ b/client/src/main/java/ru/trader/controllers/ItemDescController.java @@ -0,0 +1,54 @@ +package ru.trader.controllers; + + +import javafx.collections.FXCollections; +import javafx.fxml.FXML; + +import javafx.scene.Node; +import javafx.scene.Parent; +import javafx.scene.control.ListView; +import javafx.util.Duration; +import org.controlsfx.control.PopOver; +import ru.trader.model.*; + + +public class ItemDescController { + + + private ItemDescModel item; + + @FXML + private ListView seller; + + @FXML + private ListView buyer; + + public void setItemDesc(ItemDescModel itemDesc){ + item = itemDesc; + fill(); + } + + private void fill(){ + seller.setItems(FXCollections.observableList(item.getSeller())); + buyer.setItems(FXCollections.observableList(item.getBuyer())); + } + + private PopOver popup; + + public void popup(Node owner, Parent itemDescScreen) { + if (popup != null && popup.isShowing()) return; + if (popup == null) { + popup = new PopOver(itemDescScreen); + popup.detach(); + popup.setAutoHide(true); + + } + popup.show(owner); + } + + public void close() { + if (popup!=null){ + popup.hide(Duration.ZERO); + } + } +} diff --git a/client/src/main/java/ru/trader/controllers/ItemsController.java b/client/src/main/java/ru/trader/controllers/ItemsController.java new file mode 100644 index 0000000..2ab1c44 --- /dev/null +++ b/client/src/main/java/ru/trader/controllers/ItemsController.java @@ -0,0 +1,111 @@ +package ru.trader.controllers; + + +import javafx.collections.FXCollections; +import javafx.fxml.FXML; + +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.input.MouseButton; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.trader.model.*; +import ru.trader.model.support.ChangeMarketListener; +import ru.trader.model.support.ModelBindings; + + +public class ItemsController { + private final static Logger LOG = LoggerFactory.getLogger(ItemsController.class); + + @FXML + private TableView tblItems; + + @FXML + private TableColumn minProfit; + @FXML + private TableColumn avgProfit; + @FXML + private TableColumn maxProfit; + + @FXML + private void initialize() { + tblItems.getSelectionModel().selectedItemProperty().addListener((v, o, n) -> { + if (n!=null) Screeners.changeItemDesc(n); + }); + tblItems.setOnMouseClicked((e) -> { + if (e.getButton() == MouseButton.SECONDARY){ + Screeners.showItemDesc(tblItems); + } + }); + minProfit.setCellValueFactory((data) -> { + ItemDescModel iDesc = data.getValue(); + return ModelBindings.diff(iDesc.minBuyProperty(),iDesc.maxSellProperty()); + }); + avgProfit.setCellValueFactory((data) -> { + ItemDescModel iDesc = data.getValue(); + return iDesc.avgBuyProperty().subtract(iDesc.avgSellProperty()); + }); + maxProfit.setCellValueFactory((data) -> { + ItemDescModel iDesc = data.getValue(); + return ModelBindings.diff(iDesc.maxBuyProperty(), iDesc.minSellProperty()); + }); + init(); + } + + void init(){ + MarketModel market = MainController.getMarket(); + market.addListener(new ItemsStatChangeListener()); + tblItems.setItems(FXCollections.observableArrayList(market.itemsProperty())); + if (tblItems.getSortOrder().size()>0) + tblItems.sort(); + } + + private void refresh(OfferModel offer){ + LOG.info("Refresh item desc link with item of offer {}", offer); + for (ItemDescModel descModel : tblItems.getItems()) { + if (descModel.hasItem(offer)){ + descModel.refresh(offer.getType()); + return; + } + } + } + + private void refresh(){ + LOG.info("Refresh all stats"); + tblItems.getItems().forEach(ItemDescModel::refresh); + } + + private void addItem(ItemDescModel item){ + tblItems.getItems().add(item); + } + + private class ItemsStatChangeListener extends ChangeMarketListener { + + @Override + public void add(ItemDescModel item) { + addItem(item); + } + + @Override + public void add(OfferModel offer) { + refresh(offer); + } + + @Override + public void add(VendorModel vendor) { + refresh(); + } + + @Override + public void remove(OfferModel offer) { + refresh(offer); + } + + @Override + public void priceChange(OfferModel offer, double price, double value) { + refresh(offer); + } + } + + +} diff --git a/client/src/main/java/ru/trader/controllers/MainController.java b/client/src/main/java/ru/trader/controllers/MainController.java new file mode 100644 index 0000000..978e501 --- /dev/null +++ b/client/src/main/java/ru/trader/controllers/MainController.java @@ -0,0 +1,106 @@ +package ru.trader.controllers; + + +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.layout.BorderPane; +import javafx.stage.FileChooser; +import org.controlsfx.dialog.Dialogs; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xml.sax.SAXException; +import ru.trader.World; +import ru.trader.model.MarketModel; + +import javax.xml.stream.XMLStreamException; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.Optional; + +public class MainController { + private final static Logger LOG = LoggerFactory.getLogger(MainController.class); + + private static MarketModel world = new MarketModel(World.getMarket()); + private static MarketModel market = new MarketModel(World.getMarket()); + + + @FXML + private BorderPane mainPane; + + @FXML + private OffersController offersController; + @FXML + private ItemsController itemsController; + + public OffersController getOffersController() { + return offersController; + } + + public BorderPane getMainPane(){ + return mainPane; + } + + public static MarketModel getMarket() { + return market; + } + public static MarketModel getWorld() { + return world; + } + + public void setMarket(MarketModel market) { + MarketModel old = MainController.market; + MainController.market = market; + MainController.market.addAllListener(old.getListeners()); + } + + public void save(ActionEvent actionEvent) { + try { + World.save(); + } catch (FileNotFoundException | UnsupportedEncodingException | XMLStreamException e) { + LOG.error("Error on save file",e); + } + } + + public void importWorld(ActionEvent actionEvent) { + try { + FileChooser fileChooser = new FileChooser(); + FileChooser.ExtensionFilter extFilter = new FileChooser.ExtensionFilter("Excel files (*.xlsx)", "*.xlsx"); + fileChooser.getExtensionFilters().add(extFilter); + fileChooser.setInitialDirectory(new File(".")); + File file = fileChooser.showOpenDialog(null); + if (file !=null) { + World.imp(file); + reload(); + } + } catch (SAXException | IOException e) { + LOG.error("Error on import file", e); + } + } + + + public void addItem(ActionEvent actionEvent){ + Optional res = Dialogs.create() + .title("Добавление нового товара") + .message("Введите название товара") + .showTextInput(); + if (res.isPresent()) market.add(market.newItem(res.get())); + } + + + public void addVendor(ActionEvent actionEvent) { + Screeners.showAddVendor(); + } + + public void editVendor(ActionEvent actionEvent) { + Screeners.showEditVendor(offersController.getVendor()); + } + + private void reload(){ + world = new MarketModel(World.getMarket()); + market = world; + itemsController.init(); + offersController.init(); + } +} diff --git a/client/src/main/java/ru/trader/controllers/OffersController.java b/client/src/main/java/ru/trader/controllers/OffersController.java new file mode 100644 index 0000000..0826bba --- /dev/null +++ b/client/src/main/java/ru/trader/controllers/OffersController.java @@ -0,0 +1,163 @@ +package ru.trader.controllers; + +import javafx.collections.FXCollections; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import javafx.fxml.FXML; +import javafx.scene.control.ComboBox; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import ru.trader.core.Vendor; +import ru.trader.model.*; +import ru.trader.model.support.ChangeMarketListener; + +import java.util.Collection; +import java.util.Iterator; + + +public class OffersController { + private final static Logger LOG = LoggerFactory.getLogger(OffersController.class); + + private VendorModel vendor; + + @FXML + private ComboBox vendors; + + @FXML + private TableView tblSell; + + @FXML + private TableView tblBuy; + + // инициализируем форму данными + @FXML + private void initialize() { + vendors.getSelectionModel().selectedItemProperty().addListener((ob, oldValue, newValue) ->{ + if (newValue != null){ + LOG.info("Change vendor to {}", newValue); + setVendor(newValue); + fillTables(vendor); + } else { + vendors.getSelectionModel().select(oldValue); + } + }); + init(); + } + + void init(){ + MarketModel market = MainController.getMarket(); + market.addListener(new OffersChangeListener()); + vendors.setItems(market.vendorsProperty()); + vendors.getSelectionModel().selectFirst(); + } + + private void fillTables(VendorModel vendor){ + if (vendor != null){ + tblSell.setItems(FXCollections.observableList(vendor.getSells(this::asOfferDescModel))); + if (tblSell.getSortOrder().size()>0) + tblSell.sort(); + + tblBuy.setItems(FXCollections.observableList(vendor.getBuys(this::asOfferDescModel))); + if (tblBuy.getSortOrder().size()>0) + tblBuy.sort(); + + } else { + tblSell.getItems().clear(); + tblBuy.getItems().clear(); + } + } + + + @FXML + public void editPrice(TableColumn.CellEditEvent event){ + OfferModel offer = event.getRowValue().getOffer(); + offer.setPrice(event.getNewValue()); + } + + public VendorModel getVendor() { + return vendor; + } + + private void setVendor(Vendor vendor){ + this.vendor = MainController.getMarket().asModel(vendor); + } + + private OfferDescModel asOfferDescModel(OfferModel offer){ + return MainController.getMarket().asOfferDescModel(offer); + } + + private void addOffer(OfferModel offer){ + switch (offer.getType()){ + case SELL: tblSell.getItems().add(asOfferDescModel(offer)); + break; + case BUY: tblBuy.getItems().add(asOfferDescModel(offer)); + break; + } + } + + private void removeOffer(OfferModel offer){ + switch (offer.getType()){ + case SELL: remove(offer, tblSell.getItems()); + break; + case BUY: remove(offer, tblBuy.getItems()); + break; + } + } + + private void remove(final OfferModel offer, final Collection list){ + Iterator iterator = list.iterator(); + while (iterator.hasNext()){ + if (iterator.next().getOffer().equals(offer)){ + iterator.remove(); + break; + } + } + } + + private void refresh(OfferModel offer){ + LOG.info("Refresh lists link with item of offer {}", offer); + for (OfferDescModel descModel : tblSell.getItems()) { + if (descModel.hasItem(offer)){ + descModel.refresh(offer.getType()); + return; + } + } + for (OfferDescModel descModel : tblBuy.getItems()) { + if (descModel.hasItem(offer)){ + descModel.refresh(offer.getType()); + return; + } + } + } + + private void refresh(){ + LOG.info("Refresh lists"); + tblSell.getItems().forEach(OfferDescModel::refresh); + tblBuy.getItems().forEach(OfferDescModel::refresh); + } + + private class OffersChangeListener extends ChangeMarketListener { + + @Override + public void add(OfferModel offer) { + refresh(offer); + if (offer.hasVendor(vendor)){ + addOffer(offer); + } + + } + + @Override + public void add(VendorModel vendor) { + refresh(); + } + + @Override + public void remove(OfferModel offer) { + refresh(offer); + if (offer.hasVendor(vendor)) { + removeOffer(offer); + } + } + } +} diff --git a/client/src/main/java/ru/trader/controllers/OffersEditorController.java b/client/src/main/java/ru/trader/controllers/OffersEditorController.java new file mode 100644 index 0000000..7b4b92a --- /dev/null +++ b/client/src/main/java/ru/trader/controllers/OffersEditorController.java @@ -0,0 +1,75 @@ +package ru.trader.controllers; + +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.Parent; +import javafx.scene.control.Label; +import org.controlsfx.control.ButtonBar; +import org.controlsfx.control.action.AbstractAction; +import org.controlsfx.control.action.Action; +import org.controlsfx.dialog.Dialog; +import ru.trader.model.ItemModel; +import ru.trader.view.support.NumberField; + +import java.util.Optional; + +public class OffersEditorController { + private final Action OK = new AbstractAction("OK") { + { + ButtonBar.setType(this, ButtonBar.ButtonType.OK_DONE); + } + + + @Override + public void handle(ActionEvent event) { + Dialog dlg = (Dialog) event.getSource(); + dlg.hide(); + } + }; + + + @FXML + private Label name; + + @FXML + private NumberField sell; + + @FXML + private NumberField buy; + + + public Optional showDialog(Parent parent, Parent content, ItemModel item, Number sell, Number buy) { + name.setText(item.getName()); + + this.sell.setValue(sell); + this.buy.setValue(buy); + + OK.disabledProperty().bind(this.sell.wrongProperty().or(this.buy.wrongProperty())); + + Dialog dlg = new Dialog(parent, "Создание заказов"); + dlg.setContent(content); + dlg.getActions().addAll(OK, Dialog.Actions.CANCEL); + dlg.setResizable(false); + return Optional.ofNullable(dlg.show() == OK ? new DialogResult() : null); + } + + + public class DialogResult { + + private double _sell; + private double _buy; + + public DialogResult() { + _sell = sell.getValue().doubleValue(); + _buy = buy.getValue().doubleValue(); + } + + public double getSell() { + return _sell; + } + + public double getBuy() { + return _buy; + } + } +} diff --git a/client/src/main/java/ru/trader/controllers/OrdersController.java b/client/src/main/java/ru/trader/controllers/OrdersController.java new file mode 100644 index 0000000..fceee8e --- /dev/null +++ b/client/src/main/java/ru/trader/controllers/OrdersController.java @@ -0,0 +1,102 @@ +package ru.trader.controllers; + +import javafx.beans.property.SimpleDoubleProperty; +import javafx.collections.FXCollections; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.Parent; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.cell.TextFieldTableCell; +import javafx.util.converter.LongStringConverter; +import org.controlsfx.control.ButtonBar; +import org.controlsfx.control.action.AbstractAction; +import org.controlsfx.control.action.Action; +import org.controlsfx.dialog.Dialog; +import ru.trader.model.OfferDescModel; +import ru.trader.model.OfferModel; +import ru.trader.model.OrderModel; +import ru.trader.model.support.BindingsHelper; + +import java.util.Collection; +import java.util.Optional; + +public class OrdersController { + private final Action OK = new AbstractAction("OK") { + { + ButtonBar.setType(this, ButtonBar.ButtonType.OK_DONE); + } + + + @Override + public void handle(ActionEvent event) { + Dialog dlg = (Dialog) event.getSource(); + dlg.hide(); + } + }; + + @FXML + private TableView tblOrders; + + @FXML + private TableView tblBuyers; + + @FXML + private TableColumn count; + + @FXML + private TableColumn maxCount; + + @FXML + private TableColumn curProfit; + + private OrderModel order; + + @FXML + private void initialize() { + count.setCellFactory(TextFieldTableCell.forTableColumn(new LongStringConverter())); + maxCount.setCellFactory(TextFieldTableCell.forTableColumn(new LongStringConverter())); + tblOrders.getSelectionModel().selectedItemProperty().addListener((v, o, n) -> changeOrder(n)); + tblBuyers.getSelectionModel().selectedItemProperty().addListener((v, o, n) -> setBuyer(n)); + curProfit.setCellValueFactory(param -> { + OfferModel offer = param.getValue(); + return order !=null ? order.getProfit(offer) : new SimpleDoubleProperty(Double.NaN).asObject(); + }); + } + + + public Optional showDialog(Parent parent, Parent content, Collection offers, double balance, long max) { + + init(offers, balance, max); + + Dialog dlg = new Dialog(parent, "Создание заказов"); + dlg.setContent(content); + dlg.getActions().addAll(OK, Dialog.Actions.CANCEL); + dlg.setResizable(false); + return Optional.ofNullable(dlg.show() == OK ? tblOrders.getSelectionModel().getSelectedItem() : null); + } + + private void init(Collection offers, double balance, long max) { + tblOrders.setItems(BindingsHelper.observableList(offers, (o) -> new OrderModel(o, balance, max))); + if (tblOrders.getSortOrder().size()>0) + tblOrders.sort(); + } + + private void changeOrder(OrderModel order) { + this.order = order; + if (order != null) tblBuyers.setItems(FXCollections.observableList(order.getBuyers())); + else tblBuyers.setItems(FXCollections.emptyObservableList()); + tblBuyers.getSelectionModel().clearSelection(); + if (tblBuyers.getSortOrder().size()>0) + tblBuyers.sort(); + + } + + private void setBuyer(OfferModel offer) { + if (order != null) { + order.setBuyer(offer); + order.setCount(order.getMax()); + } + } + +} \ No newline at end of file diff --git a/client/src/main/java/ru/trader/controllers/RoutersController.java b/client/src/main/java/ru/trader/controllers/RoutersController.java new file mode 100644 index 0000000..2c65cf8 --- /dev/null +++ b/client/src/main/java/ru/trader/controllers/RoutersController.java @@ -0,0 +1,51 @@ +package ru.trader.controllers; + + +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import ru.trader.core.Vendor; +import ru.trader.model.MarketModel; +import ru.trader.model.OfferDescModel; +import ru.trader.view.support.NumberField; + +import java.util.Collection; +import java.util.stream.Collectors; + + +public class RoutersController { + + @FXML + private NumberField balance; + + @FXML + private NumberField cargo; + + @FXML + private Button buy; + + @FXML + private Button sell; + + @FXML + private ComboBox vendors; + + @FXML + private void initialize(){ + init(); + buy.disableProperty().bind(this.balance.wrongProperty().or(this.cargo.wrongProperty())); + buy.setOnAction((e) -> Screeners.showOrders(getOffers(), balance.getValue().doubleValue(), cargo.getValue().longValue())); + } + + void init(){ + MarketModel market = MainController.getMarket(); + vendors.setItems(market.vendorsProperty()); + vendors.getSelectionModel().selectFirst(); + } + + private Collection getOffers(){ + MarketModel market = MainController.getMarket(); + Vendor vendor = vendors.getSelectionModel().getSelectedItem(); + return vendor.getAllSellOffers().stream().map(market::asOfferDescModel).collect(Collectors.toList()); + } +} diff --git a/client/src/main/java/ru/trader/controllers/Screeners.java b/client/src/main/java/ru/trader/controllers/Screeners.java new file mode 100644 index 0000000..07dde23 --- /dev/null +++ b/client/src/main/java/ru/trader/controllers/Screeners.java @@ -0,0 +1,117 @@ +package ru.trader.controllers; + +import javafx.fxml.FXMLLoader; +import javafx.scene.Node; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.stage.Stage; +import org.controlsfx.control.action.Action; +import org.controlsfx.dialog.Dialogs; +import ru.trader.model.*; + +import java.io.IOException; +import java.net.URL; +import java.util.Collection; +import java.util.Optional; + +public class Screeners { + + private static Parent mainScreen; + private static Parent itemDescScreen; + private static Parent vEditorScreen; + private static Parent editOffersScreen; + private static Parent ordersScreen; + + private static MainController mainController; + private static ItemDescController itemDescController; + private static VendorEditorController vEditorController; + private static OffersEditorController oEditorController; + private static OrdersController ordersController; + + public static Parent newScreeners(URL main, String stylesheet) throws IOException { + FXMLLoader loader = new FXMLLoader(main); + mainScreen = loader.load(); + if (stylesheet!=null) + mainScreen.getStylesheets().add(stylesheet); + mainController = loader.getController(); + return mainScreen; + } + + + public static void loadItemDescStage(URL fxml) throws IOException { + FXMLLoader loader = new FXMLLoader(fxml); + itemDescScreen = loader.load(); + itemDescController = loader.getController(); + } + + public static void loadVEditorStage(URL fxml) throws IOException { + FXMLLoader loader = new FXMLLoader(fxml); + vEditorScreen = loader.load(); + vEditorController = loader.getController(); + Stage stage = new Stage(); + stage.setScene(new Scene(vEditorScreen)); + } + + public static void loadAddOfferStage(URL fxml) throws IOException { + FXMLLoader loader = new FXMLLoader(fxml); + editOffersScreen = loader.load(); + oEditorController = loader.getController(); + Stage stage = new Stage(); + stage.setScene(new Scene(editOffersScreen)); + } + + public static void loadOrdersStage(URL fxml) throws IOException { + FXMLLoader loader = new FXMLLoader(fxml); + ordersScreen = loader.load(); + ordersController = loader.getController(); + Stage stage = new Stage(); + stage.setScene(new Scene(ordersScreen)); + } + + public static void show(Node node){ + mainController.getMainPane().setCenter(node); + } + + public static void showException(Throwable e){ + if (mainScreen!=null) + Dialogs.create().owner(mainScreen).showException(e); + } + + public static Action showConfirm(String text){ + return Dialogs.create().owner(mainScreen).message(text).showConfirm(); + } + + public static Action showAddVendor(){ + return vEditorController.showDialog(mainScreen, vEditorScreen, null); + } + + public static Action showEditVendor(VendorModel vendor){ + return vEditorController.showDialog(mainScreen, vEditorScreen, vendor); + } + + public static Parent getMainScreen(){ + return mainScreen; + } + + public static Optional showEditOffers(ItemModel item, Number sell, Number buy) { + return oEditorController.showDialog(vEditorScreen, editOffersScreen, item, sell, buy); + } + + + public static Optional showOrders(Collection offers, double balance, long cargo) { + return ordersController.showDialog(mainScreen, ordersScreen, offers, balance, cargo); + } + + public static void changeItemDesc(ItemDescModel item){ + itemDescController.setItemDesc(item); + } + + public static void showItemDesc(Node owner){ + itemDescController.popup(owner, itemDescScreen); + + } + + public static void closeAll() { + itemDescController.close(); + } +} diff --git a/client/src/main/java/ru/trader/controllers/VendorEditorController.java b/client/src/main/java/ru/trader/controllers/VendorEditorController.java new file mode 100644 index 0000000..078e05e --- /dev/null +++ b/client/src/main/java/ru/trader/controllers/VendorEditorController.java @@ -0,0 +1,256 @@ +package ru.trader.controllers; + +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.ReadOnlyStringProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.Parent; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.TextField; +import javafx.util.converter.DoubleStringConverter; +import org.controlsfx.control.ButtonBar; +import org.controlsfx.control.action.AbstractAction; +import org.controlsfx.control.action.Action; +import org.controlsfx.dialog.Dialog; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.trader.core.OFFER_TYPE; +import ru.trader.model.*; +import ru.trader.model.support.BindingsHelper; +import ru.trader.view.support.cells.TextFieldCell; + + +public class VendorEditorController { + private final static Logger LOG = LoggerFactory.getLogger(VendorEditorController.class); + + private VendorModel vendor; + + private final Action actSave = new AbstractAction("Сохранить") { + { + ButtonBar.setType(this, ButtonBar.ButtonType.OK_DONE); + } + + @Override + public void handle(ActionEvent event) { + Dialog dlg = (Dialog) event.getSource(); + saveChanges(); + dlg.hide(); + } + }; + + @FXML + private TextField name; + + @FXML + private TableView items; + @FXML + private TableColumn buy; + @FXML + private TableColumn sell; + + @FXML + private void initialize() { + items.getSelectionModel().setCellSelectionEnabled(true); + buy.setCellFactory(TextFieldCell.forTableColumn(new DoubleStringConverter())); + sell.setCellFactory(TextFieldCell.forTableColumn(new DoubleStringConverter())); + } + + public Action showDialog(Parent parent, Parent content, VendorModel vendor){ + this.vendor = vendor; + if (vendor != null) { + fill(); + } else { + reset(); + } + + Dialog dlg = new Dialog(parent, vendor == null ? "Добавление станции" : "Редактирование станции"); + dlg.setContent(content); + dlg.getActions().addAll(actSave, Dialog.Actions.CANCEL); + dlg.setResizable(false); + return dlg.show(); + } + + + private void fill(){ + name.setText(vendor.getName()); + fillItems(); + vendor.getSells().forEach(this::fillOffer); + vendor.getBuys().forEach(this::fillOffer); + } + + private void reset(){ + name.setText(""); + fillItems(); + } + + private void fillItems() { + items.setItems(BindingsHelper.observableList(MainController.getMarket().itemsProperty(), (item) -> new FakeOffer(item.getItem()))); + } + + + private void fillOffer(OfferModel offer) { + for (FakeOffer o : items.getItems()) { + if (offer.hasItem(o.item)) { + switch (offer.getType()) { + case SELL: + o.setSell(offer); + break; + case BUY: + o.setBuy(offer); + break; + } + return; + } + } + } + + public void saveChanges(){ + LOG.info("Save vendor changes"); + items.getSelectionModel().clearSelection(); + final MarketModel market = MainController.getMarket(); + if (vendor == null) { + market.setAlert(false); + vendor = market.newVendor(name.getText()); + items.getItems().forEach((o) -> commit(market, vendor, o)); + market.setAlert(true); + market.add(vendor); + } else { + vendor.setName(name.getText()); + items.getItems().forEach((o) -> commit(market, vendor, o)); + } + } + + + + private void commit(MarketModel market, VendorModel vendor, FakeOffer offer){ + LOG.trace("Commit changes of offers {}", offer); + if (offer.isBlank()){ + LOG.trace("Is blank offer, skip"); + return; + } + + if (offer.isNewBuy()){ + LOG.trace("Is new buy offer"); + vendor.add(market.newOffer(OFFER_TYPE.BUY, offer.item, offer.getBprice())); + } else if (offer.isRemoveBuy()) { + LOG.trace("Is remove buy offer"); + vendor.remove(offer.buy); + } else if (offer.isChangeBuy()){ + LOG.trace("Is change buy price to {}", offer.getBprice()); + offer.buy.setPrice(offer.getBprice()); + } else { + LOG.trace("No change buy offer"); + } + + if (offer.isNewSell()){ + LOG.trace("Is new sell offer"); + vendor.add(market.newOffer(OFFER_TYPE.SELL, offer.item, offer.getSprice())); + } else if (offer.isRemoveSell()) { + LOG.trace("Is remove sell offer"); + vendor.remove(offer.sell); + } else if (offer.isChangeSell()){ + LOG.trace("Is change sell price to {}", offer.getSprice()); + offer.sell.setPrice(offer.getSprice()); + } else { + LOG.trace("No change sell offer"); + } + } + + public class FakeOffer { + private final ItemModel item; + private DoubleProperty sprice; + private DoubleProperty bprice; + private OfferModel sell; + private OfferModel buy; + + public FakeOffer(ItemModel item){ + this.item = item; + this.sprice = new SimpleDoubleProperty(0); + this.bprice = new SimpleDoubleProperty(0); + } + + public ReadOnlyStringProperty nameProperty(){ + return item.nameProperty(); + } + + public double getSprice() { + return sprice.get(); + } + + public void setSprice(double sprice) { + this.sprice.set(sprice); + } + + public double getBprice() { + return bprice.get(); + } + + public void setBprice(double bprice) { + this.bprice.set(bprice); + } + + public DoubleProperty bpriceProperty() { + return bprice; + } + + public DoubleProperty spriceProperty() { + return sprice; + } + + public boolean isChangeSell() { + return sell!=null && getSprice() != sell.getPrice(); + } + + public boolean isChangeBuy() { + return buy!=null && getBprice() != buy.getPrice(); + } + + + public boolean isNewSell() { + return sell == null && getSprice() != 0; + } + + public boolean isNewBuy() { + return buy == null && getBprice() != 0; + } + + public boolean isRemoveSell() { + return sell != null && getSprice() ==0; + } + + public boolean isRemoveBuy() { + return buy != null && getBprice() ==0; + } + + public boolean isBlank(){ + return sell == null && getSprice() == 0 && buy == null && getBprice() == 0; + } + + public boolean hasItem(ItemModel item){ + return this.item.equals(item); + } + + public void setSell(OfferModel sell) { + this.sell = sell; + sprice.set(sell.getPrice()); + } + + public void setBuy(OfferModel buy) { + this.buy = buy; + bprice.set(buy.getPrice()); + } + + @Override + public String toString() { + return "FakeOffer{" + + "item=" + item + + ", sprice=" + sprice.get() + + ", bprice=" + bprice.get() + + ", sell=" + sell + + ", buy=" + buy + + '}'; + } + } +} diff --git a/client/src/main/java/ru/trader/model/ItemDescModel.java b/client/src/main/java/ru/trader/model/ItemDescModel.java new file mode 100644 index 0000000..c0dd54e --- /dev/null +++ b/client/src/main/java/ru/trader/model/ItemDescModel.java @@ -0,0 +1,40 @@ +package ru.trader.model; + +import javafx.beans.property.*; +import ru.trader.core.OFFER_TYPE; + +import java.util.List; + + +public interface ItemDescModel { + ItemModel getItem(); + + ReadOnlyStringProperty nameProperty(); + + ReadOnlyDoubleProperty avgBuyProperty(); + + ReadOnlyObjectProperty minBuyProperty(); + + ReadOnlyObjectProperty maxBuyProperty(); + + ReadOnlyObjectProperty bestBuyProperty(); + + ReadOnlyDoubleProperty avgSellProperty(); + + ReadOnlyObjectProperty minSellProperty(); + + ReadOnlyObjectProperty maxSellProperty(); + + ReadOnlyObjectProperty bestSellProperty(); + + + boolean hasItem(OfferModel offer); + + List getSeller(); + + List getBuyer(); + + void refresh(); + + void refresh(OFFER_TYPE type); +} diff --git a/client/src/main/java/ru/trader/model/ItemDescModelImpl.java b/client/src/main/java/ru/trader/model/ItemDescModelImpl.java new file mode 100644 index 0000000..26bd80c --- /dev/null +++ b/client/src/main/java/ru/trader/model/ItemDescModelImpl.java @@ -0,0 +1,115 @@ +package ru.trader.model; + +import javafx.beans.property.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.trader.core.OFFER_TYPE; + +import java.util.List; + +public class ItemDescModelImpl implements ItemDescModel { + private final static Logger LOG = LoggerFactory.getLogger(ItemDescModelImpl.class); + + protected final ItemModel item; + protected final ItemStatModel statSell; + protected final ItemStatModel statBuy; + + public ItemDescModelImpl(ItemModel item, ItemStatModel statSell, ItemStatModel statBuy) { + this.item = item; + this.statSell = statSell; + this.statBuy = statBuy; + } + + @Override + public ItemModel getItem(){ + return item; + } + + @Override + public ReadOnlyStringProperty nameProperty() { + return item.nameProperty(); + } + + @Override + public ReadOnlyDoubleProperty avgBuyProperty() { + return statBuy.avgProperty(); + } + + @Override + public ReadOnlyObjectProperty minBuyProperty() { + return statBuy.minProperty(); + } + + @Override + public ReadOnlyObjectProperty maxBuyProperty() { + return statBuy.maxProperty(); + } + + @Override + public ReadOnlyObjectProperty bestBuyProperty() { + return statBuy.bestProperty(); + } + + @Override + public ReadOnlyDoubleProperty avgSellProperty() { + return statSell.avgProperty(); + } + + @Override + public ReadOnlyObjectProperty minSellProperty() { + return statSell.minProperty(); + } + + @Override + public ReadOnlyObjectProperty maxSellProperty() { + return statSell.maxProperty(); + } + + @Override + public ReadOnlyObjectProperty bestSellProperty() { + return statSell.bestProperty(); + } + + @Override + public List getSeller() { + return statSell.getOffers(); + } + + @Override + public List getBuyer() { + return statBuy.getOffers(); + } + + @Override + public void refresh(){ + LOG.trace("Refresh stats of itemDesc {}", this); + statBuy.refresh(); + statSell.refresh(); + } + + @Override + public void refresh(OFFER_TYPE type){ + LOG.trace("Refresh {} stat of itemDesc {}", type, this); + switch (type) { + case SELL: statSell.refresh(); + break; + case BUY: statBuy.refresh(); + break; + } + } + + public boolean hasItem(ItemModel item){ + return this.item.getItem().equals(item.getItem()); + } + + @Override + public boolean hasItem(OfferModel offer){ + return this.item.getItem().equals(offer.getOffer().getItem()); + } + + @Override + public String toString() { + return item.toString(); + } +} + diff --git a/client/src/main/java/ru/trader/model/ItemModel.java b/client/src/main/java/ru/trader/model/ItemModel.java new file mode 100644 index 0000000..8bbe17d --- /dev/null +++ b/client/src/main/java/ru/trader/model/ItemModel.java @@ -0,0 +1,52 @@ +package ru.trader.model; + +import javafx.beans.property.ReadOnlyStringProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.trader.core.Item; + +public class ItemModel{ + private final static Logger LOG = LoggerFactory.getLogger(ItemModel.class); + private final Item item; + private final MarketModel market; + private StringProperty name; + + ItemModel(Item item, MarketModel market) { + this.item = item; + this.market = market; + } + + public String getName() {return name != null ? name.get() : item.getName();} + + public void setName(String value) { + LOG.info("Change name of item {} to {}", item, name); + market.updateName(this, value); + if (name != null) name.set(value); + } + + public ReadOnlyStringProperty nameProperty() { + if (name == null) { + name = new SimpleStringProperty(item.getName()); + } + return name; + } + + @Override + public String toString() { + if (LOG.isTraceEnabled()){ + final StringBuilder sb = new StringBuilder("ItemModel{"); + sb.append("nameProp=").append(name); + sb.append(", item=").append(super.toString()); + sb.append('}'); + return sb.toString(); + } + return item.toString(); + } + + Item getItem() { + return item; + } + +} diff --git a/client/src/main/java/ru/trader/model/ItemStatModel.java b/client/src/main/java/ru/trader/model/ItemStatModel.java new file mode 100644 index 0000000..d3aae62 --- /dev/null +++ b/client/src/main/java/ru/trader/model/ItemStatModel.java @@ -0,0 +1,116 @@ +package ru.trader.model; + +import javafx.beans.property.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.trader.core.ItemStat; +import ru.trader.core.Offer; + +import java.util.List; +import java.util.stream.Collectors; + +public class ItemStatModel { + private final static Logger LOG = LoggerFactory.getLogger(ItemStatModel.class); + private ItemStat itemStat; + private final MarketModel market; + + private DoubleProperty avg; + private ObjectProperty max; + private ObjectProperty min; + private ObjectProperty best; + + ItemStatModel(ItemStat itemStat, MarketModel market) { + this.itemStat = itemStat; + this.market = market; + } + + public ReadOnlyDoubleProperty avgProperty(){ + if (avg == null) avg = new SimpleDoubleProperty(itemStat.getAvg()); + return avg; + } + + public ReadOnlyObjectProperty minProperty(){ + if (min == null){ + min = new SimpleObjectProperty<>(market.asModel(itemStat.getMin())); + } + return min; + } + + public ReadOnlyObjectProperty maxProperty(){ + if (max == null) { + max = new SimpleObjectProperty<>(market.asModel(itemStat.getMax())); + } + return max; + } + + public ReadOnlyObjectProperty bestProperty(){ + if (best == null){ + best = new SimpleObjectProperty<>(market.asModel(itemStat.getBest())); + } + return best; + } + + public double getAvg() { + return avg != null ? avg.get() : itemStat.getAvg(); + } + + public OfferModel getMax() { + return max != null ? max.get() : market.asModel(itemStat.getMax()); + } + + public OfferModel getMin() { + return min != null ? min.get() : market.asModel(itemStat.getMin()); + } + + public OfferModel getBest() { + return best != null ? best.get() : market.asModel(itemStat.getBest()); + } + + public List getOffers() { + return itemStat.getOffers().stream().map(market::asModel).collect(Collectors.toList()); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(""); + if (LOG.isTraceEnabled()){ + sb.append("ItemStatModel{"); + sb.append("avgProp=").append(avg); + sb.append(", maxProp=").append(max); + sb.append(", minProp=").append(min); + sb.append(", bestProp=").append(best); + sb.append(", itemStat=").append(itemStat.toString()); + sb.append('}'); + return sb.toString(); + } + sb.append("avg=").append(getAvg()); + sb.append(", max=").append(getMax()); + sb.append(", min=").append(getMin()); + sb.append(", best=").append(getBest()); + + return sb.toString(); + } + + public void refresh(){ + LOG.debug("Refresh model {}", this); + if (itemStat.isEmpty()) { + ItemStat fresh = market.getStat(itemStat.getType(), itemStat.getItem()); + if (itemStat != fresh) itemStat = fresh; + } + if (avg!=null) avg.setValue(itemStat.getAvg()); + refreshProp(min, itemStat.getMin()); + refreshProp(max, itemStat.getMax()); + refreshProp(best, itemStat.getBest()); + LOG.debug("Fresh model = {}", this); + } + + private void refreshProp(ObjectProperty prop, Offer offer){ + if (prop!=null ){ + OfferModel model = prop.getValue(); + if (model==null || !model.isModel(offer)){ + prop.setValue(market.asModel(offer)); + } + } + } + +} diff --git a/client/src/main/java/ru/trader/model/MarketModel.java b/client/src/main/java/ru/trader/model/MarketModel.java new file mode 100644 index 0000000..4b6fe91 --- /dev/null +++ b/client/src/main/java/ru/trader/model/MarketModel.java @@ -0,0 +1,161 @@ +package ru.trader.model; + +import javafx.beans.property.ListProperty; +import javafx.beans.property.ReadOnlyListProperty; +import javafx.beans.property.SimpleListProperty; +import javafx.collections.FXCollections; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.trader.core.*; +import ru.trader.model.support.BindingsHelper; +import ru.trader.model.support.ChangeMarketListener; + +import java.util.ArrayList; +import java.util.Collection; + + +public class MarketModel { + private final static Logger LOG = LoggerFactory.getLogger(MarketModel.class); + + private final Market market; + + private final Collection listener = new ArrayList<>(); + + private final ListProperty vendors; + private final ListProperty items; + + private boolean alert = true; + + public ReadOnlyListProperty vendorsProperty() { + return vendors; + } + + public ReadOnlyListProperty itemsProperty() { + return items; + } + + public void setAlert(boolean alert) { + this.alert = alert; + } + + public MarketModel(Market market) { + this.market = market; + items = new SimpleListProperty<>(BindingsHelper.observableList(market.getItems(), this::getItemDesc)); + vendors = new SimpleListProperty<>(FXCollections.observableArrayList(market.get())); + } + + public void addListener(ChangeMarketListener listener){ + synchronized (this.listener){ + this.listener.add(listener); + } + } + + public void removeListeners() { + synchronized (listener){ + listener.clear(); + } + } + + public void addAllListener(Collection listener){ + synchronized (this.listener){ + this.listener.addAll(listener); + } + } + + public Collection getListeners() { + return listener; + } + + void updateName(ItemModel model, String value) { + Item item = model.getItem(); + String old = item.getName(); + item.setName(value); + if (alert) listener.forEach((c) -> c.nameChange(model, old, value)); + } + + void updateName(VendorModel model, String value) { + Vendor vendor = model.getVendor(); + String old = vendor.getName(); + vendor.setName(value); + if (alert) listener.forEach((c) -> c.nameChange(model, old, value)); + } + + void updatePrice(OfferModel model, double value) { + Offer offer = model.getOffer(); + double old = offer.getPrice(); + market.updatePrice(offer, value); + if (alert) listener.forEach((c) -> c.priceChange(model, old, value)); + } + + void add(VendorModel vendor, OfferModel offer) { + market.add(vendor.getVendor(), offer.getOffer()); + if (alert) listener.forEach((c) -> c.add(offer)); + } + + void remove(VendorModel vendor, OfferModel offer) { + market.remove(vendor.getVendor(), offer.getOffer()); + if (alert) listener.forEach((c) -> c.remove(offer)); + } + + public void add(VendorModel vendor) { + LOG.info("Add vendor {} to market {}", vendor, this); + market.add(vendor.getVendor()); + if (alert) listener.forEach((c) -> c.add(vendor)); + vendors.add(vendor.getVendor()); + } + + public void add(ItemModel item) { + LOG.info("Add item {} to market {}", item, this); + market.add(item.getItem()); + ItemDescModel model = getItemDesc(item); + if (alert) listener.forEach((c) -> c.add(model)); + items.add(model); + } + + public ItemModel newItem(String name){ + return ModelFabrica.buildItemModel(name, this); + } + + public VendorModel newVendor(String name){ + return ModelFabrica.buildModel(name, this); + } + + public OfferModel newOffer(OFFER_TYPE type, ItemModel item, double price) { + return ModelFabrica.buildModel(type, item, price, this); + } + + ItemDescModel getItemDesc(Item item){ + return getItemDesc(asModel(item)); + } + + ItemDescModel getItemDesc(ItemModel item){ + return ModelFabrica.buildModel(item, market.getStatSell(item.getItem()), market.getStatBuy(item.getItem()), this); + } + + public OfferDescModel asOfferDescModel(Offer offer){ + return asOfferDescModel(asModel(offer)); + } + + public OfferDescModel asOfferDescModel(OfferModel offer){ + Item item = offer.getOffer().getItem(); + return ModelFabrica.buildModel(offer, market.getStatSell(item), market.getStatBuy(item), this); + } + + ItemStat getStat(OFFER_TYPE type, Item item){ + return market.getStat(type, item); + } + + public OfferModel asModel(Offer offer){ + return ModelFabrica.getModel(offer, this); + } + + public ItemModel asModel(Item item){ + return ModelFabrica.getModel(item, this); + } + + public VendorModel asModel(Vendor vendor) { + return ModelFabrica.getModel(vendor, this); + } + + +} diff --git a/client/src/main/java/ru/trader/model/ModelFabrica.java b/client/src/main/java/ru/trader/model/ModelFabrica.java new file mode 100644 index 0000000..088e0e2 --- /dev/null +++ b/client/src/main/java/ru/trader/model/ModelFabrica.java @@ -0,0 +1,101 @@ +package ru.trader.model; + +import ru.trader.core.*; + +import java.lang.ref.WeakReference; +import java.util.HashMap; + +public class ModelFabrica { + + private static final HashMap> items = new HashMap<>(); + private static final HashMap> vendors = new HashMap<>(); + private static final HashMap> offers = new HashMap<>(); + + private static final HashMap> stats = new HashMap<>(); + + public static ItemModel buildItemModel(String name, MarketModel market){ + return getModel(new Item(name), market); + } + + public static VendorModel buildModel(String name, MarketModel market){ + return getModel(new SimpleVendor(name), market); + } + + public static OfferModel buildModel(OFFER_TYPE type, ItemModel item, double price, MarketModel market) { + return getModel(new Offer(type, item.getItem(), price), market); + } + + public static ItemDescModel buildModel(ItemModel item, ItemStat sell, ItemStat buy, MarketModel market) { + return new ItemDescModelImpl(item, getModel(sell, market), getModel(buy, market)); + } + + public static OfferDescModel buildModel(OfferModel offer, ItemStat sell, ItemStat buy, MarketModel market){ + return new OfferDescModel(offer, getModel(sell, market), getModel(buy, market)); + } + + + public static VendorModel getModel(Vendor vendor, MarketModel market){ + if (vendor == null) return null; + VendorModel res=null; + WeakReference ref = vendors.get(vendor); + if (ref != null){ + res = ref.get(); + } + if (res == null){ + res = new VendorModel(vendor, market); + vendors.put(vendor, new WeakReference<>(res)); + } + return res; + } + + + public static ItemModel getModel(Item item, MarketModel market){ + if (item == null) return null; + ItemModel res=null; + WeakReference ref = items.get(item); + if (ref != null){ + res = ref.get(); + } + if (res == null){ + res = new ItemModel(item, market); + items.put(item, new WeakReference<>(res)); + } + return res; + } + + public static OfferModel getModel(Offer offer, MarketModel market){ + if (offer == null) return null; + OfferModel res = null; + WeakReference ref = offers.get(offer); + if (ref != null){ + res = ref.get(); + } + if (res == null){ + res = new OfferModel(offer, market); + offers.put(offer, new WeakReference<>(res)); + } + return res; + } + + public static ItemStatModel getModel(ItemStat itemStat, MarketModel market){ + if (itemStat == null) return null; + ItemStatModel res = null; + WeakReference ref = stats.get(itemStat); + if (ref != null){ + res = ref.get(); + } + if (res == null){ + res = new ItemStatModel(itemStat, market); + stats.put(itemStat, new WeakReference<>(res)); + } + return res; + } + + public static void clear(){ + items.clear(); + vendors.clear(); + offers.clear(); + stats.clear(); + } + +} diff --git a/client/src/main/java/ru/trader/model/OfferDescModel.java b/client/src/main/java/ru/trader/model/OfferDescModel.java new file mode 100644 index 0000000..675221f --- /dev/null +++ b/client/src/main/java/ru/trader/model/OfferDescModel.java @@ -0,0 +1,93 @@ +package ru.trader.model; + +import javafx.beans.binding.Bindings; +import javafx.beans.property.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.trader.model.support.ModelBindings; + +public class OfferDescModel extends ItemDescModelImpl implements ItemDescModel{ + private final static Logger LOG = LoggerFactory.getLogger(OfferDescModel.class); + + protected DoubleProperty diff; + protected final OfferModel offer; + protected DoubleProperty maxProfit; + protected DoubleProperty minProfit; + protected DoubleProperty avgProfit; + + public OfferDescModel(OfferModel offer, ItemStatModel statSell, ItemStatModel statBuy) { + super(offer.getItem(), statSell, statBuy); + this.offer = offer; + } + + public ReadOnlyDoubleProperty priceProperty() { + return offer.priceProperty(); + } + + public double getPrice(){ + return offer.getPrice(); + } + + public ReadOnlyDoubleProperty profitProperty() { + if (maxProfit == null){ + maxProfit = new SimpleDoubleProperty(0); + switch (offer.getType()) { + case SELL: maxProfit.bind(ModelBindings.diff(bestBuyProperty(), priceProperty())); + break; + case BUY: maxProfit.bind(ModelBindings.diff(priceProperty(), bestSellProperty())); + break; + } + } + return maxProfit; + } + + public ReadOnlyDoubleProperty avgProfitProperty() { + if (avgProfit == null){ + avgProfit = new SimpleDoubleProperty(0); + switch (offer.getType()) { + case SELL: avgProfit.bind(avgBuyProperty().subtract(priceProperty())); + break; + case BUY: avgProfit.bind(priceProperty().subtract(avgSellProperty())); + break; + } + } + return avgProfit; + } + + public ReadOnlyDoubleProperty minProfitProperty() { + if (minProfit == null){ + minProfit = new SimpleDoubleProperty(0); + switch (offer.getType()) { + case SELL: minProfit.bind(ModelBindings.diff(minBuyProperty(), priceProperty())); + break; + case BUY: minProfit.bind(ModelBindings.diff(priceProperty(), minSellProperty())); + break; + } + } + return minProfit; + } + + public ReadOnlyDoubleProperty diffProperty(){ + if (diff == null){ + diff = new SimpleDoubleProperty(0); + switch (offer.getType()) { + case SELL: diff.bind(Bindings.subtract(priceProperty(), avgSellProperty())); + break; + case BUY: diff.bind(Bindings.subtract(priceProperty(), avgBuyProperty())); + break; + } + } + return diff; + } + + public double getDiff(){ + return diffProperty().get(); + } + + public OfferModel getOffer(){ + return offer; + } + + + +} diff --git a/client/src/main/java/ru/trader/model/OfferModel.java b/client/src/main/java/ru/trader/model/OfferModel.java new file mode 100644 index 0000000..12529cc --- /dev/null +++ b/client/src/main/java/ru/trader/model/OfferModel.java @@ -0,0 +1,103 @@ +package ru.trader.model; + +import javafx.beans.property.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.trader.core.OFFER_TYPE; +import ru.trader.core.Offer; + +public class OfferModel{ + private final static Logger LOG = LoggerFactory.getLogger(OfferModel.class); + private final MarketModel market; + private final Offer offer; + + private DoubleProperty price; + + + OfferModel(Offer offer, MarketModel market) { + this.market = market; + this.offer = offer; + } + + + public double getPrice() {return price != null ? price.get() : offer.getPrice();} + + public void setPrice(double value) { + if (getPrice() == value) return; + LOG.info("Change price offer {} to {}", offer, value); + market.updatePrice(this, value); + if (price != null) price.set(value); + } + + public ReadOnlyDoubleProperty priceProperty() { + if (price == null) { + price = new SimpleDoubleProperty(offer.getPrice()); + } + return price; + } + + public ItemModel getItem() { + return market.asModel(offer.getItem()); + } + + public VendorModel getVendor() { + return market.asModel(offer.getVendor()); + } + + public OFFER_TYPE getType(){ + return offer.getType(); + } + + public boolean isModel(Offer offer) { + return this.offer == offer; + } + + public boolean hasVendor(VendorModel vendor){ + return offer.getVendor().equals(vendor.getVendor()); + } + + public boolean hasItem(ItemModel item){ + return offer.getItem().equals(item.getItem()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof OfferModel)) return false; + + OfferModel that = (OfferModel) o; + + return offer.equals(that.offer); + + } + + @Override + public int hashCode() { + return offer.hashCode(); + } + + @Override + public String toString() { + if (LOG.isTraceEnabled()){ + final StringBuilder sb = new StringBuilder("OfferModel{"); + sb.append("priceProp=").append(price); + sb.append(", offer=").append(offer.toString()); + sb.append('}'); + return sb.toString(); + } + return offer.toString(); + } + + Offer getOffer() { + return offer; + } + + public String toVString(){ + return offer.toVString(); + } + + public String toIString(){ + return offer.toIString(); + } + +} diff --git a/client/src/main/java/ru/trader/model/OrderModel.java b/client/src/main/java/ru/trader/model/OrderModel.java new file mode 100644 index 0000000..e2d54fa --- /dev/null +++ b/client/src/main/java/ru/trader/model/OrderModel.java @@ -0,0 +1,109 @@ +package ru.trader.model; + +import javafx.beans.property.*; +import javafx.beans.value.ObservableValue; +import ru.trader.model.support.ModelBindings; + +import java.util.List; + +public class OrderModel { + + private final OfferDescModel offer; + + private final LongProperty count; + private final ObjectProperty buyer = new SimpleObjectProperty<>(); + private long max; + private DoubleProperty profit; + private DoubleProperty bestProfit; + + public OrderModel(OfferDescModel offer) { + this.offer = offer; + this.count = new SimpleLongProperty(0){ + @Override + public void setValue(Number v) { + if (max > 0 && v.longValue() > max){ + super.setValue(max); + } else { + super.setValue(v); + } + } + }; + } + + public OrderModel(OfferDescModel offer, double balance, long limit) { + this(offer); + this.max = Math.min(limit, (long) Math.floor(balance / offer.getPrice())); + } + + public OfferModel getOffer() { + return offer.getOffer(); + } + + public ReadOnlyStringProperty nameProperty() { + return offer.nameProperty(); + } + + public ReadOnlyDoubleProperty priceProperty() { + return offer.priceProperty(); + } + + public ReadOnlyObjectProperty bestProperty(){ + return offer.bestBuyProperty(); + } + + public long getCount() { + return count.get(); + } + + public LongProperty countProperty() { + return count; + } + + public void setCount(long count) { + this.count.set(count); + } + + public ReadOnlyDoubleProperty profitProperty() { + if (profit == null){ + profit = new SimpleDoubleProperty(0); + profit.bind(ModelBindings.diff(buyer, offer.getOffer().priceProperty()).multiply(count)); + } + return profit; + } + + public ReadOnlyDoubleProperty bestProfitProperty() { + if (bestProfit == null){ + bestProfit = new SimpleDoubleProperty(0); + bestProfit.bind(offer.profitProperty().multiply(max)); + } + return bestProfit; + } + + public ObservableValue getProfit(OfferModel buyer) { + return buyer.priceProperty().subtract(offer.getOffer().priceProperty()).multiply(max).asObject(); + } + + public ObjectProperty buyerProperty() { + return buyer; + } + + public void setBuyer(OfferModel buyer) { + this.buyer.set(buyer); + } + + public OfferModel getBuyer() { + return buyer.get(); + } + + public long getMax() { + return max; + } + + public void setMax(long max) { + this.max = max; + } + + public List getBuyers(){ + return offer.getBuyer(); + } +} diff --git a/client/src/main/java/ru/trader/model/VendorModel.java b/client/src/main/java/ru/trader/model/VendorModel.java new file mode 100644 index 0000000..a46b892 --- /dev/null +++ b/client/src/main/java/ru/trader/model/VendorModel.java @@ -0,0 +1,98 @@ +package ru.trader.model; + +import javafx.beans.property.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.trader.core.Vendor; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class VendorModel { + private final static Logger LOG = LoggerFactory.getLogger(VendorModel.class); + private final Vendor vendor; + private final MarketModel market; + private StringProperty name; + + VendorModel(Vendor vendor, MarketModel market) { + this.vendor = vendor; + this.market = market; + } + + public String getName() {return name != null ? name.get() : vendor.getName();} + + public void setName(String value) { + if (getName().equals(value)) return; + LOG.info("Change name vendor {} to {}", vendor, value); + market.updateName(this, value); + if (name != null) name.set(value); + } + + public ReadOnlyStringProperty nameProperty() { + if (name == null) { + name = new SimpleStringProperty(vendor.getName()); + } + return name; + } + + public List getSells() { + return vendor.getAllSellOffers().stream().map(market::asModel).collect(Collectors.toList()); + } + + public List getBuys() { + return vendor.getAllBuyOffers().stream().map(market::asModel).collect(Collectors.toList()); + } + + public List getSells(Function mapper) { + return vendor.getAllSellOffers().stream().map(market::asModel).map(mapper).collect(Collectors.toList()); + } + + public List getBuys(Function mapper) { + return vendor.getAllBuyOffers().stream().map(market::asModel).map(mapper).collect(Collectors.toList()); + } + + + Vendor getVendor() { + return vendor; + } + + public void add(OfferModel offer){ + LOG.info("Add offer {} to vendor {}", offer, vendor); + market.add(this, offer); + } + + public void remove(OfferModel offer) { + LOG.info("Remove offer {} from vendor {}", offer, vendor); + market.remove(this, offer); + } + + public boolean hasSell(ItemModel item) { + return vendor.hasSell(item.getItem()); + } + + public boolean hasBuy(ItemModel item) { + return vendor.hasBuy(item.getItem()); + } + + @Override + public String toString() { + if (LOG.isTraceEnabled()){ + final StringBuilder sb = new StringBuilder("VendorModel{"); + sb.append("nameProp=").append(name); + sb.append(", vendor=").append(vendor.toString()); + sb.append('}'); + return sb.toString(); + } + return vendor.toString(); + } + + public Optional getSell(ItemModel item){ + return Optional.ofNullable(market.asModel(vendor.getSell(item.getItem()))); + } + + public Optional getBuy(ItemModel item){ + return Optional.ofNullable(market.asModel(vendor.getBuy(item.getItem()))); + } + +} diff --git a/client/src/main/java/ru/trader/model/support/BindingsHelper.java b/client/src/main/java/ru/trader/model/support/BindingsHelper.java new file mode 100644 index 0000000..a226235 --- /dev/null +++ b/client/src/main/java/ru/trader/model/support/BindingsHelper.java @@ -0,0 +1,71 @@ +package ru.trader.model.support; + +import com.sun.javafx.collections.ImmutableObservableList; +import javafx.beans.Observable; +import javafx.beans.binding.ListBinding; +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Function; + +public class BindingsHelper { + + public static ObservableList deepObservableList(Collection entries, Function convert){ + return deepBind(observableList(entries, convert)); + } + + public static ObservableList observableList(Collection entries, Function convert){ + List list = new ArrayList<>(entries.size()); + entries.forEach((v)->list.add(convert.apply(v))); + return FXCollections.observableList(list); + } + + public static ObservableList deepBind(final ObservableList list){ + if ((list == null)) { + throw new NullPointerException("Operands cannot be null."); + } + + return new ListBinding() { + { + super.bind(list); + list.addListener((ListChangeListener) c -> { + while (c.next()) { + if (c.wasAdded()) + for (T item : c.getRemoved()) { + super.bind(item); + } else if (c.wasRemoved()){ + for (T item : c.getAddedSubList()) { + super.unbind(item); + } + } + } + }); + + } + + @Override + public void dispose() { + list.forEach((v) -> super.unbind(v)); + super.unbind(list); + } + + @Override + protected ObservableList computeValue() { + return list; + } + + @Override + public javafx.collections.ObservableList getDependencies() { + ArrayList dependencies = new ArrayList<>(list.size()); + dependencies.addAll(list); + dependencies.add(list); + return new ImmutableObservableList<>((Observable[]) dependencies.toArray()); + } + }; + } + +} diff --git a/client/src/main/java/ru/trader/model/support/ChangeMarketListener.java b/client/src/main/java/ru/trader/model/support/ChangeMarketListener.java new file mode 100644 index 0000000..f2f8f22 --- /dev/null +++ b/client/src/main/java/ru/trader/model/support/ChangeMarketListener.java @@ -0,0 +1,34 @@ +package ru.trader.model.support; + +import ru.trader.model.ItemDescModel; +import ru.trader.model.ItemModel; +import ru.trader.model.OfferModel; +import ru.trader.model.VendorModel; + +public class ChangeMarketListener { + + public void nameChange(ItemModel item, String oldName, String newName){ + + } + + public void nameChange(VendorModel vendor, String oldName, String newName) { + + } + + public void add(ItemDescModel item) { + + } + + public void priceChange(OfferModel offer, double oldPrice, double newPrice) { + } + + public void add(OfferModel offer) { + + } + + public void add(VendorModel vendor) { + } + + public void remove(OfferModel offer) { + } +} diff --git a/client/src/main/java/ru/trader/model/support/ModelBindings.java b/client/src/main/java/ru/trader/model/support/ModelBindings.java new file mode 100644 index 0000000..58f1f8d --- /dev/null +++ b/client/src/main/java/ru/trader/model/support/ModelBindings.java @@ -0,0 +1,206 @@ +package ru.trader.model.support; + +import com.sun.javafx.collections.ImmutableObservableList; +import javafx.beans.Observable; +import javafx.beans.binding.*; +import javafx.beans.property.ReadOnlyDoubleProperty; +import javafx.beans.value.ObservableValue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.trader.model.ItemModel; +import ru.trader.model.OfferModel; +import ru.trader.model.VendorModel; + + +public class ModelBindings { + private final static Logger LOG = LoggerFactory.getLogger(ModelBindings.class); + + + public static StringBinding asString(final OfferModel offer){ + return Bindings.createStringBinding(offer::toVString, offer.priceProperty(), offer.getVendor().nameProperty()); + } + + public static StringBinding asItemString(final OfferModel offer){ + return Bindings.createStringBinding(offer::toIString, offer.priceProperty(), offer.getItem().nameProperty()); + } + + public static DoubleBinding price(final ObservableValue offer){ + ObservableValue offerBind = offerPrice(offer); + return asDouble(offerBind, offerBind); + } + + public static DoubleBinding diff(final ObservableValue of1, final ObservableValue of2){ + return price(of1).subtract(price(of2)); + } + + public static DoubleBinding diff(final ReadOnlyDoubleProperty price, final ObservableValue offer){ + return diff(offer, price).negate(); + } + + public static DoubleBinding diff(final ObservableValue offer, final ReadOnlyDoubleProperty price){ + return price(offer).subtract(price); + } + + public static StringBinding asString(final ObservableValue offer){ + ObservableValue offerBind = offerPrice(offer, true); + return asString(offerBind, offerBind); + } + + public static StringBinding asItemString(final ObservableValue offer){ + ObservableValue offerBind = offerPrice(offer, true); + return asString(offerBind, offerBind); + } + + + + private static StringBinding asItemString(final ObservableValue offer, final Observable... dependencies){ + return Bindings.createStringBinding(() -> { + OfferModel o = offer.getValue(); + return o != null ? o.toIString(): ""; + }, dependencies); + } + + + private static StringBinding asString(final ObservableValue offer, final Observable... dependencies){ + return Bindings.createStringBinding(() -> { + OfferModel o = offer.getValue(); + return o != null ? o.toVString() : ""; + }, dependencies); + } + + private static DoubleBinding asDouble(final ObservableValue offer, final Observable... dependencies){ + return Bindings.createDoubleBinding(() -> { + OfferModel o = offer.getValue(); + return o != null ? offer.getValue().getPrice() : Double.NaN; + }, dependencies); + } + + public static ObservableValue offerPrice(final ObservableValue offer){ + return offerPrice(offer, false); + } + + public static ObservableValue offerPrice(final ObservableValue offer, final boolean deep){ + if ((offer == null)) { + throw new NullPointerException("Operands cannot be null."); + } + + return new ObjectBinding() { + { + super.bind(offer); + bind(offer.getValue()); + offer.addListener((observable, oldValue, newValue) -> { + LOG.trace("unbind {}, bind {}", oldValue, newValue); + unbind(oldValue); + bind(newValue); + }); + } + + @Override + public void dispose() { + unbind(offer.getValue()); + super.unbind(offer); + } + + @Override + protected OfferModel computeValue() { + return offer.getValue(); + } + + @Override + public javafx.collections.ObservableList getDependencies() { + if (deep){ + OfferModel model = offer.getValue(); + return new ImmutableObservableList(offer, model.priceProperty(), model.getVendor().nameProperty(), model.getItem().nameProperty()); + } + else + return new ImmutableObservableList(offer, offer.getValue().priceProperty()); + } + + private void bind(OfferModel model){ + if (model == null) return; + super.bind(model.priceProperty()); + if (deep){ + super.bind(model.getVendor().nameProperty()); + super.bind(model.getItem().nameProperty()); + } + } + + private void unbind(OfferModel model){ + if (model == null) return; + super.unbind(model.priceProperty()); + if (deep){ + super.unbind(model.getVendor().nameProperty()); + super.unbind(model.getItem().nameProperty()); + } + } + }; + } + + public static ObservableValue itemName(final ObservableValue item){ + if ((item == null)) { + throw new NullPointerException("Operands cannot be null."); + } + + return new ObjectBinding() { + { + super.bind(item); + super.bind(item.getValue().nameProperty()); + item.addListener((observable, oldValue, newValue) -> { + super.unbind(oldValue.nameProperty()); + super.bind(newValue.nameProperty()); + }); + } + + @Override + public void dispose() { + super.unbind(item.getValue().nameProperty()); + super.unbind(item); + } + + @Override + protected ItemModel computeValue() { + return item.getValue(); + } + + @Override + public javafx.collections.ObservableList getDependencies() { + return new ImmutableObservableList(item, item.getValue().nameProperty()); + } + }; + } + + + public static ObservableValue vendorName(final ObservableValue vendor){ + if ((vendor == null)) { + throw new NullPointerException("Operands cannot be null."); + } + + return new ObjectBinding() { + { + super.bind(vendor); + super.bind(vendor.getValue().nameProperty()); + vendor.addListener((observable, oldValue, newValue) -> { + super.unbind(oldValue.nameProperty()); + super.bind(newValue.nameProperty()); + }); + } + + @Override + public void dispose() { + super.unbind(vendor.getValue().nameProperty()); + super.unbind(vendor); + } + + @Override + protected VendorModel computeValue() { + return vendor.getValue(); + } + + @Override + public javafx.collections.ObservableList getDependencies() { + return new ImmutableObservableList(vendor, vendor.getValue().nameProperty()); + } + }; + } + +} diff --git a/client/src/main/java/ru/trader/view/support/NaNComparator.java b/client/src/main/java/ru/trader/view/support/NaNComparator.java new file mode 100644 index 0000000..3ba89b0 --- /dev/null +++ b/client/src/main/java/ru/trader/view/support/NaNComparator.java @@ -0,0 +1,22 @@ +package ru.trader.view.support; + + +import java.util.Comparator; + +public class NaNComparator implements Comparator { + + @Override + public int compare(Number n1, Number n2) { + double d1 = n1.doubleValue(); + double d2 = n2.doubleValue(); + boolean isNaN1 = Double.isNaN(d1); + boolean isNaN2 = Double.isNaN(d2); + if (isNaN1 && isNaN2) return 0; + if (isNaN1) return -1; + if (isNaN2) return 1; + + return Double.compare(d1, d2); + + } + +} diff --git a/client/src/main/java/ru/trader/view/support/NumberField.java b/client/src/main/java/ru/trader/view/support/NumberField.java new file mode 100644 index 0000000..c73db86 --- /dev/null +++ b/client/src/main/java/ru/trader/view/support/NumberField.java @@ -0,0 +1,77 @@ +package ru.trader.view.support; + +import javafx.beans.property.*; +import javafx.geometry.Point2D; +import javafx.scene.control.TextField; +import javafx.scene.control.Tooltip; +import javafx.util.converter.NumberStringConverter; + + +public class NumberField extends TextField { + private final static NumberStringConverter converter = new NumberStringConverter("#0.#"); + private final Tooltip tooltip = new Tooltip(); + + private final ObjectProperty number = new SimpleObjectProperty<>(0); + private final BooleanProperty wrong = new SimpleBooleanProperty(false); + + public ObjectProperty numberProperty() { + return number; + } + + public Number getValue(){ + return number.get(); + } + + public void setValue(Number value){ + number.setValue(value); + setText(converter.toString(value)); + } + + public boolean isWrong() { + return wrong.get(); + } + + public BooleanProperty wrongProperty() { + return ReadOnlyBooleanWrapper.booleanProperty(wrong); + } + + public NumberField() { + super(); + tooltip.setText("Wrong number"); + tooltip.setAutoHide(true); + wrong.addListener((ob, o ,n) -> { + if (n) { + setTooltip(tooltip); + Point2D p = this.localToScene(0.0, 0.0); + tooltip.show(this, getScene().getWindow().getX() + getScene().getX() + p.getX(), + getScene().getWindow().getY() + getScene().getY() + p.getY() + getHeight() + 2); + } + else { + tooltip.hide(); + setTooltip(null); + } + }); + + setOnAction((e) -> parseNumber()); + focusedProperty().addListener((ob, o, n) -> {if (o) parseNumber();}); + } + + private void parseNumber(){ + String text = getText(); + if (text == null || text.isEmpty()) { + number.setValue(0); + return; + } + + if (text.matches("^-?\\d+([,\\.]\\d+)?([eE]-?\\d+)?$")){ + number.setValue(converter.fromString(text)); + wrong.setValue(false); + } else { + wrong.setValue(true); + selectAll(); + requestFocus(); + } + } + + +} diff --git a/client/src/main/java/ru/trader/view/support/PropertyFactory.java b/client/src/main/java/ru/trader/view/support/PropertyFactory.java new file mode 100644 index 0000000..5e15491 --- /dev/null +++ b/client/src/main/java/ru/trader/view/support/PropertyFactory.java @@ -0,0 +1,61 @@ +package ru.trader.view.support; + +import com.sun.javafx.property.PropertyReference; +import javafx.beans.NamedArg; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class PropertyFactory { + private final static Logger LOG = LoggerFactory.getLogger(PropertyFactory.class); + + private final String property; + + private Class dataClass; + private String previousProperty; + protected PropertyReference propertyRef; + + /** + * Creates a default PropertyValueFactory to extract the value from a given + * TableView row item reflectively, using the given property name. + * + * @param property The name of the property with which to attempt to + * reflectively extract a corresponding value for in a given object. + */ + public PropertyFactory(@NamedArg("property") String property) { + this.property = property; + } + + + /** + * Returns the property name provided in the constructor. + */ + public final String getProperty() { return property; } + + public final PropertyReference getPropertyRef(T data){ + return fillProperty(data)? propertyRef : null; + } + + private boolean fillProperty(T data) { + if (getProperty() == null || getProperty().isEmpty() || data == null) return false; + + try { + // we attempt to cache the property reference here, as otherwise + // performance suffers when working in large data models. For + // a bit of reference, refer to RT-13937. + if (dataClass == null || previousProperty == null || + ! dataClass.equals(data.getClass()) || + ! previousProperty.equals(getProperty())) { + + // create a new PropertyReference + this.dataClass = data.getClass(); + this.previousProperty = getProperty(); + this.propertyRef = new PropertyReference<>(data.getClass(), getProperty()); + } + } catch (IllegalStateException e) { + // log the warning and move on + LOG.warn("Can not retrieve property '{}' in PropertyValueFactory: {} with provided class type: {}", getProperty(), this, data.getClass()); + LOG.warn("",e); + } + return true; + } +} \ No newline at end of file diff --git a/client/src/main/java/ru/trader/view/support/cells/DoubleCell.java b/client/src/main/java/ru/trader/view/support/cells/DoubleCell.java new file mode 100644 index 0000000..7076534 --- /dev/null +++ b/client/src/main/java/ru/trader/view/support/cells/DoubleCell.java @@ -0,0 +1,26 @@ +package ru.trader.view.support.cells; + +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.util.Callback; +import ru.trader.view.support.NaNComparator; + +public class DoubleCell implements Callback, TableCell> { + @Override + public TableCell call(TableColumn param) { + param.setComparator(new NaNComparator<>()); + return new TableCell(){ + @Override + protected void updateItem(Double item, boolean empty) { + super.updateItem(item, empty); + if (empty) { + setText(null); + setGraphic(null); + } else { + setText(String.format("%.0f", item)); + setGraphic(null); + } + } + }; + } +} diff --git a/client/src/main/java/ru/trader/view/support/cells/EditingCell.java b/client/src/main/java/ru/trader/view/support/cells/EditingCell.java new file mode 100644 index 0000000..069c13c --- /dev/null +++ b/client/src/main/java/ru/trader/view/support/cells/EditingCell.java @@ -0,0 +1,28 @@ +package ru.trader.view.support.cells; + +import javafx.scene.control.cell.TextFieldTableCell; +import javafx.util.StringConverter; + +public abstract class EditingCell extends TextFieldTableCell { + + + protected EditingCell(StringConverter converter) { + super(converter); + } + + @Override + public void updateItem(T item, boolean empty) { + super.updateItem(item, empty); + if (!empty && !isEditing()){ + outText(); + } + } + + protected abstract void outText(); + + @Override + public void cancelEdit() { + super.cancelEdit(); + outText(); + } +} diff --git a/client/src/main/java/ru/trader/view/support/cells/OfferCellValueImpl.java b/client/src/main/java/ru/trader/view/support/cells/OfferCellValueImpl.java new file mode 100644 index 0000000..12c4544 --- /dev/null +++ b/client/src/main/java/ru/trader/view/support/cells/OfferCellValueImpl.java @@ -0,0 +1,19 @@ +package ru.trader.view.support.cells; + +import javafx.beans.NamedArg; +import javafx.beans.value.ObservableValue; +import ru.trader.model.OfferModel; +import ru.trader.model.support.ModelBindings; + + +public class OfferCellValueImpl extends PropertyCellValueFactory{ + + public OfferCellValueImpl(@NamedArg("property") String property) { + super(property); + } + + @Override + ObservableValue format(ObservableValue value) { + return ModelBindings.asString(value); + } +} \ No newline at end of file diff --git a/client/src/main/java/ru/trader/view/support/cells/OfferListCell.java b/client/src/main/java/ru/trader/view/support/cells/OfferListCell.java new file mode 100644 index 0000000..2558b67 --- /dev/null +++ b/client/src/main/java/ru/trader/view/support/cells/OfferListCell.java @@ -0,0 +1,36 @@ +package ru.trader.view.support.cells; + +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.util.Callback; +import ru.trader.model.OfferModel; +import ru.trader.model.support.ModelBindings; + +public class OfferListCell implements Callback, ListCell> { + + @Override + public ListCell call(ListView param){ + return new ListCell(){ + private OfferModel o; + + @Override + public void updateItem(OfferModel offer, boolean empty) { + super.updateItem(offer, empty); + if (!empty){ + if (o != offer){ + textProperty().unbind(); + textProperty().bind(ModelBindings.asString(offer)); + o = offer; + } + } else { + textProperty().unbind(); + o = null; + setText(null); + setGraphic(null); + } + } + + + }; + } +} diff --git a/client/src/main/java/ru/trader/view/support/cells/OfferTableCell.java b/client/src/main/java/ru/trader/view/support/cells/OfferTableCell.java new file mode 100644 index 0000000..413f3e1 --- /dev/null +++ b/client/src/main/java/ru/trader/view/support/cells/OfferTableCell.java @@ -0,0 +1,37 @@ +package ru.trader.view.support.cells; + +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.util.Callback; +import ru.trader.model.OfferModel; +import ru.trader.model.support.ModelBindings; + +public class OfferTableCell implements Callback, TableCell> { + + @Override + public TableCell call(TableColumn param) { + return new TableCell(){ + private OfferModel o; + + @Override + public void updateItem(T value, boolean empty) { + super.updateItem(value, empty); + if (!empty){ + OfferModel offer = (OfferModel) getTableRow().getItem(); + if (o != offer){ + textProperty().unbind(); + textProperty().bind(ModelBindings.asString(offer)); + o = offer; + } + } else { + textProperty().unbind(); + o = null; + setText(null); + setGraphic(null); + } + } + + + }; + } +} diff --git a/client/src/main/java/ru/trader/view/support/cells/PriceCellImpl.java b/client/src/main/java/ru/trader/view/support/cells/PriceCellImpl.java new file mode 100644 index 0000000..ab1d826 --- /dev/null +++ b/client/src/main/java/ru/trader/view/support/cells/PriceCellImpl.java @@ -0,0 +1,50 @@ +package ru.trader.view.support.cells; + +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.scene.text.Text; +import javafx.scene.text.TextFlow; +import javafx.util.Callback; +import javafx.util.converter.DoubleStringConverter; +import ru.trader.model.OfferDescModel; + + +public class PriceCellImpl implements Callback, TableCell> { + private final static String CSS_BAD = "bad"; + private final static String CSS_GOOD = "good"; + private final static String CSS_DIFF = "diff"; + + @Override + public TableCell call(TableColumn param) { + return new PriceCell(); + } + + private class PriceCell extends EditingCell { + protected PriceCell() { + super(new DoubleStringConverter()); + } + + @Override + protected void outText() { + OfferDescModel offerDesc = (OfferDescModel) getTableRow().getItem(); + if (offerDesc!=null){ + double d = offerDesc.getDiff(); + TextFlow txt = new TextFlow(); + Text price = new Text(String.format("%.0f", offerDesc.getPrice())); + Text diff = new Text(String.format(" (%+.0f)", d)); + diff.getStyleClass().add(CSS_DIFF); + txt.getChildren().addAll(price, diff); + this.getStyleClass().removeAll(CSS_BAD, CSS_GOOD); + String cssClass = (d == 0 || Double.isNaN(d) ? "" : d * offerDesc.getOffer().getType().getOrder() > 0 ? CSS_BAD : CSS_GOOD ); + this.getStyleClass().add(cssClass); + this.setText(null); + this.setGraphic(txt); + + } + } + } + + + + +} \ No newline at end of file diff --git a/client/src/main/java/ru/trader/view/support/cells/PropertyCellValueFactory.java b/client/src/main/java/ru/trader/view/support/cells/PropertyCellValueFactory.java new file mode 100644 index 0000000..496d2d3 --- /dev/null +++ b/client/src/main/java/ru/trader/view/support/cells/PropertyCellValueFactory.java @@ -0,0 +1,37 @@ +package ru.trader.view.support.cells; + +import com.sun.javafx.property.PropertyReference; +import javafx.beans.NamedArg; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.beans.value.ObservableValue; +import javafx.scene.control.TableColumn; +import javafx.util.Callback; +import ru.trader.view.support.PropertyFactory; + +public abstract class PropertyCellValueFactory extends PropertyFactory implements Callback, ObservableValue> { + + public PropertyCellValueFactory(@NamedArg("property") String property) { + super(property); + } + + + + @Override + public ObservableValue call(TableColumn.CellDataFeatures param) { + return getCellValue(param.getValue()); + } + + abstract ObservableValue format(ObservableValue value); + + private ObservableValue getCellValue(T rowData){ + ObservableValue value = null; + PropertyReference prop = getPropertyRef(rowData); + if (prop!=null){ + if (prop.hasProperty()) value = prop.getProperty(rowData); + else value = new ReadOnlyObjectWrapper<>(prop.get(rowData)); + } + if (value == null) return null; + return format(value); + } + +} \ No newline at end of file diff --git a/client/src/main/java/ru/trader/view/support/cells/TextFieldCell.java b/client/src/main/java/ru/trader/view/support/cells/TextFieldCell.java new file mode 100644 index 0000000..92daaa0 --- /dev/null +++ b/client/src/main/java/ru/trader/view/support/cells/TextFieldCell.java @@ -0,0 +1,148 @@ +package ru.trader.view.support.cells; + +import javafx.application.Platform; +import javafx.collections.ObservableList; +import javafx.scene.control.*; +import javafx.scene.input.KeyCode; +import javafx.scene.input.MouseButton; +import javafx.util.Callback; +import javafx.util.StringConverter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +public class TextFieldCell extends TableCell { + private final static Logger LOG = LoggerFactory.getLogger(TextFieldCell.class); + + private TextField textField; + private final StringConverter converter; + + + public TextFieldCell(StringConverter converter) { + this.converter = converter; + + this.setOnMouseClicked((e) -> { + if (e.getButton() == MouseButton.PRIMARY) + if (!isEditing()) + getTableView().edit(getTableRow().getIndex(), getTableColumn()); + }); + + } + + + public static Callback, TableCell> forTableColumn(final StringConverter converter) { + return list -> new TextFieldCell<>(converter); + } + + @Override + public void startEdit() { + LOG.trace("Start edit"); + if (! isEditable()) return; + super.startEdit(); + if (isEditing()){ + if (textField == null) { + createTextField(); + } else { + textField.setText(getItemText()); + } + setText(null); + setGraphic(textField); + textField.selectAll(); + textField.requestFocus(); + } + } + + @Override + public void updateItem(T item, boolean empty) { + LOG.trace("Update edit"); + super.updateItem(item, empty); + if (empty) { + setText(null); + setGraphic(null); + + } else { + if (isEditing()) { + if (textField != null) { + textField.setText(getItemText()); + } + setText(null); + setGraphic(textField); + } else { + outItem(); + } + } + } + + @Override + public void cancelEdit() { + LOG.trace("Cancel edit"); + if (!isCommit()) commit(false); + if (isCommit()) { + super.cancelEdit(); + outItem(); + } + } + + + public TextField getTextField(){ + return this.textField; + } + + private void createTextField(){ + this.textField = new TextField(getItemText()); + this.setGraphic(textField); + textField.prefWidthProperty().bind(this.getTableColumn().widthProperty()); + textField.setOnKeyPressed(t -> { + if (t.getCode() == KeyCode.ENTER) { + if (commit(true)) editNext(); + } else if (t.getCode() == KeyCode.ESCAPE) { + textField = null; + cancelEdit(); + } + + }); + } + + + private String getItemText(){ + return converter.toString(getItem()); + } + + public boolean commit(boolean noSkip) { + if (isCommit()) return true; + LOG.trace("Commit text {}", textField.getText()); + try { + commitEdit(converter.fromString(textField.getText())); + } catch (NumberFormatException e){ + if (noSkip) { + Platform.runLater(textField::requestFocus); + return false; + } + } + textField = null; + return true; + } + + protected void outItem(){ + setText(getItemText()); + setGraphic(null); + } + + protected boolean isCommit(){ + return textField == null; + } + + protected void editNext(){ + TableView.TableViewSelectionModel sm = getTableView().getSelectionModel(); + sm.selectNext(); + ObservableList> pos = sm.getSelectedCells(); + for (TablePosition p : pos) { + if (p.getTableColumn().isEditable()) { + getTableView().scrollTo(p.getRow()>0? p.getRow()-1 : 0); + getTableView().edit(p.getRow(), p.getTableColumn()); + return; + } + } + editNext(); + } +} diff --git a/client/src/main/resources/view/itemDesc.fxml b/client/src/main/resources/view/itemDesc.fxml new file mode 100644 index 0000000..26fd402 --- /dev/null +++ b/client/src/main/resources/view/itemDesc.fxml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/client/src/main/resources/view/items.fxml b/client/src/main/resources/view/items.fxml new file mode 100644 index 0000000..d3f23aa --- /dev/null +++ b/client/src/main/resources/view/items.fxml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/src/main/resources/view/main.fxml b/client/src/main/resources/view/main.fxml new file mode 100644 index 0000000..1d6dc5d --- /dev/null +++ b/client/src/main/resources/view/main.fxml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
diff --git a/client/src/main/resources/view/oEditor.fxml b/client/src/main/resources/view/oEditor.fxml new file mode 100644 index 0000000..496e53f --- /dev/null +++ b/client/src/main/resources/view/oEditor.fxml @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/client/src/main/resources/view/offers.fxml b/client/src/main/resources/view/offers.fxml new file mode 100644 index 0000000..19bbdd2 --- /dev/null +++ b/client/src/main/resources/view/offers.fxml @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/src/main/resources/view/orders.fxml b/client/src/main/resources/view/orders.fxml new file mode 100644 index 0000000..5df6c47 --- /dev/null +++ b/client/src/main/resources/view/orders.fxml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/src/main/resources/view/routers.fxml b/client/src/main/resources/view/routers.fxml new file mode 100644 index 0000000..f96a604 --- /dev/null +++ b/client/src/main/resources/view/routers.fxml @@ -0,0 +1,23 @@ + + + + + + + + + + + + +