Compare commits

...

26 Commits

Author SHA1 Message Date
8c76a32c30 fixed empty climate arrays in regions 2020-06-17 21:38:01 +02:00
8cd606dfcd Merge branch 'feature/pricesearch' of https://git.it.hs-heilbronn.de/tjohn/cc-data into feature/pricesearch 2020-06-17 21:03:39 +02:00
0e2084f21b implemented scoring for prices 2020-06-17 21:00:13 +02:00
8fedd36e22 fixed wrong date interpretation 2020-06-17 18:29:36 +02:00
Timo John
b212c5b7a6 Fixed Region endpoint and cleared up a little. 2020-06-17 18:03:47 +02:00
a7baa4b626 some fixes by TVM 2020-06-17 17:43:20 +02:00
063df9ec9d refactored search function befor implementing price search 2020-06-17 16:10:30 +02:00
8ee8cc8b15 satisfiy region interface 2020-06-17 16:09:34 +02:00
Timo John
0c58cd9410 Changed return names according to Interfacedefinition of frontend 2020-06-17 14:09:00 +02:00
809892f766 preparation for price search 2020-06-16 23:52:23 +02:00
Timo John
3f0ce4cb4b Matched getRegions to API Specification
Matched getRegionsByID to API Specification
2020-06-16 23:17:44 +02:00
Timo John
c1ebc2e790 New Version of Setup.sql 2020-06-16 21:54:29 +02:00
Timo John
6141b96ab7 New Version of Setup.sql 2020-06-16 21:00:18 +02:00
Timo John
0c7f976624 Changed Parsing from int to float
Removed comment
2020-06-16 18:59:10 +02:00
4b978842f7 finished refactoring 2020-06-16 14:31:46 +02:00
e1ef3c4a9a added sample images 2020-06-16 12:31:39 +02:00
Timo John
6435f415b6 Implemented Endpoint for getting all search presets
Endpoint for presets works now. For real.

Alternative JSON return for presets tested
2020-06-16 11:45:35 +02:00
Timo John
6fce605add Added Endpoints for Countries and Region/Country by ID 2020-06-16 10:48:54 +02:00
Timo John
3783684c2e Same for countries 2020-06-16 10:17:29 +02:00
Timo John
02e06de22d Add description and preview_img to setup.sql 2020-06-16 10:09:13 +02:00
Timo John
9c22609ae1 Search function works as before minus old syntax 2020-06-16 02:21:16 +02:00
Timo John
32e8e6cf59 Split up massive app.js in small files 2020-06-16 00:50:43 +02:00
Timo John
0f0f8eb590 Moved files to /util 2020-06-16 00:05:33 +02:00
Timo John
2dfec236b2 Refactored climate endpoint 2020-06-15 23:57:14 +02:00
Timo John
8074de0640 Stage 1: Restructuring the Express Backend 2020-06-15 15:02:33 +02:00
Timo John
24971248c9 Changed Temp from mean to mean_max
Changed parameters to temperature_mean_max

set database formatting to utf8

add .env to .gitignore

added .env.sample

.gitignore fix
2020-06-15 11:38:52 +02:00
50 changed files with 4354 additions and 1651 deletions

4
.gitignore vendored
View File

@ -1,3 +1,7 @@
# credentials
.env
config.json
# compiled output
/dist

23
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,23 @@
{
// 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": "node",
// "runtimeExecutable": "nodemon",
"skipFiles": [
"<node_internals>/**"
],
"type": "node",
"envFile": "${workspaceFolder}/backend/.env",
}
]
}

View File

@ -24,7 +24,7 @@ Campus Cup AKMC Data Traveloptimizer
- temperature=NUMBER,NUMBER
- raindays=NUMBER,NUMBER
- sunhours=NUMBER,NUMBER
- percipitation=NUMBER,NUMBER
- precipitation=NUMBER,NUMBER
__Examples:__
http://localhost:3000/v1/search?from=2020-06-14&to=2020-07-29&temperature=27,29&raindays=8,12&sunhours=250,300

View File

@ -1523,7 +1523,7 @@ CREATE TABLE IF NOT EXISTS `region_climate` (
`year` int(11) NOT NULL,
`month` int(11) NOT NULL,
`temperature` float DEFAULT NULL,
`percipitation` float DEFAULT NULL,
`precipitation` float DEFAULT NULL,
`raindays` int(11) DEFAULT NULL,
`sunshine` float DEFAULT NULL,
`humidity` float DEFAULT NULL,

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +0,0 @@
METEOSTAT_API_KEY=LMlDskju
DB_HOST=localhost
DB_USER=root
DB_PASSWORD=devtest
DB_PORT=3306

6
backend/.env.sample Normal file
View File

@ -0,0 +1,6 @@
PORT=
METEOSTAT_API_KEY=
DB_HOST=
DB_USER=
DB_PASSWORD=
DB_PORT=

View File

@ -1,18 +1,17 @@
const express = require('express')
const moment = require('moment')
const _ = require('lodash')
const db = require('./mysql')
const score = require('./score')
const transformer = require('./transformer')
const climate = require('./climate')
const base = require('./base64')
const score = require('./util/score')
const transformer = require('./util/transformer')
const base = require('./util/base64')
const app = express()
const port = 3000
//const multiplier_temp = 5
const multiplier = {
temperature_mean: 5,
percipitation: 3.5,
temperature_mean_max: 5,
precipitation: 3.5,
raindays: 3,
sunhours: 2.5,
}
@ -28,9 +27,9 @@ const samplePresets = [
app.get('/', (req, res) => res.send('Hello Timo!'))
app.get('/v1/regions', (req, res) => getAllRegions().then(x => res.json({ data: x })))
app.get('/v1/presets', (req, res) => res.json({ data: samplePresets}))
app.get('/v1/presets', (req, res) => res.json({ data: samplePresets }))
app.get('/v1/search', searchHandler)
app.get('/v1/update/climate', climateUpdateHandler)
app.get('/v1/climate/update', climateUpdateHandler)
app.listen(port, () => console.log(`Travopti backend listening at http://localhost:${port}`))
@ -53,22 +52,22 @@ function climateUpdateHandler(req, res) {
function searchHandler(req, res) {
let response = {}
response.meta = {
params: req.params,
query: req.query,
headers: req.headers
}
let q = req.query.q ? base.base64ToObj(req.query.q) : req.query
console.log('Q:', q)
let queryObj = {}
if (q.temperature) queryObj['temperature_mean'] = q.temperature
if (q.percipitation) queryObj['percipitation'] = q.percipitation
if (q.temperature) queryObj['temperature_mean_max'] = q.temperature
if (q.precipitation) queryObj['precipitation'] = q.precipitation
if (q.raindays) queryObj['raindays'] = q.raindays
if (q.sunhours) queryObj['sunhours'] = q.sunhours
scoreAndSearch(q.from, q.to, queryObj).then(searchResults => {
response.data = searchResults
res.json(response)
@ -85,27 +84,27 @@ async function scoreAndSearch(from, to, queries) {
// get Min and Max values for each Parameter
const minMax = await getClimateMinMax()
// randomize if empty queries
if (_.isEmpty(queries)) {
let t = _.round(_.random(minMax.min.temperature_mean, minMax.max.temperature_mean-5),0)
let p = _.round(_.random(minMax.min.percipitation, minMax.max.percipitation - 50), 0)
let t = _.round(_.random(minMax.min.temperature_mean_max, minMax.max.temperature_mean_max - 5), 0)
let p = _.round(_.random(minMax.min.precipitation, minMax.max.precipitation - 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 = `${t},${t + 5}`
queries.percipitation = `${p},${p + 50}`
queries.temperature_mean_max = `${t},${t + 5}`
queries.precipitation = `${p},${p + 50}`
queries.raindays = `${r},${r + 5}`
queries.sunhours = `${s},${s + 50}`
}
queries = oldToNewQuerySyntax(queries)
console.log(queries)
// TODO simplify and remove support for old query syntaax
let monthFrom = 0
let monthTo = 0
let dayFrom = 0
let dayTo = 0
if (_.isNumber(from) && _.isNumber(to)) {
let dateFrom = moment(from).toDate()
let dateTo = moment(to).toDate()
@ -121,10 +120,10 @@ async function scoreAndSearch(from, to, queries) {
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 (!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 = []
@ -168,8 +167,8 @@ async function scoreAndSearch(from, to, queries) {
});
return period
}));
// calculate ratio and transform into target object
return {
results: transformer.transform(detailScores),
@ -191,7 +190,7 @@ function calculateScores(type, regionDataRows, searchLowParam, searchMaxParam, m
value: x[type],
score: x[type] === null ? null : sc
}
})
return result
}
@ -202,7 +201,7 @@ async function getClimateMinMax() {
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(precipitation) AS precipitation,
MIN(raindays) AS raindays,
MIN(sunshine) AS sunhours
FROM region_climate`
@ -210,13 +209,13 @@ async function getClimateMinMax() {
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(precipitation) AS precipitation,
MAX(raindays) AS raindays,
MAX(sunshine) AS sunhours
FROM region_climate`
const [qResMin, qResMax] = await Promise.all([getQueryRows(sqlMin), getQueryRows(sqlMax)])
//console.log(qResMin)
return { min: qResMin[0], max: qResMax[0] }
return { min: qResMin[0], max: qResMax[0] }
}
async function getQueryRows(sql) {
@ -240,7 +239,7 @@ function getAllRegions() {
function getClimatePerRegionAndMonth(regionId, month) {
console.log('getClimatePerRegionAndMonth')
const sql = `SELECT region_id, AVG(temperature_mean), AVG(temperature_mean_min), AVG(temperature_mean_max), AVG(percipitation), AVG(sunshine) FROM region_climate WHERE month = ${month} AND region_id = ${regionId}`
const sql = `SELECT region_id, AVG(temperature_mean), AVG(temperature_mean_min), AVG(temperature_mean_max), AVG(precipitation), AVG(sunshine) FROM region_climate WHERE month = ${month} AND region_id = ${regionId}`
return getQueryRows(sql)
}
@ -253,7 +252,7 @@ function getAllRegionsWithClimatePerMonth(month) {
ROUND(AVG(region_climate.temperature_mean), 1) AS temperature_mean,
ROUND(AVG(region_climate.temperature_mean_min), 1) AS temperature_mean_min,
ROUND(AVG(region_climate.temperature_mean_max), 1) AS temperature_mean_max,
ROUND(AVG(region_climate.percipitation), 1) AS percipitation,
ROUND(AVG(region_climate.precipitation), 1) AS precipitation,
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`
@ -263,8 +262,8 @@ function getAllRegionsWithClimatePerMonth(month) {
function oldToNewQuerySyntax(queries) {
let res = {}
try {
if (queries.temperature_mean) res.temperature_mean = [queries.temperature_mean.split(',')[0], queries.temperature_mean.split(',')[1]]
if (queries.percipitation) res.percipitation = [queries.percipitation.split(',')[0], queries.percipitation.split(',')[1]]
if (queries.temperature_mean_max) res.temperature_mean_max = [queries.temperature_mean_max.split(',')[0], queries.temperature_mean_max.split(',')[1]]
if (queries.precipitation) res.precipitation = [queries.precipitation.split(',')[0], queries.precipitation.split(',')[1]]
if (queries.raindays) res.raindays = [queries.raindays.split(',')[0], queries.raindays.split(',')[1]]
if (queries.sunhours) res.sunhours = [queries.sunhours.split(',')[0], queries.sunhours.split(',')[1]]
console.log('queries successfully transformed');

View File

@ -1,124 +0,0 @@
require('dotenv').config()
const mysql = require('mysql2/promise');
const axios = require('axios')
const rangeStartDate = '2010-01'
const rangeEndDate = '2018-12'
exports.update = async function (startDate = rangeStartDate, endDate = rangeEndDate) {
console.log('update climate with:', startDate, endDate);
const connection = await mysql.createConnection({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
port: process.env.DB_PORT,
database: 'travopti'
});
const [result, fields] = await connection.execute(`SELECT * FROM regions WHERE meteostat_id IS NOT NULL`)
// let temp = await Promise.all(result.map(x => createClimateObject(x)))
// let final = temp.reduce((total, element) => total.concat(element), [])
// await writeToDatabase(connection, final)
let temp2 = await Promise.all(result.map(x => createClimateObjectFrom(x, startDate, endDate)))
let final2 = temp2.reduce((total, element) => total.concat(element), [])
await writeToDatabase(connection, final2)
connection.end();
let response = 'database update complete. see backend logs for info.'
console.log(response)
return response
}
// async function createClimateObject(src) {
// let response
// try {
// response = await axios.get(`https://api.meteostat.net/v1/climate/normals?station=${src.meteostat_id}&key=${process.env.METEOSTAT_API_KEY}`)
// } catch (error) {
// console.log("skipping: couldn't find results for following region: ")
// console.log(src.region + " with meteostat_id " + src.meteostat_id)
// return []
// }
// if (!response.data.data) {
// console.log("skipping: no data for station meteostat_id " + src.meteostat_id + " (" + src.region + ")")
// return []
// }
// let results = []
// for (let index = 1; index <= 12; index++) {
// let result = {
// region: src.region,
// region_id: src.id,
// month: index,
// temperature: Object.values(response.data.data.temperature)[index - 1] ? Object.values(response.data.data.temperature)[index - 1] : null,
// temperature_min: Object.values(response.data.data.temperature_min)[index - 1] ? Object.values(response.data.data.temperature_min)[index - 1] : null,
// temperature_max: Object.values(response.data.data.temperature_max)[index - 1] ? Object.values(response.data.data.temperature_max)[index - 1] : null,
// precipitation: Object.values(response.data.data.precipitation)[index - 1] ? Object.values(response.data.data.precipitation)[index - 1] : null,
// sunshine: Object.values(response.data.data.sunshine)[index - 1] ? Object.values(response.data.data.sunshine)[index - 1] : null,
// }
// results.push(result)
// }
// return results
// }
async function createClimateObjectFrom(src, startDate, endDate) {
let response
try {
response = await axios.get(`https://api.meteostat.net/v1/history/monthly?station=${src.meteostat_id}&start=${startDate}&end=${endDate}&key=${process.env.METEOSTAT_API_KEY}`)
} catch (error) {
console.log("skipping createClimateObjectFrom: couldn't find results for following region: ")
console.log(src.region + " with meteostat_id " + src.meteostat_id)
console.log(error)
return []
}
if (!response.data.data) {
console.log("skipping: no data for station meteostat_id " + src.meteostat_id + " (" + src.region + ")")
return []
}
let results = response.data.data.map(element => {
let result = {
region: src.region,
region_id: src.id,
year: element.month.split("-")[0],
month: element.month.split("-")[1],
temperature: element.temperature_mean,
temperature_min: element.temperature_mean_min,
temperature_max: element.temperature_mean_max,
precipitation: element.precipitation,
raindays: element.raindays,
sunshine: element.sunshine,
humidity: element.humidity ? element.humidity : null
}
//console.log(result)
return result
})
return results
}
async function writeToDatabase(dbConnection, climateObjArr) {
climateObjArr.forEach(async (element) => {
//console.log(element)
try {
if (!element.year) {
await dbConnection.execute(`
REPLACE INTO region_climate (region_id, year, month, temperature_mean, temperature_mean_min, temperature_mean_max, percipitation, sunshine)
VALUES (${element.region_id}, 0, ${element.month}, ${element.temperature}, ${element.temperature_min}, ${element.temperature_max}, ${element.precipitation}, ${element.sunshine});`)
} else {
await dbConnection.execute(`
REPLACE INTO region_climate (region_id, year, month, temperature_mean, temperature_mean_min, temperature_mean_max, percipitation, sunshine, humidity, raindays)
VALUES (${element.region_id}, ${element.year}, ${element.month}, ${element.temperature}, ${element.temperature_min}, ${element.temperature_max}, ${element.precipitation}, ${element.sunshine}, ${element.humidity}, ${element.raindays});`)
}
} catch (error) {
if (error.code !== 'ER_DUP_ENTRY') {
console.log("element which causes problems: ")
console.log(element)
console.log("query which causes problems: ")
console.log(error)
} else {
console.log(element.region + ": " + error.sqlMessage)
}
}
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 555 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 995 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 KiB

53
backend/index.js Normal file
View File

@ -0,0 +1,53 @@
const express = require("express");
const bodyParser = require("body-parser");
const path = require("path");
const morgan = require("morgan");
const dbConnection = require("./util/dbConnection");
const fs = require("fs");
require('dotenv').config()
// credentials
const port = process.env.PORT
// Router
const search = require("./routes/search");
const regions = require("./routes/regions");
const countries = require("./routes/countries");
const climate = require("./routes/climate");
const app = express();
(async () => {
try {
const dbConn = await dbConnection();
// Express middleware
app.use(morgan("dev"));
app.use(express.static(path.join(__dirname, "../../dist")));
app.use(bodyParser.json());
// Express routes
app.use(search(dbConn));
app.use(regions(dbConn));
app.use(countries(dbConn));
app.use(climate(dbConn));
app.use((err, req, res, next) => {
// 500
if (true) {
next();
} else {
res.status(500).send();
}
});
// Start webserver
app.listen(port, () => {
console.log(`Travopti backend listening at http://localhost:${port}`)
});
} catch (error) {
// TODO: logging
console.error("Failed to start the webserver");
console.error(error);
}
})();

View File

@ -0,0 +1,6 @@
{
"temperature_mean_max": 5,
"precipitation": 3.5,
"raindays": 3,
"sunhours": 2.5
}

View File

@ -0,0 +1,6 @@
{
"id": 29837,
"parameter": "temperature",
"label": "warm",
"values": [22, 25]
}

View File

@ -0,0 +1,10 @@
module.exports = async (dbConn) => {
const countries = await dbConn.query(
`SELECT countries.id AS country_id,
countries.country AS name,
countries.description
FROM countries`
);
return countries;
};

View File

@ -0,0 +1,12 @@
module.exports = async (dbConn, id) => {
const country = await dbConn.query(
`SELECT countries.id AS country_id,
countries.country AS name,
countries.description
FROM countries
WHERE countries.id = ?`,
[id]
);
return country;
};

View File

@ -0,0 +1,45 @@
exports.getBYTdataByRegion = async (dbConn, id, travelstyle = 1) => {
const res = await dbConn.query(
`SELECT
region_id,
travelstyle,
average_per_day AS average_per_day_costs,
accomodation AS accommodation_costs,
food AS food_costs,
water AS water_costs,
local_transportation AS local_transportation_costs,
entertainment AS entertainment_costs
FROM regions_byt
WHERE region_id = ? AND travelstyle = ?`,
[id, travelstyle]
);
return res;
};
exports.getAllBYTdata = async (dbConn, travelstyle = 1) => {
const res = await dbConn.query(
`SELECT
region_id,
travelstyle,
average_per_day AS average_per_day_costs,
accomodation AS accommodation_costs,
food AS food_costs,
water AS water_costs,
local_transportation AS local_transportation_costs,
entertainment AS entertainment_costs
FROM regions_byt
WHERE travelstyle = ?`,
[travelstyle]
);
return res;
};
exports.getTrivagoData = async (dbConn, id) => {
const region = await dbConn.query(
`...`,
[id]
);
return region;
};

View File

@ -0,0 +1,54 @@
const climateArrayFormatting = require("./../util/climateArrayFormatting.js")
module.exports = async (dbConn, id) => {
const region = await dbConn.query(
`SELECT regions.id AS region_id,
regions.region AS name,
countries.country AS country,
regions.description AS description,
rcma.temperature_mean_max,
rcma.precipitation AS precipitation,
rcma.raindays AS rain_days,
rcma.sunshine AS sun_hours,
regions_byt.average_per_day AS average_per_day_costs,
regions_byt.accomodation AS accommodation_costs,
regions_byt.food AS food_costs,
regions_byt.water AS water_costs,
regions_byt.local_transportation AS local_transportation_costs,
regions_byt.entertainment AS entertainment_costs,
regions_byt.alcohol AS alcohol_costs
FROM regions
LEFT JOIN countries ON regions.country_id = countries.id
LEFT JOIN (SELECT rcma.region_id,
GROUP_CONCAT(IFNULL(rcma.temperature_mean,"") ORDER BY rcma.month SEPARATOR ', ') AS temperature_mean,
GROUP_CONCAT(IFNULL(rcma.temperature_mean_min, "") ORDER BY rcma.month SEPARATOR ', ') AS temperature_mean_min,
GROUP_CONCAT(IFNULL(rcma.temperature_mean_max, "") ORDER BY rcma.month SEPARATOR ', ') AS temperature_mean_max,
GROUP_CONCAT(IFNULL(rcma.precipitation, "") ORDER BY rcma.month SEPARATOR ', ') AS precipitation,
GROUP_CONCAT(IFNULL(rcma.raindays, "") ORDER BY rcma.month SEPARATOR ', ') AS raindays,
GROUP_CONCAT(IFNULL(rcma.sunshine, "") ORDER BY rcma.month SEPARATOR ', ') AS sunshine,
GROUP_CONCAT(IFNULL(rcma.humidity, "") ORDER BY rcma.month SEPARATOR ', ') AS humidity
FROM region_climate_monthly_avg AS rcma
GROUP BY rcma.region_id) rcma ON rcma.region_id = regions.id
LEFT JOIN regions_byt ON regions.id = regions_byt.region_id
WHERE regions_byt.travelstyle = 1
AND regions.id = ?`,
[id]
);
for (k = 0; k < region.length; k++) {
//region[k].temperature_mean = climateArrayFormatting(region[k].temperature_mean);
//region[k].temperature_mean_min = climateArrayFormatting(region[k].temperature_mean_min);
region[k].temperature_mean_max = climateArrayFormatting(region[k].temperature_mean_max);
region[k].precipitation = climateArrayFormatting(region[k].precipitation);
region[k].rain_days = climateArrayFormatting(region[k].rain_days);
region[k].sun_hours = climateArrayFormatting(region[k].sun_hours);
//region[k].humidity = climateArrayFormatting(region[k].humidity);
}
const emptyArr = Array.from({ length: 12 }, () => null)
if (region.temperature_mean_max === null) region.temperature_mean_max = emptyArr
if (region.precipitation === null) region.precipitation = emptyArr
if (region.rain_days === null) region.rain_days = emptyArr
if (region.sun_hours === null) region.sun_hours = emptyArr
return region;
};

View File

@ -0,0 +1,43 @@
exports.getRegions = async (dbConn) => {
let sql = `SELECT
regions.id AS region_id,
regions.region AS name,
countries.country AS country
FROM regions
JOIN countries
ON regions.country_id = countries.id`
let regions = await dbConn.query(sql);
return regions;
};
exports.getRegionsInternal = async (dbConn) => {
let regions = await dbConn.query(`SELECT
regions.id AS region_id,
regions.region AS name,
countries.country AS country,
regions.meteostat_id AS meteostat_id
FROM regions
JOIN countries
ON regions.country_id = countries.id`);
return regions;
};
exports.getRegionsById = async (dbConn, id) => {
const region = await dbConn.query(
`SELECT regions.id AS region_id,
regions.region AS name,
regions.description,
countries.country AS country,
regions.meteostat_id AS meteostat_id
FROM regions
JOIN countries
ON regions.country_id = countries.id
WHERE regions.id = ?`,
[id]
);
return region;
};

View File

@ -0,0 +1,57 @@
const climateArrayFormatting = require("./../util/climateArrayFormatting.js");
const { takeRightWhile } = require("lodash");
module.exports = async (dbConn) => {
const regions = await dbConn.query(
`SELECT regions.id AS region_id,
regions.region AS name,
countries.country AS country,
regions.description AS description,
rcma.temperature_mean_max AS temperature_mean_max,
rcma.precipitation AS precipitation,
rcma.raindays AS rain_days,
rcma.sunshine AS sun_hours,
regions_byt.average_per_day AS average_per_day_costs,
regions_byt.accomodation AS accommodation_costs,
regions_byt.food AS food_costs,
regions_byt.water AS water_costs,
regions_byt.local_transportation AS local_transportation_costs,
regions_byt.entertainment AS entertainment_costs,
regions_byt.alcohol AS alcohol_costs
FROM regions
LEFT JOIN countries ON regions.country_id = countries.id
LEFT JOIN (SELECT rcma.region_id,
GROUP_CONCAT(IFNULL(rcma.temperature_mean,"") ORDER BY rcma.month SEPARATOR ', ') AS temperature_mean,
GROUP_CONCAT(IFNULL(rcma.temperature_mean_min, "") ORDER BY rcma.month SEPARATOR ', ') AS temperature_mean_min,
GROUP_CONCAT(IFNULL(rcma.temperature_mean_max, "") ORDER BY rcma.month SEPARATOR ', ') AS temperature_mean_max,
GROUP_CONCAT(IFNULL(rcma.precipitation, "") ORDER BY rcma.month SEPARATOR ', ') AS precipitation,
GROUP_CONCAT(IFNULL(rcma.raindays, "") ORDER BY rcma.month SEPARATOR ', ') AS raindays,
GROUP_CONCAT(IFNULL(rcma.sunshine, "") ORDER BY rcma.month SEPARATOR ', ') AS sunshine,
GROUP_CONCAT(IFNULL(rcma.humidity, "") ORDER BY rcma.month SEPARATOR ', ') AS humidity
FROM region_climate_monthly_avg AS rcma
GROUP BY rcma.region_id) rcma ON rcma.region_id = regions.id
LEFT JOIN regions_byt ON regions.id = regions_byt.region_id
WHERE regions_byt.travelstyle = 1`
);
for (k = 0; k < regions.length; k++) {
//regions[k].temperature_mean = climateArrayFormatting(regions[k].temperature_mean);
//regions[k].temperature_mean_min = climateArrayFormatting(regions[k].temperature_mean_min);
regions[k].temperature_mean_max = climateArrayFormatting(regions[k].temperature_mean_max);
regions[k].precipitation = climateArrayFormatting(regions[k].precipitation);
regions[k].rain_days = climateArrayFormatting(regions[k].rain_days);
regions[k].sun_hours = climateArrayFormatting(regions[k].sun_hours);
//regions[k].humidity = climateArrayFormatting(regions[k].humidity);
}
console.log(regions.filter(region => region.rain_days === null))
return regions.map(region => {
const emptyArr = Array.from({ length: 12 }, () => null)
if (region.temperature_mean_max === null) region.temperature_mean_max = emptyArr
if (region.precipitation === null) region.precipitation = emptyArr
if (region.rain_days === null) region.rain_days = emptyArr
if (region.sun_hours === null) region.sun_hours = emptyArr
return region
});
};

View File

@ -0,0 +1,25 @@
module.exports = async (dbConn) => {
let presets = await dbConn.query(
`SELECT search_presets.id AS preset_id,
search_presets.parameter AS parameter,
search_presets.name AS tag_label,
CASE
WHEN value_2 is NULL THEN value_1
ELSE CONCAT(search_presets.value_1,"|",search_presets.value_2)
END AS "value"
FROM search_presets`
);
for (k = 0; k < presets.length; k++) {
//if (presets[k].values.toString().includes("|")) {
const value = presets[k].value
presets[k].value = value.split("|");
for (i = 0; i < presets[k].value.length; i++) {
presets[k].value[i] = parseFloat(presets[k].value[i])
}
//} else {
// presets[k].values = parseInt(presets[k].values)
//}
}
return presets;
};

View File

@ -0,0 +1,90 @@
const axios = require('axios')
const _ = require('lodash')
// TODO: Automatically retrieve dates via aviable Data and get rid of random dates
const rangeStartDate = '2010-01' // If no date is given, this date will be used as startDate
const rangeEndDate = '2018-12'// If no date is given, this date will be used as endDate
// TODO: call method periodically, not over API
module.exports = async (dbConn, startDate = rangeStartDate, endDate = rangeEndDate) => {
console.log('update climate with:', startDate, endDate);
const result = await dbConn.query(`SELECT * FROM regions WHERE meteostat_id IS NOT NULL`)
const climateObject = await Promise.all(result.map(src => {
return createClimateObjectFrom(src, startDate, endDate)
}))
const climateObjectArr = climateObject.reduce((total, element) => total.concat(element), [])
await writeToDatabase(dbConn, climateObjectArr)
const res = `region_climate update complete. see backend logs for info.`
return res
}
async function createClimateObjectFrom(src, startDate, endDate) {
let res
if (src.meteostat_id === '') {
console.log("skipping: no meteostat id ")
return []
}
try {
res = await axios.get(`https://api.meteostat.net/v1/history/monthly?station=${src.meteostat_id}&start=${startDate}&end=${endDate}&key=${process.env.METEOSTAT_API_KEY}`)
} catch (error) {
console.log("error while getting data from meteostat: couldn't find results for following region: ")
console.log(src.region + " with meteostat_id " + src.meteostat_id)
console.log(error)
return []
}
if (!res.data.data) {
console.log("skipping: no data for station with meteostat_id " + src.meteostat_id + " (" + src.region + ")")
return []
}
const retVal = res.data.data.map(element => {
let result = {
region: src.region,
region_id: src.id,
year: element.month.split("-")[0],
month: element.month.split("-")[1],
temperature: element.temperature_mean,
temperature_min: element.temperature_mean_min,
temperature_max: element.temperature_mean_max,
precipitation: element.precipitation,
raindays: element.raindays,
sunshine: element.sunshine,
humidity: element.humidity ? element.humidity : null
}
//console.log(result)
return result
})
return retVal
}
async function writeToDatabase(dbConn, climateObjArr) {
for (const element of climateObjArr) {
//console.log(element)
try {
await dbConn.query(`
INSERT INTO region_climate
(region_id, year, month, temperature_mean, temperature_mean_min, temperature_mean_max, precipitation, sunshine, humidity, raindays)
VALUES (${element.region_id}, ${element.year}, ${element.month}, ${element.temperature}, ${element.temperature_min}, ${element.temperature_max}, ${element.precipitation}, ${element.sunshine}, ${element.humidity}, ${element.raindays})
ON DUPLICATE KEY UPDATE
temperature_mean = ${element.temperature},
temperature_mean_min = ${element.temperature_min},
temperature_mean_max = ${element.temperature_max},
precipitation = ${element.precipitation},
sunshine = ${element.sunshine},
humidity = ${element.humidity},
raindays = ${element.raindays};`)
} catch (error) {
if (error.code !== 'ER_DUP_ENTRY') {
console.log("element which causes problems: ")
console.log(element)
console.log("query which causes problems: ")
console.log(error)
} else {
console.log(element.region + ": " + error.sqlMessage)
}
}
}
};

View File

@ -1,92 +0,0 @@
var mysql = require('mysql2/promise');
require('dotenv').config()
// var connection = mysql.createConnection({
// host: process.env.DB_HOST,
// user: process.env.DB_USER,
// password: process.env.DB_PASSWORD,
// port: process.env.DB_PORT,
// database: 'travopti'
// });
const pool = mysql.createPool({
connectionLimit: 10,
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
port: process.env.DB_PORT,
database: 'travopti',
// typeCast: function (field, next) {
// if (field.type == "INT") {
// var value = field.string();
// return (value === null) ? null : Number(value);
// }
// return next();
// }
decimalNumbers: true
});
pool.getConnection()
.then(function (connection) {
console.log(`Connected to database: ${process.env.DB_HOST}`);
//pool.releaseConnection(connection)
})
.catch(function (error) {
console.error(error.message);
});
module.exports = pool;
// let travoptidb = {}
// travoptidb.all = () => {
// return new Promise((resolve, reject) => {
// pool.query(`SELECT * FROM regions`, (err, results) => {
// if (err) {
// return reject(err)
// }
// return resolve(results)
// })
// })
// }
// connection.connect((err) => {
// if (err) throw err;
// console.log('Database connected!')
// });
// exports.getRegions = () => {
// let sql = `SELECT * FROM regions`;
// console.log(connection.state)
// if (connection.state === 'disconnected') {
// setTimeout(() => console.log('waiting...'), 1000);
// }
// console.log('executed')
// let res = {}
// connection.query(sql, (error, results, fields) => {
// if (error) {
// return console.error(error.message);
// }
// console.log('innercallback(1)')
// res = results[0]
// });
// console.log('outsidecallback(2)')
// return res;
// }
// exports.getBYTdata = () => {
// connection.query(`SELECT * FROM regions_byt`, (error, results, fields) => {
// if (error) {
// return console.error(error.message);
// }
// console.log(results[0])
// nres = results.map((obj) => {
// return obj.region
// })
// //console.log(nres);
// });
// }
// exports.end = () => connection.end();
// module.exports = connection;

View File

@ -25,6 +25,16 @@
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==",
"dev": true
},
"@types/geojson": {
"version": "7946.0.7",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.7.tgz",
"integrity": "sha512-wE2v81i4C4Ol09RtsWFAqg3BUitWbHSpSlIo+bNdsCJijO9sjme+zm+73ZMCa/qMC8UEERxzGbvmr1cffo2SiQ=="
},
"@types/node": {
"version": "13.13.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.12.tgz",
"integrity": "sha512-zWz/8NEPxoXNT9YyF2osqyA9WjssZukYpgI4UYZpOjcyqwIUqWGkcCionaEb9Ki+FULyPyvNFpg/329Kd2/pbw=="
},
"abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@ -112,6 +122,14 @@
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true
},
"basic-auth": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
"integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
"requires": {
"safe-buffer": "5.1.2"
}
},
"binary-extensions": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz",
@ -645,6 +663,11 @@
"toidentifier": "1.0.0"
}
},
"httpolyglot": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/httpolyglot/-/httpolyglot-0.1.2.tgz",
"integrity": "sha1-5NNH/omEpi9GfUBg31J/GFH2mXs="
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -842,6 +865,30 @@
}
}
},
"mariadb": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/mariadb/-/mariadb-2.4.0.tgz",
"integrity": "sha512-78zrj9SpF6I3eVWMMkdm+SEfcsMb/uWVKPo7pKhhCfuGywEf3I1dK0ewSTjD0SyTEgSEuWn/H/I4TIErGgYTCQ==",
"requires": {
"@types/geojson": "^7946.0.7",
"@types/node": "^13.9.8",
"denque": "^1.4.1",
"iconv-lite": "^0.5.1",
"long": "^4.0.0",
"moment-timezone": "^0.5.31",
"please-upgrade-node": "^3.2.0"
},
"dependencies": {
"iconv-lite": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.5.2.tgz",
"integrity": "sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
}
}
},
"media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@ -901,6 +948,33 @@
"resolved": "https://registry.npmjs.org/moment/-/moment-2.26.0.tgz",
"integrity": "sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw=="
},
"moment-timezone": {
"version": "0.5.31",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.31.tgz",
"integrity": "sha512-+GgHNg8xRhMXfEbv81iDtrVeTcWt0kWmTEY1XQK14dICTXnWJnT0dxdlPspwqF3keKMVPXwayEsk1DI0AA/jdA==",
"requires": {
"moment": ">= 2.9.0"
}
},
"morgan": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz",
"integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==",
"requires": {
"basic-auth": "~2.0.1",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-finished": "~2.3.0",
"on-headers": "~1.0.2"
},
"dependencies": {
"depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
}
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@ -1025,6 +1099,11 @@
"ee-first": "1.1.1"
}
},
"on-headers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA=="
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@ -1065,6 +1144,15 @@
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
},
"path": {
"version": "0.12.7",
"resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz",
"integrity": "sha1-1NwqUGxM4hl+tIHr/NWzbAFAsQ8=",
"requires": {
"process": "^0.11.1",
"util": "^0.10.3"
}
},
"path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
@ -1076,12 +1164,25 @@
"integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==",
"dev": true
},
"please-upgrade-node": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz",
"integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==",
"requires": {
"semver-compare": "^1.0.0"
}
},
"prepend-http": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz",
"integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=",
"dev": true
},
"process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI="
},
"proxy-addr": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
@ -1214,6 +1315,11 @@
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true
},
"semver-compare": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
"integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w="
},
"semver-diff": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz",
@ -1471,6 +1577,14 @@
"prepend-http": "^2.0.0"
}
},
"util": {
"version": "0.10.4",
"resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
"integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
"requires": {
"inherits": "2.0.3"
}
},
"utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",

View File

@ -4,17 +4,22 @@
"description": "",
"main": "app.js",
"scripts": {
"start": "nodemon ./app.js"
"start": "nodemon ./index.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"axios": "^0.19.2",
"body-parser": "^1.19.0",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"httpolyglot": "^0.1.2",
"lodash": "^4.17.15",
"mariadb": "^2.4.0",
"moment": "^2.26.0",
"mysql2": "^2.1.0"
"morgan": "^1.10.0",
"mysql2": "^2.1.0",
"path": "^0.12.7"
},
"devDependencies": {
"nodemon": "^2.0.4"

11
backend/routes/climate.js Normal file
View File

@ -0,0 +1,11 @@
const router = require("express").Router()
const handleClimateUpdate = require("../models/handleClimateUpdate.js")
module.exports = dbConn => {
router.put("/api/v1/climate/update", async (req, res) => {
const update = await handleClimateUpdate(dbConn)
res.json(update)
});
return router;
};

View File

@ -0,0 +1,15 @@
const router = require("express").Router();
const getCountries = require("../models/getCountries.js");
const getCountryById = require("../models/getCountryById.js");
module.exports = dbConn => {
router.get("/api/v1/countries", async (req, res) => {
res.json(await getCountries(dbConn));
});
router.get("/api/v1/countries/:id", async (req, res) => {
const id = req.params.id;
res.json(await getCountryById(dbConn, id))
});
return router;
};

18
backend/routes/regions.js Normal file
View File

@ -0,0 +1,18 @@
const router = require("express").Router();
const getRegions = require("../models/getRegions2.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;
res.json(await getRegionById(dbConn, id))
});
return router;
};

69
backend/routes/search.js Normal file
View File

@ -0,0 +1,69 @@
const router = require("express").Router();
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", searchHandler(dbConn));
router.get("/api/v1/search/presets", presetHandler(dbConn));
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.temperature_mean_max) scoreQueryObj['temperature_mean_max'] = q.temperature_mean_max
if (q.precipitation) scoreQueryObj['precipitation'] = q.precipitation
if (q.rain_days) scoreQueryObj['rain_days'] = q.rain_days
if (q.sun_hours) scoreQueryObj['sun_hours'] = q.sun_hours
if (q.accommodation_costs) scoreQueryObj['accommodation_costs'] = q.accommodation_costs
if (q.food_costs) scoreQueryObj['food_costs'] = q.food_costs
if (q.alcohol_costs) scoreQueryObj['alcohol_costs'] = q.alcohol_costs
if (q.water_costs) scoreQueryObj['water_costs'] = q.water_costs
if (q.public_transportation_costs) scoreQueryObj['public_transportation_costs'] = q.public_transportation_costs
if (q.entertainment_costs) scoreQueryObj['entertainment_costs'] = q.entertainment_costs
if (q.average_per_day_costs) scoreQueryObj['average_per_day_costs'] = q.average_per_day_costs
//console.log(scoreQueryObj)
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)
})
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,77 +0,0 @@
const _ = require('lodash')
const fs = require('fs')
exports.transform = (data) => {
// get data
// let data = JSON.parse(fs.readFileSync('transformer-test.json'));
const types = Object.keys(data[0].scores)
// STEP 1 Create Response Array with region names from first climate object
let byRegion = data[0].climate.map(el => {
return {
region_id: el.region_id,
country_id: el.country_id,
name: el.name,
}
})
// STEP 2 Prepare flat scoreobject array and set days property
scoreObjs = _.flatten(_.map(data, (period) => {
return _.reduce(period.scores, (arr, el) => {
return arr.concat(el)
}).map(element => {
element.days = period.days
return element
})
}))
// STEP 3 Collect scoreobjects for each region
let results = byRegion.map(region => {
let scores = []
types.forEach(typ => {
let tempScores = _.filter(scoreObjs, { 'region_id': region.region_id, 'type': typ })
if (_.some(tempScores, { 'score': null })) {
console.log("found 'null' scores! skipping...")
//console.log(tempScores)
return
}
let averagedScore = {
region_id: region.region_id,
type: typ,
value: 0,
score: 0,
days: 0
}
tempScores.forEach(el => {
averagedScore.value += (el.value * el.days)
averagedScore.score += (el.score * el.days)
averagedScore.days += (el.days)
})
averagedScore.value = _.round(averagedScore.value / averagedScore.days, 1)
averagedScore.score = _.round(averagedScore.score / averagedScore.days, 1)
delete averagedScore.region_id
delete averagedScore.days
scores.push(averagedScore)
})
region.scores = scores
// STEP 4 Calculate Average Score
region.score = calculateAverage(region.scores)
//console.log(region)
return region
})
// console.log(results)
return _.orderBy(results, 'score', 'desc')
//end
}
function calculateAverage(scores) {
let sum = 0
scores.forEach(el => sum += el.score)
//console.log(sum)
return _.round(sum / scores.length, 2)
}

View File

@ -0,0 +1,10 @@
module.exports = (array) => {
if (array !== null && array !== undefined) {
const value = array
array = value.split(",");
for (i = 0; i < array.length; i++) {
array[i] = parseFloat(array[i])
}
}
return array;
}

View File

@ -0,0 +1,47 @@
const mariadb = require("mariadb");
let dbConn;
let conPool;
// mariadb doc: https://github.com/MariaDB/mariadb-connector-nodejs/blob/master/documentation/promise-api.md
async function reconnect() {
try {
dbConn = await conPool.getConnection();
} catch (e) {
if (e.code === "ECONNREFUSED") {
let err = new Error("Lost connection to the database");
err.code = "ERR_DB_CONN_LOST";
throw err;
} else {
throw e;
}
}
}
module.exports = async config => {
conPool = mariadb.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
port: process.env.DB_PORT,
database: 'travopti',
connectionLimit: 10
});
dbConn = await conPool.getConnection();
return {
async query(q, p) {
let res;
try {
res = await dbConn.query(q, p);
} catch (e) {
if (e.code === "ER_CMD_CONNECTION_CLOSED") {
await reconnect();
await this.query(q, p);
} else {
throw e;
}
}
return res;
}
};
};

View File

@ -0,0 +1,26 @@
module.exports = function (dbConn) {
return async function getAllRegionsWithClimatePerMonth(month) {
console.log('getAllRegionsWithClimatePerMonth')
const sql = `SELECT
region_climate.region_id AS region_id,
region_climate.month AS month,
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,
ROUND(AVG(region_climate.temperature_mean_max), 1) AS temperature_mean_max,
ROUND(AVG(region_climate.precipitation), 1) AS precipitation,
ROUND(AVG(region_climate.raindays), 1) AS rain_days,
ROUND(AVG(region_climate.sunshine), 1) AS sun_hours
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
}
}

View File

@ -0,0 +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(precipitation) AS precipitation,
MIN(raindays) AS rain_days,
MIN(sunshine) AS sun_hours
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(precipitation) AS precipitation,
MAX(raindays) AS rain_days,
MAX(sunshine) AS sun_hours
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] }
}

View File

@ -0,0 +1,5 @@
function getClimatePerRegionAndMonth(regionId, month) {
console.log('getClimatePerRegionAndMonth')
const sql = `SELECT region_id, AVG(temperature_mean), AVG(temperature_mean_min), AVG(temperature_mean_max), AVG(precipitation), AVG(sunshine) FROM region_climate WHERE month = ${month} AND region_id = ${regionId}`
return getQueryRows(sql)
}

View File

@ -0,0 +1,22 @@
exports.oldToNewQuerySyntax = function (queries) {
let res = {}
try {
if (queries.temperature_mean_max) res.temperature_mean_max = [Number(queries.temperature_mean_max.split(',')[0]), Number(queries.temperature_mean_max.split(',')[1])]
if (queries.precipitation) res.precipitation = [Number(queries.precipitation.split(',')[0]), Number(queries.precipitation.split(',')[1])]
if (queries.rain_days) res.rain_days = [Number(queries.rain_days.split(',')[0]), Number(queries.rain_days.split(',')[1])]
if (queries.sun_hours) res.sun_hours = [Number(queries.sun_hours.split(',')[0]), Number(queries.sun_hours.split(',')[1])]
if (queries.accommodation_costs) res.accommodation_costs = [Number(queries.accommodation_costs.split(',')[0]), Number(queries.accommodation_costs.split(',')[1])]
if (queries.food_costs) res.food_costs = [Number(queries.food_costs.split(',')[0]), Number(queries.food_costs.split(',')[1])]
if (queries.alcohol_costs) res.alcohol_costs = [Number(queries.alcohol_costs.split(',')[0]), Number(queries.alcohol_costs.split(',')[1])]
if (queries.water_costs) res.water_costs = [Number(queries.water_costs.split(',')[0]), Number(queries.water_costs.split(',')[1])]
if (queries.public_transportation_costs) res.public_transportation_costs = [Number(queries.public_transportation_costs.split(',')[0]), Number(queries.public_transportation_costs.split(',')[1])]
if (queries.entertainment_costs) res.entertainment_costs = [Number(queries.entertainment_costs.split(',')[0]), Number(queries.entertainment_costs.split(',')[1])]
if (queries.average_per_day_costs) res.average_per_day_costs = [Number(queries.average_per_day_costs.split(',')[0]), Number(queries.average_per_day_costs.split(',')[1])]
console.log('queries successfully transformed');
} catch (error) {
console.log('oldToNewQuerySyntax error');
return queries
}
return res
}

View File

@ -1,31 +1,19 @@
/**
*
*/
const multiplier_temperature = 5;
const _ = require('lodash')
/**
*
* @param {...any} scores expects objects which contains score and their weight
*/
exports.calculateAvgScore = (...scores) => {
return avgScore = scores.reduce((total, score) => total += score) / scores.length;
}
exports.calculateScoreRange = (min, max, multiplier, regionVal, sLowVal, sHighVal) => {
//console.log('scores.calculateScoreRange:', min, max, multiplier, regionVal, sLowVal, sHighVal)
console.log('scores.calculateScoreRange:', min, max, multiplier, regionVal, sLowVal, sHighVal)
// return full score when in range
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))
}

View File

@ -0,0 +1,260 @@
const _ = require('lodash')
const moment = require("moment")
const getClimateMinMax = require("./getClimateMinMax.js")
const oldToNewQuerySyntax = require("./oldToNewQuerySyntax.js")
const getAllRegionsWithClimatePerMonth = require('./getAllRegionsWithClimatePerMonth')
const score = require('./score')
// const getRegions = require('../models/getRegions.js').getRegionsInternal
const getRegions = require('../models/getRegions2.js')
const SHOW_ALL_SCOREOBJECTS = false
const MULTIPLIER = {
temperature_mean_max: 5,
precipitation: 3.5,
rain_days: 3,
sun_hours: 2.5,
accommodation_costs: 5,
food_costs: 5,
alcohol_costs: 5,
water_costs: 5,
public_transportation_costs: 5,
entertainment_costs: 5,
average_per_day_costs: 5
}
module.exports = function (dbConn) {
return async function (from, to, queries) {
console.log('search')
// PREPARE SEARCH
// validate dates
const dates = validateDates(from, to)
// transform syntax and seperate climate queries from price queries
queries = oldToNewQuerySyntax.oldToNewQuerySyntax(queries)
console.log(queries)
const q = prepareQueries(queries)
console.log('q', q)
// for calculating average if traveldates are in more than one month
const travelPeriods = travelPeriodsFromDates(dates)
// FETCH DATA FROM DB
const boundaryClimate = await getClimateMinMax.getClimateMinMax(dbConn)
let regions = await getRegions(dbConn)
regions.forEach(reg => reg.scores = [])
const boundaryCosts = {
max: {
accommodation_costs: 500,
food_costs: 100,
alcohol_costs: 100,
water_costs: 100,
public_transportation_costs: 100,
entertainment_costs: 100,
average_per_day_costs: 1000
},
min: {
accommodation_costs: 0,
food_costs: 0,
alcohol_costs: 0,
water_costs: 0,
public_transportation_costs: 0,
entertainment_costs: 0,
average_per_day_costs: 0
}
}
// little tweak to show score object without request
if (SHOW_ALL_SCOREOBJECTS) {
if (!q.climate.temperature_mean_max) q.climate.temperature_mean_max = [null, null]
if (!q.climate.precipitation) q.climate.precipitation = [null, null]
if (!q.climate.rain_days) q.climate.rain_days = [null, null]
if (!q.climate.sun_hours) q.climate.sun_hours = [null, null]
if (!q.climate.accommodation_costs) q.climate.accommodation_costs = [null, null]
}
// CALCULATE SCORES FOR CLIMATE PROPS
regions.forEach(reg => {
Object.entries(q.climate).forEach(([key, value]) => {
// console.log('key', key)
// console.log('val', value[0], value[1])
let finalScoreObj = getScoreAndAverageFromClimate(key, travelPeriods, reg, value[0], value[1], boundaryClimate)
reg.scores.push(finalScoreObj)
});
// CALCULATE SCORES FOR PRICE PROPS
Object.entries(q.costs).forEach(([key, value]) => {
console.log('key', key)
console.log('val', value[0], value[1])
let finalScoreObj = getScoreFromCosts(key, reg, value[0], value[1], boundaryCosts)
console.log(finalScoreObj);
reg.scores.push(finalScoreObj)
});
// CALCULATE AVERAGE SCORE
reg.score = calculateAverage(reg.scores)
})
return _.orderBy(regions, ({ score }) => score || 0, 'desc') //.filter(el => !_.isNaN(el.score))
}
function calculateAverage(scores) {
let sum = 0
scores.forEach(el => sum += el.score)
//console.log(sum)
return _.round(sum / scores.length, 2)
}
function prepareQueries(queries) {
let q = {
climate: {},
costs: {}
}
// climate
if (queries.temperature_mean_max) q.climate.temperature_mean_max = queries.temperature_mean_max
if (queries.precipitation) q.climate.precipitation = queries.precipitation
if (queries.rain_days) q.climate.rain_days = queries.rain_days
if (queries.sun_hours) q.climate.sun_hours = queries.sun_hours
// costs
if (queries.accommodation_costs) q.costs.accommodation_costs = queries.accommodation_costs
if (queries.food_costs) q.costs.food_costs = queries.food_costs
if (queries.alcohol_costs) q.costs.alcohol_costs = queries.alcohol_costs
if (queries.water_costs) q.costs.water_costs = queries.water_costs
if (queries.public_transportation_costs) q.costs.public_transportation_costs = queries.public_transportation_costs
if (queries.entertainment_costs) q.costs.entertainment_costs = queries.entertainment_costs
if (queries.average_per_day_costs) q.costs.average_per_day_costs = queries.average_per_day_costs
return q
}
function travelPeriodsFromDates(dates) {
//console.log(dates);
let travelPeriods = []
if (dates.from.month === dates.to.month) {
let period = {
month: dates.from.month,
days: dates.to.day - dates.from.day
}
travelPeriods.push(period)
} else {
for (let i = dates.from.month; i <= dates.to.month; i++) {
let period = {}
if (i === dates.from.month) {
period = {
month: i,
days: 32 - dates.from.day
}
} else if (i === dates.to.month) {
period = {
month: i,
days: dates.to.day
}
} else {
period = {
month: i,
days: 30
}
}
travelPeriods.push(period)
}
}
return travelPeriods
}
function validateDates(from, to) {
let fromAndTo = {
from: {},
to: {}
}
if (_.isNumber(from) && _.isNumber(to)) {
let dateFrom = new Date(from)
fromAndTo.from.day = dateFrom.getDate()
fromAndTo.from.month = dateFrom.getMonth() + 1
let dateTo = new Date(to)
fromAndTo.to.day = dateTo.getDate()
fromAndTo.to.month = dateTo.getMonth() + 1
//console.log(dateFrom.toUTCString(), dateTo.toUTCString())
if (moment(dateFrom).add(23, 'hours').isAfter(moment(dateTo))) throw new Error("ERR: 'to' must be at least one day after 'from'.")
} else {
// this block to still support old query syntax, validating from and to parameter
let re = /([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))/i;
fromAndTo.from.month = Number(from.split("-")[1])
fromAndTo.to.month = Number(to.split("-")[1])
fromAndTo.from.day = Number(from.split("-")[2])
fromAndTo.to.day = 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'.")
}
console.log(fromAndTo)
return fromAndTo
}
function getScoreAndAverageFromClimate(type, travelPeriods, region, searchLowParam, searchMaxParam, minMax) {
console.log('getScoreAndAverageFromClimate for', region.name, type)
// console.log(type, travelPeriods, searchLowParam, searchMaxParam)
const singleScores = travelPeriods.map(period => {
const sc = _.round(score.calculateScoreRange(minMax.min[type], minMax.max[type], MULTIPLIER[type], region[type][period.month - 1], searchLowParam, searchMaxParam), 2)
let res = {
//region_id: x.region_id,
type: type,
value: region[type][period.month - 1],
score: (region[type][period.month - 1] === null || searchLowParam === null) ? null : sc,
days: period.days
}
//console.log(res);
return res
})
let averagedScore = {
type: type,
value: 0,
score: 0,
days: 0
}
singleScores.forEach(el => {
if (el.value !== null) {
//console.log(el)
averagedScore.value += (el.value * el.days)
averagedScore.score += (el.score * el.days)
averagedScore.days += (el.days)
} else {
console.log('skip averaging')
console.log(el)
}
})
averagedScore.value = _.round(averagedScore.value / averagedScore.days, 1)
averagedScore.score = _.round(averagedScore.score / averagedScore.days, 1)
if (searchLowParam === null) averagedScore.score = null
delete averagedScore.days
return averagedScore
}
function getScoreFromCosts(type, region, searchLowParam, searchMaxParam, minMax) {
console.log('getScoreFromCosts for', region.name, type)
// console.log(type, travelPeriods, searchLowParam, searchMaxParam)
const sc = _.round(score.calculateScoreRange(minMax.min[type], minMax.max[type], MULTIPLIER[type], region[type], searchLowParam, searchMaxParam), 2)
let finScore = {
type: type,
value: region[type],
score: sc,
}
finScore.value = _.round(finScore.value, 1)
finScore.score = _.round(finScore.score, 1)
if (searchLowParam === null) finScore.score = null
return finScore
}
//end
}

11
package-lock.json generated Normal file
View File

@ -0,0 +1,11 @@
{
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"httpolyglot": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/httpolyglot/-/httpolyglot-0.1.2.tgz",
"integrity": "sha1-5NNH/omEpi9GfUBg31J/GFH2mXs="
}
}
}