From 4b978842f75445c41642694eb58013a58853319d Mon Sep 17 00:00:00 2001 From: Timo Volkmann Date: Tue, 16 Jun 2020 14:31:46 +0200 Subject: [PATCH] finished refactoring --- .vscode/launch.json | 22 +++ backend/index.js | 1 - backend/models/getRegions.js | 3 - backend/routes/regions.js | 4 + backend/routes/search.js | 68 ++++++-- backend/util/calculateScores.js | 15 -- .../util/getAllRegionsWithClimatePerMonth.js | 22 ++- backend/util/getClimateMinMax.js | 32 ++-- backend/util/score.js | 16 +- backend/util/scoreAndSearch.js | 158 ++++++++++-------- backend/util/transformer.js | 3 +- 11 files changed, 207 insertions(+), 137 deletions(-) create mode 100644 .vscode/launch.json delete mode 100644 backend/util/calculateScores.js diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..efeef13 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + // Verwendet IntelliSense zum Ermitteln möglicher Attribute. + // Zeigen Sie auf vorhandene Attribute, um die zugehörigen Beschreibungen anzuzeigen. + // Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "name": "NodeJS: nodemon debug", + "program": "${workspaceFolder}/backend/index.js", + "request": "launch", + "restart": true, + "runtimeExecutable": "nodemon", + "skipFiles": [ + "/**" + ], + "type": "node", + "envFile": "${workspaceFolder}/backend/.env", + } + ] +} \ No newline at end of file diff --git a/backend/index.js b/backend/index.js index 3cf2720..a20c60f 100644 --- a/backend/index.js +++ b/backend/index.js @@ -4,7 +4,6 @@ const path = require("path"); const morgan = require("morgan"); const dbConnection = require("./util/dbConnection"); const fs = require("fs"); -const httpolyglot = require("httpolyglot"); require('dotenv').config() // credentials diff --git a/backend/models/getRegions.js b/backend/models/getRegions.js index ba86da8..6bbf0b4 100644 --- a/backend/models/getRegions.js +++ b/backend/models/getRegions.js @@ -2,9 +2,6 @@ module.exports = async (dbConn) => { const regions = await dbConn.query( `SELECT regions.id AS region_id, regions.region AS name, - regions.description, - regions.preview_img, - regions.country_id AS country_id, countries.country AS country, regions.meteostat_id AS meteostat_id FROM regions diff --git a/backend/routes/regions.js b/backend/routes/regions.js index 821a669..653b04c 100644 --- a/backend/routes/regions.js +++ b/backend/routes/regions.js @@ -1,11 +1,15 @@ const router = require("express").Router(); const getRegions = require("../models/getRegions.js"); const getRegionById = require("../models/getRegionById.js"); +const path = require("path"); module.exports = dbConn => { router.get("/api/v1/regions", async (req, res) => { res.json(await getRegions(dbConn)); }); + router.get('/api/v1/regions/:id/image', (req, res) => { + res.sendFile(path.join(__dirname, `../data/regions/images/${req.params.id}.jpg`)) + }) router.get("/api/v1/regions/:id", async (req, res) => { const id = req.params.id; diff --git a/backend/routes/search.js b/backend/routes/search.js index c8d17f4..e861441 100644 --- a/backend/routes/search.js +++ b/backend/routes/search.js @@ -1,20 +1,60 @@ const router = require("express").Router(); -const getSearchResults = require("../models/getSearchResults.js"); +const _ = require('lodash') const getSearchPresets = require("../models/getSearchPresets.js"); +const base64 = require("../util/base64.js") +const sas = require("../util/scoreAndSearch.js") + module.exports = dbConn => { - router.get("/api/v1/search", async (req, res) => { - const query = req.query.q; - if (query != undefined) { - res.json(await getSearchResults(dbConn, req)); - } else { - res.status(400).send(); - } - }); + router.get("/api/v1/search", searchHandler(dbConn)); + router.get("/api/v1/search/presets", presetHandler(dbConn)); - router.get("/api/v1/search/presets", async (req, res) => { - res.json(await getSearchPresets(dbConn)); - }); - - return router; + return router; }; + +function presetHandler(dbConn) { + return function (req, res) { + getSearchPresets(dbConn).then(presets => { + res.json(presets) + }).catch(error => { + // TODO error handling + }) + } +} + +function searchHandler(dbConn) { + const scoreAndSearch = sas(dbConn) + return function (req, res) { + let response = {} + + response.meta = { + params: req.params, + query: req.query, + headers: req.headers + } + + let q = req.query.q ? base64.base64ToObj(req.query.q) : req.query + console.log('Q:', q) + + let scoreQueryObj = {} + if (q.temperature) scoreQueryObj['temperature_mean_max'] = q.temperature + if (q.percipitation) scoreQueryObj['percipitation'] = q.percipitation + if (q.raindays) scoreQueryObj['raindays'] = q.raindays + if (q.sunhours) scoreQueryObj['sunhours'] = q.sunhours + // @TimoJ 1. hier die Parameter die gescored werden sollen hinufügen + + if (_.isEmpty(scoreQueryObj)) { + res.status(400).send('provide at least one search parameter.'); + } + scoreAndSearch(q.from, q.to, scoreQueryObj).then(searchResults => { + + // TODO hier könnten Suchergebnisse gefiltert werden: für Textsuche oder Exludes.... + response.data = searchResults + res.json(response) + }).catch(e => { + // TODO error handling + console.log(e) + res.json(e.message) + }) + } +} \ No newline at end of file diff --git a/backend/util/calculateScores.js b/backend/util/calculateScores.js deleted file mode 100644 index 8cb63df..0000000 --- a/backend/util/calculateScores.js +++ /dev/null @@ -1,15 +0,0 @@ -function calculateScores(type, regionDataRows, searchLowParam, searchMaxParam, minMax) { - console.log('calculateScores for', type) - let result = regionDataRows.map(x => { - const sc = Math.round(score.calculateScoreRange(minMax.min[type], minMax.max[type], multiplier[type], x[type], searchLowParam, searchMaxParam) * 100) / 100 - - return { - region_id: x.region_id, - type: type, - value: x[type], - score: x[type] === null ? null : sc - } - - }) - return result -} \ No newline at end of file diff --git a/backend/util/getAllRegionsWithClimatePerMonth.js b/backend/util/getAllRegionsWithClimatePerMonth.js index ead7583..fc69f8d 100644 --- a/backend/util/getAllRegionsWithClimatePerMonth.js +++ b/backend/util/getAllRegionsWithClimatePerMonth.js @@ -1,8 +1,11 @@ -function getAllRegionsWithClimatePerMonth(month) { - console.log('getAllRegionsWithClimatePerMonth') - const sql = `SELECT + + +module.exports = function (dbConn) { + return async function getAllRegionsWithClimatePerMonth(month) { + console.log('getAllRegionsWithClimatePerMonth') + const sql = `SELECT region_climate.region_id AS region_id, - regions.country_id AS country_id, + countries.country AS country, regions.region AS name, ROUND(AVG(region_climate.temperature_mean), 1) AS temperature_mean, ROUND(AVG(region_climate.temperature_mean_min), 1) AS temperature_mean_min, @@ -10,6 +13,13 @@ function getAllRegionsWithClimatePerMonth(month) { ROUND(AVG(region_climate.percipitation), 1) AS percipitation, ROUND(AVG(region_climate.raindays), 1) AS raindays, ROUND(AVG(region_climate.sunshine), 1) AS sunhours - FROM region_climate JOIN regions ON region_climate.region_id = regions.id WHERE region_climate.month = ${month} GROUP BY region_id` - return getQueryRows(sql) + FROM region_climate + JOIN regions ON region_climate.region_id = regions.id + JOIN countries ON regions.country_id = countries.id + WHERE region_climate.month = ${month} GROUP BY region_id` + let response = await dbConn.query(sql) + console.log(response[0]); + + return response + } } \ No newline at end of file diff --git a/backend/util/getClimateMinMax.js b/backend/util/getClimateMinMax.js index 0a756e5..e4acd71 100644 --- a/backend/util/getClimateMinMax.js +++ b/backend/util/getClimateMinMax.js @@ -1,22 +1,22 @@ exports.getClimateMinMax = async function (dbConn) { console.log('getClimateMinMax') const sqlMin = `SELECT - MIN(temperature_mean) AS temperature_mean, - MIN(temperature_mean_min) AS temperature_mean_min, - MIN(temperature_mean_max) AS temperature_mean_max, - MIN(percipitation) AS percipitation, - MIN(raindays) AS raindays, - MIN(sunshine) AS sunhours - FROM region_climate` + MIN(temperature_mean) AS temperature_mean, + MIN(temperature_mean_min) AS temperature_mean_min, + MIN(temperature_mean_max) AS temperature_mean_max, + MIN(percipitation) AS percipitation, + MIN(raindays) AS raindays, + MIN(sunshine) AS sunhours + FROM region_climate` const sqlMax = `SELECT - MAX(temperature_mean) AS temperature_mean, - MAX(temperature_mean_min) AS temperature_mean_min, - MAX(temperature_mean_max) AS temperature_mean_max, - MAX(percipitation) AS percipitation, - MAX(raindays) AS raindays, - MAX(sunshine) AS sunhours - FROM region_climate` - const [qResMin, qResMax] = await Promise.all([await dbConn.query(sqlMin), await dbConn.query(sqlMax)]) - //console.log(qResMin) + MAX(temperature_mean) AS temperature_mean, + MAX(temperature_mean_min) AS temperature_mean_min, + MAX(temperature_mean_max) AS temperature_mean_max, + MAX(percipitation) AS percipitation, + MAX(raindays) AS raindays, + MAX(sunshine) AS sunhours + FROM region_climate` + const [qResMin, qResMax] = await Promise.all([dbConn.query(sqlMin), dbConn.query(sqlMax)]) + // console.log(qResMin) return { min: qResMin[0], max: qResMax[0] } } \ No newline at end of file diff --git a/backend/util/score.js b/backend/util/score.js index 4096aed..0a3a75a 100644 --- a/backend/util/score.js +++ b/backend/util/score.js @@ -1,13 +1,3 @@ -/** - * - */ -const multiplier_temperature = 5; - - -/** - * - * @param {...any} scores expects objects which contains score and their weight - */ exports.calculateAvgScore = (...scores) => { return avgScore = scores.reduce((total, score) => total += score) / scores.length; } @@ -18,14 +8,10 @@ exports.calculateScoreRange = (min, max, multiplier, regionVal, sLowVal, sHighVa if (regionVal >= sLowVal && regionVal <= sHighVal) return 10; // choose value with smallest distance let sVal = Math.abs(regionVal - sLowVal) < Math.abs(regionVal - sHighVal) ? sLowVal : sHighVal; - //console.log('nearest value',sVal, regionVal) return this.calculateScore(min, max, multiplier, regionVal, sVal); } exports.calculateScore = (min, max, multiplier, regionVal, searchVal) => { - let score = 1 - (Math.abs(searchVal - regionVal) / (max - min) * multiplier); return score <= 0 ? 0 : score * 10; -} - -console.log('test score calculation. result: ' + this.calculateScoreRange(-15, 45, 5, 24, 15, 22)) \ No newline at end of file +} \ No newline at end of file diff --git a/backend/util/scoreAndSearch.js b/backend/util/scoreAndSearch.js index 4818238..d70e40b 100644 --- a/backend/util/scoreAndSearch.js +++ b/backend/util/scoreAndSearch.js @@ -1,27 +1,36 @@ const _ = require('lodash') +const moment = require("moment") const getClimateMinMax = require("./getClimateMinMax.js") const oldToNewQuerySyntax = require("./oldToNewQuerySyntax.js") -const moment = require("moment") +const getAllRegionsWithClimatePerMonth = require('./getAllRegionsWithClimatePerMonth') +const score = require('./score') +const transformer = require('./transformer') -exports.scoreAndSearch = async function (from, to, queries, dbConn) { - // TODO break funtion into parts when implementing non-climate queries and modularize (new file) +const MULTIPLIER = { + temperature_mean_max: 5, + percipitation: 3.5, + raindays: 3, + sunhours: 2.5, +} +module.exports = function (dbConn) { + return async function (from, to, queries) { console.log('search') // get Min and Max values for each Parameter const minMax = await getClimateMinMax.getClimateMinMax(dbConn) - // randomize if empty queries - // TODO: WHY - if (_.isEmpty(queries)) { - let t = _.round(_.random(minMax.min.temperature_mean_max, minMax.max.temperature_mean_max-5),0) - let p = _.round(_.random(minMax.min.percipitation, minMax.max.percipitation - 50), 0) - let r = _.round(_.random(minMax.min.raindays, minMax.max.raindays - 5), 0) - let s = _.round(_.random(minMax.min.sunhours, minMax.max.sunhours - 50), 0) - queries.temperature_mean_max = `${t},${t + 5}` - queries.percipitation = `${p},${p + 50}` - queries.raindays = `${r},${r + 5}` - queries.sunhours = `${s},${s + 50}` + // randomize if query contains randomize=true + if (queries.randomize) { + let t = _.round(_.random(minMax.min.temperature_mean_max, minMax.max.temperature_mean_max - 5), 0) + let p = _.round(_.random(minMax.min.percipitation, minMax.max.percipitation - 50), 0) + let r = _.round(_.random(minMax.min.raindays, minMax.max.raindays - 5), 0) + let s = _.round(_.random(minMax.min.sunhours, minMax.max.sunhours - 50), 0) + queries.temperature_mean_max = `${t},${t + 5}` + queries.percipitation = `${p},${p + 50}` + queries.raindays = `${r},${r + 5}` + queries.sunhours = `${s},${s + 50}` + delete queries.randomize } queries = oldToNewQuerySyntax.oldToNewQuerySyntax(queries) console.log(queries) @@ -33,75 +42,92 @@ exports.scoreAndSearch = async function (from, to, queries, dbConn) { let dayTo = 0 if (_.isNumber(from) && _.isNumber(to)) { - let dateFrom = moment(from).toDate() - let dateTo = moment(to).toDate() - monthFrom = dateFrom.getMonth() - monthTo = dateTo.getMonth() - dayFrom = dateFrom.getDay() - dayTo = dateTo.getDay() - if (moment(dateFrom).add(23, 'hours').isAfter(moment(dateTo))) throw new Error("ERR: 'to' must be at least one day after 'from'.") + let dateFrom = moment(from).toDate() + let dateTo = moment(to).toDate() + monthFrom = dateFrom.getMonth() + monthTo = dateTo.getMonth() + dayFrom = dateFrom.getDay() + dayTo = dateTo.getDay() + if (moment(dateFrom).add(23, 'hours').isAfter(moment(dateTo))) throw new Error("ERR: 'to' must be at least one day after 'from'.") } else { - // to still support old query syntax - let re = /([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))/i; - monthFrom = Number(from.split("-")[1]) - monthTo = Number(to.split("-")[1]) - dayFrom = Number(from.split("-")[2]) - dayTo = Number(to.split("-")[2]) - if (!from.match(re) || !to.match(re)) throw new Error("ERR: invalid parameter:",from,to) - if (moment(from, 'YYYY-MM-DD').add(23, 'hours').isAfter(moment(to, 'YYYY-MM-DD'))) throw new Error("ERR: 'to' must be at least one day after 'from'.") + // to still support old query syntax + let re = /([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))/i; + monthFrom = Number(from.split("-")[1]) + monthTo = Number(to.split("-")[1]) + dayFrom = Number(from.split("-")[2]) + dayTo = Number(to.split("-")[2]) + if (!from.match(re) || !to.match(re)) throw new Error("ERR: invalid parameter:", from, to) + if (moment(from, 'YYYY-MM-DD').add(23, 'hours').isAfter(moment(to, 'YYYY-MM-DD'))) throw new Error("ERR: 'to' must be at least one day after 'from'.") } // -- Prepare search -- // to calculate average if traveldates are in more than one month let travelPeriods = [] if (monthFrom === monthTo) { - let element = { - month: monthFrom, - days: dayTo - dayFrom + let element = { + month: monthFrom, + days: dayTo - dayFrom + } + travelPeriods.push(element) + } else { + for (let index = monthFrom; index <= monthTo; index++) { + let element = {} + if (index === monthFrom) { + element = { + month: index, + days: 32 - dayFrom + } + } else if (index === monthTo) { + element = { + month: index, + days: dayTo + } + } else { + element = { + month: index, + days: 30 + } } travelPeriods.push(element) - } else { - for (let index = monthFrom; index <= monthTo; index++) { - let element = {} - if (index === monthFrom) { - element = { - month: index, - days: 32 - dayFrom - } - } else if (index === monthTo) { - element = { - month: index, - days: dayTo - } - } else { - element = { - month: index, - days: 30 - } - } - travelPeriods.push(element) - } + } } // calculate detail scores let detailScores = await Promise.all(travelPeriods.map(async period => { - period.climate = await getAllRegionsWithClimatePerMonth(period.month) - period.scores = {} - Object.entries(queries).forEach(([key, value]) => { - // console.log('key',key) - // console.log('val', value) - period.scores[key] = calculateScores(key, period.climate, value[0], value[1], minMax) - }); - return period + period.climate = await getAllRegionsWithClimatePerMonth(dbConn)(period.month) + period.scores = {} + Object.entries(queries).forEach(([key, value]) => { + // console.log('key',key) + // console.log('val', value) + period.scores[key] = calculateScoresWrapper(key, period.climate, value[0], value[1], minMax) + }); + return period })); // calculate ratio and transform into target object return { - results: transformer.transform(detailScores), - debug: { - detailScores: detailScores, - minMax: minMax - } + results: transformer.transform(detailScores), + debug: { + detailScores: detailScores, + minMax: minMax + } } + } + + function calculateScoresWrapper(type, regionDataRows, searchLowParam, searchMaxParam, minMax) { + console.log('calculateScores for', type) + let result = regionDataRows.map(x => { + const sc = Math.round(score.calculateScoreRange(minMax.min[type], minMax.max[type], MULTIPLIER[type], x[type], searchLowParam, searchMaxParam) * 100) / 100 + + return { + region_id: x.region_id, + type: type, + value: x[type], + score: x[type] === null ? null : sc + } + + }) + return result + } } \ No newline at end of file diff --git a/backend/util/transformer.js b/backend/util/transformer.js index 9ac37f2..b30fa43 100644 --- a/backend/util/transformer.js +++ b/backend/util/transformer.js @@ -9,10 +9,11 @@ exports.transform = (data) => { // STEP 1 Create Response Array with region names from first climate object let byRegion = data[0].climate.map(el => { + // return el return { region_id: el.region_id, - country_id: el.country_id, name: el.name, + country: el.country, } })