little cleanup

This commit is contained in:
Timo Volkmann 2020-06-14 13:33:07 +02:00
parent 2e4ec7b24e
commit bbf483e445
6 changed files with 125 additions and 168 deletions

View File

@ -2,9 +2,45 @@
Campus Cup AKMC Data Traveloptimizer Campus Cup AKMC Data Traveloptimizer
## Installation ## Backend
### Requirements
- MariaDB or MySQL
- node `10.12` or higher
- Configure database in `.env`-file or environment variables. See `.env` for reference
- Set API-Key for meteostat.net in `.env`-file or environment variable
- import `setup.sql` for sample data
### Start
- Run `$(cd backend && npm run start)`
- call http://localhost:3000/v1/update/climate to fetch climate data for sample entries.
### Search
Customize your search with query parameters. For now, only climate parameters are supported. If you omit climate queries, all climate parameters will be randomized.
Following queries are supperted by now:
- from=YYYY-MM-DD _(required)_
- to=YYYY-MM-DD _(required)_
- temperature=NUMBER,NUMBER
- raindays=NUMBER,NUMBER
- sunhours=NUMBER,NUMBER
- percipitation=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
http://localhost:3000/v1/search?from=2020-06-14&to=2020-07-29
### More
To get more search results, add more entries with meteostat station IDs to the `regions` table in the database
## Frontend
### Installation
- Install node 10.15.3 - Install node 10.15.3
- Run "(cd frontend && npm i)" - Run "(cd frontend && npm i)"
# Start dev server ### Start dev server
- Run "(cd frontend && npm run start)" - Run "(cd frontend && npm run start)"

View File

@ -4,6 +4,8 @@ var sampledata = require('./sampledata')
var db = require('./mysql') var db = require('./mysql')
var score = require('./score') var score = require('./score')
var transformer = require('./transformer') var transformer = require('./transformer')
var climate = require('./climate')
var moment = require('moment')
const app = express() const app = express()
const port = 3000 const port = 3000
@ -34,7 +36,29 @@ const samplePresets = [
app.get('/', (req, res) => res.send('Hello Timo!')) app.get('/', (req, res) => res.send('Hello Timo!'))
app.get('/v1/regions', (req, res) => res.json(sampleRegions)) app.get('/v1/regions', (req, res) => res.json(sampleRegions))
app.get('/v1/presets', (req, res) => res.json(samplePresets)) app.get('/v1/presets', (req, res) => res.json(samplePresets))
app.get('/v1/search', (req, res) => { app.get('/v1/search', searchHandler)
app.get('/v1/update/climate', climateUpdateHandler)
app.listen(port, () => console.log(`Travopti backend listening at http://localhost:${port}`))
function climateUpdateHandler(req, res) {
let parameter = []
if (req.query.startDate) parameter.push(req.query.startDate)
if (req.query.endDate) parameter.push(req.query.endDate)
climate.update(...parameter).then(x => {
res.send(x)
}).catch(e => {
let result = {
message: 'error during update process. check backend logs.',
error: e
}
res.send(result)
}
)
}
function searchHandler(req, res) {
let response = {} let response = {}
response.meta = { response.meta = {
@ -42,7 +66,7 @@ app.get('/v1/search', (req, res) => {
query: req.query, query: req.query,
headers: req.headers headers: req.headers
} }
console.log('log params') console.log('log params')
console.log(req.query.from) console.log(req.query.from)
console.log(req.query.to) console.log(req.query.to)
@ -57,15 +81,14 @@ app.get('/v1/search', (req, res) => {
search(req.query.from, req.query.to, validQueries).then(searchResults => { search(req.query.from, req.query.to, validQueries).then(searchResults => {
response.data = searchResults response.data = searchResults
res.json(response) res.json(response)
}).catch(e => {
console.log(e)
res.json(e.message)
}) })
}) }
app.listen(port, () => console.log(`Travopti backend listening at http://localhost:${port}`))
async function search(from, to, queries) { async function search(from, to, queries) {
console.log('search')
// get Min and Max values for each Parameter // get Min and Max values for each Parameter
const minMax = await getClimateMinMax() const minMax = await getClimateMinMax()
// randomize if empty queries // randomize if empty queries
@ -79,23 +102,28 @@ async function search(from, to, queries) {
queries.raindays = `${r},${r + 5}` queries.raindays = `${r},${r + 5}`
queries.sunhours = `${s},${s + 50}` queries.sunhours = `${s},${s + 50}`
} }
console.log('search')
// validate regex: YYYY-MM-DD // validate regex: YYYY-MM-DD
let re = /([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))/i; let re = /([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))/i;
if (!from.match(re) || !to.match(re)) throw new Error("invalid parameter: " + from + " " + to) if (!from.match(re) || !to.match(re)) throw new Error("ERR: invalid parameter:",from,to)
// check for valid period
if (moment(from, 'YYYY-MM-DD').isAfter(moment(to, 'YYYY-MM-DD'))) {
console.log(moment(from, 'YYYY-MM-DD'))
console.log(moment(to, 'YYYY-MM-DD'))
console.log(moment(from, 'YYYY-MM-DD').isAfter(moment(to, 'YYYY-MM-DD')))
throw new Error("ERR: from is before to date.")
}
// -- Prepare search -- // -- Prepare search --
// calculate average if traveldates are in more than one month // calculate average if traveldates are in more than one month
let monthFrom = Number(from.split("-")[1]) let monthFrom = Number(from.split("-")[1])
let monthTo = Number(to.split("-")[1]) let monthTo = Number(to.split("-")[1])
let travelPeriods = [] let travelPeriods = []
let virtDays = 0
if (monthFrom === monthTo) { if (monthFrom === monthTo) {
let element = { let element = {
month: monthFrom, month: monthFrom,
days: Number(to.split("-")[2]) - Number(from.split("-")[2]) days: Number(to.split("-")[2]) - Number(from.split("-")[2])
} }
virtDays = element.days
travelPeriods.push(element) travelPeriods.push(element)
} else { } else {
for (let index = monthFrom; index <= monthTo; index++) { for (let index = monthFrom; index <= monthTo; index++) {
@ -116,7 +144,6 @@ async function search(from, to, queries) {
days: 30 days: 30
} }
} }
virtDays += element.days
travelPeriods.push(element) travelPeriods.push(element)
} }
} }
@ -142,51 +169,11 @@ async function search(from, to, queries) {
minMax: minMax minMax: minMax
} }
} }
// return { TODO___scores: detailScores }
// return { TODO___scores: calculateAverageScore(detailScores, Object.keys(queries)) }
}
function calculateAverageScore(scorePeriods, types) {
const days = scorePeriods.reduce((total, period) => total += period.days)
let totalScores = {}
let finalRegionObj = {
}
scorePeriods.forEach(element => {
types.forEach(type => {
element.scores[type].forEach(regionScore => {
})
})
})
}
function transformToRegionObjects(scorePeriods) {
let response = []
// create region objects
scorePeriods[0].climate.forEach(element => {
let obj = {
region_id: element.region_id,
country_id: element.country_id,
name: element.name,
}
response.push(obj)
})
//
} }
function calculateScores(type, regionDataRows, searchLowParam, searchMaxParam, minMax) { function calculateScores(type, regionDataRows, searchLowParam, searchMaxParam, minMax) {
console.log('calculateScores for', type) console.log('calculateScores for', type)
// console.log(searchLowParam)
// console.log(searchMaxParam)
// console.log(minMax)
//console.log(regionDataRows)
let result = regionDataRows.map(x => { let result = regionDataRows.map(x => {
// console.log(x.temperature_mean)
const sc = Math.round(score.calculateScoreRange(minMax.min[type], minMax.max[type], multiplier[type], x[type], searchLowParam, searchMaxParam) * 100) / 100 const sc = Math.round(score.calculateScoreRange(minMax.min[type], minMax.max[type], multiplier[type], x[type], searchLowParam, searchMaxParam) * 100) / 100
return { return {

View File

@ -5,7 +5,9 @@ const axios = require('axios')
const rangeStartDate = '2010-01' const rangeStartDate = '2010-01'
const rangeEndDate = '2018-12' const rangeEndDate = '2018-12'
async function main() { exports.update = async function (startDate = rangeStartDate, endDate = rangeEndDate) {
console.log('update climate with:', startDate, endDate);
const connection = await mysql.createConnection({ const connection = await mysql.createConnection({
host: process.env.DB_HOST, host: process.env.DB_HOST,
user: process.env.DB_USER, user: process.env.DB_USER,
@ -26,39 +28,42 @@ async function main() {
await writeToDatabase(connection, final2) await writeToDatabase(connection, final2)
connection.end(); connection.end();
let response = 'database update complete. see backend logs for info.'
console.log(response)
return response
} }
async function createClimateObject(src) { // async function createClimateObject(src) {
let response // let response
try { // try {
response = await axios.get(`https://api.meteostat.net/v1/climate/normals?station=${src.meteostat_id}&key=${process.env.METEOSTAT_API_KEY}`) // response = await axios.get(`https://api.meteostat.net/v1/climate/normals?station=${src.meteostat_id}&key=${process.env.METEOSTAT_API_KEY}`)
} catch (error) { // } catch (error) {
console.log("skipping: couldn't find results for following region: ") // console.log("skipping: couldn't find results for following region: ")
console.log(src.region + " with meteostat_id " + src.meteostat_id) // console.log(src.region + " with meteostat_id " + src.meteostat_id)
return [] // return []
} // }
if (!response.data.data) { // if (!response.data.data) {
console.log("skipping: no data for station meteostat_id " + src.meteostat_id + " (" + src.region + ")") // console.log("skipping: no data for station meteostat_id " + src.meteostat_id + " (" + src.region + ")")
return [] // return []
} // }
let results = [] // let results = []
for (let index = 1; index <= 12; index++) { // for (let index = 1; index <= 12; index++) {
let result = { // let result = {
region: src.region, // region: src.region,
region_id: src.id, // region_id: src.id,
month: index, // month: index,
temperature: Object.values(response.data.data.temperature)[index - 1] ? Object.values(response.data.data.temperature)[index - 1] : null, // 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_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, // 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, // 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, // sunshine: Object.values(response.data.data.sunshine)[index - 1] ? Object.values(response.data.data.sunshine)[index - 1] : null,
} // }
results.push(result) // results.push(result)
} // }
return results // return results
} // }
async function createClimateObjectFrom(src, startDate = '2010-01', endDate = '2018-12') { async function createClimateObjectFrom(src, startDate, endDate) {
let response let response
try { 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}`) 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}`)
@ -92,17 +97,17 @@ async function createClimateObjectFrom(src, startDate = '2010-01', endDate = '20
return results return results
} }
async function writeToDatabase(dbConnection, src) { async function writeToDatabase(dbConnection, climateObjArr) {
src.forEach(async (element) => { climateObjArr.forEach(async (element) => {
//console.log(element) //console.log(element)
try { try {
if (!element.year) { if (!element.year) {
await dbConnection.execute(` await dbConnection.execute(`
INSERT INTO region_climate (region_id, year, month, temperature_mean, temperature_mean_min, temperature_mean_max, percipitation, sunshine) 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});`) VALUES (${element.region_id}, 0, ${element.month}, ${element.temperature}, ${element.temperature_min}, ${element.temperature_max}, ${element.precipitation}, ${element.sunshine});`)
} else { } else {
await dbConnection.execute(` await dbConnection.execute(`
INSERT INTO region_climate (region_id, year, month, temperature_mean, temperature_mean_min, temperature_mean_max, percipitation, sunshine, humidity, raindays) 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});`) 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) { } catch (error) {
@ -116,6 +121,4 @@ async function writeToDatabase(dbConnection, src) {
} }
} }
}); });
} }
main()

View File

@ -112,11 +112,6 @@
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true "dev": true
}, },
"bignumber.js": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz",
"integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A=="
},
"binary-extensions": { "binary-extensions": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz",
@ -349,11 +344,6 @@
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
}, },
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"crypto-random-string": { "crypto-random-string": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
@ -787,11 +777,6 @@
"integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==", "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==",
"dev": true "dev": true
}, },
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"json-buffer": { "json-buffer": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz",
@ -911,22 +896,16 @@
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true "dev": true
}, },
"moment": {
"version": "2.26.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.26.0.tgz",
"integrity": "sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw=="
},
"ms": { "ms": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
}, },
"mysql": {
"version": "2.18.1",
"resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz",
"integrity": "sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==",
"requires": {
"bignumber.js": "9.0.0",
"readable-stream": "2.3.7",
"safe-buffer": "5.1.2",
"sqlstring": "2.3.1"
}
},
"mysql2": { "mysql2": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-2.1.0.tgz", "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-2.1.0.tgz",
@ -1103,11 +1082,6 @@
"integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=",
"dev": true "dev": true
}, },
"process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"proxy-addr": { "proxy-addr": {
"version": "2.0.6", "version": "2.0.6",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
@ -1180,20 +1154,6 @@
"strip-json-comments": "~2.0.1" "strip-json-comments": "~2.0.1"
} }
}, },
"readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"readdirp": { "readdirp": {
"version": "3.4.0", "version": "3.4.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz",
@ -1375,14 +1335,6 @@
} }
} }
}, },
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"requires": {
"safe-buffer": "~5.1.0"
}
},
"strip-ansi": { "strip-ansi": {
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
@ -1519,11 +1471,6 @@
"prepend-http": "^2.0.0" "prepend-http": "^2.0.0"
} }
}, },
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"utils-merge": { "utils-merge": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",

View File

@ -13,7 +13,7 @@
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"express": "^4.17.1", "express": "^4.17.1",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"mysql": "^2.18.1", "moment": "^2.26.0",
"mysql2": "^2.1.0" "mysql2": "^2.1.0"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1,16 +0,0 @@
var sampledata = {}
sampledata.search_response_model = [
{
region_id: 1,
name: 'Chicago',
country: 'USA',
avg_score: 0.0,
scores: {
temperature: 0.0,
humidity: 0.0,
}
}
]
module.exports = sampledata;