Compare commits

..

No commits in common. "develop" and "bugfix/showscore" have entirely different histories.

81 changed files with 707 additions and 4649 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,13 @@ 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,14 +28,13 @@ 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))
app.use((err, req, res, next) => {
// 500
@ -88,6 +50,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,13 +1,12 @@
const axios = require("axios")
const getPlacePhoto = require("./getPlacePhoto.js")
const fields = "photos,place_id,name,rating,geometry" // Parameters for Google Place API
module.exports = async (q) => {
const res = await axios.get(
`https://maps.googleapis.com/maps/api/place/findplacefromtext/json?inputtype=textquery&fields=${fields}&input=${q}&key=${process.env.GOOGLE_CLOUD_APIS}`)
`https://maps.googleapis.com/maps/api/place/findplacefromtext/json?inputtype=textquery&fields=photos,formatted_address,name,rating,opening_hours,geometry&input=${q}&key=${process.env.GOOGLE_CLOUD_APIS}`)
console.log(res.data)
// Photo url is not returned by default since it overuses Google Place API
// Photo url is not returned since it overuses Google Place API
/*
for (let candidate of res.data.candidates) {
for (let photo of candidate.photos) {

View File

@ -1,15 +1,14 @@
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"
module.exports = async (lat, lng) => {
const res = await axios.get(
`https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=${lat},${lng}&radius=${radius}&type=${types}&rankby=${rankby}&key=${process.env.GOOGLE_CLOUD_APIS}`
`https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=${lat},${lng}&radius=${radius}&rankby=${rankby}&key=${process.env.GOOGLE_CLOUD_APIS}`
);
// Photo url is not returned by default since it overuses Google Place API
// Photo url is not returned since it overuses Google Place API
/*
for (let result of res.data.results) {
for (let photo of result.photos) {

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

@ -6,8 +6,6 @@ module.exports = async (dbConn, id) => {
regions.region AS name,
countries.country AS country,
regions.description AS description,
regions.lon AS lon,
regions.lat AS lat,
rcma.temperature_mean AS temperature_mean,
rcma.temperature_mean_min AS temperature_mean_min,
rcma.temperature_mean_max AS temperature_mean_max,
@ -26,18 +24,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 +54,17 @@ 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

@ -1,21 +0,0 @@
const arrayFormatting = require("../util/databaseArrayFormatting.js")
module.exports = async (dbConn, id) => {
const region_nearby = await dbConn.query(
`SELECT id as place_id,
region_id as region_id,
name as place_name,
lon as lon,
lat as lat,
rating as rating,
vicinity as vicinity,
photo_reference as photo_reference,
img_url as img_url
FROM regions_nearby
WHERE region_id = ?`,
[id]
);
return region_nearby;
};

View File

@ -1,13 +1,12 @@
const arrayFormatting = require("../util/databaseArrayFormatting.js");
const { allTagsWithValues } = require("./getTags.js");
const { takeRightWhile } = require("lodash");
module.exports = async (dbConn) => {
const sqlRegions = `SELECT regions.id AS region_id,
const regions = await dbConn.query(
`SELECT regions.id AS region_id,
regions.region AS name,
countries.country AS country,
regions.description AS description,
regions.lon AS lon,
regions.lat AS lat,
rcma.temperature_mean AS temperature_mean,
rcma.temperature_mean_min AS temperature_mean_min,
rcma.temperature_mean_max AS temperature_mean_max,
@ -26,24 +25,23 @@ 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++) {
regions[k].avg_price_relative = arrayFormatting(regions[k].avg_price_relative);
@ -54,15 +52,19 @@ 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 => {
region.tags = tags.filter(tag => tag.region_id === region.region_id).map(tag => {
delete tag.region_id
return tag
})
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
return region
});
};

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

@ -1,22 +0,0 @@
exports.getUniqueTags = async (dbConn) => {
let tags = await dbConn.query(
`SELECT DISTINCT name FROM region_feedback;`
);
return tags;
};
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};`
);
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,34 +0,0 @@
const axios = require("axios")
const getRegions = require("../models/getRegions.js")
const fields = "geometry" // Parameters for Google Places API
module.exports = async (dbConn) => {
const regions = await getRegions(dbConn)
for (let region of regions) {
try {
const q = region.name
const place = await axios.get(
`https://maps.googleapis.com/maps/api/place/findplacefromtext/json?inputtype=textquery&fields=geometry&input=${q}&key=${process.env.GOOGLE_CLOUD_APIS}`)
const region_id = region.region_id
const lon = parseFloat(place.data.candidates[0].geometry.location.lng)
const lat = parseFloat(place.data.candidates[0].geometry.location.lat)
await dbConn.query(
`UPDATE regions
SET lon=${lon}, lat=${lat}
WHERE id = ${region_id}`
);
console.log("Updating coordinates for region: ", region_id, q, " ", "lon:", lon, "lat", lat)
} catch (e) {
console.log(e)
}
}
const res = "lon lat update finished"
return res
}

View File

@ -1,48 +0,0 @@
const axios = require("axios")
const getRegions = require("../models/getRegions.js")
const getPlaceNearby = require("../models/getPlaceNearby.js")
module.exports = async (dbConn) => {
const regions = await getRegions(dbConn)
for (let region of regions) {
try {
const region_id = region.region_id
const region_lon = region.lon
const region_lat = region.lat
console.log("Updating nearby for region: ", region_id, region.name)
const places = await getPlaceNearby(region_lat, region_lon)
for (let result of places.results) {
const name = result.name
const rating = result.rating === undefined ? null : result.rating
const lon = result.geometry.location.lng
const lat = result.geometry.location.lat
const photo_ref = result.photos[0].photo_reference
const vicinity = result.vicinity
console.log("# New tourist attraction:", region_id, region.name, name)
await dbConn.query(
`INSERT INTO regions_nearby
(region_id,name,lon,lat,rating,vicinity,photo_reference)
VALUES
(${region_id},"${name}",${lon},${lat},${rating},"${vicinity}","${photo_ref}")
ON DUPLICATE KEY UPDATE
lon = ${lon},
lat = ${lat},
rating = ${rating},
vicinity = "${vicinity}",
photo_reference = "${photo_ref}"`
);
}
} catch (e) {
console.log(e)
}
}
const res = "region nearby update finished"
return res
}

View File

@ -1,51 +0,0 @@
const axios = require("axios")
const getRegionById = require("../models/getRegionById.js")
const getPlaceNearby = require("../models/getPlaceNearby.js")
module.exports = async (dbConn, id) => {
const region = await getRegionById(dbConn, id)
try {
const region_id = region.region_id
const region_lon = region.lon
const region_lat = region.lat
console.log("Updating nearby for region: ", region_id, region.name)
const places = await getPlaceNearby(region_lat, region_lon)
for (let result of places.results) {
try {
const name = result.name
const rating = result.rating === undefined ? null : result.rating
const lon = result.geometry.location.lng
const lat = result.geometry.location.lat
const photo_ref = result.photos[0].photo_reference
const vicinity = result.vicinity
console.log("# New tourist attraction:", region_id, region.name, name)
await dbConn.query(
`INSERT INTO regions_nearby
(region_id,name,lon,lat,rating,vicinity,photo_reference)
VALUES
(${region_id},"${name}",${lon},${lat},${rating},"${vicinity}","${photo_ref}")
ON DUPLICATE KEY UPDATE
lon = ${lon},
lat = ${lat},
rating = ${rating},
vicinity = "${vicinity}",
photo_reference = "${photo_ref}"`
);
} catch (e) {
console.log(e)
}
}
} catch (e) {
console.log(e)
}
const res = "region nearby by id update finished"
return res
}

View File

@ -1,32 +0,0 @@
const getRegionNearbyById = require("../models/getRegionNearbyById.js")
const getPlacePhoto = require("../models/getPlacePhoto.js")
module.exports = async (dbConn) => {
try {
const region_ids = await dbConn.query(`
SELECT distinct region_id
FROM regions_nearby
ORDER BY region_id`)
for (let region_id of region_ids) {
const nearby = await getRegionNearbyById(dbConn, region_id.region_id)
for (let place of nearby) {
const url = await getPlacePhoto(place.photo_reference)
console.log("# Setting image Url:", region_id, place.place_name, url)
await dbConn.query(`
UPDATE regions_nearby
SET img_url = "${url}"
WHERE id = ${place.place_id}`)
}
}
} catch (e) {
console.log(e)
}
const res = "region nearby img url update finished"
return res
}

View File

@ -1,33 +0,0 @@
const getRegionNearbyById = require("../models/getRegionNearbyById.js")
const getPlacePhoto = require("../models/getPlacePhoto.js")
module.exports = async (dbConn,id) => {
try {
const region_ids = await dbConn.query(`
SELECT distinct region_id
FROM regions_nearby
WHERE region_id = ?`,
[id])
for (let region_id of region_ids) {
const nearby = await getRegionNearbyById(dbConn, region_id.region_id)
for (let place of nearby) {
const url = await getPlacePhoto(place.photo_reference)
console.log("# Setting image Url:", region_id, place.place_name, url)
await dbConn.query(`
UPDATE regions_nearby
SET img_url = "${url}"
WHERE id = ${place.place_id}`)
}
}
} catch (e) {
console.log(e)
}
const res = "region nearby img url update finished"
return res
}

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,15 +1405,10 @@
"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",
"integrity": "sha512-vF4ZbYdKS8OnoJAWBmMxCQDkiEBkGQYU7UZPtL8flbDRSNkhaXvRJ279ZtI6M+zDaQovVU4tuRgzK5fVhvFAhg=="
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz",
"integrity": "sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A="
},
"statuses": {
"version": "1.5.0",
@ -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

@ -20,10 +20,7 @@
"moment": "^2.26.0",
"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"
"path": "^0.12.7"
},
"devDependencies": {
"nodemon": "^2.0.4"

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

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

View File

@ -1,53 +1,14 @@
/**
* @swagger
* tags:
* name: Countries
* description: Access country data.
*/
const router = require("express").Router();
// Models
const getCountries = require("../models/getCountries.js");
const getCountryById = require("../models/getCountryById.js");
// Utils
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);
const id = req.params.id;
res.json(await getCountryById(dbConn, id))
});
return router;

View File

@ -1,93 +1,21 @@
/**
* @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
const getPlace = require("../models/getPlace.js")
const getPlaceNearby = require("../models/getPlaceNearby.js")
const getPlacePhoto = require("../models/getPlacePhoto.js")
// Utils
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
const place = await getPlaceNearby(lat, lng)
const place = await getPlaceNearby(req.query.lat, req.query.lng)
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)
const photo = await getPlacePhoto(req.query.photoref)
res.json(photo)
});

View File

@ -1,112 +1,29 @@
/**
* @swagger
* tags:
* name: Regions
* description: Access region data.
*/
const router = require("express").Router();
// Models
const getRegions = require("../models/getRegions.js");
const getRegionById = require("../models/getRegionById.js");
const getRegionNearbyById = require("../models/getRegionNearbyById.js")
// Utils
const path = require("path");
const fs = require("fs");
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) {
const randomize = sqlSanitzer(req.query.randomize)
res.json(_.sampleSize(data, randomize))
res.json(_.sampleSize(data, req.query.randomize))
} else {
res.json(data);
}
});
/**
* @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);
console.log(id)
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))
router.get("/api/v1/regions/:id", async (req, res) => {
const id = req.params.id;
res.json(await getRegionById(dbConn, id))
});
return router;
};

View File

@ -1,80 +1,19 @@
/**
* @swagger
* tags:
* name: Search
* description: Access the search algorithm and the data provided for searching.
*/
const router = require("express").Router();
// Models
const getRegions = require('../models/getRegions.js');
const getSearchPresets = require("../models/getSearchPresets.js");
// Utils
const _ = require('lodash')
const getSearchPresets = require("../models/getSearchPresets.js");
const base64 = require("../util/base64.js")
const scoreAndSearch = require("../util/scoreAndSearch.js");
const sas = require("../util/scoreAndSearch.js");
const oldToNewQuerySyntax = require("../util/oldToNewQuerySyntax.js")
const { getUniqueTags } = require("../models/getTags.js");
const getRegions = require('../models/getRegions.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;
};
function tagsHandler(dbConn) {
return function (req, res) {
getUniqueTags(dbConn).then(tags => {
res.json(tags.map(tag => tag.name))
}).catch(error => {
// TODO error handling
})
}
}
function presetHandler(dbConn) {
return function (req, res) {
getSearchPresets(dbConn).then(presets => {
@ -86,6 +25,7 @@ function presetHandler(dbConn) {
}
function searchHandler(dbConn) {
const scoreAndSearch = sas(dbConn)
return async function (req, res) {
let response = {}
@ -106,18 +46,18 @@ 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 data = {
regions: regions,
}
let data = await getRegions(dbConn)
// FILTER if query contains filterString
if (q.textfilter) {
data.regions = filterByString(data.regions, q.textfilter, q.fulltext)
data = filterByString(data, q.textfilter, q.fulltext)
}
scoreAndSearch(data, q.from, q.to, scoreQueryObj).then(searchResults => {
//response.data = searchResults
const cutScores = !(_.isEmpty(scoreQueryObj.climate) && _.isEmpty(scoreQueryObj.costs) && _.isEmpty(scoreQueryObj.others))
// only dev:
if (process.env.SHOW_MATCH_VALUE === '1') searchResults.forEach(reg => reg.name = `${reg.name} (${_.round(reg.score * 10, 1)}% match)`)
@ -126,8 +66,32 @@ 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
if (_.isEmpty(searchResults)) {
res.status(204).json(searchResults)
return
}
// response.data = searchResults
// res.json(response)
res.json(searchResults)
}).catch(e => {
@ -176,7 +140,6 @@ function prepareQueries(queries) {
// others
if (queries.avg_price_relative) q.others.avg_price_relative = queries.avg_price_relative
if (queries.tags) q.others.tags = queries.tags
return q
}

View File

@ -1,135 +0,0 @@
/**
* @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")
const handleUpdateRegionNearbyImgUrlById = require("../models/handleUpdateRegionNearbyImgUrlById.js")
// Utils
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) => {
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) => {
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) => {
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) => {
const id = sqlSanitzer(req.params.id);
res.json(await handleUpdateRegionNearbyImgUrlById(dbConn, id))
});
return router
}

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')

View File

@ -17,18 +17,7 @@ module.exports = function (queries) {
if (queries.local_transportation_costs) res.local_transportation_costs = [Number(queries.local_transportation_costs.split(',')[0]), Number(queries.local_transportation_costs.split(',')[1])]
if (queries.entertainment_costs) res.entertainment_costs = [Number(queries.entertainment_costs.split(',')[0]), Number(queries.entertainment_costs.split(',')[1])]
if (queries.average_per_day_costs) res.average_per_day_costs = [Number(queries.average_per_day_costs.split(',')[0]), Number(queries.average_per_day_costs.split(',')[1])]
if (queries.avg_price_relative) res.avg_price_relative = [Number(queries.avg_price_relative.split(',')[0]), Number(queries.avg_price_relative.split(',')[1])]
if (queries.tags) {
res.tags = []
if (queries.tags.includes(',')) {
res.tags.push(...queries.tags.split(',').map(el => el.trim()))
} else {
res.tags.push(queries.tags)
}
}
// console.log(res);
// console.log('queries successfully transformed');
// } catch (error) {
// console.log('oldToNewQuerySyntax error');

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,306 +1,284 @@
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')
module.exports = async function (data, from, to, q) {
console.log('search')
console.log(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)) {
throw new Error('database error')
}
let regionsArr = data.regions
// PREPARE SEARCH
// validate dates
const dates = validateDates(from, to)
// for calculating average if traveldates are in more than one month
const travelPeriods = travelPeriodsFromDates(dates)
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 = function (dbConn) {
return async function (regions, from, to, q) {
console.log('search')
if ((_.isNil(to) || _.isNil(from)) && !(_.isEmpty(q.climate) || _.isEmpty(q.costs) || _.isEmpty(q.others))) {
throw new Error('invalid query')
}
// PREPARE SEARCH
// validate dates
const dates = validateDates(from, to)
// for calculating average if traveldates are in more than one month
const travelPeriods = travelPeriodsFromDates(dates)
// CALCULATE PROPERTIES FOR EACH REGION
regionsArr.forEach(reg => {
// FETCH DATA FROM DB
const boundaryClimate = await getClimateMinMax.getClimateMinMax(dbConn)
// let regions = await getRegions(dbConn)
regions.forEach(reg => reg.scores = [])
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
}
}
// little tweak to show score object without request DEPRECATED
if (SHOW_ALL_SCOREOBJECTS) {
if (!q.climate.temperature_mean_max) q.climate.temperature_mean_max = [null, null]
if (!q.climate.precipitation) q.climate.precipitation = [null, null]
if (!q.climate.rain_days) q.climate.rain_days = [null, null]
if (!q.climate.sun_hours) q.climate.sun_hours = [null, null]
if (!q.climate.accommodation_costs) q.climate.accommodation_costs = [null, null]
}
// CALCULATE SCORES FOR CLIMATE PROPS
reg.scores = []
Object.entries(q.climate).forEach(([key, value]) => {
let finalScoreObj = calculateScoreForPeriod(key, travelPeriods, reg, value[0], value[1])
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])
regions.forEach(reg => {
Object.entries(q.climate).forEach(([key, value]) => {
let finalScoreObj = calculateScoreForPeriod(key, travelPeriods, reg, value[0], value[1], boundaryClimate)
reg.scores.push(finalScoreObj)
});
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)
reg.scores.push(offSeasonScoreObj)
}
// CALCULATE SCORE FOR TAGS
if (_.has(q, 'others.tags')) {
reg.scores.push(...scoresFromTags(reg.tags, q.others.tags))
}
// CALCULATE PRICE TENDENCY FOR TIMEFRAME
reg.price_tendency_relative = getAverageFromTrivago(travelPeriods, reg)
// CALCULATE SUM FOR ACCOMODATION FROM AVERAGE PRICES AND APPROX LIFESTYLE COSTS
reg.total_accommodation_costs = _.round(sumForRangeAvg(dates.from, dates.to, reg.accommodation_costs), 2)
reg.total_avg_lifestyle = _.round(sumForRangeAvg(dates.from, dates.to, reg.average_per_day_costs - reg.accommodation_costs), 2)
//reg.name = `${reg.name} ca. ${_.round(sumForRangeAvg(dates.from, dates.to, reg.accommodation_costs), 2)}€`
// CALCULATE TOTAL PRICE WITH TRANSPORTATION
// CALCULATE TOTAL PRICE WITH TRANSPORTATION AND ESTEEMED LIFE COSTS
// CALCULATE AVERAGE SCORE Stage 1
let scoreSubGroups = []
if (!_.isEmpty(q.climate)) scoreSubGroups.push(calculateAverage(reg.scores.filter(el => _.some(Object.keys(q.climate), entry => entry === el.type ) )) )
if (!_.isEmpty(q.costs)) scoreSubGroups.push(calculateAverage(reg.scores.filter(el => _.some(Object.keys(q.costs), entry => entry === el.type ))) )
if (!_.isEmpty(q.others.avg_price_relative)) scoreSubGroups.push(calculateAverage(reg.scores.filter(el => 'avg_price_relative' === el.type ) ))
if (!_.isEmpty(q.others.tags)) scoreSubGroups.push(calculateAverage(reg.scores.filter(el => _.some(q.others.tags, entry => entry === el.type ))) )
// CALCULATE AVERAGE SCORE Stage 2
// reg.score = calculateAverage(reg.scores)
reg.score = _.round(_.sum(scoreSubGroups) / scoreSubGroups.length, 3)
})
return _.orderBy(regionsArr, ({ score }) => score || 0, 'desc') //.filter(el => !_.isNaN(el.score))
}
function sumForRangeAvg(from, to, avg) {
let duration = moment(to).diff(moment(from), 'days')
return duration * avg
}
function sumForRangeFromDailyValues(from, to, dailyValues) {
// NOT NEEDED YET
// for (var m = moment(from).subtract(1, 'months'); m.isSameOrBefore(moment(to).subtract(1, 'months')); m.add(1, 'day')) {
// console.log(m);
// }
}
function calculateAverage(scores) {
let sum = 0
let cnt = 0
scores.forEach(el => {
if (el.score !== null && el.score !== undefined && !_.isNaN(el.score)) {
cnt++
sum += el.score
}
if (el.score === null || el.score === undefined || _.isNaN(el.score)) {
cnt++
sum += -1
}
})
//if (sum === 0 && cnt === 0) return 0
return _.round(sum / cnt, 3)
}
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()) {
let period = {
month: start.month()+1,
days: end.date() - start.date()
}
travelPeriods.push(period)
} else {
for (var m = moment(start); m.isSameOrBefore(moment(end).endOf("month")); m.add(1, 'months')) {
console.log(m);
// CALCULATE SCORES FOR PRICE PROPS
Object.entries(q.costs).forEach(([key, value]) => {
let finalScoreObj = calculateSimpleScore(key, reg, value[0], value[1], boundaryStatic)
reg.scores.push(finalScoreObj)
});
travelPeriods.push(createPeriod(start, end, m.month()+1, m.year()))
}
}
return travelPeriods
}
function validateDates(from, to) {
let fromAndTo = {
from: {},
to: {}
// 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], boundaryStatic, 'easeOut', 2)
reg.scores.push(offSeasonScoreObj)
}
reg.price_tendency_relative = getAverageFromTrivago(travelPeriods, reg)
// CALCULATE AVERAGE SCORE Stage 1
let scoreSubGroups = []
if (!_.isEmpty(q.climate)) scoreSubGroups.push(calculateAverage(reg.scores.filter(el => _.some(Object.keys(q.climate), entry => entry === el.type ) )) )
if (!_.isEmpty(q.costs)) scoreSubGroups.push(calculateAverage(reg.scores.filter(el => _.some(Object.keys(q.costs), entry => entry === el.type ))) )
if (!_.isEmpty(q.others)) scoreSubGroups.push(calculateAverage(reg.scores.filter(el => _.some(Object.keys(q.others), entry => entry === el.type ) )) )
// CALCULATE AVERAGE SCORE Stage 2
reg.score = calculateAverage(reg.scores)
// reg.score = _.sum(scoreSubGroups) / scoreSubGroups.length
})
return _.orderBy(regions, ({ score }) => score || 0, 'desc') //.filter(el => !_.isNaN(el.score))
}
if (_.isNumber(from) && _.isNumber(to)) {
let dateFrom = new Date(from)
fromAndTo.from.day = dateFrom.getDate()
fromAndTo.from.month = dateFrom.getMonth() + 1
fromAndTo.from.year = dateFrom.getFullYear()
let dateTo = new Date(to)
fromAndTo.to.day = dateTo.getDate()
fromAndTo.to.month = dateTo.getMonth() + 1
fromAndTo.to.year = dateTo.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;
fromAndTo.from.year = Number(from.split("-")[0])
fromAndTo.to.year = Number(to.split("-")[0])
fromAndTo.from.month = Number(from.split("-")[1])
fromAndTo.to.month = Number(to.split("-")[1])
fromAndTo.from.day = Number(from.split("-")[2])
fromAndTo.to.day = Number(to.split("-")[2])
if (!from.match(re) || !to.match(re)) throw new Error("ERR: invalid parameter:", from, to)
function calculateAverage(scores) {
let sum = 0
let cnt = 0
scores.forEach(el => {
if (el.score !== null && el.score !== undefined && !_.isNaN(el.score)) {
cnt++
sum += el.score
}
if (el.score === null || el.score === undefined || _.isNaN(el.score)) {
cnt++
sum += -1
}
})
//if (sum === 0 && cnt === 0) return 0
return _.round(sum / cnt, 3)
}
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'.")
return fromAndTo
}
function createPeriod(start, end, currentMonth, currentYear) {
let period = {}
console.log(start, end, currentMonth, currentYear);
if (currentMonth === start.month() + 1 && currentYear === start.year()) {
console.log('first month')
period = {
month: currentMonth,
days: 32 - start.date()
}
} else if (currentMonth === end.month() + 1) {
console.log('end month')
period = {
month: currentMonth,
days: end.date()
}
} else {
console.log('middle month')
period = {
month: currentMonth,
days: 30
function travelPeriodsFromDates(dates) {
let travelPeriods = []
if (dates.from.month === dates.to.month && dates.from.year === dates.to.year) {
let period = {
month: dates.from.month,
days: dates.to.day - dates.from.day
}
travelPeriods.push(period)
} else {
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
}
return period
}
function calculateScoreForPeriod(type, travelPeriods, region, searchLowParam, searchMaxParam) {
// console.log('getScoreAndAverageFromClimate for', region.name, type)
const singleScores = travelPeriods. map(period => {
let res = {
function validateDates(from, to) {
let fromAndTo = {
from: {},
to: {}
}
if (_.isNumber(from) && _.isNumber(to)) {
let dateFrom = new Date(from)
fromAndTo.from.day = dateFrom.getDate()
fromAndTo.from.month = dateFrom.getMonth() + 1
fromAndTo.from.year = dateFrom.getFullYear()
let dateTo = new Date(to)
fromAndTo.to.day = dateTo.getDate()
fromAndTo.to.month = dateTo.getMonth() + 1
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;
fromAndTo.from.year = Number(from.split("-")[0])
fromAndTo.to.year = Number(to.split("-")[0])
fromAndTo.from.month = Number(from.split("-")[1])
fromAndTo.to.month = Number(to.split("-")[1])
fromAndTo.from.day = Number(from.split("-")[2])
fromAndTo.to.day = Number(to.split("-")[2])
if (!from.match(re) || !to.match(re)) throw new Error("ERR: invalid parameter:", from, to)
}
if (moment(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(from, to, currentMonth, currentYear) {
let period = {}
if (currentMonth === from.month && currentYear === from.year) {
period = {
month: currentMonth,
days: 32 - from.day
}
} else if (currentMonth === to.month) {
period = {
month: currentMonth,
days: to.day
}
} else {
period = {
month: currentMonth,
days: 30
}
}
return period
}
function calculateScoreForPeriod(type, travelPeriods, region, searchLowParam, searchMaxParam, minMax) {
// console.log('getScoreAndAverageFromClimate for', region.name, type)
const singleScores = travelPeriods.map(period => {
let res = {
type: type,
value: region[type][period.month - 1],
days: period.days
}
return res
})
let averagedScore = {
type: type,
value: region[type] !== null ? region[type][period.month - 1] : null,
days: period.days
value: 0,
days: 0
}
singleScores.forEach(el => {
if (el.value !== null && !_.isNaN(el.value)) {
averagedScore.value += (el.value * el.days)
averagedScore.days += (el.days)
} else {
// console.log('skip averaging')
// console.log(el)
}
})
averagedScore.value = _.round(averagedScore.value / averagedScore.days, 3)
delete averagedScore.days
return res
})
let averagedScore = {
type: type,
value: 0,
days: 0
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
return averagedScore
}
singleScores.forEach(el => {
if (el.value !== null && !_.isNaN(el.value)) {
averagedScore.value += (el.value * el.days)
averagedScore.days += (el.days)
} else {
// console.log('skip averaging')
// console.log(el)
function calculateSimpleScore(type, region, searchLowParam, searchMaxParam, minMax) {
// console.log('getScoreFromCosts for', region.name, type)
const sc = _.round(scorer.calculateScoreRange(minMax.min[type], minMax.max[type], SETTINGS[type][0], region[type], searchLowParam, searchMaxParam), 3)
let finScore = {
type: type,
value: region[type],
score: scorer[SETTINGS[type][1]](sc, SETTINGS[type][2]),
}
finScore.value = _.round(finScore.value, 1)
finScore.score = _.round(finScore.score, 3)
if (searchLowParam === null) finScore.score = null
return finScore
}
function getAverageFromTrivago(travelPeriods, region) {
// console.log('getAverageFromTrivago for', region.name)
const singleScores = travelPeriods.map(period => {
let res = {
value: region.avg_price_relative[period.month - 1],
days: period.days
}
}
})
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)
averagedScore.score = _.round(scorer[SETTINGS[type][1]](sc, SETTINGS[type][2]), 3)
// console.log('score', averagedScore.score)
if (searchLowParam === null) averagedScore.score = null
return averagedScore
}
function scoresFromTags(regionTags, tagStringsFromQueries) {
return tagStringsFromQueries.map(tagQuery => {
const tag = regionTags.find(tag => tagQuery === tag.name)
let retVal = {
type: tagQuery,
value: null,
score: null,
}
if (_.isNil(tag)) return retVal
retVal.value = tag.value
retVal.score = /* tag.value <= 0 ? 0 : */ _.round(scorer.calculateScoreRange(60, tag.value, 100, 100), 3)
console.log(retVal);
return res
})
return retVal
})
}
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)
let finScore = {
type: type,
value: region[type],
score: scorer[SETTINGS[type][1]](sc, SETTINGS[type][2]),
}
finScore.value = _.round(finScore.value, 1)
finScore.score = _.round(finScore.score, 3)
if (searchLowParam === null) finScore.score = null
return finScore
}
function getAverageFromTrivago(travelPeriods, region) {
// console.log('getAverageFromTrivago for', region.name)
const singleScores = travelPeriods.map(period => {
let res = {
value: region.avg_price_relative[period.month - 1],
days: period.days
let averagedScore = {
value: 0,
days: 0
}
singleScores.forEach(el => {
if (el.value !== null && !_.isNaN(el.value)) {
averagedScore.value += (el.value * el.days)
averagedScore.days += (el.days)
} else {
// console.log('skip averaging')
// console.log(el)
}
})
averagedScore.value = _.round(averagedScore.value / averagedScore.days, 2)
return res
})
let averagedScore = {
value: 0,
days: 0
return averagedScore.value
}
singleScores.forEach(el => {
if (el.value !== null && !_.isNaN(el.value)) {
averagedScore.value += (el.value * el.days)
averagedScore.days += (el.days)
} else {
// console.log('skip averaging')
// console.log(el)
}
})
averagedScore.value = _.round(averagedScore.value / averagedScore.days, 2)
return averagedScore.value
//end
}

View File

@ -1,14 +0,0 @@
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)
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

@ -25,10 +25,8 @@ import {
MatBadgeModule,
MatButtonToggleModule,
MatCheckboxModule,
MatChipsModule,
MatDialogModule,
MatDividerModule,
MatListModule,
MatRadioModule,
MatSliderModule,
MatSlideToggleModule,
@ -49,8 +47,6 @@ import {ShareDialogComponent} from './dialogs/share-dialog/share-dialog.componen
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({
@ -70,9 +66,7 @@ import {MultiTagSelectComponent} from './components/multi-tag-select/multi-tag-s
ShareButtonComponent,
ShareDialogComponent,
TeamComponent,
ToggleSliderComponent,
PlaceComponent,
MultiTagSelectComponent
ToggleSliderComponent
],
imports: [
BrowserModule,
@ -101,9 +95,7 @@ import {MultiTagSelectComponent} from './components/multi-tag-select/multi-tag-s
MatStepperModule,
MatRadioModule,
MatSlideToggleModule,
MatSliderModule,
MatChipsModule,
MatListModule
MatSliderModule
],
providers: [],
bootstrap: [AppComponent],

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

@ -1,6 +0,0 @@
<mat-card (click)="onClick($event)">
<img [src]="place.img_url" alt="Image of {{place.place_name}}">
<div class="footer">
<span class="name">{{place.place_name}}</span>
</div>
</mat-card>

View File

@ -1,28 +0,0 @@
mat-card {
padding: 0;
display: flex;
flex-direction: column;
}
img {
flex: 0 0 auto;
width: 20rem;
height: 11.25rem;
object-fit: cover;
}
.footer {
width: 20rem;
padding: 0.5rem;
box-sizing: border-box;
flex: 0 1 auto;
display: flex;
flex-direction: row;
.name {
flex: 0 1 auto;
overflow-x: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}

View File

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

View File

@ -1,24 +0,0 @@
import {Component, Input, OnInit} from '@angular/core';
import {Place} from '../../interfaces/places.interface';
@Component({
selector: 'app-place',
templateUrl: './place.component.html',
styleUrls: ['./place.component.scss']
})
export class PlaceComponent implements OnInit {
@Input()
place: Place;
constructor() {
}
ngOnInit() {
}
onClick($event: MouseEvent) {
$event.stopPropagation();
window.open(`https://google.com/maps?q=${encodeURI(this.place.place_name)}`, '_blank');
}
}

View File

@ -2,62 +2,38 @@
<img alt="Picture of {{result.name}}" class="result-img"
src="https://travopti.de/api/v1/regions/{{result.region_id}}/image">
<div class="result-title">
<div class="result-header">
<span class="result-name">{{result.name}}<span *ngIf="debug"> ({{result.score}})</span></span>
<div class="result-name">
<span class="result-name">{{result.name}}</span>
<span class="result-country">| {{result.country}}</span>
</div>
<app-bookmark-button [region]="result"></app-bookmark-button>
<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>
</div>
<mat-divider *ngIf="result.scores.length > 0"></mat-divider>
<div *ngIf="result.scores.length > 0" class="searched-params">
<table>
<tr *ngFor="let score of result.scores" [ngClass]="{'undefined': score.value == undefined}">
<td>
<div class="cell space">
<mat-icon>{{PROPERTY_VIS_DEF[score.type] ? PROPERTY_VIS_DEF[score.type].icon : 'bar_chart'}}</mat-icon>
<span>{{score.type|translate}}:</span>
</div>
</td>
<td>
<div class="cell right">
<span>{{score.value != undefined ? (score.value|number:PROPERTY_VIS_DEF[score.type].decimals) : 'N/A'}}</span>
</div>
</td>
<td>
<div class="cell">
<span>{{PROPERTY_VIS_DEF[score.type] ? PROPERTY_VIS_DEF[score.type].unit : ''}}</span>
</div>
</td>
<td *ngIf="debug">
<div class="cell">
<span>({{score.score}})</span>
</div>
</td>
</tr>
</table>
</div>
<mat-divider></mat-divider>
<div class="result-desc">Estimated values for {{duration}} {{duration > 1 ? 'days' : 'day'}}</div>
<table>
<tr *ngFor="let score of result.scores" [ngClass]="{'undefined': score.value == undefined}">
<td>
<div class="cell space">
<mat-icon>{{PROPERTY_VIS_DEF[score.type] ? PROPERTY_VIS_DEF[score.type].icon : 'bar_chart'}}</mat-icon>
<span>{{score.type|translate}}:</span>
</div>
</td>
<td>
<div class="cell right">
<span>{{score.value != undefined ? (score.value|number:'1.2-2') : 'N/A'}}</span>
</div>
</td>
<td>
<div class="cell">
<span>{{PROPERTY_VIS_DEF[score.type] ? PROPERTY_VIS_DEF[score.type].unit : ''}}</span>
</div>
</td>
<td>
<div class="cell">
<span>({{score.score}})</span>
</div>
</td>
</tr>
</table>
</div>
</div>

View File

@ -15,73 +15,33 @@
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 0.25rem;
margin-bottom: 0.5rem;
> .result-header {
> .result-name {
flex: 1 1 auto;
display: flex;
flex-direction: row;
margin: 0.25rem 0;
align-items: center;
> .result-name {
font-weight: bold;
font-size: larger;
margin-right: 0.25rem;
}
> .result-country {
text-transform: uppercase;
font-size: small;
align-self: center;
}
> .result-name {
font-weight: bold;
font-size: larger;
align-self: center;
margin-right: 0.25rem;
overflow-y: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
> .result-details {
display: flex;
flex-direction: column;
> .total-price-container {
display: flex;
flex-direction: row;
margin: 0.5rem 0;
> div {
display: flex;
flex-direction: row;
align-items: center;
margin-right: 1rem;
> mat-icon {
margin-right: 0.25rem;
}
}
}
> .searched-params {
margin: 0.5rem 0
}
.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,11 @@ 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 +21,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>
@ -61,7 +53,7 @@
<!-- Date -->
<section class="group">
<span class="title">Date</span>
<div class="content vertical-wrap">
<div class=" content vertical-wrap">
<mat-form-field appearance="outline">
<mat-label>Start</mat-label>
<input (change)="checkDates()" [(ngModel)]="from" [min]="today" matInput required type="date">
@ -76,53 +68,30 @@
<section class="group">
<span class="title">Text</span>
<div class="content vertical-wrap">
<mat-form-field appearance="outline" style="margin-bottom: -1.25em">
<mat-form-field class="text-input">
<mat-label>Text search</mat-label>
<input [(ngModel)]="textFilter" matInput>
<button (click)="textFilter = ''" *ngIf="textFilter.length" mat-icon-button matSuffix>
<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">
<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,22 +53,12 @@
align-items: center;
&.space {
>* {
margin: 0.5rem;
> * {
margin-right: 1rem;
}
}
&.center {
justify-content: space-around;
}
&.center-desc {
justify-content: center;
}
}
.vertical {
display: flex;
flex-direction: column;
@ -91,30 +68,6 @@
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
margin-left: -0.5rem;
margin-right: -0.5rem;
>* {
flex-grow: 1;
margin: 0 0.5rem;
}
justify-content: space-around;
}
.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

@ -4,11 +4,7 @@
<span *ngIf="bookmarks.length === 0">You have no bookmarks :(</span>
</div>
<div *ngIf="isLoading && !message" class="spinner">
<div *ngIf="isLoading" class="spinner">
<mat-spinner></mat-spinner>
</div>
<div *ngIf="message" class="spinner">
<span>{{message}}</span>
</div>

View File

@ -2,10 +2,9 @@ import {Component, OnDestroy, OnInit} from '@angular/core';
import {Region} from '../../interfaces/region.interface';
import {BookmarkService} from '../../services/bookmark.service';
import {DataService} from '../../services/data.service';
import {catchError, switchMap, takeUntil, tap} from 'rxjs/operators';
import {switchMap, takeUntil, tap} from 'rxjs/operators';
import {Router} from '@angular/router';
import {Subject} from 'rxjs';
import {HttpErrorResponse} from '@angular/common/http';
@Component({
selector: 'app-bookmark-list',
@ -16,7 +15,6 @@ export class BookmarkListComponent implements OnInit, OnDestroy {
bookmarks: Region[];
isLoading = true;
message: string;
private destroyed$ = new Subject<void>();
constructor(private bs: BookmarkService, private ds: DataService, private router: Router) {
@ -29,14 +27,9 @@ export class BookmarkListComponent implements OnInit, OnDestroy {
switchMap(ids => {
const regions: Promise<Region>[] = ids.map(id => this.ds.getRegion(id));
return Promise.all(regions);
}),
catchError((err, caught) => {
this.message = `${(err as HttpErrorResponse).status}: ${(err as HttpErrorResponse).statusText} :/`;
return caught;
})
).subscribe(regions => {
this.bookmarks = regions;
this.message = undefined;
this.isLoading = false;
});
}

View File

@ -1,9 +1,6 @@
<app-search-input></app-search-input>
<h2>Explore the world</h2>
<div *ngIf="!loading && !message" class="region-container">
<span *ngIf="!regions || regions.length === 0">No regions to explore :/</span>
<div class="region-container">
<app-region (click)="onRegionClick(region)" *ngFor="let region of regions" [region]="region"></app-region>
</div>
<mat-spinner *ngIf="loading" class="central"></mat-spinner>
<span *ngIf="message" class="central">{{message}}</span>

View File

@ -17,8 +17,3 @@ app-search-input {
margin-bottom: 2rem;
}
}
.central {
align-self: center;
margin: 4rem 0;
}

View File

@ -2,7 +2,6 @@ import {Component, OnInit} from '@angular/core';
import {Region} from '../../interfaces/region.interface';
import {DataService} from '../../services/data.service';
import {Router} from '@angular/router';
import {HttpErrorResponse} from '@angular/common/http';
@Component({
selector: 'app-home',
@ -14,21 +13,12 @@ export class HomeComponent implements OnInit {
private readonly MAX_REGIONS = 10;
regions: Region[];
message: string;
loading = true;
constructor(private ds: DataService, private router: Router) {
}
async ngOnInit() {
try {
this.regions = await this.ds.getAllRegions(this.MAX_REGIONS);
} catch (e) {
this.message = `${(e as HttpErrorResponse).status}: ${(e as HttpErrorResponse).statusText} :/`;
} finally {
this.loading = false;
}
this.regions = await this.ds.getAllRegions(this.MAX_REGIONS);
}
onRegionClick(region: Region) {

View File

@ -27,47 +27,45 @@
<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>
</mat-tab-group>
</div>
<div class="places-container">
<span class="group-title">Places</span>
<div class="places">
<app-place *ngFor="let place of places" [place]="place"></app-place>
</div>
</div>
</div>
<div *ngIf="!region" class="spinner">

View File

@ -42,7 +42,6 @@
> .more-btn {
color: #8f8f8f;
cursor: pointer;
}
}
@ -67,28 +66,6 @@
}
.places-container {
display: flex;
flex-direction: column;
> .group-title {
font-size: 1.5rem;
margin-bottom: 1rem;
}
> .places {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-evenly;
app-place {
margin: 0.5rem;
cursor: pointer;
}
}
}
.spinner {
flex: 1 1 auto;
display: flex;

View File

@ -4,7 +4,6 @@ import {ActivatedRoute, ParamMap} from '@angular/router';
import {DataService} from '../../services/data.service';
import {switchMap} from 'rxjs/operators';
import {SearchParameter} from '../../interfaces/search-request.interface';
import {Place} from '../../interfaces/places.interface';
@Component({
@ -37,8 +36,6 @@ export class RegionDetailsComponent implements AfterViewInit {
/** Extend the description text */
isDescExtended = false;
places: Place[] = [];
constructor(private route: ActivatedRoute, private ds: DataService) {
}
@ -48,7 +45,6 @@ export class RegionDetailsComponent implements AfterViewInit {
).subscribe((region: Region) => {
this.region = region;
this.uriRegionName = encodeURI(this.region.name.toLowerCase());
this.ds.getPlacesByRegion(this.region.region_id).then(places => this.places = places);
setTimeout(() => {
if (this.container) {
this.container.nativeElement.scrollIntoView();

View File

@ -3,25 +3,9 @@
<span #result></span>
<div *ngIf="results && results.length > 0">
<div class="result-header">
<span class="title">Results ({{results.length}})</span>
<div class="sorting">
<button (click)="sortDes = !sortDes; onSortDirChange()" mat-icon-button>
<mat-icon>{{sortDes ? 'arrow_downwards' : 'arrow_upwards'}}</mat-icon>
</button>
<mat-form-field>
<mat-label>Sort by</mat-label>
<mat-select (selectionChange)="onSortChange()" [(ngModel)]="sortBy">
<mat-option *ngFor="let option of sortOptions" [value]="option.property">
{{option.name|translate}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<h2>Results ({{results.length}}):</h2>
<div class="result-container">
<app-result (click)="onResultClick(result)" *ngFor="let result of results" [debug]="debug"
[duration]="duration" [result]="result"></app-result>
<app-result (click)="onResultClick(result)" *ngFor="let result of results" [result]="result"></app-result>
</div>
</div>

View File

@ -8,24 +8,6 @@
}
}
.result-header {
display: flex;
align-items: center;
margin: 1rem 0;
.title {
flex: 1 1 auto;
font-size: 1.5rem;
font-weight: bold;
}
.sorting {
display: flex;
flex-direction: row;
align-items: center;
}
}
.result-container {
display: flex;
flex-direction: column;

View File

@ -2,15 +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;
property: string;
descending?: boolean;
isScore?: boolean;
}
@Component({
selector: 'app-search',
@ -19,22 +10,9 @@ interface SortOption {
})
export class SearchComponent implements OnInit {
/** Current base64 encoded query */
queryString: string;
/** Current results */
results: Result[];
/** The property to sort by */
sortBy = 'score';
/** Sort descending */
sortDes = true;
/** Available sort options */
sortOptions: SortOption[] = [];
/** The difference between from and to in days */
duration: number;
debug = false;
@ViewChild('result', {static: false})
resultDiv: ElementRef;
@ -44,7 +22,6 @@ export class SearchComponent implements OnInit {
async ngOnInit() {
this.route.queryParams.subscribe(async params => {
this.queryString = params.q;
this.debug = params.debug || false;
this.results = undefined;
if (!this.queryString || this.queryString.length === 0) {
@ -54,64 +31,10 @@ export class SearchComponent implements OnInit {
this.results = await this.ss.executeSearch(this.queryString);
this.resultDiv.nativeElement.scrollIntoView({behavior: 'smooth', block: 'start'});
this.sortOptions = [
{name: 'Relevance', property: 'score', descending: true},
{name: 'Region name', property: 'name'},
{name: 'Price', property: 'average_per_day_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) {
if (!this.sortOptions.find(i => i.property === type)) {
this.sortOptions.push({name: type, property: type, isScore: true, descending: tags.includes(type)});
}
}
}
// 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);
});
}
onResultClick(result: Result) {
this.router.navigate(['/region', result.region_id]).catch(console.log);
}
onSortDirChange() {
const option = this.sortOptions.find(i => i.property === this.sortBy);
this.sortResults(option, this.sortDes);
}
onSortChange() {
const option = this.sortOptions.find(i => i.property === this.sortBy);
this.sortDes = option.descending;
this.onSortDirChange();
}
/**
* Sorts the results array by the given property.
* @param property The property to sort by
* @param isScore If the property is in scores
* @param descending Sort descending instead of ascending
*/
private sortResults({property, isScore}: SortOption, descending: boolean) {
this.results = this.results.sort(isScore ? sortByScore : sortByPorperty);
function sortByPorperty(a: Result, b: Result): number {
const result = a[property] > b[property] ? 1 : -1;
return descending ? result * -1 : result;
}
function sortByScore(a: Result, b: Result): number {
const result = a.scores.find(i => i.type === property).value > b.scores.find(i => i.type === property).value ? 1 : -1;
return descending ? result * -1 : result;
}
}
}

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

@ -1,20 +0,0 @@
export interface Place {
/** The place id */
place_id: number;
/** The id of the parent region */
region_id: number;
/** The name of the place in english */
place_name: string;
/** The longitude position */
lon: number;
/** The latitude position */
lat: number;
/** The user rating between 0 and 5 */
rating: number;
/** Nearby address */
vicinity: string;
/** Google photo reference */
photo_reference: string;
/** URL to the image */
img_url: string;
}

View File

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

View File

@ -3,11 +3,7 @@ import {HttpClient, HttpParams} from '@angular/common/http';
import {Preset} from '../interfaces/preset.interface';
import {Result} from '../interfaces/result.interface';
import {Region} from '../interfaces/region.interface';
import {Place} from '../interfaces/places.interface';
/**
* The data service handles all API interactions.
*/
@Injectable({
providedIn: 'root'
})
@ -20,27 +16,16 @@ export class DataService {
constructor(private http: HttpClient) {
}
/**
* Get results for specific search query.
* @param query The search query
*/
public searchRegions(query: string): Promise<Result[]> {
const params = new HttpParams().set('q', query);
return this.http.get<Result[]>(this.API_URL + '/search', {params}).toPromise();
}
/**
* Get all search presets.
*/
public getAllPresets(): Promise<Preset[]> {
return this.http.get<Preset[]>(this.API_URL + '/search/presets').toPromise();
}
/**
* Gets all regions.
* @param max Limit the returned regions. Selected regions are random.
*/
public async getAllRegions(max?: number): Promise<Region[]> {
let params = new HttpParams();
if (max) {
@ -53,10 +38,6 @@ export class DataService {
return regions;
}
/**
* Gets one region by its id.
* @param id The region id
*/
public async getRegion(id: number): Promise<Region> {
if (this.regionCache.has(id)) {
return this.regionCache.get(id);
@ -67,167 +48,65 @@ export class DataService {
return region;
}
/**
* Returns POIs near the region.
* @param id The regions id
*/
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();
}
}
/**
* Defines meta data for all region parameters.
*/
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 +116,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);