From 0e4e507733c59f667958d5cba100d94e4be09b10 Mon Sep 17 00:00:00 2001 From: Eilert Tunheim <emtunhei@stud.ntnu.no> Date: Mon, 25 Apr 2022 10:47:39 +0200 Subject: [PATCH] Added SQL for manual measurements in getZeroPoint function, need to move to kwh function --- .../java/com/application/DB/Constants.java | 4 + src/main/java/com/application/DB/DB.java | 259 +++++------------- .../GUI/LineChartFunctionality.java | 14 +- .../GUI/LineChartFunctionality.class | Bin 9781 -> 9257 bytes 4 files changed, 75 insertions(+), 202 deletions(-) diff --git a/src/main/java/com/application/DB/Constants.java b/src/main/java/com/application/DB/Constants.java index 8ca102c..ff18115 100644 --- a/src/main/java/com/application/DB/Constants.java +++ b/src/main/java/com/application/DB/Constants.java @@ -85,6 +85,10 @@ public final class Constants { //public static final int ARJANG_VALUE_ID = 19; public static final ArrayList<Integer> ARJANG_KWH_CHAMBER_IDS = new ArrayList<>(Arrays.asList(19,19)); + // Moisture Manual measurements + public static final String ARJANG_MANFUKTREG = "int_ds_manfuktreg"; + public static final int ARJANG_NR_ADDED_DAYS = 2; + diff --git a/src/main/java/com/application/DB/DB.java b/src/main/java/com/application/DB/DB.java index 19a5f0b..9a72927 100644 --- a/src/main/java/com/application/DB/DB.java +++ b/src/main/java/com/application/DB/DB.java @@ -1,13 +1,8 @@ package com.application.DB; -import com.google.auth.oauth2.GoogleCredentials; -import com.google.auth.oauth2.ServiceAccountCredentials; import com.google.cloud.bigquery.*; import org.joda.time.DateTime; -import java.io.File; -import java.io.FileInputStream; -import java.text.SimpleDateFormat; import java.util.*; import static com.application.DB.Constants.*; @@ -20,37 +15,6 @@ import static com.application.DB.Constants.*; */ public class DB { - /** - * Creates a simple date format to use for converting millis in numbers to a usefull date format - * - * @return returns the date format - */ - private static SimpleDateFormat getDateFormat() { - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - return dateFormat; - } - - /** - * Retrieves the credentials file - * - * @return the credentials - * @throws Exception for potential errors - */ - private static GoogleCredentials getCredentials() throws Exception { - File credentialsPath = new File("./src/main/resources/com.application/sf-drying-optimization-1e234ad2b0f4.json"); - - // Load credentials from JSON key file. If you can't set the GOOGLE_APPLICATION_CREDENTIALS - // environment variable, you can explicitly load the credentials file to construct the - // credentials. - GoogleCredentials credentials; - try (FileInputStream serviceAccountStream = new FileInputStream(credentialsPath)) { - credentials = ServiceAccountCredentials.fromStream(serviceAccountStream); - } - return credentials; - } - - /** * Launching the functions * @@ -66,69 +30,6 @@ public class DB { } - /** - * Creates a bigquery builder - * - * @return a builder - * @throws Exception returns potential error - */ - private static BigQuery getBuilder() throws Exception { - // Step 1: Initialize BigQuery service - // Here we set our project ID and get the `BigQuery` service object - // this is the interface to our BigQuery instance that - // we use to execute jobs on - return BigQueryOptions.newBuilder(). - setCredentials(getCredentials()). - setProjectId(PROJECT_ID) - .build().getService(); - } - - - /** - * Creates a job - * - * @param queryConfig query configuration information - * @return a job - * @throws Exception returns potential error - */ - private static Job getJob(JobConfiguration queryConfig) throws Exception { - // Step 3: Run the job on BigQuery - // create a `Job` instance from the job configuration using the BigQuery service - // the job starts executing once the `create` method executes - - Job queryJob = getBuilder().create(JobInfo.newBuilder(queryConfig).build()); - queryJob = queryJob.waitFor(); - // the waitFor method blocks until the job completes - // and returns `null` if the job doesn't exist anymore - if (queryJob == null) { - throw new Exception("job no longer exists"); - } - // once the job is done, check if any error occured - if (queryJob.getStatus().getError() != null) { - throw new Exception(queryJob.getStatus().getError().toString()); - } - return queryJob; - } - - /** - * This function creates a query job that uses the query statement - * in order to retrieve information from the database - * - * @param sqlStatement input for the query statement - * @return returns the queryjob with the results - * @throws Exception Throws exception in case of error - */ - private static TableResult createQueryJob(String sqlStatement) throws Exception { - - // Creates a job configuration - Job queryJob = getJob(QueryJobConfiguration.newBuilder(sqlStatement).build()); - - // Retrieves the results from the queryjob - return queryJob.getQueryResults(); - - } - - public static Map<Integer, Map<String, Number>> setInputParameters() throws Exception { Map<Integer, Map<String, Number>> allDryingPeriods = new HashMap<>(); @@ -320,51 +221,6 @@ public class DB { } - - /** - * - * - * @throws Exception - */ - /* - public static void getManualMeasurements() throws Exception { - - // Sqlstatement - final String sqlStatement = ""; - - System.out.println(sqlStatement); - - // Retrieves the results from the queryjob - TableResult result = createQueryJob(sqlStatement); - - //System.out.println("InTidTork\t\t\tUtTidTork"); - // Iterating through the results - for (FieldValueList row : result.iterateAll()) { - - row.get("DryingStarted").getTimestampValue(); - } - - // Defining a treemap to sort the data incrementally - //NavigableMap<String, String> sortedFinalResults = new TreeMap<>(dates); - - System.out.println("\n"); - - for (Map.Entry<String, String> entry : sortedFinalResults.entrySet()) { - System.out.printf("Intid: \t%s\t\t\tUttid: \t%s\n",entry.getKey(),entry.getValue()); - } - - - - //System.out.printf("Size of dates: %s\n\n", sortedFinalResults.size()); - - - // Returns a treemap that sorts the dates incrementally - //return new TreeMap<>(sortedFinalResults); - } - */ - - - /** * Retrieves information about kWh and the corresponding date * @@ -376,6 +232,8 @@ public class DB { String timestamp, String valueIDName, int valueID, int index) throws Exception { + System.out.println("GetMan size: " + HelpingFunctions.getManMoist().size()); + // Initializing the data map to store the results Map<Integer, Map<String, Number>> finalResults = new HashMap<>(); @@ -399,7 +257,7 @@ public class DB { System.out.println(sqlStatement); // Iterating through the results - TableResult result = createQueryJob(sqlStatement); + TableResult result = HelpingFunctions.createQueryJob(sqlStatement); //System.out.println("Timestamp \t kWh"); int baseline = 0; @@ -417,7 +275,7 @@ public class DB { // Retrieving the wanted data long timeStamp = row.get("" + timestamp + "").getTimestampValue() / 1000; // Riktig format, men i string - String formatedTimeStamp = getDateFormat().format(timeStamp); + String formatedTimeStamp = HelpingFunctions.getDateFormat().format(timeStamp); // Checks for negative values if (variantValue > 0) { @@ -482,8 +340,9 @@ public class DB { String formatedUtTidTork = ""; String formatedUtTidTorkPlussDays = ""; - // Initializing the dates map to store the results + // Initializing maps to store the results Map<String, String> dates = new HashMap<>(); + Map<String, String> manMoistData = new HashMap<>(); // Defining extra parameters if required @@ -513,10 +372,15 @@ public class DB { System.out.println(sqlStatement); + // Increments kilinID back to the correct id nr + if(locationID == 174){ + kilinID += 1; + } + // Retrieves the results from the queryjob - TableResult result = createQueryJob(sqlStatement); + TableResult result = HelpingFunctions.createQueryJob(sqlStatement); //System.out.println("InTidTork\t\t\tUtTidTork"); // Iterating through the results @@ -534,10 +398,10 @@ public class DB { if (nameParameters[i].contains("Gran")) { treeSort = "G"; } - if (nameParameters[i].matches("\\d{2}x\\d{3}")) { + if (nameParameters[i].matches("\\d+x\\d+")) { String[] dimensions = nameParameters[i].split("x"); - width = dimensions[0]; - thickness = dimensions[1]; + thickness = dimensions[0]; + width = dimensions[1]; } if (nameParameters[i].matches("\\d{1}ex")) { sawSet = nameParameters[i]; @@ -559,7 +423,7 @@ public class DB { long doubleValue = row.get("DryingStarted").getTimestampValue(); long InTidTorkLong = doubleValue/1000; // Formating the data from long to a string in the correct date format - formatedInTidTork = getDateFormat().format(InTidTorkLong); + formatedInTidTork = HelpingFunctions.getDateFormat().format(InTidTorkLong); //System.out.println("LONG!!@@@@"); } catch(NumberFormatException e){ //not long value, must be of type string @@ -590,7 +454,7 @@ public class DB { long doubleValue = row.get("DryingCompleted").getTimestampValue(); long utTidTorkLong = doubleValue/1000; // Formating the data from long to a string in the correct date format - formatedUtTidTork = getDateFormat().format(utTidTorkLong); + formatedUtTidTork = HelpingFunctions.getDateFormat().format(utTidTorkLong); } catch(NumberFormatException e) { //not long value, must be of type string @@ -606,61 +470,62 @@ public class DB { } - //System.out.println("Inn formated: "+formatedInTidTork); - //System.out.println("Ut formated: "+formatedUtTidTork+"\n"); - // Checks if intidtork or outtidtork is empty, if so they are ignored and not added to the list - if (!formatedInTidTork.isEmpty() && !formatedUtTidTork.isEmpty()){ - // Adds the data to the dates map - dates.put(formatedInTidTork,formatedUtTidTork); - } - //System.out.printf("%s\t\t\t%s\n",formatedInTidTork,formatedUtTidTork); - } - - - // Adding days to search between - DateTime addedDays = new DateTime(formatedUtTidTork.split(" ")[0]); - formatedUtTidTorkPlussDays = addedDays.plusDays(2).toString(); - formatedUtTidTorkPlussDays = formatedUtTidTorkPlussDays.split("T")[0]; - - - // Getting manual measurement - // Sqlstatement - final String manMeasurement = "SELECT Tork, Dato ,Treslag, Tykkelse, Bredde, Planketype, Malfukt, GjeFukt " + - "FROM `sf-drying-optimization.174.int_ds_manfuktreg` " + - "WHERE Tork LIKE " + kilinID + " " + - "AND Treslag = " + treeSort + " " + - "AND Tykkelse = " + thickness + " " + - "AND Bredde = " + width + " " + - "AND Planketype = " + sawSet + " " + - "AND Dato BETWEEN \""+ formatedUtTidTork.split(" ")[0] +"\" AND \"" + formatedUtTidTorkPlussDays + "\" " + - "ORDER BY Dato"; - - System.out.println(manMeasurement); + // Getting manual measurement: + // Adding days to search between + String date = formatedUtTidTork.split(" ")[0]; + DateTime addedDays = new DateTime(date); + formatedUtTidTorkPlussDays = addedDays.plusDays(ARJANG_NR_ADDED_DAYS).toString(); + formatedUtTidTorkPlussDays = formatedUtTidTorkPlussDays.split("T")[0]; + if (locationID == ARJANG_LOCATION_ID) { - // Retrieves the results from the queryjob - TableResult resultManMeasurement = createQueryJob(manMeasurement); + // Sqlstatement + final String manMeasurementStatement = "SELECT Tork, Dato, Treslag, Tykkelse, Bredde, Planketype, Diff " + + "FROM `" + PROJECT_ID + "." + locationID + "." + ARJANG_MANFUKTREG + "` " + + "WHERE Tork LIKE \"%" + kilinID + "%\" " + + "AND Treslag LIKE \"%" + treeSort + "%\" " + + "AND Tykkelse = " + thickness + " " + + "AND Bredde = " + width + " " + + "AND Planketype = \"" + sawSet + "\" " + + "AND Dato BETWEEN \"" + date + "\" AND \"" + formatedUtTidTorkPlussDays + "\" " + + "ORDER BY Dato"; - //System.out.println("InTidTork\t\t\tUtTidTork"); - // Iterating through the results - for (FieldValueList row : resultManMeasurement.iterateAll()) { + //System.out.println(manMeasurementStatement); - System.out.println(row); - moistureGoal = row.get("Malfukt").getValue().toString(); - moistureMeasured = row.get("GjeFukt").getValue().toString(); + // Retrieves the results from the queryjob + TableResult resultManMeasurement = HelpingFunctions.createQueryJob(manMeasurementStatement); - } + //System.out.println("InTidTork\t\t\tUtTidTork"); + // Iterating through the results + for (FieldValueList manMeasurement : resultManMeasurement.iterateAll()) { + // Checks if value is null + if(!manMeasurement.get("Diff").isNull()){ + moistureMeasured = manMeasurement.get("Diff").getValue().toString(); + } + } + //System.out.println("moistureMeasured: " + moistureMeasured); + if(!moistureMeasured.isEmpty()){ + manMoistData.put(formatedUtTidTork,moistureMeasured); + } + } - System.out.println("moistureGoal: " + moistureGoal); - System.out.println("moistureMeasured: " + moistureMeasured); + //System.out.println("Inn formated: "+formatedInTidTork); + //System.out.println("Ut formated: "+formatedUtTidTork+"\n"); + // Checks if intidtork or outtidtork is empty, if so they are ignored and not added to the list + if (!formatedInTidTork.isEmpty() && !formatedUtTidTork.isEmpty()){ + // Adds the data to the dates map + dates.put(formatedInTidTork,formatedUtTidTork); + } + //System.out.printf("%s\t\t\t%s\n",formatedInTidTork,formatedUtTidTork); + } // Defining a treemap to sort the data incrementally NavigableMap<String, String> sortedFinalResults = new TreeMap<>(dates); @@ -671,10 +536,14 @@ public class DB { System.out.printf("Intid: \t%s\t\t\tUttid: \t%s\n",entry.getKey(),entry.getValue()); } + System.out.printf("Size of dates: %s\n\n", sortedFinalResults.size()); - System.out.printf("Size of dates: %s\n\n", sortedFinalResults.size()); + System.out.println("manMoistData size: " + manMoistData.size()); + for (Map.Entry<String, String> entry : manMoistData.entrySet()) { + HelpingFunctions.getManMoist().put(entry.getKey(), entry.getValue()); + } // Returns a treemap that sorts the dates incrementally return new TreeMap<>(sortedFinalResults); diff --git a/src/main/java/com/application/GUI/LineChartFunctionality.java b/src/main/java/com/application/GUI/LineChartFunctionality.java index 3352b3e..d0f1c25 100644 --- a/src/main/java/com/application/GUI/LineChartFunctionality.java +++ b/src/main/java/com/application/GUI/LineChartFunctionality.java @@ -49,9 +49,9 @@ public class LineChartFunctionality { private static Map<Integer, ArrayList<Double>> statistics(Map<Integer, ArrayList<Double>> multiMap){ - System.out.println("\n\nMultimap: \n"); + //System.out.println("\n\nMultimap: \n"); for (Map.Entry<Integer, ArrayList<Double>> entry : multiMap.entrySet()) { - System.out.printf("\nIndex: \t%s\t\t\tkWh: \t%s\n", entry.getKey(), entry.getValue()); + //System.out.printf("\nIndex: \t%s\t\t\tkWh: \t%s\n", entry.getKey(), entry.getValue()); if(entry.getValue().size()>1){ SummaryStatistics stats = new SummaryStatistics(); @@ -61,10 +61,10 @@ public class LineChartFunctionality { // Calculate 95% confidence interval double ci = calcMeanCI(stats, CONFIDENCE_INTERVAL); - System.out.println(String.format("Mean: %f", stats.getMean())); + //System.out.println(String.format("Mean: %f", stats.getMean())); double lower = stats.getMean() - ci; double upper = stats.getMean() + ci; - System.out.println(String.format("Confidence Interval 95%%: %f, %f", lower, upper)); + //System.out.println(String.format("Confidence Interval 95%%: %f, %f", lower, upper)); // Deletes entries if they are out of bounds with the confidence interval entry.getValue().removeIf(value -> Double.compare(value, lower) < 0 || Double.compare(value, upper) > 0); @@ -138,7 +138,7 @@ public class LineChartFunctionality { double[][] data = new double[confidenceIntervalData.size()*jMaxSize][2]; int index = 0; - System.out.println(confidenceIntervalData); + //System.out.println(confidenceIntervalData); for (int i = 0; i < confidenceIntervalData.size(); i++) { ArrayList<Double> list = confidenceIntervalData.get(i); @@ -181,7 +181,7 @@ public class LineChartFunctionality { for (double[] datum : data) { for (double v : datum) { - System.out.println("Data: "+v); + //System.out.println("Data: "+v); } } @@ -191,7 +191,7 @@ public class LineChartFunctionality { for (int i = 0; i < confidenceIntervalData.size(); i++) { // Connect the data to a series - System.out.println(simpleRegression.predict(i)); + //System.out.println(simpleRegression.predict(i)); regressionSeries.getData().add(new XYChart.Data<String, Number>(String.valueOf(i), simpleRegression.predict(i))); } updateLineChart(regressionSeries); diff --git a/target/classes/com/application/GUI/LineChartFunctionality.class b/target/classes/com/application/GUI/LineChartFunctionality.class index 5af8d726187d02dc53af7c8a0c5c4932f7360d17..5114429200f1c808b85caad3617510f534fe86cf 100644 GIT binary patch delta 3964 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 delta 4400 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 -- GitLab