diff --git a/README.md b/README.md index a452ec5..d5aebb6 100644 --- a/README.md +++ b/README.md @@ -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)" diff --git a/backend/app.js b/backend/app.js index 1321d14..c3b7ffa 100644 --- a/backend/app.js +++ b/backend/app.js @@ -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 { diff --git a/backend/climate.js b/backend/climate.js index 96ff87c..f0ca4a2 100644 --- a/backend/climate.js +++ b/backend/climate.js @@ -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() \ No newline at end of file +} \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json index 249085f..7d000fc 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -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", diff --git a/backend/package.json b/backend/package.json index c07ce33..c02c6df 100644 --- a/backend/package.json +++ b/backend/package.json @@ -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": { diff --git a/backend/sampledata.js b/backend/sampledata.js deleted file mode 100644 index 5c5e8a0..0000000 --- a/backend/sampledata.js +++ /dev/null @@ -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; \ No newline at end of file