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
## 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
- Run "(cd frontend && npm i)"
# Start dev server
### Start dev server
- Run "(cd frontend && npm run start)"

View File

@ -4,6 +4,8 @@ var sampledata = require('./sampledata')
var db = require('./mysql')
var score = require('./score')
var transformer = require('./transformer')
var climate = require('./climate')
var moment = require('moment')
const app = express()
const port = 3000
@ -34,7 +36,29 @@ const samplePresets = [
app.get('/', (req, res) => res.send('Hello Timo!'))
app.get('/v1/regions', (req, res) => res.json(sampleRegions))
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 = {}
response.meta = {
@ -42,7 +66,7 @@ app.get('/v1/search', (req, res) => {
query: req.query,
headers: req.headers
}
console.log('log params')
console.log(req.query.from)
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 => {
response.data = searchResults
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) {
console.log('search')
// get Min and Max values for each Parameter
const minMax = await getClimateMinMax()
// randomize if empty queries
@ -79,23 +102,28 @@ async function search(from, to, queries) {
queries.raindays = `${r},${r + 5}`
queries.sunhours = `${s},${s + 50}`
}
console.log('search')
// validate regex: YYYY-MM-DD
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 --
// calculate average if traveldates are in more than one month
let monthFrom = Number(from.split("-")[1])
let monthTo = Number(to.split("-")[1])
let travelPeriods = []
let virtDays = 0
if (monthFrom === monthTo) {
let element = {
month: monthFrom,
days: Number(to.split("-")[2]) - Number(from.split("-")[2])
}
virtDays = element.days
travelPeriods.push(element)
} else {
for (let index = monthFrom; index <= monthTo; index++) {
@ -116,7 +144,6 @@ async function search(from, to, queries) {
days: 30
}
}
virtDays += element.days
travelPeriods.push(element)
}
}
@ -142,51 +169,11 @@ async function search(from, to, queries) {
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) {
console.log('calculateScores for', type)
// console.log(searchLowParam)
// console.log(searchMaxParam)
// console.log(minMax)
//console.log(regionDataRows)
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
return {

View File

@ -5,7 +5,9 @@ const axios = require('axios')
const rangeStartDate = '2010-01'
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({
host: process.env.DB_HOST,
user: process.env.DB_USER,
@ -26,39 +28,42 @@ async function main() {
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 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 = '2010-01', endDate = '2018-12') {
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}`)
@ -92,17 +97,17 @@ async function createClimateObjectFrom(src, startDate = '2010-01', endDate = '20
return results
}
async function writeToDatabase(dbConnection, src) {
src.forEach(async (element) => {
async function writeToDatabase(dbConnection, climateObjArr) {
climateObjArr.forEach(async (element) => {
//console.log(element)
try {
if (!element.year) {
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});`)
} else {
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});`)
}
} catch (error) {
@ -116,6 +121,4 @@ async function writeToDatabase(dbConnection, src) {
}
}
});
}
main()
}

View File

@ -112,11 +112,6 @@
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"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": {
"version": "2.0.0",
"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",
"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": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
@ -787,11 +777,6 @@
"integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==",
"dev": true
},
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"json-buffer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz",
@ -911,22 +896,16 @@
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"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": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"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": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-2.1.0.tgz",
@ -1103,11 +1082,6 @@
"integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=",
"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": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
@ -1180,20 +1154,6 @@
"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": {
"version": "3.4.0",
"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": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
@ -1519,11 +1471,6 @@
"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": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",

View File

@ -13,7 +13,7 @@
"dotenv": "^8.2.0",
"express": "^4.17.1",
"lodash": "^4.17.15",
"mysql": "^2.18.1",
"moment": "^2.26.0",
"mysql2": "^2.1.0"
},
"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;