Compare commits

..

No commits in common. "develop" and "feature/tags-search-backend" have entirely different histories.

64 changed files with 478 additions and 2690 deletions

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -1,6 +1,5 @@
PORT=3000
METEOSTAT_API_KEY=LMlDskju
METEOSTAT_API_KEY_V2=O9X1xxKjheNwF1vfLcdRMmQ9JlobOugL
DB_HOST=lhinderberger.dev
DB_USER=root
DB_PASSWORD=devtest

View File

@ -14,50 +14,14 @@ const port = process.env.PORT
const search = require("./routes/search");
const regions = require("./routes/regions");
const countries = require("./routes/countries");
const climate = require("./routes/climate");
const places = require("./routes/place");
const update = require("./routes/update");
const app = express();
// Swagger API doc set up
const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');
const swaggerOptions = {
swaggerDefinition: {
openapi: "3.0.0",
info: {
title: "TravOpti API",
version: "1.0.0",
description:
"Enable intrest controlled region searching with this API\n" +
"No API Key required.",
license: {
name: "Licensing Pending",
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ&feature=youtu.be"
},
contact: {
name: "travOpti",
url: "https://travopti.de/home",
email: "feedback@travopti.de"
}
},
servers: [
{
url: "https://travopti.de/api/v1"
}
]
},
apis: [
"./Routes/*.js",
"./Models/handleClimateUpdate.js",
"./Models/handleClimateUpdateV2.js",
]
};
const swaggerDocs = swaggerJsdoc(swaggerOptions);
(async () => {
try {
// Connect to MariaDB
const dbConn = await dbConnection();
// Express middleware
@ -65,12 +29,12 @@ const swaggerDocs = swaggerJsdoc(swaggerOptions);
app.use(express.static(path.join(__dirname, "../../dist")));
app.use(bodyParser.json());
app.use(cors());
app.use('/api/v1/doc', swaggerUi.serve, swaggerUi.setup(swaggerDocs, {explorer: false, docExpansion: "list"}));
// Express routes
app.use(search(dbConn));
app.use(regions(dbConn));
app.use(countries(dbConn));
app.use(climate(dbConn));
app.use(places(dbConn));
app.use(update(dbConn))
@ -88,6 +52,7 @@ const swaggerDocs = swaggerJsdoc(swaggerOptions);
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

@ -1,7 +1,7 @@
const axios = require("axios")
const getPlacePhoto = require("./getPlacePhoto.js")
const fields = "photos,place_id,name,rating,geometry" // Parameters for Google Place API
const fields = "photos,place_id,name,rating,geometry"
module.exports = async (q) => {
const res = await axios.get(

View File

@ -1,9 +1,9 @@
const axios = require("axios")
const getPlacePhoto = require("./getPlacePhoto.js")
const radius = 20000 // Search radius in meters
const rankby = "prominence" // Sorting of results
const types = "tourist_attraction" // Category which shall be searched
const radius = 20000
const rankby = "prominence"
const types = "tourist_attraction"
module.exports = async (lat, lng) => {
const res = await axios.get(

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

@ -26,18 +26,18 @@ module.exports = async (dbConn, id) => {
FROM regions
LEFT JOIN countries ON regions.country_id = countries.id
LEFT JOIN (SELECT rcma.region_id,
GROUP_CONCAT(rcma.temperature_mean ORDER BY rcma.month SEPARATOR ', ') AS temperature_mean,
GROUP_CONCAT(rcma.temperature_mean_min ORDER BY rcma.month SEPARATOR ', ') AS temperature_mean_min,
GROUP_CONCAT(rcma.temperature_mean_max ORDER BY rcma.month SEPARATOR ', ') AS temperature_mean_max,
GROUP_CONCAT(rcma.precipitation ORDER BY rcma.month SEPARATOR ', ') AS precipitation,
GROUP_CONCAT(rcma.rain_days ORDER BY rcma.month SEPARATOR ', ') AS rain_days,
GROUP_CONCAT(rcma.sun_hours ORDER BY rcma.month SEPARATOR ', ') AS sun_hours,
GROUP_CONCAT(rcma.humidity ORDER BY rcma.month SEPARATOR ', ') AS humidity
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.rain_days, "") ORDER BY rcma.month SEPARATOR ', ') AS rain_days,
GROUP_CONCAT(IFNULL(rcma.sun_hours, "") ORDER BY rcma.month SEPARATOR ', ') AS sun_hours,
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
LEFT JOIN (SELECT rtma.region_id,
GROUP_CONCAT(rtma.avg_price_relative ORDER BY rtma.month SEPARATOR ', ') AS avg_price_relative
GROUP_CONCAT(IFNULL(rtma.avg_price_relative,"") ORDER BY rtma.month SEPARATOR ', ') AS avg_price_relative
FROM regions_trivago_monthly_avg AS rtma
GROUP BY rtma.region_id) rtma
ON regions.id = rtma.region_id
@ -56,6 +56,18 @@ module.exports = async (dbConn, id) => {
region.sun_hours = arrayFormatting(region.sun_hours);
region.humidity = arrayFormatting(region.humidity);
const emptyArr = Array.from({length: 12}, () => null)
if (region.avg_price_relative === null) region.avg_price_relative = emptyArr
if (region.temperature_mean === null) region.temperature_mean = emptyArr
if (region.temperature_mean_min === null) region.temperature_mean_min = emptyArr
if (region.temperature_mean_max === null) region.temperature_mean_max = emptyArr
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
if (region.humidity === null) region.humidity = emptyArr
return region;
};

View File

@ -26,23 +26,22 @@ module.exports = async (dbConn) => {
FROM regions
LEFT JOIN countries ON regions.country_id = countries.id
LEFT JOIN (SELECT rcma.region_id,
GROUP_CONCAT(rcma.temperature_mean ORDER BY rcma.month SEPARATOR ', ') AS temperature_mean,
GROUP_CONCAT(rcma.temperature_mean_min ORDER BY rcma.month SEPARATOR ', ') AS temperature_mean_min,
GROUP_CONCAT(rcma.temperature_mean_max ORDER BY rcma.month SEPARATOR ', ') AS temperature_mean_max,
GROUP_CONCAT(rcma.precipitation ORDER BY rcma.month SEPARATOR ', ') AS precipitation,
GROUP_CONCAT(rcma.rain_days ORDER BY rcma.month SEPARATOR ', ') AS rain_days,
GROUP_CONCAT(rcma.sun_hours ORDER BY rcma.month SEPARATOR ', ') AS sun_hours,
GROUP_CONCAT(rcma.humidity ORDER BY rcma.month SEPARATOR ', ') AS humidity
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.rain_days, "") ORDER BY rcma.month SEPARATOR ', ') AS rain_days,
GROUP_CONCAT(IFNULL(rcma.sun_hours, "") ORDER BY rcma.month SEPARATOR ', ') AS sun_hours,
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
LEFT JOIN (SELECT rtma.region_id,
GROUP_CONCAT(rtma.avg_price_relative ORDER BY rtma.month SEPARATOR ', ') AS avg_price_relative
GROUP_CONCAT(IFNULL(rtma.avg_price_relative,"") ORDER BY rtma.month SEPARATOR ', ') AS avg_price_relative
FROM regions_trivago_monthly_avg AS rtma
GROUP BY rtma.region_id) rtma
ON regions.id = rtma.region_id
WHERE regions_byt.travelstyle = 1`
const [regions, tags] = await Promise.all([dbConn.query(sqlRegions), allTagsWithValues(dbConn)])
for (k = 0; k < regions.length; k++) {
@ -54,10 +53,20 @@ module.exports = async (dbConn) => {
regions[k].rain_days = arrayFormatting(regions[k].rain_days);
regions[k].sun_hours = arrayFormatting(regions[k].sun_hours);
regions[k].humidity = arrayFormatting(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.avg_price_relative === null) region.avg_price_relative = emptyArr
if (region.temperature_mean === null) region.temperature_mean = emptyArr
if (region.temperature_mean_min === null) region.temperature_mean_min = emptyArr
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
if (region.humidity === null) region.humidity = emptyArr
region.tags = tags.filter(tag => tag.region_id === region.region_id).map(tag => {
delete tag.region_id
return tag

View File

@ -11,11 +11,15 @@ module.exports = async (dbConn) => {
);
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

@ -7,16 +7,7 @@ exports.getUniqueTags = async (dbConn) => {
exports.allTagsWithValues = async (dbConn) => {
let tags = await dbConn.query(
`SELECT region_id, name, value FROM region_feedback;`
);
return tags;
};
exports.getTagsByRegionId = async (dbConn, id) => {
let tags = await dbConn.query(
`SELECT region_id, name, value
FROM region_feedback
WHERE region_id = ${id};`
`SELECT * FROM region_feedback;`
);
return tags;
};

View File

@ -1,12 +1,11 @@
const axios = require('axios')
const _ = require('lodash')
// Constants
// TODO: Automatically retrieve dates via aviable Data from database and get rid of "random" dates
// TODO: Automatically retrieve dates via aviable Data and get rid of random dates
const rangeStartDate = '2019-01' // If no date is given, this date will be used as startDate
const rangeEndDate = '2020-05'// If no date is given, this date will be used as endDate
// TODO: call method periodically, not over API (fine for prototyping, tho)
// TODO: call method periodically, not over API
module.exports = async (dbConn, startDate = rangeStartDate, endDate = rangeEndDate) => {
console.log('update climate with:', startDate, endDate);
@ -55,6 +54,7 @@ async function createClimateObjectFrom(src, startDate, endDate) {
sun_hours: element.sunshine,
humidity: element.humidity ? element.humidity : null
}
//console.log(result)
return result
})
return retVal
@ -62,6 +62,7 @@ async function createClimateObjectFrom(src, startDate, endDate) {
async function writeToDatabase(dbConn, climateObjArr) {
for (const element of climateObjArr) {
//console.log(element)
try {
await dbConn.query(`
INSERT INTO region_climate

View File

@ -1,110 +0,0 @@
const axios = require('axios')
const _ = require('lodash')
// Constants
// TODO: Automatically retrieve dates via aviable Data from database and get rid of "random" dates
const rangeStartDate = '2019-01-01' // If no date is given, this date will be used as startDate
const rangeEndDate = '2019-12-31'// If no date is given, this date will be used as endDate
// TODO: call method periodically, not over API (fine for prototyping, tho)
module.exports = async (dbConn, startDate = rangeStartDate, endDate = rangeEndDate) => {
console.log('update climate with:', startDate, endDate);
const result = await dbConn.query(`SELECT id, region, lon, lat FROM regions`)
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 v2 complete. see backend logs for info.`
return climateObjectArr
}
async function createClimateObjectFrom(src, startDate, endDate) {
let res
try {
res = await axios.get(
`https://api.meteostat.net/v2/point/daily?lat=${src.lat}&lon=${src.lon}&start=${startDate}&end=${endDate}`,
{
headers: {
"x-api-key": process.env.METEOSTAT_API_KEY_V2
}//,
//httpsAgent: agent
})
} catch (error) {
console.log("error while getting data from meteostat: couldn't find results for following region: ")
console.log(src.region,"with coords:",src.lon,src.lat)
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.date.split("-")[0],
month: element.date.split("-")[1],
day: element.date.split("-")[2],
temperature_mean: element.tavg,
temperature_mean_min: element.tmin,
temperature_mean_max: element.tmax,
precipitation: element.prcp,
rain_days: element.prcp > 2 ? 1:0, // More than 2mm => rainday
sun_hours: element.tsun/60
}
//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_day
(region_id, year, month, day, temperature_mean, temperature_mean_min, temperature_mean_max, precipitation, sun_hours, rain_days)
VALUES (${element.region_id}, ${element.year}, ${element.month}, ${element.day}, ${element.temperature_mean}, ${element.temperature_mean_min}, ${element.temperature_mean_max}, ${element.precipitation}, ${element.sun_hours}, ${element.rain_days})
ON DUPLICATE KEY UPDATE
temperature_mean = ${element.temperature_mean},
temperature_mean_min = ${element.temperature_mean_min},
temperature_mean_max = ${element.temperature_mean_max},
precipitation = ${element.precipitation},
sun_hours = ${element.sun_hours},
rain_days = ${element.rain_days};`)
} 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)
}
}
}
};
/*
INSERT INTO region_climate
(region_id, YEAR, MONTH, temperature_mean, temperature_mean_min, temperature_mean_max, precipitation, rain_days, sun_hours)
SELECT
region_id,
YEAR,
MONTH,
ROUND(AVG(temperature_mean),2) AS temperature_mean,
MIN(temperature_mean_min) AS temperature_mean_min,
MAX(temperature_mean_max) AS temperature_mean_max,
ROUND(SUM(precipitation),2) AS precipitation,
SUM(rain_days) AS rain_days,
SUM(sun_hours) AS sun_hours
FROM region_climate_day
GROUP BY region_id, YEAR, month
*/

View File

@ -1,7 +1,7 @@
const axios = require("axios")
const getRegions = require("../models/getRegions.js")
const fields = "geometry" // Parameters for Google Places API
const fields = "geometry"
module.exports = async (dbConn) => {
const regions = await getRegions(dbConn)

View File

@ -4,45 +4,6 @@
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@apidevtools/json-schema-ref-parser": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-8.0.0.tgz",
"integrity": "sha512-n4YBtwQhdpLto1BaUCyAeflizmIbaloGShsPyRtFf5qdFJxfssj+GgLavczgKJFa3Bq+3St2CKcpRJdjtB4EBw==",
"requires": {
"@jsdevtools/ono": "^7.1.0",
"call-me-maybe": "^1.0.1",
"js-yaml": "^3.13.1"
}
},
"@apidevtools/openapi-schemas": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.0.3.tgz",
"integrity": "sha512-QoPaxGXfgqgGpK1p21FJ400z56hV681a8DOcZt3J5z0WIHgFeaIZ4+6bX5ATqmOoCpRCsH4ITEwKaOyFMz7wOA=="
},
"@apidevtools/swagger-methods": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.1.tgz",
"integrity": "sha512-1Vlm18XYW6Yg7uHunroXeunWz5FShPFAdxBbPy8H6niB2Elz9QQsCoYHMbcc11EL1pTxaIr9HXz2An/mHXlX1Q=="
},
"@apidevtools/swagger-parser": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-9.0.1.tgz",
"integrity": "sha512-Irqybg4dQrcHhZcxJc/UM4vO7Ksoj1Id5e+K94XUOzllqX1n47HEA50EKiXTCQbykxuJ4cYGIivjx/MRSTC5OA==",
"requires": {
"@apidevtools/json-schema-ref-parser": "^8.0.0",
"@apidevtools/openapi-schemas": "^2.0.2",
"@apidevtools/swagger-methods": "^3.0.0",
"@jsdevtools/ono": "^7.1.0",
"call-me-maybe": "^1.0.1",
"openapi-types": "^1.3.5",
"z-schema": "^4.2.2"
}
},
"@jsdevtools/ono": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.2.tgz",
"integrity": "sha512-qS/a24RA5FEoiJS9wiv6Pwg2c/kiUo3IVUQcfeM9JvsR6pM8Yx+yl/6xWYLckZCT5jpLNhslgjiA8p/XcGyMRQ=="
},
"@sindresorhus/is": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
@ -142,14 +103,6 @@
"picomatch": "^2.0.4"
}
},
"argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"requires": {
"sprintf-js": "~1.0.2"
}
},
"array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@ -166,7 +119,8 @@
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true
},
"basic-auth": {
"version": "2.0.1",
@ -219,6 +173,7 @@
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -270,11 +225,6 @@
}
}
},
"call-me-maybe": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz",
"integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms="
},
"camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
@ -369,15 +319,11 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"commander": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-5.0.0.tgz",
"integrity": "sha512-JrDGPAKjMGSP1G0DUoaceEJ3DZgAfr/q6X7FVk4+U5KxUSKviYGM2k6zWkfyyBHy5rAtzgYJFa1ro2O9PtoxwQ=="
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true
},
"configstore": {
"version": "5.0.1",
@ -475,14 +421,6 @@
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
},
"doctrine": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
"integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
"requires": {
"esutils": "^2.0.2"
}
},
"dot-prop": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz",
@ -544,11 +482,6 @@
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
},
"esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="
},
"etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
@ -642,11 +575,6 @@
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"fsevents": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz",
@ -671,19 +599,6 @@
"pump": "^3.0.0"
}
},
"glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"glob-parent": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
@ -788,15 +703,6 @@
"integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
"dev": true
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"requires": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
@ -903,15 +809,6 @@
"integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==",
"dev": true
},
"js-yaml": {
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
"integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
"requires": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
}
},
"json-buffer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz",
@ -941,16 +838,6 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
},
"lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
},
"lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
},
"long": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
@ -1054,6 +941,7 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -1234,15 +1122,11 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"requires": {
"wrappy": "1"
}
},
"openapi-types": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-1.3.5.tgz",
"integrity": "sha512-11oi4zYorsgvg5yBarZplAqbpev5HkuVNPlZaPTknPDzAynq+lnJdXAmruGWP0s+dNYZS7bjM+xrTpJw7184Fg=="
},
"p-cancelable": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz",
@ -1283,11 +1167,6 @@
"util": "^0.10.3"
}
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
},
"path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
@ -1526,11 +1405,6 @@
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==",
"dev": true
},
"sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
},
"sqlstring": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.2.tgz",
@ -1605,39 +1479,6 @@
"has-flag": "^3.0.0"
}
},
"swagger-jsdoc": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-4.0.0.tgz",
"integrity": "sha512-wHrmRvE/OQa3d387YIrRNPvsPwxkJc0tAYeCVa359gUIKPjC4ReduFhqq/+4erLUS79kY1T5Fv0hE0SV/PgBig==",
"requires": {
"commander": "5.0.0",
"doctrine": "3.0.0",
"glob": "7.1.6",
"js-yaml": "3.13.1",
"swagger-parser": "9.0.1"
}
},
"swagger-parser": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-9.0.1.tgz",
"integrity": "sha512-oxOHUaeNetO9ChhTJm2fD+48DbGbLD09ZEOwPOWEqcW8J6zmjWxutXtSuOiXsoRgDWvORYlImbwM21Pn+EiuvQ==",
"requires": {
"@apidevtools/swagger-parser": "9.0.1"
}
},
"swagger-ui-dist": {
"version": "3.28.0",
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.28.0.tgz",
"integrity": "sha512-aPkfTzPv9djSiZI1NUkWr5HynCUsH+jaJ0WSx+/t19wq7MMGg9clHm9nGoIpAtqml1G51ofI+I75Ym72pukzFg=="
},
"swagger-ui-express": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.1.4.tgz",
"integrity": "sha512-Ea96ecpC+Iq9GUqkeD/LFR32xSs8gYqmTW1gXCuKg81c26WV6ZC2FsBSPVExQP6WkyUuz5HEiR0sEv/HCC343g==",
"requires": {
"swagger-ui-dist": "^3.18.1"
}
},
"term-size": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.0.tgz",
@ -1763,11 +1604,6 @@
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
},
"validator": {
"version": "12.2.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-12.2.0.tgz",
"integrity": "sha512-jJfE/DW6tIK1Ek8nCfNFqt8Wb3nzMoAbocBF6/Icgg1ZFSBpObdnwVY2jQj6qUqzhx5jc71fpvBWyLGO7Xl+nQ=="
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@ -1785,7 +1621,8 @@
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true
},
"write-file-atomic": {
"version": "3.0.3",
@ -1809,25 +1646,6 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
},
"z-schema": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/z-schema/-/z-schema-4.2.3.tgz",
"integrity": "sha512-zkvK/9TC6p38IwcrbnT3ul9in1UX4cm1y/VZSs4GHKIiDCrlafc+YQBgQBUdDXLAoZHf2qvQ7gJJOo6yT1LH6A==",
"requires": {
"commander": "^2.7.1",
"lodash.get": "^4.4.2",
"lodash.isequal": "^4.5.0",
"validator": "^12.0.0"
},
"dependencies": {
"commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"optional": true
}
}
}
}
}

View File

@ -21,9 +21,7 @@
"morgan": "^1.10.0",
"mysql2": "^2.1.0",
"path": "^0.12.7",
"sqlstring": "^2.3.2",
"swagger-jsdoc": "^4.0.0",
"swagger-ui-express": "^4.1.4"
"sqlstring": "^2.3.2"
},
"devDependencies": {
"nodemon": "^2.0.4"

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

@ -0,0 +1,13 @@
const router = require("express").Router()
// Models
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

@ -1,10 +1,3 @@
/**
* @swagger
* tags:
* name: Countries
* description: Access country data.
*/
const router = require("express").Router();
// Models
@ -15,37 +8,10 @@ const getCountryById = require("../models/getCountryById.js");
const sqlSanitzer = require("../util/sqlstring_sanitizer.js")
module.exports = dbConn => {
/**
* @swagger
* /countries:
* get:
* summary: Get all countries
* tags: [Countries]
* responses:
* "200":
* description: Returns aviable data for all countries
*/
router.get("/api/v1/countries", async (req, res) => {
res.json(await getCountries(dbConn));
});
/**
* @swagger
* /countries/{id}:
* get:
* summary: Get a specific country by id
* tags: [Countries]
* parameters:
* - name: "id"
* in: "path"
* required: true
* type: int
* example: 23
* responses:
* "200":
* description: Returns aviable data for the country
* example: test
*/
router.get("/api/v1/countries/:id", async (req, res) => {
const id = sqlSanitzer(req.params.id);
res.json(await getCountryById(dbConn, id))

View File

@ -1,10 +1,3 @@
/**
* @swagger
* tags:
* name: Places
* description: Access to the Google Place API via the Key used in backend. Only for manual use in the prototype application!
*/
const router = require("express").Router()
// Models
@ -16,51 +9,11 @@ const getPlacePhoto = require("../models/getPlacePhoto.js")
const sqlSanitzer = require("../util/sqlstring_sanitizer.js")
module.exports = dbConn => {
/**
* @swagger
* /place:
* get:
* summary: Get a specific place
* tags: [Places]
* parameters:
* - name: "q"
* in: "query"
* required: true
* type: int
* description: "Querystring, by which the place is searched"
* example: Berlin
* responses:
* "200":
* description: Returns a place from the google places API.
*/
router.get("/api/v1/place", async (req, res) => {
const place = await getPlace(req.query.q)
res.json(place)
});
/**
* @swagger
* /place/nearby:
* get:
* summary: Get nearby touristic places
* tags: [Places]
* parameters:
* - name: "lat"
* in: "query"
* required: true
* type: float
* description: "Latitiude"
* example: 52.520365
* - name: "lng"
* in: "query"
* required: true
* type: float
* description: "Longitude"
* example: 13.403509
* responses:
* "200":
* description: Returns nearby places from the google places API.
*/
router.get("/api/v1/place/nearby", async (req, res) => {
const lat = req.query.lat
const lng = req.query.lng
@ -68,23 +21,6 @@ module.exports = dbConn => {
res.json(place)
});
/**
* @swagger
* /place/photo:
* get:
* summary: Get a photo for a place
* tags: [Places]
* parameters:
* - name: "photoref"
* in: "query"
* required: true
* type: int
* description: "Photo_Reference which is returned for a place by Google Places API"
* example: CmRaAAAAbupojmH94negtiCnLGdfx2azxhVTEDI1rtTrYnQ7KclEI-Yy9_YGxN9h63AKrCzd22kk5z-UiK7fS4-zXnO5OqfNRZu2hrmfcp8b77rItediibAVovOOA5LnyJ9YYuofEhAAr0Im0zuiAtbDKPjbPUSBGhTFkSrH6FZxenbo1bCkdCXaUMhOug
* responses:
* "200":
* description: Returns the matching url to the photo.
*/
router.get("/api/v1/place/photo", async (req, res) => {
const photoref = req.query.photoref
const photo = await getPlacePhoto(photoref)

View File

@ -1,10 +1,3 @@
/**
* @swagger
* tags:
* name: Regions
* description: Access region data.
*/
const router = require("express").Router();
// Models
@ -19,16 +12,6 @@ const _ = require('lodash')
const sqlSanitzer = require("../util/sqlstring_sanitizer.js")
module.exports = dbConn => {
/**
* @swagger
* /regions:
* get:
* summary: Get all regions
* tags: [Regions]
* responses:
* "200":
* description: Returns available data for all regions
*/
router.get("/api/v1/regions", async (req, res) => {
const data = await getRegions(dbConn)
if (req.query.randomize) {
@ -39,21 +22,6 @@ module.exports = dbConn => {
}
});
/**
* @swagger
* /regions/{id}:
* get:
* summary: Get a specific region by id
* tags: [Regions]
* parameters:
* - name: "id"
* in: "path"
* required: true
* type: int
* responses:
* "200":
* description: Returns available data for the region
*/
router.get("/api/v1/regions/:id", async (req, res) => {
console.log(typeof req.params.id)
const id = sqlSanitzer(req.params.id);
@ -61,49 +29,14 @@ module.exports = dbConn => {
res.json(await getRegionById(dbConn, id))
});
/**
* @swagger
* /regions/{id}/image:
* get:
* summary: Get image for specific region
* tags: [Regions]
* parameters:
* - name: "id"
* in: "path"
* required: true
* type: int
* responses:
* "200":
* description: Returns the image for a specific region
* "404":
* description: Returns a placeholder image for the region
*/
router.get('/api/v1/regions/:id/image', (req, res) => {
console.log("HERE")
if (fs.existsSync(path.join(__dirname, `../data/regions/images/${req.params.id}.jpg`))) {
console.log("EXISTS")
res.status(200).sendFile(path.join(__dirname, `../data/regions/images/${req.params.id}.jpg`))
res.sendFile(path.join(__dirname, `../data/regions/images/${req.params.id}.jpg`))
} else {
console.log("NOT EXISTS")
res.status(404).sendFile(path.join(__dirname, `../data/regions/images/x.png`))
res.sendFile(path.join(__dirname, `../data/regions/images/x.png`))
}
})
/**
* @swagger
* /regions/{id}/nearby:
* get:
* summary: Get nearby places of a specific region by id
* tags: [Regions]
* parameters:
* - name: "id"
* in: "path"
* required: true
* type: int
* responses:
* "200":
* description: Returns all nearby places for the region
*/
router.get("/api/v1/regions/:id/nearby", async (req,res) => {
const id = sqlSanitzer(req.params.id);
res.json(await getRegionNearbyById(dbConn,id))

View File

@ -1,10 +1,3 @@
/**
* @swagger
* tags:
* name: Search
* description: Access the search algorithm and the data provided for searching.
*/
const router = require("express").Router();
// Models
@ -16,50 +9,12 @@ const _ = require('lodash')
const base64 = require("../util/base64.js")
const scoreAndSearch = require("../util/scoreAndSearch.js");
const oldToNewQuerySyntax = require("../util/oldToNewQuerySyntax.js")
const { getUniqueTags } = require("../models/getTags.js");
const { allTagsWithValues, getUniqueTags } = require("../models/getTags.js");
const { getClimateMinMax } = require("../util/getClimateMinMax.js");
module.exports = dbConn => {
/**
* @swagger
* /search:
* get:
* summary: Get Searchresults
* tags: [Search]
* parameters:
* - name: "q"
* in: "query"
* required: true
* type: int
* description: "Base64 encoded JS-Object with searchparameters"
* example: eyJmcm9tIjoxNTkzNjQ4MDAwMDAwLCJ0byI6MTU5NDI1MjgwMDAwMCwidGFncyI6W119
* responses:
* "200":
* description: Returns the region information and scores for the searchresults
*/
router.get("/api/v1/search", searchHandler(dbConn));
/**
* @swagger
* /search/presets:
* get:
* summary: Get the presets for the search parameters
* tags: [Search]
* responses:
* "200":
* description: Returns all presets for the search parameters
*/
router.get("/api/v1/search/presets", presetHandler(dbConn));
/**
* @swagger
* /search/tags:
* get:
* summary: Get the existing searchtags
* tags: [Search]
* responses:
* "200":
* description: Returns all existing searchtags
*/
router.get("/api/v1/search/tags", tagsHandler(dbConn));
return router;
@ -106,9 +61,10 @@ function searchHandler(dbConn) {
// CHOOSE PARAMS WHICH SHALL BE PASSED TO SCORE AND SEARCH
let scoreQueryObj = prepareQueries(q)
let [regions] = await Promise.all([getRegions(dbConn)])
let [regions, boundaryClimate] = await Promise.all([getRegions(dbConn), getClimateMinMax(dbConn)])
let data = {
regions: regions,
boundaryClimate: boundaryClimate
}
// FILTER if query contains filterString
@ -126,8 +82,28 @@ function searchHandler(dbConn) {
console.log('without null scores');
searchResults.forEach(el => console.log('region:', el.name, 'score:', el.score))
searchResults = searchResults.filter(el => !_.some(el.scores, score => _.isNaN(score.score) || _.isNil(score.score) || score.score <= 0))
// searchResults = searchResults.filter(el => !(_.isNil(el.score) || _.isNaN(el.score)) )
// searchResults = searchResults.filter(el => {
// let nullcnt = 0
// el.scores.forEach(sc => {
// if (_.isNaN(sc.score) || sc.score <= 0 || _.isNil(el.score)) {
// nullcnt++
// }
// })
// console.log(el.name, nullcnt)
// return nullcnt >= 2 ? false : true
// })
/*searchResults = searchResults.filter(el => {
console.log('scorrrrrr', el.score);
let keepIt = true
//if (_.some(el.scores, score => score.score <= 0) && el.score < 1) keepIt = false
return cutScores ? keepIt : true
})//.filter(el => !_.isNaN(el.score))*/
}
// SEND RESPONSE
// response.data = searchResults
// res.json(response)
res.json(searchResults)
}).catch(e => {

View File

@ -1,15 +1,6 @@
/**
* @swagger
* tags:
* name: Update
* description: Endpoint for updating region specific data. Only for manual use in the prototype application!
*/
const router = require("express").Router();
// Models
const handleClimateUpdate = require("../models/handleClimateUpdate.js")
const handleClimateUpdateV2 = require("../models/handleClimateUpdateV2.js")
const handleUpdateRegionNearby = require("../models/handleUpdateRegionNearby.js")
const handleUpdateRegionNearbyById = require("../models/handleUpdateRegionNearbyById.js")
const handleUpdateRegionNearbyImgUrl = require("../models/handleUpdateRegionNearbyImgUrl.js")
@ -19,114 +10,24 @@ const handleUpdateRegionNearbyImgUrlById = require("../models/handleUpdateRegion
const sqlSanitzer = require("../util/sqlstring_sanitizer.js")
module.exports = dbConn => {
/**
* @swagger
* /update/climate/v1:
* put:
* summary: Pull monthly data from meteostat API V1
* tags: [Update]
* responses:
* "200":
* description: Update information is logged in backend
*/
router.put("/api/v1/update/climate/v1", async (req, res) => {
const update = await handleClimateUpdate(dbConn)
res.json(update)
});
/**
* @swagger
* /update/climate/v2:
* put:
* summary: Pull daily data from meteostat API V2. Data is written to Travopti database and must be processed manually before it can be used.
* tags: [Update]
* responses:
* "200":
* description: Update information is logged in backend
*/
router.put("/api/v1/update/climate/v2", async (req, res) => {
const update = await handleClimateUpdateV2(dbConn)
res.json(update)
});
/**
* @swagger
* /update/regions/all/nearby:
* put:
* summary: Updates all nearby data for all regions
* tags: [Update]
* responses:
* "200":
* description: Updates all nearby data for all regions
*/
router.put("/api/v1/update/regions/all/nearby", async (req, res) => {
router.patch("/api/v1/update/regions/all/nearby", async (req, res) => {
res.json(await handleUpdateRegionNearby(dbConn))
});
/**
* @swagger
* /update/regions/all/lonlat:
* put:
* summary: Updates all coordinate data for all regions
* tags: [Update]
* responses:
* "200":
* description: Updates all coordinate data for all regions
*/
router.put("/api/v1/update/regions/all/lonlat", async (req,res) => {
router.patch("/api/v1/update/regions/all/lonlat", async (req,res) => {
res.json(await handleRegionLonLat(dbConn))
});
/**
* @swagger
* /update/regions/all/nearby/image:
* put:
* summary: Updates the nearby image urls for all regions
* tags: [Update]
* responses:
* "200":
* description: Updates the nearby image urls for all regions
*/
router.put("/api/v1/update/regions/all/nearby/image", async (req, res) => {
res.json(await handleUpdateRegionNearbyImgUrl(dbConn))
});
/**
* @swagger
* /update/regions/{id}/nearby:
* put:
* summary: Updates the nearby data for a specific region
* tags: [Update]
* parameters:
* - name: "id"
* in: "path"
* required: true
* type: int
* responses:
* "200":
* description: Updates the nearby data for a specific region
*/
router.put("/api/v1/update/regions/:id/nearby", async (req, res) => {
router.patch("/api/v1/update/regions/:id/nearby", async (req, res) => {
const id = sqlSanitzer(req.params.id);
res.json(await handleUpdateRegionNearbyById(dbConn, id))
});
/**
* @swagger
* /update/regions/{id}/nearby/image:
* put:
* summary: Updates the nearby image urls for a specific region
* tags: [Update]
* parameters:
* - name: "id"
* in: "path"
* required: true
* type: int
* responses:
* "200":
* description: Updates the nearby image urls for a specific region
*/
router.put("/api/v1/update/regions/:id/nearby/image", async (req, res) => {
router.patch("/api/v1/update/regions/all/nearby/imgurl", async (req, res) => {
res.json(await handleUpdateRegionNearbyImgUrl(dbConn))
});
router.patch("/api/v1/update/regions/:id/nearby/imgurl", async (req, res) => {
const id = sqlSanitzer(req.params.id);
res.json(await handleUpdateRegionNearbyImgUrlById(dbConn, id))
});

View File

@ -1,62 +0,0 @@
module.exports = {
scoring: {
// parameter: [transition range, transition function, transistion slope exponent]
temperature_mean_max: [12, 'easeInOut', 2],
precipitation: [60, 'easeInOut', 2], // [170, 'easeInOut', 2],
rain_days: [5, 'easeInOut', 2],
sun_hours: [80, 'easeInOut', 2],
accommodation_costs: [30, 'linear', null],
food_costs: [25, 'linear', null],
alcohol_costs: [15, 'linear', null],
water_costs: [15, 'linear', null],
local_transportation_costs: [20, 'linear', null],
entertainment_costs: [20, 'easeInOut', 0.6],
average_per_day_costs: [100, 'linear', null],
avg_price_relative: [30, 'easeOut', 2],
},
boundaries: {
climate: {
min: {
temperature_mean: -9.6,
temperature_mean_min: -14.5,
temperature_mean_max: -4.7,
precipitation: 0,
rain_days: 0,
sun_hours: 3
},
max: {
temperature_mean: 38.3,
temperature_mean_min: 33.5,
temperature_mean_max: 43.7,
precipitation: 1145,
rain_days: 28,
sun_hours: 416
}
},
static: {
max: {
accommodation_costs: 500,
food_costs: 100,
alcohol_costs: 100,
water_costs: 100,
local_transportation_costs: 100,
entertainment_costs: 100,
average_per_day_costs: 1000,
avg_price_relative: 100
},
min: {
accommodation_costs: 0,
food_costs: 0,
alcohol_costs: 0,
water_costs: 0,
local_transportation_costs: 0,
entertainment_costs: 0,
average_per_day_costs: 0,
avg_price_relative: 0
}
}
}
}

9
backend/test.js Normal file
View File

@ -0,0 +1,9 @@
const moment = require('moment')
let date = {
year: 2012,
month: 3,
day: 13
}
console.log(moment(date));

View File

@ -14,20 +14,10 @@ exports.base64ToObj = function(base64) {
return JSON.parse(atob(base64));
}
/**
* Decodes a base64 encoded object.
* @param base64 encoded object
* @returns {string} decoded object
*/
function atob(base64) {
return Buffer.from(base64, 'base64').toString('binary')
}
/**
* Encodes an object as base64 string.
* @param string The object to encode
* @returns {string} base64 encoded object
*/
function btoa(string) {
return Buffer.from(string).toString('base64')
}

View File

@ -1,8 +1,3 @@
/**
* Seperate Strings created via GROUP_CONCAT by database into an array
* @param array String with comma-seperated values
* @returns [float] array of float values
*/
module.exports = (array) => {
if (array !== null && array !== undefined) {
const value = array

View File

@ -1,3 +1,5 @@
module.exports = function (dbConn) {
return async function getAllRegionsWithClimatePerMonth(month) {
console.log('getAllRegionsWithClimatePerMonth')

0
backend/util/prices.js Normal file
View File

View File

@ -4,17 +4,17 @@ exports.calculateAvgScore = (...scores) => {
return avgScore = scores.reduce((total, score) => total += score) / scores.length;
}
exports.calculateScoreRange = (transitionRange, regionVal, sLowVal, sHighVal) => {
exports.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;
return this.calculateScore(transitionRange, regionVal, sVal);
return this.calculateScore(min, max, multiplier, regionVal, sVal);
}
exports.calculateScore = (transitionRange, regionVal, searchVal) => {
let score = 1 - (Math.abs(searchVal - regionVal) / transitionRange);
exports.calculateScore = (min, max, multiplier, regionVal, searchVal) => {
let score = 1 - (Math.abs(searchVal - regionVal) / (max - min) * multiplier);
return (score) * 10;
//return score <= 0 ? 0 : score * 10;
}
@ -53,14 +53,4 @@ exports.sigmoid = function (x, exponent) {
const sigm = 10 / (1 + 8 * Math.pow(Math.E, 3/4 * -x))
console.log('sigmoid (IN/OUT):', _.round(x,3), _.round(sigm, 3))
return sigm
}
exports.increaseTransitionForHighValues = function (transitionRange, searchVal) {
//console.log(transitionRange);
// console.log(transitionRange);
// console.log(((Math.pow(searchVal / 20, 2) / 100) + 1));
// console.log(((Math.pow(searchVal / 20, 2) / 100) + 1) * transitionRange);
let transFactor = ((Math.pow(searchVal / 20, 2) / 100) + 1)
return transFactor >= 4 ? 4 * transitionRange : transFactor * transitionRange
}

View File

@ -1,7 +1,26 @@
const _ = require('lodash')
const moment = require("moment")
const getClimateMinMax = require("./getClimateMinMax.js")
const scorer = require('./score')
const SETTINGS = require('../settings').scoring
const getRegions = require('../models/getRegions.js')
const SHOW_ALL_SCOREOBJECTS = false
const SETTINGS = {
temperature_mean_max: [4.5, 'easeOut', 2],
precipitation: [4, 'easeInOut', 2],
rain_days: [2, 'easeInOut', 2],
sun_hours: [3.6, 'easeInOut', 2],
accommodation_costs: [17, 'linear', null],
food_costs: [4, 'linear', null],
alcohol_costs: [4, 'linear', null],
water_costs: [10, 'linear', null],
local_transportation_costs: [5, 'linear', null],
entertainment_costs: [5, 'easeInOut', 0.6],
average_per_day_costs: [5, 'linear', null],
avg_price_relative: [3, 'easeOut', 2],
}
module.exports = async function (data, from, to, q) {
console.log('search')
@ -9,7 +28,7 @@ module.exports = async function (data, from, to, q) {
if ((_.isNil(to) || _.isNil(from)) && !(_.isEmpty(q.climate) || _.isEmpty(q.costs) || _.isEmpty(q.others))) {
throw new Error('invalid query')
}
if (_.isNil(data) || _.isEmpty(data.regions)) {
if (_.isNil(data) || _.isEmpty(data.regions) || _.isEmpty(data.boundaryClimate)) {
throw new Error('database error')
}
let regionsArr = data.regions
@ -18,27 +37,51 @@ module.exports = async function (data, from, to, q) {
const dates = validateDates(from, to)
// for calculating average if traveldates are in more than one month
const travelPeriods = travelPeriodsFromDates(dates)
const boundaryStatic = {
max: {
accommodation_costs: 500,
food_costs: 100,
alcohol_costs: 100,
water_costs: 100,
local_transportation_costs: 100,
entertainment_costs: 100,
average_per_day_costs: 1000,
avg_price_relative: 100
},
min: {
accommodation_costs: 0,
food_costs: 0,
alcohol_costs: 0,
water_costs: 0,
local_transportation_costs: 0,
entertainment_costs: 0,
average_per_day_costs: 0,
avg_price_relative: 0
}
}
// CALCULATE PROPERTIES FOR EACH REGION
regionsArr.forEach(reg => {
// CALCULATE SCORES FOR CLIMATE PROPS
reg.scores = []
Object.entries(q.climate).forEach(([key, value]) => {
let finalScoreObj = calculateScoreForPeriod(key, travelPeriods, reg, value[0], value[1])
let finalScoreObj = calculateScoreForPeriod(key, travelPeriods, reg, value[0], value[1], data.boundaryClimate)
reg.scores.push(finalScoreObj)
});
// CALCULATE SCORES FOR PRICE PROPS
Object.entries(q.costs).forEach(([key, value]) => {
let finalScoreObj = scoreFromSimpleRegionProperty(key, reg, value[0], value[1])
let finalScoreObj = scoreFromSimpleRegionProperty(key, reg, value[0], value[1], boundaryStatic)
reg.scores.push(finalScoreObj)
});
// CALCULATE SCORE FOR OFFSEASON
if (_.has(q, 'others.avg_price_relative')) {
let offSeasonScoreObj = calculateScoreForPeriod('avg_price_relative', travelPeriods, reg, q.others.avg_price_relative[0], q.others.avg_price_relative[1], 'easeOut', 2)
let offSeasonScoreObj = calculateScoreForPeriod('avg_price_relative', travelPeriods, reg, q.others.avg_price_relative[0], q.others.avg_price_relative[1], boundaryStatic, 'easeOut', 2)
reg.scores.push(offSeasonScoreObj)
}
@ -105,33 +148,18 @@ function calculateAverage(scores) {
}
function travelPeriodsFromDates(dates) {
let start = moment(`${dates.from.year}-${dates.from.month}-${dates.from.day}`, 'YYYY-MM-DD')
let end = moment(`${dates.to.year}-${dates.to.month}-${dates.to.day}`, 'YYYY-MM-DD')
console.log('start:', moment(start));
console.log('end:', moment(end));
// console.log('start:', moment(start).toISOString());
// console.log('end:', moment(end).toISOString());
// console.log('start:', moment(dates.from));
// console.log('end:', moment(dates.to));
console.log();
console.log();
console.log();
let travelPeriods = []
if (start.month() === end.month() && start.year() === end.year()) {
if (dates.from.month === dates.to.month && dates.from.year === dates.to.year) {
let period = {
month: start.month()+1,
days: end.date() - start.date()
month: dates.from.month,
days: dates.to.day - dates.from.day
}
travelPeriods.push(period)
} else {
for (var m = moment(start); m.isSameOrBefore(moment(end).endOf("month")); m.add(1, 'months')) {
console.log(m);
travelPeriods.push(createPeriod(start, end, m.month()+1, m.year()))
for (var m = moment(dates.from).subtract(1, 'months'); m.isSameOrBefore(moment(dates.to).subtract(1, 'months').endOf("month")); m.add(1, 'months')) {
travelPeriods.push(createPeriod(dates.from, dates.to, m.month() + 1, m.year()))
}
}
return travelPeriods
@ -151,7 +179,7 @@ function validateDates(from, to) {
let dateTo = new Date(to)
fromAndTo.to.day = dateTo.getDate()
fromAndTo.to.month = dateTo.getMonth() + 1
fromAndTo.to.year = dateTo.getFullYear()
fromAndTo.to.year = dateFrom.getFullYear()
} 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;
@ -163,30 +191,24 @@ function validateDates(from, to) {
fromAndTo.to.day = Number(to.split("-")[2])
if (!from.match(re) || !to.match(re)) throw new Error("ERR: invalid parameter:", from, to)
}
console.log(moment(`${fromAndTo.from.year}-${fromAndTo.from.month}-${fromAndTo.from.day}`, 'YYYY-MM-DD'))
console.log(moment(`${fromAndTo.to.year}-${fromAndTo.to.month}-${fromAndTo.to.day}`, 'YYYY-MM-DD'))
if (moment(`${fromAndTo.from.year}-${fromAndTo.from.month}-${fromAndTo.from.day}`, 'YYYY-MM-DD').add(23, 'hours').isAfter(moment(`${fromAndTo.to.year}-${fromAndTo.to.month}-${fromAndTo.to.day}`, 'YYYY-MM-DD'))) throw new Error("ERR: 'to' must be at least one day after 'from'.")
if (moment(fromAndTo.from).add(23, 'hours').isAfter(moment(fromAndTo.to))) throw new Error("ERR: 'to' must be at least one day after 'from'.")
return fromAndTo
}
function createPeriod(start, end, currentMonth, currentYear) {
function createPeriod(from, to, currentMonth, currentYear) {
let period = {}
console.log(start, end, currentMonth, currentYear);
if (currentMonth === start.month() + 1 && currentYear === start.year()) {
console.log('first month')
if (currentMonth === from.month && currentYear === from.year) {
period = {
month: currentMonth,
days: 32 - start.date()
days: 32 - from.day
}
} else if (currentMonth === end.month() + 1) {
console.log('end month')
} else if (currentMonth === to.month) {
period = {
month: currentMonth,
days: end.date()
days: to.day
}
} else {
console.log('middle month')
period = {
month: currentMonth,
days: 30
@ -196,13 +218,13 @@ function createPeriod(start, end, currentMonth, currentYear) {
}
function calculateScoreForPeriod(type, travelPeriods, region, searchLowParam, searchMaxParam) {
function calculateScoreForPeriod(type, travelPeriods, region, searchLowParam, searchMaxParam, minMax) {
// console.log('getScoreAndAverageFromClimate for', region.name, type)
const singleScores = travelPeriods. map(period => {
const singleScores = travelPeriods.map(period => {
let res = {
type: type,
value: region[type] !== null ? region[type][period.month - 1] : null,
value: region[type][period.month - 1],
days: period.days
}
@ -226,14 +248,8 @@ function calculateScoreForPeriod(type, travelPeriods, region, searchLowParam, se
})
averagedScore.value = _.round(averagedScore.value / averagedScore.days, 3)
delete averagedScore.days
let transitionRange = SETTINGS[type][0]
// special for precipitation
if (type === 'precipitation') {
transitionRange = scorer.increaseTransitionForHighValues(SETTINGS[type][0], searchLowParam)
}
let sc = scorer.calculateScoreRange(transitionRange, averagedScore.value, searchLowParam, searchMaxParam)
let sc = scorer.calculateScoreRange(minMax.min[type], minMax.max[type], SETTINGS[type][0], averagedScore.value, searchLowParam, searchMaxParam)
averagedScore.score = _.round(scorer[SETTINGS[type][1]](sc, SETTINGS[type][2]), 3)
// console.log('score', averagedScore.score)
if (searchLowParam === null) averagedScore.score = null
@ -251,7 +267,7 @@ function scoresFromTags(regionTags, tagStringsFromQueries) {
}
if (_.isNil(tag)) return retVal
retVal.value = tag.value
retVal.score = /* tag.value <= 0 ? 0 : */ _.round(scorer.calculateScoreRange(60, tag.value, 100, 100), 3)
retVal.score = /* tag.value <= 0 ? 0 : */ _.round(scorer.calculateScoreRange(40, 100, 1, tag.value, 100, 100), 3)
console.log(retVal);
return retVal
@ -260,7 +276,7 @@ function scoresFromTags(regionTags, tagStringsFromQueries) {
function scoreFromSimpleRegionProperty(type, region, searchLowParam, searchMaxParam, minMax) {
// console.log('getScoreFromCosts for', region.name, type)
const sc = _.round(scorer.calculateScoreRange(SETTINGS[type][0], region[type], searchLowParam, searchMaxParam), 3)
const sc = _.round(scorer.calculateScoreRange(minMax.min[type], minMax.max[type], SETTINGS[type][0], region[type], searchLowParam, searchMaxParam), 3)
let finScore = {
type: type,

View File

@ -1,12 +1,7 @@
const sqlstring = require("sqlstring")
/**
* Sanitizes value if it isn't a numerical value
* @param val
* @returns string Sanitized String
*/
module.exports = (val) => {
if(!isNaN(val)) { // Checks if the value is a numerical value (in a string)
if(!isNaN(val)) {
return val
} else {
return sqlstring.escape(val)

View File

@ -2,26 +2,23 @@
<button (click)="drawer.toggle()" *ngIf="isMobile" class="menu-btn" mat-icon-button>
<mat-icon>menu</mat-icon>
</button>
<img alt="Travopti logo" class="title" src="assets/logo.svg">
<h1 class="title">Travopti - Prototype</h1>
</mat-toolbar>
<mat-drawer-container autosize class="drawer-container">
<mat-drawer-container autosize class="drawer">
<mat-drawer #drawer [mode]="isMobile?'over':'side'" [opened]="!isMobile">
<div class="drawer">
<div class="side-nav">
<a (click)="isMobile&&drawer.close()" mat-button routerLink="home" routerLinkActive="active">
<mat-icon>home</mat-icon>
<span>Home</span>
</a>
<a (click)="isMobile&&drawer.close()" mat-button routerLink="bookmark" routerLinkActive="active">
<mat-icon>bookmark</mat-icon>
<span>Your bookmarks</span>
</a>
<a (click)="isMobile&&drawer.close()" mat-button routerLink="team" routerLinkActive="active">
<mat-icon>group</mat-icon>
<span>About us</span>
</a>
</div>
<a class="feedback" href="mailto:feedback@travopti.de">Feedback</a>
<div class="side-nav">
<a (click)="isMobile&&drawer.close()" mat-button routerLink="home" routerLinkActive="active">
<mat-icon>home</mat-icon>
<span>Home</span>
</a>
<a (click)="isMobile&&drawer.close()" mat-button routerLink="bookmark" routerLinkActive="active">
<mat-icon>bookmark</mat-icon>
<span>Your bookmarks</span>
</a>
<a (click)="isMobile&&drawer.close()" mat-button routerLink="team" routerLinkActive="active">
<mat-icon>group</mat-icon>
<span>About us</span>
</a>
</div>
</mat-drawer>
<mat-drawer-content class="content">
@ -30,3 +27,6 @@
</div>
</mat-drawer-content>
</mat-drawer-container>

View File

@ -10,32 +10,24 @@
display: flex;
flex-flow: row;
align-items: center;
height: 4rem;
.menu-btn {
margin-right: 1rem;
}
.title {
flex: 0 1 auto;
height: 2.5rem;
flex: 1 1 auto;
margin: 0;
}
}
.drawer-container {
height: 100%;
}
.drawer {
flex: 1 1 auto;
display: flex;
flex-direction: column;
height: 100%;
.side-nav {
flex: 1 1 auto;
display: flex;
flex-direction: column;
height: 100%;
min-width: 12.5vw;
box-sizing: border-box;
padding: 1rem 0;
@ -53,16 +45,10 @@
}
&.active {
color: #00a0d2;
color: #00ae00;
}
}
}
.feedback {
margin: 1rem;
text-align: center;
color: gray;
}
}
.content {

View File

@ -50,7 +50,6 @@ import {TeamComponent} from './containers/team/team.component';
import {DeviceDetectorModule} from 'ngx-device-detector';
import {ToggleSliderComponent} from './components/toggle-slider/toggle-slider.component';
import {PlaceComponent} from './components/place/place.component';
import {MultiTagSelectComponent} from './components/multi-tag-select/multi-tag-select.component';
@NgModule({
@ -71,8 +70,7 @@ import {MultiTagSelectComponent} from './components/multi-tag-select/multi-tag-s
ShareDialogComponent,
TeamComponent,
ToggleSliderComponent,
PlaceComponent,
MultiTagSelectComponent
PlaceComponent
],
imports: [
BrowserModule,

View File

@ -20,8 +20,6 @@ export class GraphComponent implements AfterViewInit {
formatSting: string;
@Input()
graphType = 'line';
@Input()
minMax: number[];
readonly randomId = uuidv4();
@ -62,14 +60,6 @@ export class GraphComponent implements AfterViewInit {
data[i].color = this.colors[i];
}
let axisY: CanvasJS.ChartOptions.ChartAxisY;
if (this.minMax) {
axisY = {
minimum: this.minMax[0],
maximum: this.minMax[1]
};
}
const chart = new CanvasJS.Chart(this.randomId, {
animationEnabled: true,
@ -80,7 +70,6 @@ export class GraphComponent implements AfterViewInit {
horizontalAlign: 'left',
dockInsidePlotArea: true
},
axisY,
data
});

View File

@ -1,5 +0,0 @@
<mat-chip-list>
<!--suppress AngularInvalidExpressionResultType -->
<mat-chip (click)="onChipClick(tag)" *ngFor="let tag of availableTags" [color]="isSelected(tag) ? 'accent' : 'none'"
selected>{{tag|translate}}</mat-chip>
</mat-chip-list>

View File

@ -1,25 +0,0 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {MultiTagSelectComponent} from './multi-tag-select.component';
describe('MultiTagSelectComponent', () => {
let component: MultiTagSelectComponent;
let fixture: ComponentFixture<MultiTagSelectComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [MultiTagSelectComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MultiTagSelectComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,59 +0,0 @@
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
@Component({
selector: 'app-multi-tag-select',
templateUrl: './multi-tag-select.component.html',
styleUrls: ['./multi-tag-select.component.scss']
})
export class MultiTagSelectComponent implements OnInit {
@Input()
availableTags: string[] = [];
@Output()
selection = new EventEmitter<string[]>();
rawValue: string[] = [];
@Output() modelChange: EventEmitter<string[]> = new EventEmitter<string[]>();
constructor() {
}
get value(): string[] {
return this.rawValue;
}
set value(value: string[]) {
this.rawValue = value;
this.modelChange.emit(value);
}
@Input()
set model(value: string[]) {
this.rawValue = value;
}
ngOnInit() {
}
onChipClick(tag: string) {
if (this.isSelected(tag)) {
this.deselectTag(tag);
} else {
this.selectTag(tag);
}
}
isSelected(tag: string) {
return this.value && this.value.includes(tag);
}
private deselectTag(tag: string) {
this.value = this.value.filter(i => i !== tag);
}
private selectTag(tag: string) {
this.value = this.value.concat(tag);
}
}

View File

@ -10,27 +10,13 @@
<app-share-button [region]="result"></app-share-button>
</div>
<div class="result-details">
<mat-divider *ngIf="totalCosts"></mat-divider>
<div *ngIf="totalCosts" class="total-price-container">
<div matTooltip="Total">
<mat-icon>euro</mat-icon>
<span>{{totalCosts|number:'1.0-0'}}€</span>
</div>
<div matTooltip="Accommodation">
<mat-icon>hotel</mat-icon>
<span>{{totalAccommodation|number:'1.0-0'}}€</span>
</div>
<div matTooltip="Lifestyle">
<mat-icon>people</mat-icon>
<span>{{totalLifeStyle|number:'1.0-0'}}€</span>
</div>
<div (click)="onTravelCostRequest($event)" matTooltip="Travel (request)">
<mat-icon>commute</mat-icon>
<span>- - -</span>
</div>
<mat-divider *ngIf="result.total_accommodation_costs"></mat-divider>
<div *ngIf="result.total_accommodation_costs" class="total-accommodation">
<mat-icon>euro</mat-icon>
<span matTooltip="Total accommodation">{{result.total_accommodation_costs|number:'1.2-2'}}€</span>
</div>
<mat-divider *ngIf="result.scores.length > 0"></mat-divider>
<div *ngIf="result.scores.length > 0" class="searched-params">
<div class="searched-params">
<table>
<tr *ngFor="let score of result.scores" [ngClass]="{'undefined': score.value == undefined}">
<td>
@ -41,7 +27,7 @@
</td>
<td>
<div class="cell right">
<span>{{score.value != undefined ? (score.value|number:PROPERTY_VIS_DEF[score.type].decimals) : 'N/A'}}</span>
<span>{{score.value != undefined ? (score.value|number:'1.2-2') : 'N/A'}}</span>
</div>
</td>
<td>
@ -57,7 +43,5 @@
</tr>
</table>
</div>
<mat-divider></mat-divider>
<div class="result-desc">Estimated values for {{duration}} {{duration > 1 ? 'days' : 'day'}}</div>
</div>
</div>

View File

@ -46,21 +46,18 @@
display: flex;
flex-direction: column;
> .total-price-container {
> .total-accommodation {
margin: 0.5rem 0;
display: flex;
flex-direction: row;
margin: 0.5rem 0;
align-items: center;
font-weight: bold;
> div {
display: flex;
flex-direction: row;
align-items: center;
margin-right: 1rem;
> mat-icon {
margin-right: 0.25rem;
}
> mat-icon {
margin-right: 0.5rem;
margin-left: 3px;
}
}
> .searched-params {
@ -70,18 +67,6 @@
.undefined {
color: #8f8f8f;
}
.result-desc {
color: #b5b9be;
text-transform: uppercase;
font-size: small;
margin-right: 0.25rem;
margin-top: 0.5rem;
overflow-y: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}

View File

@ -1,4 +1,4 @@
import {Component, Input, OnChanges, OnInit, SimpleChanges} from '@angular/core';
import {Component, Input, OnInit} from '@angular/core';
import {Result} from '../../interfaces/result.interface';
import {REGION_PARAM_VIS} from '../../services/data.service';
@ -7,23 +7,14 @@ import {REGION_PARAM_VIS} from '../../services/data.service';
templateUrl: './result.component.html',
styleUrls: ['./result.component.scss']
})
export class ResultComponent implements OnInit, OnChanges {
export class ResultComponent implements OnInit {
@Input()
result: Result;
/** Date difference in days */
@Input()
duration: number;
@Input()
debug = false;
totalCosts: number;
totalAccommodation: number;
totalLifeStyle: number;
totalTravel: number;
/** Contains the visual definitions */
readonly PROPERTY_VIS_DEF = REGION_PARAM_VIS;
@ -33,25 +24,4 @@ export class ResultComponent implements OnInit, OnChanges {
ngOnInit() {
}
ngOnChanges(changes: SimpleChanges): void {
if (changes.result || changes.duration) {
this.calculateTotalPrices();
}
}
onTravelCostRequest(event: MouseEvent) {
event.stopPropagation();
window.open(`https://www.google.com/flights?q=flight+to+${encodeURI(this.result.name)}`, '_blank');
}
private calculateTotalPrices() {
// Guard: undefined values
if (!this.result || !this.duration) {
return;
}
this.totalCosts = Math.round(this.result.average_per_day_costs * this.duration);
this.totalAccommodation = Math.round(this.result.accommodation_costs * this.duration);
this.totalLifeStyle = this.totalCosts - this.totalAccommodation;
}
}

View File

@ -8,9 +8,9 @@
<mat-tab label="Guided">
<mat-vertical-stepper>
<!-- Date input -->
<mat-step>
<ng-template matStepLabel>When do you want to travel?</ng-template>
<ng-template matStepLabel>When is your trip?</ng-template>
<div class="vertical-wrap">
<mat-form-field appearance="outline">
<mat-label>Start</mat-label>
@ -22,24 +22,22 @@
</mat-form-field>
</div>
</mat-step>
<!-- Multi presets -->
<mat-step>
<ng-template matStepLabel>Which climate do you prefer?</ng-template>
<div class="flexer">
<div *ngFor="let key of multiPresetsKeys" class="sub-group">
<span class="label">{{key|translate}}:</span><br>
<mat-radio-group [ngModel]="multiPresetSelection[key]" [value]="undefined">
<mat-radio-button
#btn
(click)="btn.checked = onMultiPresetSelect(preset)"
*ngFor="let preset of multiPresets.get(key)"
[value]="preset.preset_id"
>{{preset.tag_label|translate}}</mat-radio-button>
</mat-radio-group>
</div>
<ng-template matStepLabel>Which climate would you prefer?</ng-template>
<div *ngFor="let key of multiPresetsKeys" class="sub-group">
<span class="label">{{key|translate}}:</span><br>
<mat-radio-group [ngModel]="multiPresetSelection[key]" [value]="undefined">
<mat-radio-button
#btn
(click)="btn.checked = onMultiPresetSelect(preset)"
*ngFor="let preset of multiPresets.get(key)"
[value]="preset.preset_id"
>{{preset.tag_label|translate}}</mat-radio-button>
</mat-radio-group>
</div>
</mat-step>
<!-- Single presets -->
<mat-step>
<ng-template matStepLabel>What else is important to you?</ng-template>
<div class="vertical">
@ -47,12 +45,6 @@
[(ngModel)]="singlePresetSelection[preset.preset_id]">{{preset.tag_label|translate}}</mat-checkbox>
</div>
</mat-step>
<!-- Tag selection -->
<mat-step>
<ng-template matStepLabel>What are you interested in?</ng-template>
<app-multi-tag-select [(model)]="selectedTags" [availableTags]="tags"></app-multi-tag-select>
</mat-step>
</mat-vertical-stepper>
</mat-tab>
@ -83,46 +75,23 @@
<mat-icon>clear</mat-icon>
</button>
</mat-form-field>
<div class="horizontal space center-desc">
<mat-slide-toggle [(ngModel)]="fullText"></mat-slide-toggle>
<div class="horizontal space center">
<span>Search in description </span>
<mat-slide-toggle [(ngModel)]="fullText"></mat-slide-toggle>
</div>
</div>
</section>
<!-- Climate Params -->
<section class="group">
<div class="horizontal">
<span class="title">Climate</span>
<span class="desc">| sweetspot selection</span>
</div>
<app-toggle-slider [(model)]="temperatureMeanMax" [label]="'Temperature (°C)'" [max]="45"
[min]="0"></app-toggle-slider>
<app-toggle-slider [(model)]="precipitation" [label]="'Precipitation (mm)'" [max]="500"
[min]="0"></app-toggle-slider>
<app-toggle-slider [(model)]="sunHours" [label]="'Sun hours'" [max]="400"
[min]="0"></app-toggle-slider>
<app-toggle-slider [(model)]="rainDays" [label]="'Rain days'" [max]="31"
<span class="title">Climate</span>
<app-toggle-slider [(model)]="temperatureMeanMax" [label]="'Max Temp'" [max]="45" [min]="0"></app-toggle-slider>
<app-toggle-slider [(model)]="precipitation" [label]="'Precipitation'" [max]="500"
[min]="0"></app-toggle-slider>
</section>
<!-- Financial -->
<section class="group">
<div class="horizontal">
<span class="title">Financial</span>
<span class="desc">| sweetspot selection (€/day)</span>
</div>
<app-toggle-slider [(model)]="costPerDay" [label]="'Total cost'" [max]="400" [min]="0"></app-toggle-slider>
<app-toggle-slider [(model)]="accommodation" [label]="'Accommodation'" [max]="200"
[min]="0"></app-toggle-slider>
<app-toggle-slider [(model)]="localTransport" [label]="'Local transport'" [max]="20"
[min]="0"></app-toggle-slider>
</section>
<!-- Tags -->
<section class="group">
<div class="horizontal">
<span class="title">Tags</span>
<span class="desc">| Search by user ratings</span>
</div>
<app-multi-tag-select [(model)]="selectedTags" [availableTags]="tags"></app-multi-tag-select>
<span class="title">Fincancial</span>
<app-toggle-slider [(model)]="accommodation" [label]="'Accommodation'" [max]="60" [min]="0"></app-toggle-slider>
</section>
</mat-tab>

View File

@ -2,7 +2,7 @@
display: flex;
flex-direction: column;
>.search-btn {
> .search-btn {
margin-top: 1rem;
}
}
@ -31,33 +31,20 @@
display: flex;
flex-direction: column;
margin: 1rem 0;
overflow: hidden;
.title {
> .title {
font-size: 1.2rem;
font-weight: bold;
margin-bottom: 0.5rem;
}
.desc {
font-size: smaller;
margin-left: 0.25rem;
text-align: center;
text-transform: uppercase;
color: gray;
}
>.content {
// margin: 0 2.5rem;
margin-top: 0.5rem;
> .content {
margin: 0 2.5rem;
.text-input {
min-width: 14rem;
}
}
> app-multi-tag-select {
margin: 1rem;
}
}
.horizontal {
@ -66,7 +53,7 @@
align-items: center;
&.space {
>* {
> * {
margin: 0.5rem;
}
}
@ -75,10 +62,6 @@
justify-content: space-around;
}
&.center-desc {
justify-content: center;
}
}
@ -91,30 +74,12 @@
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
justify-content: space-evenly;
align-items: center;
margin-left: -0.5rem;
margin-right: -0.5rem;
>* {
flex-grow: 1;
margin: 0 0.5rem;
> * {
width: 45%;
min-width: 15rem;
}
}
.small-bottom-margin {
margin-bottom: 0.4rem !important;
}
.flexer {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
margin-right: -1rem;
> .sub-group {
flex-grow: 1;
margin-right: 1rem;
}
}

View File

@ -6,7 +6,6 @@ import {PresetService} from '../../services/preset.service';
import {Preset} from '../../interfaces/preset.interface';
import {formatDate} from '@angular/common';
import {SearchService} from '../../services/search.service';
import {toMinMaxArray} from '../../utils/toMinMaxArray';
@Component({
selector: 'app-search-input',
@ -22,9 +21,6 @@ export class SearchInputComponent implements OnInit {
singlePresets: Preset[];
multiPresets: Map<string, Preset[]>;
multiPresetsKeys: string[];
selectedTags: string[] = [];
tags: string[];
from: string;
to: string;
@ -38,12 +34,7 @@ export class SearchInputComponent implements OnInit {
fullText = false;
temperatureMeanMax: number;
precipitation: number;
rainDays: number;
sunHours: number;
accommodation: number;
costPerDay: number;
entertainment: number;
localTransport: number;
readonly today = this.from = formatDate(new Date(), 'yyyy-MM-dd', 'en-GB');
@ -64,8 +55,6 @@ export class SearchInputComponent implements OnInit {
this.multiPresets = this.ps.multiPresets;
this.multiPresetsKeys = [...this.multiPresets.keys()];
this.tags = await this.ss.getAvailableTags();
this.loadSearch();
}
@ -73,7 +62,6 @@ export class SearchInputComponent implements OnInit {
this.saveSearch(isAdvanced);
const query = isAdvanced ? this.getQueryFromAdvanced() : this.getQueryFromGuided();
console.log(query);
await this.router.navigate(['/search'], {queryParams: {q: objToBase64(query)}});
}
@ -108,7 +96,6 @@ export class SearchInputComponent implements OnInit {
const query: Query = {
from: new Date(this.from).getTime(),
to: new Date(this.to).getTime(),
tags: this.selectedTags
};
for (const preset of this.singlePresets) {
@ -130,7 +117,6 @@ export class SearchInputComponent implements OnInit {
const query: Query = {
from: new Date(this.from).getTime(),
to: new Date(this.to).getTime(),
tags: this.selectedTags
};
if (this.textFilter.length > 0) {
@ -138,14 +124,9 @@ export class SearchInputComponent implements OnInit {
query.textfilter = this.textFilter;
}
query.temperature_mean_max = toMinMaxArray(this.temperatureMeanMax);
query.precipitation = toMinMaxArray(this.precipitation);
query.sun_hours = toMinMaxArray(this.sunHours);
query.rain_days = toMinMaxArray(this.rainDays);
query.average_per_day_costs = toMinMaxArray(this.costPerDay);
query.accommodation_costs = toMinMaxArray(this.accommodation);
query.entertainment_costs = toMinMaxArray(this.entertainment);
query.local_transportation_costs = toMinMaxArray(this.localTransport);
query.temperature_mean_max = this.temperatureMeanMax ? [this.temperatureMeanMax, this.temperatureMeanMax] : undefined;
query.precipitation = this.precipitation ? [this.precipitation, this.precipitation] : undefined;
query.accommodation_costs = this.accommodation ? [this.accommodation, this.accommodation] : undefined;
return query;
}
@ -157,17 +138,11 @@ export class SearchInputComponent implements OnInit {
to: this.to,
singlePresetSelection: this.singlePresetSelection,
multiPresetSelection: this.multiPresetSelection,
tags: this.selectedTags,
fullText: this.fullText,
textFiler: this.textFilter,
tempMeanMax: this.temperatureMeanMax,
precipitation: this.precipitation,
rain_days: this.rainDays,
sun_hours: this.sunHours,
average_per_day_costs: this.costPerDay,
accommodation: this.accommodation,
entertainment_costs: this.entertainment,
local_transportation_costs: this.localTransport,
});
}
@ -179,18 +154,12 @@ export class SearchInputComponent implements OnInit {
this.to = prevInput.to;
this.singlePresetSelection = prevInput.singlePresetSelection;
this.multiPresetSelection = prevInput.multiPresetSelection;
this.selectedTags = prevInput.tags;
this.textFilter = prevInput.textFiler;
this.fullText = prevInput.fullText;
this.selectedTab = prevInput.wasAdvanced ? 1 : 0;
this.temperatureMeanMax = prevInput.tempMeanMax;
this.precipitation = prevInput.precipitation;
this.rainDays = prevInput.rain_days;
this.sunHours = prevInput.sun_hours;
this.costPerDay = prevInput.average_per_day_costs;
this.accommodation = prevInput.accommodation;
this.entertainment = prevInput.entertainment_costs;
this.localTransport = prevInput.local_transportation_costs;
}
}

View File

@ -27,36 +27,40 @@
<mat-tab-group>
<mat-tab *ngIf="region.avg_price_relative" label="Price Deviation">
<ng-template matTabContent>
<app-graph [monthlyDatas]="[region.avg_price_relative]" class="graph" formatSting="##0.##'%'"
graphType="column">
<app-graph
[monthlyDatas]="[region.avg_price_relative]"
class="graph"
formatSting="##,##%"
graphType="column">
</app-graph>
</ng-template>
</mat-tab>
<mat-tab *ngIf="region.temperature_mean_max" label="Temperatures">
<mat-tab *ngIf="region.temperature_mean_max && region.temperature_mean_max[0]" label="Temperatures">
<ng-template matTabContent>
<app-graph [colors]="['blue', 'red']" [labels]="['Min', 'Max']"
[monthlyDatas]="[region.temperature_mean_min, region.temperature_mean_max]" class="graph"
[minMax]="[-25, 50]" formatSting="##,##°C">
<app-graph [colors]="['blue', 'red']"
[labels]="['Min', 'Max']"
[monthlyDatas]="[region.temperature_mean_min, region.temperature_mean_max]"
class="graph"
formatSting="##,##°C">
</app-graph>
</ng-template>
</mat-tab>
<mat-tab *ngIf="region.precipitation" label="Precipitation">
<mat-tab *ngIf="region.precipitation && region.precipitation[0]" label="Precipitation">
<ng-template matTabContent>
<app-graph [minMax]="[0, 1200]" [monthlyDatas]="[region.precipitation]" class="graph" formatSting="####'mm'">
<app-graph
[monthlyDatas]="[region.precipitation]"
class="graph"
formatSting="####mm">
</app-graph>
</ng-template>
</mat-tab>
<mat-tab *ngIf="region.rain_days" label="Rain days">
<mat-tab *ngIf="region.rain_days && region.rain_days[0]" label="Rain days">
<ng-template matTabContent>
<app-graph [minMax]="[0, 31]" [monthlyDatas]="[region.rain_days]" class="graph" formatSting="####"
graphType="column">
</app-graph>
</ng-template>
</mat-tab>
<mat-tab *ngIf="region.sun_hours" label="Sun hours">
<ng-template matTabContent>
<app-graph [minMax]="[0, 450]" [monthlyDatas]="[region.sun_hours]" class="graph" formatSting="####"
graphType="column">
<app-graph
[monthlyDatas]="[region.rain_days]"
class="graph"
formatSting="####"
graphType="column">
</app-graph>
</ng-template>
</mat-tab>

View File

@ -84,7 +84,6 @@
app-place {
margin: 0.5rem;
cursor: pointer;
}
}
}

View File

@ -21,7 +21,7 @@
</div>
<div class="result-container">
<app-result (click)="onResultClick(result)" *ngFor="let result of results" [debug]="debug"
[duration]="duration" [result]="result"></app-result>
[result]="result"></app-result>
</div>
</div>

View File

@ -2,8 +2,6 @@ import {Component, ElementRef, OnInit, ViewChild} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {Result} from '../../interfaces/result.interface';
import {SearchService} from '../../services/search.service';
import {base64ToObj} from '../../utils/base64conversion';
import {Query} from '../../interfaces/search-request.interface';
interface SortOption {
name: string;
@ -30,8 +28,6 @@ export class SearchComponent implements OnInit {
sortDes = true;
/** Available sort options */
sortOptions: SortOption[] = [];
/** The difference between from and to in days */
duration: number;
debug = false;
@ -57,26 +53,18 @@ export class SearchComponent implements OnInit {
this.sortOptions = [
{name: 'Relevance', property: 'score', descending: true},
{name: 'Region name', property: 'name'},
{name: 'Price', property: 'average_per_day_costs'}
{name: 'Name', property: 'name'},
{name: 'Accommodation', property: 'accommodation_costs'}
];
this.sortBy = 'score';
this.sortDes = true;
if (this.results.length > 0) {
const tags = await this.ss.getAvailableTags();
for (const {type} of this.results[0].scores) {
this.results[0].scores.forEach(({type}) => {
if (!this.sortOptions.find(i => i.property === type)) {
this.sortOptions.push({name: type, property: type, isScore: true, descending: tags.includes(type)});
this.sortOptions.push({name: type, property: type, isScore: true});
}
}
});
}
// Calculate duration
const {from, to} = base64ToObj(this.queryString) as Query;
const difference: number = (new Date(to)).getTime() - (new Date(from)).getTime();
this.duration = Math.round(difference / 1000 / 60 / 60 / 24);
});
}

View File

@ -24,41 +24,41 @@ export class TeamComponent implements OnInit {
},
{
name: 'Lucas Hinderberger',
position: 'Operations / Backend Developer',
position: 'Operations',
course: 'Software Engineering (SEB)',
semester: 6
},
{
name: 'Timo John',
position: 'Database / Backend Developer',
position: 'Database/Backend Developer',
course: 'Software Engineering (SEB)',
semester: 6
},
{
name: 'Timo Volkmann',
position: 'Project Lead / Backend Developer',
position: 'Backend Developer',
course: 'Software Engineering (SEB)',
semester: 6
},
{
name: 'Yannick von Hofen',
position: 'Marketing / Sales',
position: 'Management',
course: 'Transport und Logistik (MTL)',
semester: 2
},
{
name: 'Thomas Schapper',
position: 'Management Lead',
position: 'Management',
course: 'Transport und Logistik (MTL)',
semester: 2
},
{
name: 'Nicolas Karon',
position: 'Controlling',
position: 'Management',
course: 'Transport und Logistik (MTL)',
semester: 2
}
].sort((a, b) => a.name > b.name ? 1 : -1);
];
constructor() {
}

View File

@ -15,7 +15,6 @@ export interface Query {
fulltext?: boolean;
textfilter?: string;
showRegionsWithNullScore?: boolean;
tags?: string[];
}
export enum SearchParameter {

View File

@ -75,13 +75,6 @@ export class DataService {
public getPlacesByRegion(id: number): Promise<Place[]> {
return this.http.get<Place[]>(`${this.API_URL}/regions/${id}/nearby`).toPromise();
}
/**
* Returns all available search tags.
*/
public getAllTags(): Promise<string[]> {
return this.http.get<string[]>(`${this.API_URL}/search/tags`).toPromise();
}
}
/**
@ -90,144 +83,60 @@ export class DataService {
export const REGION_PARAM_VIS: RegionParamVisLookup = {
temperature_mean: {
icon: 'wb_sunny',
unit: '°C',
decimals: '1.0-0'
unit: '°C'
},
temperature_mean_min: {
icon: 'wb_sunny',
unit: '°C',
decimals: '1.0-0'
unit: '°C'
},
temperature_mean_max: {
icon: 'wb_sunny',
unit: '°C',
decimals: '1.0-0'
unit: '°C'
},
precipitation: {
icon: 'opacity',
unit: 'mm',
decimals: '1.0-0'
unit: 'mm'
},
humidity: {
icon: 'grain',
unit: '%',
decimals: '1.0-0'
unit: '%'
},
sun_hours: {
icon: 'flare',
unit: 'h',
decimals: '1.0-0'
unit: 'h'
},
rain_days: {
icon: 'date_range',
unit: '',
decimals: '1.0-0'
unit: ''
},
food_costs: {
icon: 'local_dining',
unit: '€/day',
decimals: '1.0-0'
unit: '€/day'
},
alcohol_costs: {
icon: 'local_bar',
unit: '€/day',
decimals: '1.2-2'
unit: '€/day'
},
water_costs: {
icon: 'local_cafe',
unit: '€/day',
decimals: '1.2-2'
unit: '€/day'
},
local_transportation_costs: {
icon: 'commute',
unit: '€/day',
decimals: '1.2-2'
unit: '€/day'
},
entertainment_costs: {
icon: 'local_activity',
unit: '€/day',
decimals: '1.2-2'
unit: '€/day'
},
accommodation_costs: {
icon: 'hotel',
unit: '€/day',
decimals: '1.2-2'
unit: '€/day'
},
average_per_day_costs: {
icon: 'euro',
unit: '€/day',
decimals: '1.2-2'
},
avg_price_relative: {
icon: 'hotel',
unit: '%',
decimals: '1.1-1'
},
beach: {
icon: 'stars',
unit: '%',
decimals: '1.0-0'
},
history: {
icon: 'stars',
unit: '%',
decimals: '1.0-0'
},
nature: {
icon: 'stars',
unit: '%',
decimals: '1.0-0'
},
art: {
icon: 'stars',
unit: '%',
decimals: '1.0-0'
},
culture: {
icon: 'stars',
unit: '%',
decimals: '1.0-0'
},
mountains: {
icon: 'stars',
unit: '%',
decimals: '1.0-0'
},
architecture: {
icon: 'stars',
unit: '%',
decimals: '1.0-0'
},
rainforest: {
icon: 'stars',
unit: '%',
decimals: '1.0-0'
},
nightlife: {
icon: 'stars',
unit: '%',
decimals: '1.0-0'
},
desert: {
icon: 'stars',
unit: '%',
decimals: '1.0-0'
},
food: {
icon: 'stars',
unit: '%',
decimals: '1.0-0'
},
shopping: {
icon: 'stars',
unit: '%',
decimals: '1.0-0'
},
volcanoes: {
icon: 'stars',
unit: '%',
decimals: '1.0-0'
},
unit: '€/day'
}
};
export interface RegionParamVisLookup {
@ -237,5 +146,4 @@ export interface RegionParamVisLookup {
export interface RegionParamVis {
icon: string;
unit: string;
decimals: string;
}

View File

@ -8,17 +8,11 @@ export interface SearchInput {
to: string;
singlePresetSelection: object;
multiPresetSelection: object;
tags: string[];
textFiler: string;
fullText: boolean;
tempMeanMax: number;
precipitation: number;
rain_days: number;
sun_hours: number;
average_per_day_costs: number;
accommodation: number;
entertainment_costs: number;
local_transportation_costs: number;
}
@Injectable({
@ -27,7 +21,6 @@ export interface SearchInput {
export class SearchService {
private searchInput: SearchInput;
private cachedTags: string[];
constructor(private ds: DataService) {
}
@ -43,13 +36,4 @@ export class SearchService {
public loadSearchInput(): SearchInput {
return this.searchInput;
}
public async getAvailableTags(): Promise<string[]> {
if (this.cachedTags) {
return this.cachedTags;
}
const tags: string[] = await this.ds.getAllTags();
this.cachedTags = tags;
return tags;
}
}

View File

@ -1,7 +0,0 @@
/**
* Transforms a value into a min max array.
* @param value The value
*/
export function toMinMaxArray(value: number): number[] {
return value ? [value, value] : undefined;
}

View File

View File

@ -1,15 +0,0 @@
<svg height="144.036" viewBox="0 0 108.027 108.027" width="144.036" xmlns="http://www.w3.org/2000/svg">
<defs>
<style>.a {
fill: #fff
}
.b {
fill: #1d1d1b
}</style>
</defs>
<path class="a"
d="M107.95 13.937c-.017-.172-.051-.34-.074-.51-.044-.331-.086-.662-.15-.986-.04-.201-.097-.396-.144-.595-.069-.284-.135-.57-.219-.848-.064-.212-.142-.416-.215-.624-.089-.255-.176-.51-.278-.76-.087-.214-.188-.421-.285-.63-.107-.231-.212-.462-.33-.686-.112-.214-.235-.42-.357-.628-.121-.208-.242-.414-.373-.615-.137-.21-.282-.413-.428-.616a15.53 15.53 0 0 0-.41-.547c-.16-.204-.328-.401-.5-.596a15.357 15.357 0 0 0-1.009-1.05q-.222-.21-.452-.412a15.47 15.47 0 0 0-.648-.538 15.574 15.574 0 0 0-.46-.344c-.238-.172-.479-.34-.726-.498-.15-.097-.303-.187-.457-.278-.264-.157-.53-.31-.804-.451-.146-.075-.296-.144-.445-.215a15.325 15.325 0 0 0-.884-.394c-.138-.055-.279-.102-.418-.153-.321-.118-.643-.233-.974-.33-.12-.035-.245-.061-.366-.094a15.299 15.299 0 0 0-1.079-.256c-.088-.017-.179-.025-.268-.04A15.51 15.51 0 0 0 92.551 0H15.477A15.477 15.477 0 0 0 0 15.477V92.55c0 .52.028 1.033.078 1.539.017.173.05.34.073.51.044.331.086.662.15.986.04.201.097.396.145.595.068.285.134.57.218.848.064.212.142.417.215.624.09.255.176.511.278.76.088.214.188.421.285.63.107.232.213.462.33.687.113.213.235.42.357.628.122.207.243.414.374.615.136.21.281.412.428.615.133.185.267.369.409.548.161.203.329.4.5.596a15.344 15.344 0 0 0 1.01 1.05q.221.21.452.411c.211.185.426.364.648.538.15.118.305.232.46.344.238.172.478.34.725.499.15.096.304.186.457.277.264.157.53.31.804.451.146.076.297.144.446.215.29.139.583.274.884.394.137.055.278.102.418.153.32.118.642.233.973.33.12.035.245.062.367.094a15.3 15.3 0 0 0 1.078.256c.088.017.18.026.268.041a15.51 15.51 0 0 0 2.646.242h77.075a15.477 15.477 0 0 0 15.476-15.476V15.477c0-.52-.027-1.033-.077-1.54z"/>
<path class="b"
d="M92.55 0H15.478A15.477 15.477 0 0 0 0 15.477V92.55a15.473 15.473 0 0 0 14.055 15.405l-.417-3.343a6.255 6.255 0 0 0 .69-2.26c0-.19-.063-.252-.251-.252 1.066-6.844 2.197-13.374 3.515-19.53l-.125-.564q6.877-30.424 9.231-41.13c-9.86.44-12.81-2.136-8.729-7.725 6.72.252 10.174 0 10.362-.69.25-.753 1.571-7.034 4.019-18.901a10.315 10.315 0 0 1 8.98-3.831c2.951 2.072 4.332 4.333 4.144 6.657a8.654 8.654 0 0 1 .063 1.757c-.25 0-.691 2.387-1.318 7.222h.187l-.502.941c0 1.697-.125 3.519-.314 5.464l7.284.564c4.081-.94 5.274.944 3.58 5.716-.817 1.883-5.086 2.7-12.936 2.385-1.004 5.212-1.947 9.672-2.888 13.376l.125.502c-.125.315-.315 1.006-.627 2.197h.25q-.47.754-3.39 15.826c-.19 0-.315.25-.502.753l.062 1.256c-.44-.062-1.76 6.907-4.019 20.786q-2.6 10.326-4.046 12.895H92.55a15.477 15.477 0 0 0 15.476-15.476V15.477A15.477 15.477 0 0 0 92.551 0zM80.766 53.435a7.127 7.127 0 0 1-6.782 3.453c0-.312-.19-.44-.502-.44H71.91a5.483 5.483 0 0 1-4.207-1.632h-.691q-.847.472-3.013 5.275a80.485 80.485 0 0 1-7.097 9.357v.188l.25.44-2.949 13.25.188.439c-1.697 9.105-2.888 13.689-3.58 13.689q-.094 1.413-3.58 2.261a2.046 2.046 0 0 1-.626-.188c-2.449.313-3.894.44-4.333.44-2.322-.44-3.455-1.006-3.392-1.822l.314-1.38a4.557 4.557 0 0 1-1.256-2.514l.315-.44.125-2.762.25-1.319a1.906 1.906 0 0 1-.188-.69 172.516 172.516 0 0 1 3.14-17.143l-.25-.44c2.826-11.177 4.27-17.018 4.27-17.52h-.25c1.694-6.216 2.698-9.67 3.013-10.299a14.49 14.49 0 0 1 4.647-1.568 7.397 7.397 0 0 1 7.284 5.023c.188 0 .314.816.375 2.511l-.312 1.13.187 1.383a17.633 17.633 0 0 0 5.59-12.12 4.424 4.424 0 0 1 2.762-2.01c4.96 0 7.724 2.197 8.352 6.594 2.387 4.334 3.58 7.033 3.517 8.226z"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -1,48 +1,33 @@
{
"temperature_mean_max": "Day temperature",
"temperature_mean_max": "Max Temperature Average",
"temperature": "Temperature",
"rain_days": "Rain days",
"sun_hours": "Sunshine",
"rain_days": "Rainy days",
"sun_hours": "Sunny hours",
"precipitation": "Precipitation",
"humidity": "Humidity",
"alcohol_costs": "Alcohol",
"food_costs": "Food",
"water_costs": "Water",
"cheap_alcohol": "cheap alcohol",
"cheap_alcohol": "Cheap alcohol",
"local_transportation_costs": "Public transport",
"average_per_day_costs": "Total costs",
"entertainment_costs": "Entertainment",
"accommodation_costs": "Accommodation price",
"avg_price_relative": "Price tendency",
"cheap_food": "cheap food",
"cheap_water": "cheap water",
"cheap_transportations": "cheap public transport",
"cheap_entertainment": "cheap entertainment",
"cheap_accommodation": "cheap accommodation",
"off_season": "low season",
"accommodation_costs": "Accommodation",
"cheap_food": "Cheap food",
"cheap_water": "Cheap water",
"cheap_transportations": "Cheap public transport",
"cheap_entertainment": "Cheap entertainment",
"cheap_accommodation": "Cheap accommodation",
"off_season": "Off-season",
"warm": "warm",
"chilly": "cold",
"mild:": "mild",
"cold": "freezing",
"sunny": "all day long",
"dark": "the sun scares me",
"normal": "better not so much",
"almost_no_rain": "no rain",
"little_rain": "a little is good",
"floodlike_rain": "i like it pouring",
"sunny": "sunny",
"dark": "dark",
"almost_no_rain": "almost none",
"little_rain": "little",
"floodlike_rain": "flooding",
"few_raindays": "few",
"many_raindays": "many",
"art": "Art",
"beach": "Beach",
"history": "History",
"nature": "Nature",
"culture": "Culture",
"mountains": "Mountains",
"architecture": "Architecture",
"rainforest": "Rainforest",
"desert": "Desert",
"food": "Food",
"shopping": "Shopping",
"volcanoes": "Volcanoes",
"nightlife": "Nightlife"
"many_raindays": "many"
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 KiB

BIN
frontend/src/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 948 B

View File

@ -7,7 +7,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="assets/favicon.svg" rel="shortcut icon" type="image/svg+xml">
</head>
<body>
<app-root></app-root>

View File

@ -39,21 +39,21 @@ $travopti-black: (
)
);
$travopti-blue: (
50: #00a0d2,
100: #00a0d2,
200: #00a0d2,
300: #00a0d2,
400: #00a0d2,
500: #00a0d2,
600: #00a0d2,
700: #00a0d2,
800: #00a0d2,
900: #00a0d2,
A100: #00a0d2,
A200: #00a0d2,
A400: #00a0d2,
A700: #00a0d2,
$travopti-green: (
50: #016500,
100: #006500,
200: #006500,
300: #005a00,
400: #008000,
500: #006000,
600: #005a00,
700: #005a00,
800: #2e7d32,
900: #1b5e20,
A100: #00ae00,
A200: #009000,
A400: #006400,
A700: #004d00,
contrast: (
50: $light-primary-text,
100: $light-primary-text,
@ -77,7 +77,7 @@ $travopti-blue: (
// (imported above). For each palette, you can optionally specify a default, lighter, and darker
// hue. Available color palettes: https://material.io/design/color/
$travopti-app-primary: mat-palette($travopti-black);
$travopti-app-accent: mat-palette($travopti-blue, A200, A100, A400);
$travopti-app-accent: mat-palette($travopti-green, A200, A100, A400);
// The warn palette is optional (defaults to red).
$travopti-app-warn: mat-palette($mat-red);