/*
 * CollectData.java
 *
 * Created on April 10, 2008, 7:18 PM
 */
import java.net.*;
import java.io.*;
import java.util.*;

public class CollectData {
    private URL  reAddress;
    private URLConnection connection;
    private StringBuffer dataBuffer;
    
    StringBuffer buffer;
    
    java.text.SimpleDateFormat formatter = new java.text.SimpleDateFormat ("dd-MMM-yyyy");									
        
    boolean abortPostcode = false;
    
    // Stores all instances of ReData (see below).
    // Key = (String)("" + postcode).
    // Value = ReData object with data for that postcode.
    HashMap dataMap = new HashMap();
    
    int[] postCodeIndex =
    {
        2007, // BROADWAY : ULTIMO ...
        2008, // CHIPPENDALE : DARLINGTON ...
        2009, // PYRMONT : DARLING ISLAND ...
        2015, // ALEXANDRIA : BEACONSFIELD ...
        2055, // NORTH SYDNEY ...
        2105, // CHURCH POINT : ELVINA BAY ...
        2312, // ARCADIA VALE : AWABA ...
        
    };
    
    // Used for when 200+ results come back.  Refines searches to
    // specific price ranges.  If you still get too many results
    // for some post codes just reduce the difference in the price
    // indices.  Eg, instead of 500000 -> 750000, try 500000 -> 600000.
    int[] priceIndex =
    {
        150000,
        250000,
        400000,
        500000,
        750000,
        100000000
                
    };
    
    // Internal class for storing data for each post code.
    // We put post code specific data into one of these and
    // collect all instances in the dataMap(a hashtable).
    class ReData {
        int postcode;
        int count;
        int priceCount;
        LinkedList priceList;
        String suburbNames;
        
        public ReData(int postcode) {
            this.postcode = postcode;
            this.count = 0;
            
            this.priceList = new LinkedList();
            this.priceCount = 0;
        }
        
        public void addCount(int results) {
            count += results;
        }
        
        public void setCount(int value) {
            count = value;
            
            if (count == 0) // Reset all prices
                priceList.clear();
        }
        
        public int getPostCode() {
            return postcode;
        }
        
        public int getCount() {
            return count;
        }
        
        public String toString() {
            return "[" + postcode +"]: " + count + " results found.";
        }
        
        public float getMedianPrice() {
            Object[] priceArray = priceList.toArray();
            Arrays.sort(priceArray);
            
            int middle = priceArray.length/2;
            
            if (priceArray.length == 0)
            {
                return 0.0f;
            }

            if (priceArray.length == 1) {
                Float medianFloat = (Float)priceArray[0];
                return medianFloat.floatValue();
            }
            
            if (priceArray.length%2 == 1) {
                Float medianFloat = (Float)priceArray[middle];
                return medianFloat.floatValue();
            }
            
            Float f1 = (Float)priceArray[middle -1];
            Float f2 = (Float)priceArray[middle];
            
            return (f1.floatValue() + f2.floatValue())/2.0f;
        }
        
        public float getAveragePrice() {
            return getTotalPrice()/priceList.size();
        }
        
        public float getTotalPrice() {
            float total = 0.0f;
            
            for (Iterator iter = priceList.iterator(); iter.hasNext(); ) {
                Float priceFloat = (Float)iter.next();
                
                total += priceFloat.floatValue();
            }
            
            return total;
        }
        
        public int getPriceListSize() {
            return priceList.size();
        }
        
        public LinkedList getPriceList() {
            return priceList;
        }
        
        public void addPrice(float value) {
            priceList.add(new Float(value));
        }
        
        public void setSuburbNames(String value) { suburbNames = new String(value); }
        public String getSuburbNames() { return suburbNames; }
    };
    
    public CollectData() {
        dataBuffer = new StringBuffer(8196);
    }
    
    public void downloadData() throws java.io.IOException {
        
        dataMap.clear();
        buffer = new StringBuffer();
        
        for (int i = 0; i < postCodeIndex.length; i++) 
        {
            int postcode = postCodeIndex[i];
            abortPostcode = false;
                
            // First try basic query.
            String params = "id=" + postcode + "&a=qfp&cu=fn-rea&t=res&p=200";

            boolean usePriceFilter = false;
            System.out.println("\n===> Getting data for postcode: " + postcode + "...<br>");

            if (!downloadData_(postcode , params)) {
                if (abortPostcode)
                    continue;

                // Too many results.  Try filtering postcode with number of beds.
                
                for (int beds = 1; beds < 6; beds++) {
                    params = "id=" + postcode + "&a=qfp&cu=fn-rea&t=res&minbed=" + beds + "&maxbed=" + beds + "&p=200";

                    System.out.println("Filtering on " + beds + " beds...");
                    
                    if (!downloadData_(postcode , params)) {
                        // Too many results again.  Clean out and try with Pricing refinements
                        usePriceFilter = true;

                        ReData reData = (ReData)dataMap.get("" + postcode);

                        if (reData != null) {
                            reData.setCount(0);
                            break;
                        }
                    }

                    if (usePriceFilter)
                        break;
                }

                if (usePriceFilter) {
                    for (int beds = 1; beds < 6; beds ++) {
                        for (int price = 0; price < (priceIndex.length - 1); price++) {
                            int pm = priceIndex[price];
                            int px = priceIndex[price + 1];
                            params = "id=" + postcode  + "&a=qfp&cu=fn-rea&t=res&minbed=" + beds + "&maxbed=" + beds + "&pm=" + pm + "&px=" + px + "&pme=" + pm + "&pxe=" + px + "&p=200";

                            System.out.println("Filtering on " + beds + " beds, price $" + pm + " - $" + px + "...");
                            
                            if (!downloadData_(postcode, params)) {
                                // Remove data for this postcode if still can't get valid info.
                                ReData reData = (ReData)dataMap.get("" + postcode);
                                if (reData != null)
                                    dataMap.remove("" + postcode);

                                usePriceFilter = false;
                            }

                            if (!usePriceFilter)
                                break;
                        }
                        if (!usePriceFilter)
                            break;
                    }
                }
                 
            }                 
        }
    }
    
    // Applies a query and looks for various bits of info from the returned HTML.
    // Very time consuming way of ripping data from a source but without direct
    // access to the SQL backend we don't have many other options.
    private boolean downloadData_(int postcode, String params) throws java.io.IOException {
        String data;
        
        // Need to reopen a new URL connection for each query being made.
        // Can't just recycle existing connection as each HTTP request is discrete.
        try {
            try {
                System.out.println("Opening www.realestate.com.au...");
                
                reAddress = new URL("http://www.realestate.com.au");
                connection = reAddress.openConnection();
                connection.setDoInput(true);
                connection.setDoOutput(true);
                connection.setUseCaches(false);
                connection.setDefaultUseCaches(false);
                connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
                connection.setRequestProperty("Content-Length", String.valueOf(params.length()));
            } catch(java.net.MalformedURLException mfe) {
                System.out.print(mfe.getMessage());
                return false;
            } catch(java.io.IOException ioe) {
                System.out.print(ioe.getMessage());
                return false;
            }
            
            System.out.println("Sending request: " + params + "...");
            
            // Send the request parameters.
            OutputStream os = connection.getOutputStream();
            OutputStreamWriter osw = new OutputStreamWriter(os);
            osw.write(params);
            osw.flush();
            osw.close();
            
            // Collect the response (which will be the HTML text).
            InputStream is = connection.getInputStream();
            InputStreamReader isr = new InputStreamReader(is);
            BufferedReader br = new BufferedReader(isr);
            
            dataBuffer = new StringBuffer();
            
            System.out.println("Parsing for data...");
            
            while ((data = br.readLine()) != null) {
                dataBuffer.append(data);
            }
            br.close();
            isr.close();
            
            String rawHTML = dataBuffer.toString();
            
            if (rawHTML.indexOf("The postcode is not valid") != -1)
            {
                System.out.println("\n*** Invalid postcode: " + postcode + "\n");
                abortPostcode = true;
                return false;
            }
            
            if (rawHTML.indexOf("No exact matches found") != -1)
            {
                ReData reData = (ReData)dataMap.get("" + postcode);

                if (reData == null) {
                    reData = new ReData(postcode);
                    dataMap.put("" + postcode, reData);
                }
                
                reData.setCount(0);
                abortPostcode = true;
                return false;
            }
            
            // [ STEP 1 ]
            // Look for # of listings.  It's located at the following string in the HTML:
            //       <li class="resultsCount">Your search returned <strong>56</strong>
            
            // If too many results (>200) we have to refine the query to
            // narrow down results.  Skip these results and return false.
            // The main downloadData() method will catch this and attempt
            // to apply more filtering.
            if (rawHTML.indexOf("Your search returned <strong>200+</strong>") != -1) {
                System.out.println("Postcode " + postcode + " has too many results (200+), refining search...");
                return false;
            }
            
            // Otherwise strip out the number of results returned for this
            // query.
            int index = rawHTML.indexOf("Your search returned");
            
            if (index < 0)
            {
                System.out.println("\n*** No header found for postcode: " + postcode + "\n");
                return false;
            }
            
            rawHTML= rawHTML.substring(index);
            
            rawHTML= rawHTML.substring(rawHTML.indexOf('>') + 1);
            String rawResultHTML = rawHTML.substring(0, rawHTML.indexOf('<'));
            
            int results = Integer.parseInt(rawResultHTML);
            
            ReData reData = (ReData)dataMap.get("" + postcode);
            
            if (reData == null) {
                reData = new ReData(postcode);
                dataMap.put("" + postcode, reData);
            }
            
            // Add results to the data for this postcode.
            reData.addCount(results);
            
            // [ STEP 2 ]
            // Get suburb names.
            int sIndex = rawHTML.indexOf("name=\"u\"");
            
            if (sIndex > 0) {
                rawHTML = rawHTML.substring(sIndex);
                rawHTML = rawHTML.substring(rawHTML.indexOf("value="));
                rawHTML = rawHTML.substring(rawHTML.indexOf('='));
                rawHTML = rawHTML.substring(rawHTML.indexOf('\"') + 1);
                
                String suburbString = rawHTML.substring(0, rawHTML.indexOf('\"'));

                System.out.println("Grabbing data for postcode " + postcode + " (" + suburbString + ")");
                
                reData.setSuburbNames(suburbString);
            }
            
            // [ STEP 3 ]
            // Looks for price data.
            // It's located within each propOverview div as follows:
                          /*
                                <div class="propOverview featureProperty">
                                        <div class="header">
                                        <h2>SUBURB NAME</h2>
                                        <h3>Low-Mid $300,000's</h3>
                           */
            
            String priceHTML = rawHTML;
                        
            System.out.println("Extracting prices...\n");
                        
            for ( ; ; ) {
                int index1 = priceHTML.indexOf("propOverview");

                if (index1 == -1)
                    break;
                
                priceHTML = priceHTML.substring(index1);
                                
                if (index1 < 0)
                    break;
                // Prices located between the <h3> tag.
                int h3StartIndex = priceHTML.indexOf("<h3>");
                int h3EndIndex = priceHTML.indexOf("</h3>");
                
                if (h3StartIndex < 0 ||h3EndIndex < 0)
                    break;
                
                // Get this specific price info within the <h3></h3> tags.
                String sectionHTML = priceHTML.substring(0, h3EndIndex);
                
                priceHTML = priceHTML.substring(h3EndIndex);
                
                int dollarIndex = sectionHTML.indexOf('$');
                
                if (dollarIndex == -1)
                    continue;
                
                // Remove any commas in the numbers
                String dollarHTML = sectionHTML.substring(dollarIndex + 1);
                dollarHTML = dollarHTML.replace(",", "");
                                
                // Fudge pricing directives.
                boolean isRange = false;
                boolean isLow = false;
                boolean isMid = false;
                boolean isHigh = false;
                boolean isAbove = false;
                
                // System.out.println("dollarHTML = " + dollarHTML);
                
                if (sectionHTML.indexOf("-") >= 0) // eg, $465000 - $485000.
                {
                    isRange = true;                    
                }
                else if (sectionHTML.indexOf("Above") >= 0  // eg, Offers Above $320,000
                        || sectionHTML.indexOf("Excess") >= 0       // eg, In excess of $320000
                        || sectionHTML.indexOf("From") >= 0       // eg, From $600k
                        || sectionHTML.indexOf("Over") >= 0       // eg, Neg. Over $320000
                        || sectionHTML.indexOf("+") >= 0)           // eg, $320000+
                    isAbove = true;
                else {
                    if (sectionHTML.indexOf("Low") >= 0) // eg, Neg. Low 400s
                        isLow = true;                        
                    if (sectionHTML.indexOf("Mid") >= 0) // eg, Mid 400s
                        isMid = true;
                    if (sectionHTML.indexOf("High") >= 0) // eg, High 400s
                        isHigh = true;
                }
                                
                StringBuffer valueBuffer = new StringBuffer();
                
                for (int d = 0; d < dollarHTML.length(); d++) {
                    char c = dollarHTML.charAt(d);
                    if (!Character.isDigit( c ))
                        break;
                    
                    valueBuffer.append(c);
                }
                
                if (valueBuffer.toString().length() < 3)
                    continue;
                
                if (isRange) {
                    int rangeIndex = dollarHTML.indexOf('-');
                    dollarIndex = dollarHTML.lastIndexOf('$');
                    
                    if (rangeIndex >= 0 && dollarIndex >= 0) {
                        dollarHTML = dollarHTML.substring(dollarIndex + 1);
                        
                        StringBuffer valueBuffer2 = new StringBuffer();
                        
                        try {
                            for (int d = 0; d < dollarHTML.length(); d++) {
                                char c = dollarHTML.charAt(d);
                                if (!Character.isDigit( c ))
                                    break;
                                
                                valueBuffer2.append(c);
                            }
                            
                            float value1 = Float.parseFloat(valueBuffer.toString());
                            float value2 = Float.parseFloat(valueBuffer2.toString());
                            
                            if (value1 < 1000) // eg, Low 400s --> 400000
                                value1 *= 1000;
                            
                            if (value2 < 1000)
                                value2 *= 1000;
                            
                            if (value1 > value2)
                                continue;
                            
                            float midrange = (value1 + value2)/2;
                            valueBuffer = new StringBuffer("" + midrange);
                        } catch (NumberFormatException nfe) {
                        }
                    }
                }
                
                try {
                    float value = Float.parseFloat(valueBuffer.toString());
                    
                    // Fudge the price based on the directives.
                    // Offsets I've chosen are just guesses on
                    // the sort of pricing people would accept when
                    // stating things like "Above $X" or "Mid $X".
                    if (isAbove)
                        value *= 1.04; // 4% premium
                    else if (!isRange) {
                        if (isLow && isMid) {
                            value += 35000;
                        } else if (isMid && isHigh) {
                            value += 75000;
                        } else if (isHigh) {
                            value += 87500;
                        } else if (isMid) {
                            value += 55000;
                        } else if (isLow) {
                            value += 25000;
                        }
                    }
                    
                    // Add this price to the postcode's information.                
                    // System.out.println("VALUE = " + value);
                    reData.addPrice( value );
                } catch (NumberFormatException nfe) {
                }
                
                // Move the main priceHTML forward for the next iteration.
                if (h3StartIndex > 3000) // Seems to have problem with very large index jumps.
                        break;
                rawHTML = priceHTML.substring(h3StartIndex + 1);
            }
        } catch (java.io.IOException ioe) {
        }
        
        return true;
    }
    
    public StringBuffer generateHTMLReport() {
        long timeMillis = System.currentTimeMillis();
        java.util.Calendar calendar = java.util.Calendar.getInstance();
        calendar.setTimeInMillis(timeMillis);
        
        if (buffer == null)
            buffer = new StringBuffer();
        
        java.util.Date currentDate = calendar.getTime();
        String heading = "Realesate.com.au report for " + formatter.format(currentDate);
        
        buffer.append("<html><title>" + heading + "</title>");
        buffer.append("<p><b>" + heading + "</b></p>");
        
        if (dataMap.isEmpty()) {
            buffer.append("<b>No data acquired.  Please call downloadData() before using this method.</b>");
            return buffer;
        }
        
        Comparator comparator = new Comparator(){
            public int compare(Object o1, Object o2) {
                // these will be string key objects
                String key1 = (String)o1;//key1
                String key2 = (String)o2;//key2
                
                ReData value1 = (ReData)(dataMap.get(o1));//value1
                ReData value2 = (ReData)(dataMap.get(o2));//value2
                
                int postcode1 = value1.getPostCode();
                int postcode2 = value2.getPostCode();
                
                return (postcode1 - postcode2);
            }
        };
        
        Map sortByPostcodeMap = new TreeMap(comparator);
        sortByPostcodeMap.putAll(dataMap);
        
        buffer.append("<style>");
        buffer.append("#cell0 { padding-left:4px;padding-right:4px;padding-top:2px;padding-bottom:2px;border-right:1px solid gray; }");
        buffer.append("#cell { padding-left:4px;padding-right:4px;padding-top:2px;padding-bottom:2px;border-top:1px solid gray;border-right:1px solid gray; }");
        buffer.append("</style>");
        
        buffer.append("<table ellpadding=0 cellspacing=0 style=\"border:1px solid gray;\">");
        buffer.append("<tr bgcolor=\"#333333\">");
        buffer.append("<td id=\"cell0\"><font color=\"white\">Postcode</font></td>");
        buffer.append("<td id=\"cell0\"><font color=\"white\">Suburbs</font></td>");
        buffer.append("<td id=\"cell0\"><font color=\"white\">Count</font></td>");
        buffer.append("<td id=\"cell0\"><font color=\"white\">Average Price</font></td>");
        buffer.append("<td id=\"cell0\"><font color=\"white\">Median Price</font></td>");
        buffer.append("</tr>");
        
        int total = 0;
        
        for (Iterator iter = sortByPostcodeMap .values().iterator(); iter.hasNext(); ) {
            ReData reData = (ReData)iter.next();
            
            int postcode = reData.getPostCode();
            int count = reData.getCount();
            float average = reData.getAveragePrice();
            float median = reData.getMedianPrice();
            
            total += count;
            
            int averageRounded = (int)average;
            int medianRounded = (int)median;
            
            buffer.append("<tr>");
            buffer.append("<td id=\"cell\">" + postcode + "</td>");
            buffer.append("<td id=\"cell\">" + reData.getSuburbNames() + "</td>");
            buffer.append("<td id=\"cell\">" + count + "</td>");
            buffer.append("<td id=\"cell\">$" + averageRounded + "</td>");
            buffer.append("<td id=\"cell\">$" + medianRounded + "</td>");
            buffer.append("</tr>");
        }
        
        buffer.append("<tr bgcolor=\"#ccccff\">");
        buffer.append("<td id=\"cell\">TOTALS</td>");
        buffer.append("<td id=\"cell\">&nbsp;</td>");
        buffer.append("<td id=\"cell\">" + total + "</td>");
        buffer.append("<td id=\"cell\">$" + ((int)getTotalAverage())  + "</td>");
        buffer.append("<td id=\"cell\">$" + ((int)getTotalMedian()) + "</td>");
        buffer.append("</tr>");
        
        buffer.append("</table>");
        
        buffer.append("<p>NOTES:");
        buffer.append("<br>");
        buffer.append("1. Total Average Price is the average across all individual properties found during the search.  It's <b>not</b> the average of the spearate postcode averages.");
        buffer.append("<p>");
        buffer.append("2. Total Median Price is the mid-value for all individual prices found during the search.  It's <b>not</b> the median of the postcode medians.");
        
        buffer.append("</body></html>");
        
        return buffer;
    }
    
    // Outputs the ReData to a CSV file specified by 'filename'.
    public void generateCSVReport(String filename) {
        Comparator comparator = new Comparator(){
            public int compare(Object o1, Object o2) {
                // these will be string key objects
                String key1 = (String)o1;//key1
                String key2 = (String)o2;//key2
                
                ReData value1 = (ReData)(dataMap.get(o1));//value1
                ReData value2 = (ReData)(dataMap.get(o2));//value2
                
                int postcode1 = value1.getPostCode();
                int postcode2 = value2.getPostCode();
                
                return (postcode1 - postcode2);
            }
        };
        
        
        Map sortByPostcodeMap = new TreeMap(comparator);
        sortByPostcodeMap.putAll(dataMap);
        
        int total = 0;
        
        PrintStream ps = null;
        try {
            ps = new PrintStream(new FileOutputStream(filename));
            if (ps != null) {
                long timeMillis = System.currentTimeMillis();
                java.util.Calendar calendar = java.util.Calendar.getInstance();
                calendar.setTimeInMillis(timeMillis);

                ps.print("Postcode,Suburbs,Properties Listed,Average Price, Median Price\n");
                LinkedList totalPriceList = new LinkedList();

                for (Iterator iter = sortByPostcodeMap.values().iterator(); iter.hasNext(); ) {
                    ReData reData = (ReData)iter.next();
                    
                    int postcode = reData.getPostCode();
                    int count = reData.getCount();
                    float average = reData.getAveragePrice();
                    float median = reData.getMedianPrice();
                    
                    int averageRounded = (int)average;
                    int medianRounded = (int)median;
                    
                    ps.print("" + postcode + ",\"" + reData.getSuburbNames() + "\"," + count + ",$" + averageRounded + ",$" + medianRounded + "\n");
                    
                    total += count;
                    
                    totalPriceList.add(reData.getPriceList());
                }
                
                
                ps.print("TOTALS=,," + total+ ",$" + ((int)getTotalAverage()) + ",$" + ((int)getTotalMedian()));
                
                ps.close();
            }
        } catch (IOException e) {
            System.out.println("Unable to open " + filename + " for CSV output.");
        }        
    }
    
    public float getTotalAverage() {
        LinkedList totalPriceList = new LinkedList();
        
        for (Iterator iter = dataMap.values().iterator(); iter.hasNext(); ) {
            ReData reData = (ReData)iter.next();
                        
            for (Iterator iter2 = reData.getPriceList().iterator(); iter2.hasNext(); ) {
                Float price = (Float)iter2.next();
                totalPriceList.add(price);
            }                        
        }
        
        float totalPrice = 0.0f;
        
        for (Iterator iter = totalPriceList.iterator(); iter.hasNext(); ) {
            Float value = (Float)iter.next();
            totalPrice += value.floatValue();
        }
        
        float totalAverage = 0.0f;
        
        if (totalPriceList.size() > 0)
            totalAverage = totalPrice/totalPriceList.size();
        
        return totalAverage;
    }
    
    public float getTotalMedian() {
        LinkedList totalPriceList = new LinkedList();
        
        for (Iterator iter = dataMap.values().iterator(); iter.hasNext(); ) {
            ReData reData = (ReData)iter.next();

            for (Iterator iter2 = reData.getPriceList().iterator(); iter2.hasNext(); ) {
                Float price = (Float)iter2.next();
                totalPriceList.add(price);
            }                        
        }
        
        Object[] priceArray = totalPriceList.toArray();
        Arrays.sort(priceArray);
        
        int middle = priceArray.length/2;
        float totalMedian = 0.0f;
        
        if (priceArray.length == 0)
            return totalMedian;
        else if (priceArray.length == 1 || middle == 0)
        {
            Float value = (Float)priceArray[0];
            totalMedian = value.floatValue();
        }
        else if (priceArray.length%2 == 1)
        {
            Float value = (Float)priceArray[middle];
            totalMedian = value.floatValue();
        }
        else {
            Float value1 = (Float)priceArray[middle -1];
            Float value2 = (Float)priceArray[middle];
            totalMedian = (value1.floatValue() + value2.floatValue())/2.0f;
        }
        
        return totalMedian;
    }
    
    public static void main(String[] args) {

        CollectData collectData = new CollectData();
        long timeMillis = System.currentTimeMillis();
        java.util.Calendar calendar = java.util.Calendar.getInstance();
        calendar.setTimeInMillis(timeMillis);
    
        java.text.SimpleDateFormat dateNameFormatter = new java.text.SimpleDateFormat ("dd_MMM_yyyy");									

        try {
            collectData.downloadData();

            StringBuffer htmlBuffer = collectData.generateHTMLReport();

            String filenameHTML = "redata" + dateNameFormatter.format(calendar.getTime()) + ".html";
            
            try {
                PrintStream ps = null;
                ps = new PrintStream(new FileOutputStream(filenameHTML));
                if (ps != null) 
                {
                    ps.println(htmlBuffer.toString());
                    ps.close();      
                    
                    System.out.println("\nHTML report written to file '" + filenameHTML + "'");
                }
            } catch (IOException e) {
                System.out.println("Unable to open redata.html for HTML output.");
            }        

            String filenameCSV = "redata" + dateNameFormatter.format(calendar.getTime()) + ".csv";            
            collectData.generateCSVReport(filenameCSV);
            System.out.println("CSV report written to file '" + filenameHTML + "'\n");
        }
        catch(java.io.IOException ioe) { ioe.printStackTrace(); }    
    }
    
}


