diff --git a/utils/src/main/java/ru/trader/maddavo/ItemConverter.java b/utils/src/main/java/ru/trader/maddavo/ItemConverter.java new file mode 100644 index 0000000..79bdc6f --- /dev/null +++ b/utils/src/main/java/ru/trader/maddavo/ItemConverter.java @@ -0,0 +1,100 @@ +package ru.trader.maddavo; + +import java.util.HashMap; +import java.util.Map; + +public class ItemConverter { + private final static Map IDS = new HashMap<>(85, 0.9f); + + + static { + IDS.put("Explosives", "explosives"); + IDS.put("Hydrogen Fuel", "hydrogenfuel"); + IDS.put("Mineral Oil", "mineraloil"); + IDS.put("Pesticides", "pesticides"); + IDS.put("Clothing", "clothing"); + IDS.put("Consumer Technology", "consumertechnology"); + IDS.put("Domestic Appliances", "domesticappliances"); + IDS.put("Algae", "algae"); + IDS.put("Animal Meat", "animalmeat"); + IDS.put("Coffee", "coffee"); + IDS.put("Energy Drinks", "energydrinks"); + IDS.put("Fish", "fish"); + IDS.put("Food Cartridges", "foodcartridges"); + IDS.put("Fruit And Vegetables", "fruitandvegetables"); + IDS.put("Grain", "grain"); + IDS.put("Synthetic Meat", "syntheticmeat"); + IDS.put("Tea", "tea"); + IDS.put("Polymers", "polymers"); + IDS.put("Semiconductors", "semiconductors"); + IDS.put("Superconductors", "superconductors"); + IDS.put("Legal Drugs", "group.drugs"); + IDS.put("Beer", "beer"); + IDS.put("Liquor", "liquor"); + IDS.put("Narcotics", "basicnarcotics"); + IDS.put("Tobacco", "tobacco"); + IDS.put("Wine", "wine"); + IDS.put("Atmospheric Processors", "atmosphericprocessors"); + IDS.put("Crop Harvesters", "cropharvesters"); + IDS.put("Marine Equipment", "marinesupplies"); + IDS.put("Microbial Furnaces", "microbialfurnaces"); + IDS.put("Mineral Extractors", "mineralextractors"); + IDS.put("Power Generators", "powergenerators"); + IDS.put("Water Purifiers", "waterpurifiers"); + IDS.put("Agri-Medicines", "agriculturalmedicines"); + IDS.put("Basic Medicines", "basicmedicines"); + IDS.put("Combat Stabilisers", "combatstabilisers"); + IDS.put("Performance Enhancers", "performanceenhancers"); + IDS.put("Progenitor Cells", "progenitorcells"); + IDS.put("Aluminium", "aluminium"); + IDS.put("Beryllium", "beryllium"); + IDS.put("Copper", "copper"); + IDS.put("Cobalt", "cobalt"); + IDS.put("Gallium", "gallium"); + IDS.put("Gold", "gold"); + IDS.put("Indium", "indium"); + IDS.put("Lithium", "lithium"); + IDS.put("Palladium", "palladium"); + IDS.put("Platinum", "platinum"); + IDS.put("Tantalum", "tantalum"); + IDS.put("Titanium", "titanium"); + IDS.put("Silver", "silver"); + IDS.put("Uranium", "uranium"); + IDS.put("Minerals", "group.minerals"); + IDS.put("Bauxite", "bauxite"); + IDS.put("Bertrandite", "bertrandite"); + IDS.put("Coltan", "coltan"); + IDS.put("Gallite", "gallite"); + IDS.put("Indite", "indite"); + IDS.put("Lepidolite", "lepidolite"); + IDS.put("Rutile", "rutile"); + IDS.put("Uraninite", "uraninite"); + IDS.put("Imperial Slaves", "imperialslaves"); + IDS.put("Slaves", "slaves"); + IDS.put("Advanced Catalysers", "advancedcatalysers"); + IDS.put("Animal Monitors", "animalmonitors"); + IDS.put("Aquaponic Systems", "aquaponicsystems"); + IDS.put("Auto-Fabricatos", "autofabricators"); + IDS.put("Bioreducing Lichen", "bioreducinglichen"); + IDS.put("Computer Components", "computercomponents"); + IDS.put("H.E. Suits", "hazardousenvironmentsuits"); + IDS.put("Resonating Separators", "resonatingseparators"); + IDS.put("Robotics", "robotics"); + IDS.put("Land Enrichment Systems", "landenrichmentsystems"); + IDS.put("Leather", "leather"); + IDS.put("Natural Fabrics", "naturalfabrics"); + IDS.put("Synthetic Fabrics", "syntheticfabrics"); + IDS.put("Biowaste", "biowaste"); + IDS.put("Chemical Waste", "chemicalwaste"); + IDS.put("Scrap", "scrap"); + IDS.put("Battle Weapons", "battleweapons"); + IDS.put("Non-Lethal Weapons", "nonlethalweapons"); + IDS.put("Personal Weapons", "personalweapons"); + IDS.put("Reactive Armour", "reactivearmour"); + } + + public static String getItemId(String name){ + String id = IDS.get(name); + return id != null ? id : name; + } +} diff --git a/utils/src/main/java/ru/trader/maddavo/OffersHandler.java b/utils/src/main/java/ru/trader/maddavo/OffersHandler.java new file mode 100644 index 0000000..f48a20a --- /dev/null +++ b/utils/src/main/java/ru/trader/maddavo/OffersHandler.java @@ -0,0 +1,177 @@ +package ru.trader.maddavo; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.trader.core.*; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class OffersHandler implements ParseHandler { + private final static Logger LOG = LoggerFactory.getLogger(OffersHandler.class); + + private final Market market; + private final boolean withRemove; + private Vendor station; + + + private final Map sellUpdates = new HashMap<>(100, 0.9f); + private final Map buyUpdates = new HashMap<>(100, 0.9f); + + protected OffersHandler(Market market, boolean withRemove) { + this.market = market; + this.withRemove = withRemove; + } + + @Override + public void parse(String str) throws IOException { + if (str.isEmpty()) { + if (station != null){ + updateStation(); + } + return; + } + if (str.startsWith("#")) return; + if (str.startsWith("@")){ + if (station != null){ + updateStation(); + } + parseStation(str); + } else { + if (station == null){ + LOG.trace("Station not exists, skip"); + return; + } + parseLine(str); + } + } + + private void updateStation() { + LOG.trace("Update offers of station {}", station); + station.getAllBuyOffers().forEach(this::updateOffer); + station.getAllSellOffers().forEach(this::updateOffer); + buyUpdates.entrySet().forEach( entry -> addOffer(entry, OFFER_TYPE.BUY)); + sellUpdates.entrySet().forEach( entry -> addOffer(entry, OFFER_TYPE.SELL)); + buyUpdates.clear(); + sellUpdates.clear(); + station = null; + } + + private void updateOffer(Offer offer) { + Map offerDatas = offer.getType() == OFFER_TYPE.SELL ? sellUpdates : buyUpdates; + OfferData data = offerDatas.get(offer.getItem()); + if (data != null){ + if (data.price != offer.getPrice()) offer.setPrice(data.price); + if (data.count != null && data.count != offer.getCount()) offer.setCount(data.count); + data.isnew = false; + } else { + if (withRemove && offer.getItem().getGroup().isMarket()){ + station.remove(offer); + } + } + } + + private void addOffer(Map.Entry entry, OFFER_TYPE type){ + OfferData offer = entry.getValue(); + if (offer.isnew){ + station.addOffer(type, entry.getKey(), offer.price, offer.count != null ? offer.count : 0); + } + } + + private void parseStation(String str) { + StringBuilder sb = new StringBuilder(20); + String system = ""; + String name; + LOG.trace("Parse system line: {}", str); + for (int i = 1; i < str.length(); i++) { + char c = str.charAt(i); + + if (c == '#') break; + if (c == ' '){ + //trim + if (sb.length() == 0 || i == str.length()-1) continue; + char next = str.charAt(i+1); + if (next == ' ' || next == '/') continue; + } + if (c == '/'){ + system = sb.toString(); + sb = new StringBuilder(20); + } else { + sb.append(c); + } + } + + name = sb.toString(); + LOG.trace("system: {}, station: {}", system, name); + + Place sys = market.get(system); + if (sys != null){ + station = sys.get(name); + if (station == null){ + LOG.warn("Station {} not found", name); + } + } else { + LOG.warn("System {} not found", system); + } + } + + private final static String NAME_REGEXP = "(.+\\S)"; + private final static String BUY_SELL_REGEXP = "([\\d]+|[\\?-])"; + private final static String SUPPLY_DEMAND_REGEXP = "([\\d]+|[\\?-])([LMH\\?])?"; + private final static String DATE_REGEXP = "(\\d+(?:[- :]+\\d+)+)"; + private final static Pattern PRICE_REGEXP = Pattern.compile("\\s+" + NAME_REGEXP + "\\s+" + BUY_SELL_REGEXP + "\\s+" + BUY_SELL_REGEXP + "\\s+"+ SUPPLY_DEMAND_REGEXP + "\\s+"+ SUPPLY_DEMAND_REGEXP + "\\s+"+ DATE_REGEXP +"\\s*(:?#.+)?"); + + + private void parseLine(String str){ + Matcher matcher = PRICE_REGEXP.matcher(str); + if (matcher.find()){ + String name = matcher.group(1); + Double buy = getDoubleValue(matcher.group(2)); + Double sell = getDoubleValue(matcher.group(3)); + Long demand = getLongValue(matcher.group(4)); + Long supply = getLongValue(matcher.group(6)); + Item item = market.getItem(ItemConverter.getItemId(name)); + if (item != null){ + if (buy != null && buy > 0){ + buyUpdates.put(item, new OfferData(buy, demand)); + } + if (sell != null && sell > 0){ + sellUpdates.put(item, new OfferData(sell, supply)); + } + } else { + LOG.warn("Item {} not found", name); + } + + } else { + LOG.trace("Line is not prices: {}", str); + } + + } + + private Double getDoubleValue(String string){ + if ("?".equals(string)) return null; + if ("-".equals(string)) return 0.0; + return Double.valueOf(string); + } + + private Long getLongValue(String string){ + if ("?".equals(string)) return null; + if ("-".equals(string)) return 0L; + return Long.valueOf(string); + } + + private class OfferData { + private Double price; + private Long count; + private boolean isnew; + + private OfferData(Double price, Long count) { + this.price = price; + this.count = count; + isnew = true; + } + } +} diff --git a/utils/src/test/java/ru/trader/maddavo/PricesImportTest.java b/utils/src/test/java/ru/trader/maddavo/PricesImportTest.java new file mode 100644 index 0000000..11e6cd3 --- /dev/null +++ b/utils/src/test/java/ru/trader/maddavo/PricesImportTest.java @@ -0,0 +1,211 @@ +package ru.trader.maddavo; + +import org.junit.Assert; +import org.junit.Test; +import org.xml.sax.SAXException; +import ru.trader.core.*; +import ru.trader.store.simple.Store; + +import javax.xml.parsers.ParserConfigurationException; +import java.io.IOException; +import java.io.InputStream; + + +public class PricesImportTest extends Assert { + + private static final String Strings = + "# \n" + + "# Item Name Sell Cr Buy Cr Demand Stock Timestamp\n" + + "\n" + + "@ 51 AQUILAE/Thirsk Station\n" + + " + Chemicals\n" + + " Hydrogen Fuel 106 111 ? 995704M 2015-02-28 06:15:53 # EObded06f0_EliteOCR_0.5.2.3\n" + + " Mineral Oil 127 143 ? 415528M 2015-02-28 06:15:53 # EObded06f0_EliteOCR_0.5.2.3\n" + + " Pesticides 362 0 790443H - 2015-02-28 06:15:53 # EObded06f0_EliteOCR_0.5.2.3\n" + + " + Minerals\n" + + " Bauxite 58 70 ? 2587092M 2015-02-28 06:25:16 # EOa679fe7b_EliteOCR_0.5.3\n" + + " Bertrandite 2295 2345 ? 137390M 2015-02-28 06:25:16 # EOa679fe7b_EliteOCR_0.5.3\n" + + " Coltan 1225 1268 ? 217931M 2015-02-28 06:25:21 # EOa679fe7b_EliteOCR_0.5.3\n" + + " Gallite 1701 1738 ? 380162M 2015-02-28 06:25:21 # EOa679fe7b_EliteOCR_0.5.3\n" + + " Indite 2001 2044 ? 151604M 2015-02-28 06:25:21 # EOa679fe7b_EliteOCR_0.5.3\n" + + " Lepidolite 475 500 ? 447705M 2015-02-28 06:25:21 # EOa679fe7b_EliteOCR_0.5.3\n" + + " Rutile 234 252 ? 786446M 2015-02-28 06:25:21 # EOa679fe7b_EliteOCR_0.5.3\n" + + " Uraninite 669 694 ? 501917H 2015-02-28 06:25:21 # EOa679fe7b_EliteOCR_0.5.3\n" + + " + Technology\n" + + " Bioreducing Lichen 1236 0 2654132H - 2015-02-28 06:25:21 # EOa679fe7b_EliteOCR_0.5.3\n" + + " H.E. Suits 426 0 1727091H - 2015-02-28 06:25:21 # EOa679fe7b_EliteOCR_0.5.3\n" + + " + Waste\n" + + " Biowaste 15 20 ? 32001M 2015-02-28 06:25:21 # EOa679fe7b_EliteOCR_0.5.3\n" + + " + Weapons\n" + + " Non-Lethal Weapons 2191 0 1757H - 2015-02-28 06:25:21 # EOa679fe7b_EliteOCR_0.5.3\n" + + " Personal Weapons 4836 0 11393H - 2015-02-28 06:25:21 # EOa679fe7b_EliteOCR_0.5.3\n" + + " Reactive Armour 2467 0 4778H - 2015-02-28 06:25:21 # EOa679fe7b_EliteOCR_0.5.3\n" + + "@ BONDE/Aksyonov Platform\n" + + " + Chemicals\n" + + " Explosives 209 224 ? 2680? 2015-01-23 14:26:43\n" + + " Hydrogen Fuel 107 112 ? 3239? 2015-01-23 14:26:43\n" + + " Mineral Oil 195 0 4734? - 2015-01-23 14:26:43\n" + + " + Consumer Items\n" + + " Clothing 319 0 426? - 2015-01-23 14:26:43\n" + + " Consumer Technology 6871 0 32? - 2015-01-23 14:26:43\n" + + " Domestic Appliances 533 0 223? - 2015-01-23 14:26:43\n" + + " + Foods\n" + + " Animal Meat 1388 0 49? - 2015-01-23 14:26:43\n" + + " Coffee 1388 0 72? - 2015-01-23 14:26:43\n" + + " Fish 430 0 260? - 2015-01-23 14:26:43\n" + + " Food Cartridges 143 0 229? - 2015-01-23 14:26:43\n" + + " Fruit And Vegetables 319 0 261? - 2015-01-23 14:26:43\n" + + " Grain 210 0 712? - 2015-01-23 14:26:43\n" + + " Synthetic Meat 255 0 168? - 2015-01-23 14:26:43\n" + + " Tea 1570 0 54? - 2015-01-23 14:26:43\n" + + " + Industrial Materials\n" + + " Polymers 63 0 ? - 2015-01-23 14:26:46\n" + + " Semiconductors 767 788 ? 1441? 2015-01-23 14:26:46\n" + + " Superconductors 6485 6556 ? 2937? 2015-01-23 14:26:46\n" + + " + Legal Drugs\n" + + " Beer 177 0 675? - 2015-01-23 14:26:46\n" + + " Liquor 862 0 393? - 2015-01-23 14:26:46\n" + + " Tobacco 4758 0 131? - 2015-01-23 14:26:46\n" + + " Wine 255 0 211? - 2015-01-23 14:26:46\n" + + " + Machinery\n" + + " Microbial Furnaces 202 0 14989? - 2015-01-23 14:26:46\n" + + " Power Generators 533 0 1019? - 2015-01-23 14:26:46\n" + + " Water Purifiers 304 0 180? - 2015-01-23 14:26:46\n" + + " + Medicines\n" + + " Basic Medicines 319 200 175? 30L 2015-01-23 14:26:46\n" + + " Performance Enhancers 6871 0 75? - 2015-01-23 14:26:46\n" + + " Progenitor Cells 6871 0 9? - 2015-01-23 14:26:46\n" + + " + Metals\n" + + " Aluminium 236 252 ? 3626? 2015-01-23 14:26:46\n" + + " Beryllium 8010 8096 ? 250? 2015-01-23 14:26:48\n" + + " Cobalt 580 604 ? 1769? 2015-01-23 14:26:48\n" + + " Copper 373 389 ? 25340? 2015-01-23 14:26:48\n" + + " Gallium 4947 5001 ? 3603? 2015-01-23 14:26:48\n" + + " Gold 9289 9289 ? 306? 2015-01-23 14:26:48\n" + + " Lithium 1413 1448 ? 905? 2015-01-23 14:26:48\n" + + " Silver 4589 4640 ? 381? 2015-01-23 14:26:48\n" + + " Tantalum 3740 3783 ? 446? 2015-01-23 14:26:48\n" + + " Titanium 884 907 ? 13008? 2015-01-23 14:26:48\n" + + " Uranium 2467 2496 ? 599? 2015-01-23 14:26:48\n" + + " + Minerals\n" + + " Bauxite 268 0 26144? - 2015-01-23 14:26:48\n" + + " Bertrandite 2467 0 424? - 2015-01-23 14:26:48\n" + + " Coltan 1708 0 2466? - 2015-01-23 14:26:48\n" + + " Gallite 2127 0 8867? - 2015-01-23 14:26:48\n" + + " Indite 2634 0 12385? - 2015-01-23 14:26:48\n" + + " Lepidolite 622 0 1946? - 2015-01-23 14:26:48\n" + + " Rutile 493 0 31584? - 2015-01-23 14:26:50\n" + + " Uraninite 1177 0 25404? - 2015-01-23 14:26:50\n" + + " + Technology\n" + + " Advanced Catalysers 2809 0 2555? - 2015-01-23 14:26:50\n" + + " H.E. Suits 278 0 995? - 2015-01-23 14:26:50\n" + + " Resonating Separators 5807 0 219? - 2015-01-23 14:26:50\n" + + " + Textiles\n" + + " Synthetic Fabrics 90 118 ? 7438? 2015-01-23 14:26:50\n" + + " + Waste\n" + + " Biowaste 15 20 ? 422? 2015-01-23 14:26:50\n" + + " Chemical Waste 109 0 1761? - 2015-01-23 14:26:50\n" + + " Scrap 71 0 736? - 2015-01-23 14:26:50\n" + + " + Weapons\n" + + " Non-Lethal Weapons 1891 0 52? - 2015-01-23 14:26:50\n" + + " Reactive Armour 2145 0 39? - 2015-01-23 14:26:50\n" + + "\n" + + "@ BONITOU/Hughes-Fulford Terminal\n" + + " + Chemicals\n" + + " Hydrogen Fuel 126 0 132112L - 2014-12-30 17:33:30\n" + + " Mineral Oil 106 120 ? 12759M 2014-12-30 17:33:30\n" + + " + Consumer Items\n" + + " Clothing 466 0 879226H - 2014-12-30 17:33:30\n" + + " Consumer Technology 7041 0 77568M - 2014-12-30 17:33:30\n" + + " Domestic Appliances 713 0 276516M - 2014-12-30 17:33:30\n" + + " + Foods\n" + + " Algae 39 53 ? 124331M 2014-12-30 17:33:30\n" + + " Animal Meat 1143 1184 ? 373M 2014-12-30 17:33:30\n" + + " Coffee 1158 1199 ? 354M 2014-12-30 17:33:30\n" + + " Fish 328 347 ? 13231M 2014-12-30 17:33:30\n" + + " Fruit And Vegetables 221 239 ? 688M 2014-12-30 17:33:30\n" + + " Grain 121 136 ? 1140M 2014-12-30 17:33:30\n" + + " Tea 1308 1354 ? 352M 2014-12-30 17:33:30\n" + + ""; + + private static final String[] lines = Strings.split("\\n"); + + private Market createMarket(){ + InputStream is = getClass().getResourceAsStream("/test_world.xml"); + Market world; + try { + world = Store.loadFromFile(is); + } catch (ParserConfigurationException | SAXException | IOException e) { + throw new AssertionError(e); + } + return world; + } + + @Test + public void testImport() throws Exception { + Market market = createMarket(); + OffersHandler handler = new OffersHandler(market, true); + for (String line : lines) { + handler.parse(line); + } + + assertEquals(6, market.getVendors().size()); + + Vendor station = market.get("Bonde").get("Aksyonov Platform"); + assertNotNull(station); + Item item = market.getItem("fish"); + Offer offer = station.getBuy(item); + assertNotNull(offer); + assertEquals(430, offer.getPrice(), 0.0); + assertEquals(260, offer.getCount()); + offer = station.getSell(item); + assertNull(offer); + + item = market.getItem("syntheticfabrics"); + offer = station.getBuy(item); + assertNotNull(offer); + assertEquals(90, offer.getPrice(), 0.0); + assertEquals(0, offer.getCount()); + offer = station.getSell(item); + assertEquals(118, offer.getPrice(), 0.0); + assertEquals(7438, offer.getCount()); + + item = market.getItem("polymers"); + offer = station.getBuy(item); + assertNotNull(offer); + assertEquals(63, offer.getPrice(), 0.0); + assertEquals(0, offer.getCount()); + offer = station.getSell(item); + assertNull(offer); + + //check add + item = market.getItem("basicmedicines"); + offer = station.getBuy(item); + assertNotNull(offer); + assertEquals(319, offer.getPrice(), 0.0); + assertEquals(175, offer.getCount()); + offer = station.getSell(item); + assertEquals(200, offer.getPrice(), 0.0); + assertEquals(30, offer.getCount()); + + //Check remove + item = market.getItem("indium"); + offer = station.getBuy(item); + assertNull(offer); + offer = station.getSell(item); + assertNull(offer); + + //Check removed market items only + item = market.getItem("Power Plant C4"); + offer = station.getBuy(item); + assertNull(offer); + offer = station.getSell(item); + assertNotNull(offer); + + item = market.getItem("Sidewinder"); + offer = station.getBuy(item); + assertNull(offer); + offer = station.getSell(item); + assertNotNull(offer); + } +} diff --git a/utils/src/test/resources/test_world.xml b/utils/src/test/resources/test_world.xml new file mode 100644 index 0000000..908d362 --- /dev/null +++ b/utils/src/test/resources/test_world.xml @@ -0,0 +1,954 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file