From 363fc645ad95ae242bb6a77f9e6120baa481887a Mon Sep 17 00:00:00 2001
From: s1924442 <b.arendarczyk@sms.ed.ac.uk>
Date: Mon, 11 Oct 2021 20:55:24 +0100
Subject: [PATCH] Land cover areas serialized to XML for improved performance.

---
 debug_config.properties                       |  11 +-
 src/ac/ed/lurg/ModelConfig.java               |   1 +
 src/ac/ed/lurg/ModelMain.java                 |  20 +-
 src/ac/ed/lurg/carbon/CarbonFluxReader.java   |   4 +-
 src/ac/ed/lurg/forestry/WoodYieldReader.java  |   6 +-
 src/ac/ed/lurg/landuse/Intensity.java         |   4 +
 src/ac/ed/lurg/landuse/LandCoverTile.java     |   8 +
 src/ac/ed/lurg/landuse/LandUseItem.java       |  15 +-
 src/ac/ed/lurg/landuse/LandUseSerializer.java | 270 ++++++++++++++++++
 9 files changed, 326 insertions(+), 13 deletions(-)
 create mode 100644 src/ac/ed/lurg/landuse/LandUseSerializer.java

diff --git a/debug_config.properties b/debug_config.properties
index 8c6c1246..9c61e2bd 100644
--- a/debug_config.properties
+++ b/debug_config.properties
@@ -1,6 +1,7 @@
 BASE_DIR=C:/Users/Bart/git/plumv2
 
 YIELD_DIR=C:/Users/Bart/Documents/PhD/LURG/rcp60
+YIELD_DIR_TOP=rcp60
 
 OUTPUT_DIR = C:/Users/Bart/git/plumv2/output
 
@@ -9,18 +10,18 @@ WOOD_AND_CARBON_DIR = C:/Users/Bart/Documents/PhD/Carbon and wood yields/forestr
 BASE_YEAR=2010
 START_TIMESTEP=0
 TIMESTEP_SIZE=1
-END_TIMESTEP=0
+END_TIMESTEP=10
 
-IS_CALIBRATION_RUN=true
+IS_CALIBRATION_RUN=false
 END_FIRST_STAGE_CALIBRATION=0
 
 GENERATE_NEW_YIELD_CLUSTERS=false
 
 YIELD_FILENAME=yield.out
 
-DEBUG_LIMIT_COUNTRIES=false
-DEBUG_COUNTRY_NAME=United States of America
-GAMS_COUNTRY_TO_SAVE=United States of America
+DEBUG_LIMIT_COUNTRIES=true
+DEBUG_COUNTRY_NAME=United Kingdom
+GAMS_COUNTRY_TO_SAVE=United Kingdom
 
 INIT_WOOD_PRICE=0.4
 INIT_WOOD_STOCK=1000
diff --git a/src/ac/ed/lurg/ModelConfig.java b/src/ac/ed/lurg/ModelConfig.java
index da4518f5..17c3ee4d 100755
--- a/src/ac/ed/lurg/ModelConfig.java
+++ b/src/ac/ed/lurg/ModelConfig.java
@@ -276,6 +276,7 @@ public class ModelConfig {
 	public static final String CALIB_DIR = IS_CALIBRATION_RUN ? OUTPUT_DIR : getProperty("CALIB_DIR", OUTPUT_DIR);
 	public static final int END_FIRST_STAGE_CALIBRATION = getIntProperty("END_FIRST_STAGE_CALIBRATION", 10);
 	public static final String SERIALIZED_LAND_USE_FILE = CALIB_DIR + File.separator +  "landUseRaster.ser";
+	public static final String SERIALIZED_LAND_COVER_FILE = CALIB_DIR + File.separator +  "landCoverData.xml";
 	public static final String SERIALIZED_CROP_USAGE_FILE = CALIB_DIR + File.separator +  "countryCropUsages.ser";
 	public static final String SERIALIZED_INTERNATIONAL_MARKET_FILE = CALIB_DIR + File.separator +  "internationalMarket.ser";
 	public static final boolean MARKET_ADJ_PRICE = getBooleanProperty("MARKET_ADJ_PRICE", true);
diff --git a/src/ac/ed/lurg/ModelMain.java b/src/ac/ed/lurg/ModelMain.java
index 12c0b33a..6eca89f0 100644
--- a/src/ac/ed/lurg/ModelMain.java
+++ b/src/ac/ed/lurg/ModelMain.java
@@ -8,6 +8,7 @@ import java.io.IOException;
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.Map;
 import java.util.Map.Entry;
 import ac.ed.lurg.carbon.CarbonFluxRasterSet;
@@ -41,7 +42,9 @@ import ac.ed.lurg.landuse.IrrigiationCostReader;
 import ac.ed.lurg.landuse.LccKey;
 import ac.ed.lurg.landuse.LandCoverItem;
 import ac.ed.lurg.landuse.LandCoverReader;
+import ac.ed.lurg.landuse.LandCoverTile;
 import ac.ed.lurg.landuse.LandUseItem;
+import ac.ed.lurg.landuse.LandUseSerializer;
 import ac.ed.lurg.landuse.LandTileReader;
 import ac.ed.lurg.landuse.MaxCropAreaReader;
 import ac.ed.lurg.landuse.ProtectedAreasReader;
@@ -477,7 +480,7 @@ public class ModelMain {
 			}
 			outputWaterAvailablity(timestep, currentIrrigationData);  // uses the year directory structure created above
 		}
-
+		
 		if (ModelConfig.IS_CALIBRATION_RUN) {
 			if (timestep.isFinalTimestep()) {
 				serializeLandUse(landUseRaster);
@@ -614,17 +617,28 @@ public class ModelMain {
 	}
 
 	private void serializeLandUse(RasterSet<LandUseItem> landUseRaster) {
+		RasterSet<LandUseItem> rasterToSerialise = new RasterSet<LandUseItem>(desiredProjection);
+		for (Map.Entry<RasterKey, LandUseItem> entry : landUseRaster.entrySet()) {
+			LandUseItem newLuItem = new LandUseItem(entry.getValue());
+			newLuItem.overwriteLandCoverAreas(new HashMap<LandCoverType, LandCoverTile>()); // Clear data
+			rasterToSerialise.put(entry.getKey(), newLuItem);
+		}
 		try {
 			LogWriter.println("Starting serializing LandUse to " + ModelConfig.SERIALIZED_LAND_USE_FILE);
 			FileOutputStream fileOut = new FileOutputStream(ModelConfig.SERIALIZED_LAND_USE_FILE);
 			ObjectOutputStream out = new ObjectOutputStream(fileOut);
-			out.writeObject(landUseRaster);
+			out.writeObject(rasterToSerialise);
 			out.close();
 			fileOut.close();
 			LogWriter.println("Serialized data is saved");
 		} catch (IOException i) {
 			i.printStackTrace();
 		}
+
+		LogWriter.println("Starting serializing LandCover to " + ModelConfig.SERIALIZED_LAND_COVER_FILE);
+		LandUseSerializer luSer = new LandUseSerializer();
+		luSer.serialiseLandUse(globalLandUseRaster);
+		LogWriter.println("LandCover data is saved");
 	}
 
 	@SuppressWarnings("unchecked")
@@ -637,6 +651,8 @@ public class ModelMain {
 			in.close();
 			fileIn.close();
 			LogWriter.println("Deserialized " + ModelConfig.SERIALIZED_LAND_USE_FILE);
+			LandUseSerializer luSer = new LandUseSerializer();
+			luSer.deserializeLandUse(initLU);
 			return initLU;
 		} catch (IOException i) {
 			LogWriter.printlnError("Problem deserializing " + ModelConfig.SERIALIZED_LAND_USE_FILE);
diff --git a/src/ac/ed/lurg/carbon/CarbonFluxReader.java b/src/ac/ed/lurg/carbon/CarbonFluxReader.java
index 79f40500..3c4d8c7e 100644
--- a/src/ac/ed/lurg/carbon/CarbonFluxReader.java
+++ b/src/ac/ed/lurg/carbon/CarbonFluxReader.java
@@ -68,7 +68,7 @@ public class CarbonFluxReader {
 			protected void setData(RasterKey key, CarbonFluxItem item, Map<String, Double> rowValues) {
 
 				Double[] fluxes = getArrayFromRowValues(rowValues);
-				item.calcConversionCarbonFlux(lccKey, fluxes, landUseRaster.get(key).getLandUseTiles(), timestep);
+				item.calcConversionCarbonFlux(lccKey, fluxes, landUseRaster.get(key).getLandCoverTiles(), timestep);
 			}			
 		};
 	
@@ -81,7 +81,7 @@ public class CarbonFluxReader {
 			protected void setData(RasterKey key, CarbonFluxItem item, Map<String, Double> rowValues) {
 
 				Double[] fluxes = getArrayFromRowValues(rowValues);
-				item.calcEcosystemCarbonFlux(lcType, fluxes, landUseRaster.get(key).getLandUseTiles(), timestep);
+				item.calcEcosystemCarbonFlux(lcType, fluxes, landUseRaster.get(key).getLandCoverTiles(), timestep);
 			}			
 		};
 	
diff --git a/src/ac/ed/lurg/forestry/WoodYieldReader.java b/src/ac/ed/lurg/forestry/WoodYieldReader.java
index ab2c998c..467b23f6 100644
--- a/src/ac/ed/lurg/forestry/WoodYieldReader.java
+++ b/src/ac/ed/lurg/forestry/WoodYieldReader.java
@@ -50,8 +50,8 @@ public class WoodYieldReader {
 					}	
 				}
 				
-				item.calcYieldData(yieldMap, landUseRaster.get(key).getLandUseTiles(), timestep);
-				item.calcRotationData(yieldMap, landUseRaster.get(key).getLandUseTiles(), timestep, woodPrice);
+				item.calcYieldData(yieldMap, landUseRaster.get(key).getLandCoverTiles(), timestep);
+				item.calcRotationData(yieldMap, landUseRaster.get(key).getLandCoverTiles(), timestep, woodPrice);
 			}			
 		};
 	
@@ -78,7 +78,7 @@ public class WoodYieldReader {
 					}	
 				}
 				
-				item.calcYieldData(yieldMap, landUseRaster.get(key).getLandUseTiles(), timestep);
+				item.calcYieldData(yieldMap, landUseRaster.get(key).getLandCoverTiles(), timestep);
 			}			
 		};
 		
diff --git a/src/ac/ed/lurg/landuse/Intensity.java b/src/ac/ed/lurg/landuse/Intensity.java
index f694ef0c..fecb2b2c 100644
--- a/src/ac/ed/lurg/landuse/Intensity.java
+++ b/src/ac/ed/lurg/landuse/Intensity.java
@@ -58,6 +58,10 @@ public class Intensity implements Serializable {
 		return maxIrrigRate * irrigationIntensity;
 	}
 
+	public double getMaxIrrigRate() {
+		return maxIrrigRate;
+	}
+
 	public Intensity(Intensity from, Intensity to, double factor) {
 		this(
 			Interpolator.interpolate(from.fertiliserIntensity, to.fertiliserIntensity, factor), 
diff --git a/src/ac/ed/lurg/landuse/LandCoverTile.java b/src/ac/ed/lurg/landuse/LandCoverTile.java
index 659f11b7..73c2b0f5 100644
--- a/src/ac/ed/lurg/landuse/LandCoverTile.java
+++ b/src/ac/ed/lurg/landuse/LandCoverTile.java
@@ -57,6 +57,10 @@ public class LandCoverTile implements Serializable, InterpolatingRasterItem<Land
 		}
 	}
 	
+	public double getProtectedArea(int age) {
+		return protectedAreas[age];
+	}
+	
 	public void setProtectedArea(int age, double area) {
 		protectedAreas[age] = area;
 	}
@@ -89,6 +93,10 @@ public class LandCoverTile implements Serializable, InterpolatingRasterItem<Land
 		}
 	}
 	
+	public double getUnavailableArea() {
+		return unavailableArea;
+	}
+	
 	public void addUnavailableArea(double d) {
 		unavailableArea += d;
 	}
diff --git a/src/ac/ed/lurg/landuse/LandUseItem.java b/src/ac/ed/lurg/landuse/LandUseItem.java
index 003ca8ef..58c0f3f2 100644
--- a/src/ac/ed/lurg/landuse/LandUseItem.java
+++ b/src/ac/ed/lurg/landuse/LandUseItem.java
@@ -46,6 +46,11 @@ public class LandUseItem implements InterpolatingRasterItem<LandUseItem>, Serial
 		protectedFraction = (luItemToCopy.protectedFraction);
 	}
 
+	public void overwriteLandCoverAreas(Map<LandCoverType, LandCoverTile> landCoverAreas) {
+		this.landCoverAreas = new HashMap<LandCoverType, LandCoverTile>();
+		this.landCoverAreas.putAll(landCoverAreas);
+	}
+
 	public void addLandCoverTiles(LandCoverItem landCover) {		
 		double protectableFraction = 0;
 		for (LandCoverType lcType : LandCoverType.getProtectibleTypes()) {
@@ -184,7 +189,7 @@ public class LandUseItem implements InterpolatingRasterItem<LandUseItem>, Serial
 		}
 	}
 	
-	public Map<LandCoverType, LandCoverTile> getLandUseTiles() {
+	public Map<LandCoverType, LandCoverTile> getLandCoverTiles() {
 		return landCoverAreas;
 	}
 	
@@ -311,6 +316,10 @@ public class LandUseItem implements InterpolatingRasterItem<LandUseItem>, Serial
 			protectedFraction = 1.0;		
 	}
 	
+	public Map<CropType, Intensity> getIntensityMap() {
+		return intensityMap;
+	}
+	
 	public Intensity getIntensity(CropType crop) {
 		return intensityMap.get(crop);
 	}
@@ -430,6 +439,10 @@ public class LandUseItem implements InterpolatingRasterItem<LandUseItem>, Serial
 		}
 		return totalFract;
 	}
+	
+	public Map<CropType, Double> getCropFractionMap() {
+		return cropFractions;
+	}
 
 	public void setCropFraction(CropType c, double areaFract) {
 		cropFractions.put(c, areaFract);
diff --git a/src/ac/ed/lurg/landuse/LandUseSerializer.java b/src/ac/ed/lurg/landuse/LandUseSerializer.java
new file mode 100644
index 00000000..3b9cdb4e
--- /dev/null
+++ b/src/ac/ed/lurg/landuse/LandUseSerializer.java
@@ -0,0 +1,270 @@
+package ac.ed.lurg.landuse;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.UnsupportedEncodingException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.xml.stream.FactoryConfigurationError;
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+import ac.ed.lurg.ModelConfig;
+import ac.ed.lurg.types.CropType;
+import ac.ed.lurg.types.LandCoverType;
+import ac.ed.lurg.utils.LogWriter;
+import ac.sac.raster.RasterKey;
+import ac.sac.raster.RasterSet;
+
+public class LandUseSerializer {
+	
+	XMLStreamWriter xmlWriter;
+	
+	public void LandUseSerialiser() {}
+
+	public void serialiseLandUse(RasterSet<LandUseItem> luRaster) {
+		
+		try {
+
+		OutputStream outStream = new FileOutputStream(new File(ModelConfig.SERIALIZED_LAND_COVER_FILE));
+		
+		xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(
+				new OutputStreamWriter(outStream, "utf-8"));
+		
+		xmlWriter.writeStartDocument("UTF-8", "1.0");
+		xmlWriter.writeStartElement("LandUseItems");
+
+		int counter = 1;
+		for (Map.Entry<RasterKey, LandUseItem> entry : luRaster.entrySet()) {
+			RasterKey key = entry.getKey();
+			LandUseItem luItem = entry.getValue();
+			
+			xmlWriter.writeStartElement("LandUseItem");
+			xmlWriter.writeAttribute("id", Integer.toString(counter));
+			writeElement("col", key.getCol());
+			writeElement("row", key.getRow());
+			
+			//writeIntensityData(luItem.getIntensityMap());
+			//writeCropData(luItem.getCropFractionMap());
+			writeLandCoverData(luItem.getLandCoverTiles());
+			//writeElement("protectedFraction", luItem.getProtectedFraction());
+			
+			xmlWriter.writeEndElement(); // LandUseItem
+			xmlWriter.flush();
+			counter++;
+			//if (counter > 1) break;
+		}
+
+		xmlWriter.writeEndElement(); // LandUseItems
+		xmlWriter.writeEndDocument();
+		
+		xmlWriter.flush();
+		xmlWriter.close();
+		
+		} catch (FileNotFoundException e) {
+			LogWriter.print(e);
+		} catch (XMLStreamException e) {
+			LogWriter.print(e);
+		} catch (UnsupportedEncodingException e) {
+			LogWriter.print(e);
+		} catch (FactoryConfigurationError e) {
+			LogWriter.print(e.getException());
+		}
+	}
+	
+	private void writeIntensityData(Map<CropType, Intensity> intensityMap) {
+		try {
+			xmlWriter.writeStartElement("intensity");
+			for (Map.Entry<CropType, Intensity> entry : intensityMap.entrySet()) {
+				CropType crop = entry.getKey();
+				Intensity inten = entry.getValue();
+				if (inten == null) {
+					continue;
+				}
+				xmlWriter.writeStartElement(crop.getGamsName());
+				writeElement("fertiliserIntensity", inten.getFertiliserIntensity());
+				writeElement("irrigationIntensity", inten.getIrrigationIntensity());
+				writeElement("otherIntensity", inten.getOtherIntensity());
+				writeElement("yield", inten.getYield());
+				writeElement("unitEnergy", inten.getUnitEnergy());
+				writeElement("maxIrrigRate", inten.getMaxIrrigRate());
+				xmlWriter.writeEndElement();
+			}
+			xmlWriter.writeEndElement();
+
+
+		} catch (XMLStreamException e) {
+			LogWriter.print(e);
+		} 
+
+	}
+	
+	private void writeCropData(Map<CropType, Double> cropFractMap) {
+		try {
+			xmlWriter.writeStartElement("cropFractions");
+			for (Map.Entry<CropType, Double> entry : cropFractMap.entrySet()) {
+				CropType crop = entry.getKey();
+				Double fract = entry.getValue();
+				if (fract == null) {
+					continue;
+				}
+				writeElement(crop.getGamsName(), fract.doubleValue());
+
+			}
+			xmlWriter.writeEndElement();
+
+		} catch (XMLStreamException e) {
+			LogWriter.print(e);
+		} 
+
+	}
+	
+	private void writeLandCoverData(Map<LandCoverType, LandCoverTile> lcData) {
+		try {
+			xmlWriter.writeStartElement("landCover");
+			for (Map.Entry<LandCoverType, LandCoverTile> entry : lcData.entrySet()) {
+				LandCoverType lcType = entry.getKey();
+				LandCoverTile tiles = entry.getValue();
+				if (tiles == null) {
+					continue;
+				}
+				xmlWriter.writeStartElement(lcType.getName());
+				
+				xmlWriter.writeStartElement("convertible");
+				for (int a = 0; a <= LandCoverTile.getMaxAgeBin(); a++) {
+					double area = tiles.getConvertibleArea(a);
+					if (area == 0) 
+						continue;
+					writeElement("X"+Integer.toString(a), area);
+				}
+				xmlWriter.writeEndElement();
+				
+				xmlWriter.writeStartElement("protected");
+				for (int a = 0; a <= LandCoverTile.getMaxAgeBin(); a++) {
+					double area = tiles.getProtectedArea(a);
+					if (area == 0) 
+						continue;
+					writeElement("X"+Integer.toString(a), area);
+				}
+				xmlWriter.writeEndElement();
+				
+				writeElement("unavailable", tiles.getUnavailableArea());
+				
+				xmlWriter.writeEndElement();
+			}
+			xmlWriter.writeEndElement();
+
+
+		} catch (XMLStreamException e) {
+			LogWriter.print(e);
+		} 
+
+	}
+
+	private void writeElement(String element, double value) {
+		try {
+			xmlWriter.writeStartElement(element);
+			xmlWriter.writeCharacters(Double.toString(value));
+			xmlWriter.writeEndElement();
+		} catch (XMLStreamException e) {
+			LogWriter.print(e);
+		}
+	}
+	
+	private void writeElement(String element, int value) {
+		try {
+			xmlWriter.writeStartElement(element);
+			xmlWriter.writeCharacters(Integer.toString(value));
+			xmlWriter.writeEndElement();
+		} catch (XMLStreamException e) {
+			LogWriter.print(e);
+		}
+	}
+	
+	public void deserializeLandUse(RasterSet<LandUseItem> luRaster) {
+		DocumentBuilderFactory fact = DocumentBuilderFactory.newInstance();
+		DocumentBuilder builder;
+
+		Document doc;
+		try {
+			builder = fact.newDocumentBuilder();
+			doc = builder.parse(new File(ModelConfig.SERIALIZED_LAND_COVER_FILE));
+			doc.getDocumentElement().normalize();
+			
+			Element root = doc.getDocumentElement();
+			NodeList nodeLParent = doc.getElementsByTagName("LandUseItems");
+			NodeList nodeL = nodeLParent.item(0).getChildNodes();
+			
+			for (int i = 0; i < nodeL.getLength(); i++) {
+				Node node = nodeL.item(i);
+				Map<LandCoverType, LandCoverTile> landCoverAreas = new HashMap<LandCoverType, LandCoverTile>();
+				for (LandCoverType lcType: LandCoverType.values()) {
+					landCoverAreas.put(lcType, new LandCoverTile());
+				}
+				if (node.getNodeType() == Node.ELEMENT_NODE) {
+					Element itemElement = (Element) node;
+					int col = Integer.parseInt(itemElement.getElementsByTagName("col").item(0).getTextContent()); // TODO store as attribute
+					int row = Integer.parseInt(itemElement.getElementsByTagName("row").item(0).getTextContent());
+					
+					NodeList lcNodeListParent = itemElement.getElementsByTagName("landCover");
+					NodeList lcNodeList = lcNodeListParent.item(0).getChildNodes();
+					Element lcNodeElement = (Element) lcNodeList;
+					
+					
+					for (int j = 0; j < lcNodeList.getLength(); j++) {
+						Node lcNode = lcNodeList.item(j);
+						
+						if (node.getNodeType() == Node.ELEMENT_NODE) {
+							LandCoverType lcType = LandCoverType.getForName(lcNode.getNodeName());
+							Element lcElement = (Element) lcNode;
+							NodeList convElements = lcElement.getElementsByTagName("convertible").item(0).getChildNodes();
+							NodeList protElements = lcElement.getElementsByTagName("protected").item(0).getChildNodes();
+							double unavArea = Double.parseDouble(lcElement.getElementsByTagName("unavailable").item(0).getTextContent());
+							landCoverAreas.get(lcType).addUnavailableArea(unavArea);
+							
+							for (int k = 0; k < convElements.getLength(); k++) {
+								Node areaNode = convElements.item(k);
+								int age = Integer.parseInt(areaNode.getNodeName().replace("X", ""));
+								//double area = Double.parseDouble(areaNode.getNodeValue());
+								double area = Double.parseDouble(areaNode.getChildNodes().item(0).getNodeValue());
+								landCoverAreas.get(lcType).setConvertibleArea(age, area);
+							}
+							
+							for (int k = 0; k < protElements.getLength(); k++) {
+								Node areaNode = protElements.item(k);
+								int age = Integer.parseInt(areaNode.getNodeName().replace("X", ""));
+								//double area = Double.parseDouble(areaNode.getNodeValue());
+								double area = Double.parseDouble(areaNode.getChildNodes().item(0).getNodeValue());
+								landCoverAreas.get(lcType).setProtectedArea(age, area);
+							}
+						}
+					}	
+					luRaster.get(new RasterKey(col, row)).overwriteLandCoverAreas(landCoverAreas);
+				}
+				
+			}
+			
+		} catch (SAXException | IOException | ParserConfigurationException e) {
+			// TODO Auto-generated catch block
+			LogWriter.print(e);;
+		}
+
+	}
+}
-- 
GitLab