Compare commits

..

No commits in common. "develop" and "master" have entirely different histories.

203 changed files with 192 additions and 15655 deletions

View File

@ -1,2 +0,0 @@
node_modules
npm-debug.log

6
.gitignore vendored
View File

@ -1,7 +1,3 @@
# credentials
.env
config.json
# compiled output
/dist
@ -25,5 +21,3 @@ config.json
# System Files
.DS_Store
Thumbs.db
node_modules

2
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,2 @@
# Default ignored files
/workspace.xml

12
.idea/cc-data.iml generated Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/frontend/dist" />
<excludeFolder url="file://$MODULE_DIR$/frontend/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

16
.idea/checkstyle-idea.xml generated Normal file
View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CheckStyle-IDEA">
<option name="configuration">
<map>
<entry key="checkstyle-version" value="8.30" />
<entry key="copy-libs" value="true" />
<entry key="location-0" value="BUNDLED:(bundled):Sun Checks" />
<entry key="location-1" value="BUNDLED:(bundled):Google Checks" />
<entry key="scan-before-checkin" value="false" />
<entry key="scanscope" value="JavaOnly" />
<entry key="suppress-errors" value="false" />
</map>
</option>
</component>
</project>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="TsLint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

6
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptSettings">
<option name="languageLevel" value="ES6" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/cc-data.iml" filepath="$PROJECT_DIR$/.idea/cc-data.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

23
.vscode/launch.json vendored
View File

@ -1,23 +0,0 @@
{
// Verwendet IntelliSense zum Ermitteln möglicher Attribute.
// Zeigen Sie auf vorhandene Attribute, um die zugehörigen Beschreibungen anzuzeigen.
// Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"name": "NodeJS: nodemon debug",
"program": "${workspaceFolder}/backend/index.js",
"request": "launch",
// "restart": true,
"runtimeExecutable": "node",
// "runtimeExecutable": "nodemon",
"skipFiles": [
"<node_internals>/**"
],
"type": "node",
"envFile": "${workspaceFolder}/backend/.env",
}
]
}

View File

@ -1,24 +0,0 @@
FROM node:12
# Create app directory
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
# Install app dependencies
# A wildcard is used to ensure both package.json AND package-lock.json are copied
# where available (npm@5+)
COPY backend/package*.json ./
RUN npm install
# Bundle app source
COPY backend .
EXPOSE 3000
ENV PORT=3000
ENV METEOSTAT_API_KEY=LMlDskju
ENV DB_HOST=lhinderberger.dev
ENV DB_USER=root
ENV DB_PASSWORD=devtest
ENV DB_PORT=3306
CMD [ "node", "index.js" ]

Binary file not shown.

View File

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

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -1,10 +0,0 @@
PORT=3000
METEOSTAT_API_KEY=LMlDskju
METEOSTAT_API_KEY_V2=O9X1xxKjheNwF1vfLcdRMmQ9JlobOugL
DB_HOST=lhinderberger.dev
DB_USER=root
DB_PASSWORD=devtest
DB_PORT=3306
DATABASE=travopti
GOOGLE_CLOUD_APIS=AIzaSyCeMBLfpqTp0IVB7Xipx6ekRQFUBjPacQc
SHOW_MATCH_VALUE=1

View File

@ -1,27 +0,0 @@
FROM node:12
# Create app directory
WORKDIR /usr/src/app
# COPY package.json .
# For npm@5 or later, copy package-lock.json as well
COPY package.json package-lock.json ./
# Install app dependencies
RUN npm install
# Bundle app source
COPY . .
ENV PORT=3000
ENV METEOSTAT_API_KEY=LMlDskju
ENV DB_HOST=127.0.0.1
ENV DB_USER=root
ENV DB_PASSWORD=devtest
ENV DB_PORT=3306
ENV DATABASE=travopti
EXPOSE 3000
# Start Node server
CMD [ "node", "./index.js" ]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 555 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 323 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 995 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 487 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 398 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 267 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 504 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -1,94 +0,0 @@
const express = require("express");
const bodyParser = require("body-parser");
const path = require("path");
const morgan = require("morgan");
const dbConnection = require("./util/dbConnection");
const fs = require("fs");
const cors = require("cors");
require('dotenv').config()
// credentials
const port = process.env.PORT
// Router
const search = require("./routes/search");
const regions = require("./routes/regions");
const countries = require("./routes/countries");
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
app.use(morgan("dev"));
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(places(dbConn));
app.use(update(dbConn))
app.use((err, req, res, next) => {
// 500
if (true) {
next();
} else {
res.status(500).send();
}
});
// Start webserver
app.listen(port, () => {
console.log(`Travopti backend listening at http://localhost:${port}`)
});
} catch (error) {
console.error("Failed to start the webserver");
console.error(error);
}
})();

View File

@ -1,10 +0,0 @@
module.exports = async (dbConn) => {
const countries = await dbConn.query(
`SELECT countries.id AS country_id,
countries.country AS name,
countries.description
FROM countries`
);
return countries;
};

View File

@ -1,12 +0,0 @@
module.exports = async (dbConn, id) => {
const country = await dbConn.query(
`SELECT countries.id AS country_id,
countries.country AS name,
countries.description
FROM countries
WHERE countries.id = ?`,
[id]
);
return country;
};

View File

@ -1,19 +0,0 @@
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}`)
// Photo url is not returned by default since it overuses Google Place API
/*
for (let candidate of res.data.candidates) {
for (let photo of candidate.photos) {
photo.url = await getPlacePhoto(photo.photo_reference)
}
}
*/
return res.data
}

View File

@ -1,20 +0,0 @@
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
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}`
);
// Photo url is not returned by default since it overuses Google Place API
/*
for (let result of res.data.results) {
for (let photo of result.photos) {
photo.url = await getPlacePhoto(photo.photo_reference)
}
}*/
return res.data
}

View File

@ -1,10 +0,0 @@
const axios = require("axios")
module.exports = async (photoref) => {
const res = await axios.get(
`https://maps.googleapis.com/maps/api/place/photo?photoreference=${photoref}&maxwidth=1600&key=${process.env.GOOGLE_CLOUD_APIS}`
);
const url = res.request.res.responseUrl
return url
}

View File

@ -1,61 +0,0 @@
const arrayFormatting = require("../util/databaseArrayFormatting.js")
module.exports = async (dbConn, id) => {
const res = 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,
rcma.precipitation AS precipitation,
rcma.rain_days AS rain_days,
rcma.sun_hours AS sun_hours,
rcma.humidity AS humidity,
regions_byt.average_per_day_costs AS average_per_day_costs,
rtma.avg_price_relative AS avg_price_relative,
regions_byt.accommodation_costs AS accommodation_costs,
regions_byt.food_costs AS food_costs,
regions_byt.water_costs AS water_costs,
regions_byt.local_transportation_costs AS local_transportation_costs,
regions_byt.entertainment_costs AS entertainment_costs,
regions_byt.alcohol_costs AS alcohol_costs
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
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
FROM regions_trivago_monthly_avg AS rtma
GROUP BY rtma.region_id) rtma
ON regions.id = rtma.region_id
WHERE regions_byt.travelstyle = 1
AND regions.id = ?`,
[id]
);
const region = res[0]
region.avg_price_relative = arrayFormatting(region.avg_price_relative);
region.temperature_mean = arrayFormatting(region.temperature_mean);
region.temperature_mean_min = arrayFormatting(region.temperature_mean_min);
region.temperature_mean_max = arrayFormatting(region.temperature_mean_max);
region.precipitation = arrayFormatting(region.precipitation);
region.rain_days = arrayFormatting(region.rain_days);
region.sun_hours = arrayFormatting(region.sun_hours);
region.humidity = arrayFormatting(region.humidity);
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,69 +0,0 @@
const arrayFormatting = require("../util/databaseArrayFormatting.js");
const { allTagsWithValues } = require("./getTags.js");
module.exports = async (dbConn) => {
const sqlRegions = `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,
rcma.precipitation AS precipitation,
rcma.rain_days AS rain_days,
rcma.sun_hours AS sun_hours,
rcma.humidity AS humidity,
regions_byt.average_per_day_costs AS average_per_day_costs,
rtma.avg_price_relative AS avg_price_relative,
regions_byt.accommodation_costs AS accommodation_costs,
regions_byt.food_costs AS food_costs,
regions_byt.water_costs AS water_costs,
regions_byt.local_transportation_costs AS local_transportation_costs,
regions_byt.entertainment_costs AS entertainment_costs,
regions_byt.alcohol_costs AS alcohol_costs
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
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
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);
regions[k].temperature_mean = arrayFormatting(regions[k].temperature_mean);
regions[k].temperature_mean_min = arrayFormatting(regions[k].temperature_mean_min);
regions[k].temperature_mean_max = arrayFormatting(regions[k].temperature_mean_max);
regions[k].precipitation = arrayFormatting(regions[k].precipitation);
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);
}
return regions.map(region => {
region.tags = tags.filter(tag => tag.region_id === region.region_id).map(tag => {
delete tag.region_id
return tag
})
return region
});
};

View File

@ -1,21 +0,0 @@
module.exports = async (dbConn) => {
let presets = await dbConn.query(
`SELECT search_presets.id AS preset_id,
search_presets.parameter AS parameter,
search_presets.name AS tag_label,
CASE
WHEN value_2 is NULL THEN value_1
ELSE CONCAT(search_presets.value_1,"|",search_presets.value_2)
END AS "value"
FROM search_presets`
);
for (k = 0; k < presets.length; k++) {
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])
}
}
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,89 +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' // 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)
module.exports = async (dbConn, startDate = rangeStartDate, endDate = rangeEndDate) => {
console.log('update climate with:', startDate, endDate);
const result = await dbConn.query(`SELECT * FROM regions WHERE meteostat_id IS NOT NULL`)
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 complete. see backend logs for info.`
return res
}
async function createClimateObjectFrom(src, startDate, endDate) {
let res
if (src.meteostat_id === '') {
console.log("skipping: no meteostat id ")
return []
}
try {
res = await axios.get(`https://api.meteostat.net/v1/history/monthly?station=${src.meteostat_id}&start=${startDate}&end=${endDate}&key=${process.env.METEOSTAT_API_KEY}`)
} catch (error) {
console.log("error while getting data from meteostat: couldn't find results for following region: ")
console.log(src.region + " with meteostat_id " + src.meteostat_id)
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.month.split("-")[0],
month: element.month.split("-")[1],
temperature_mean: element.temperature_mean,
temperature_mean_min: element.temperature_mean_min,
temperature_mean_max: element.temperature_mean_max,
precipitation: element.precipitation,
rain_days: element.raindays,
sun_hours: element.sunshine,
humidity: element.humidity ? element.humidity : null
}
return result
})
return retVal
}
async function writeToDatabase(dbConn, climateObjArr) {
for (const element of climateObjArr) {
try {
await dbConn.query(`
INSERT INTO region_climate
(region_id, year, month, temperature_mean, temperature_mean_min, temperature_mean_max, precipitation, sun_hours, humidity, rain_days)
VALUES (${element.region_id}, ${element.year}, ${element.month}, ${element.temperature_mean}, ${element.temperature_mean_min}, ${element.temperature_mean_max}, ${element.precipitation}, ${element.sun_hours}, ${element.humidity}, ${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},
humidity = ${element.humidity},
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)
}
}
}
};

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
}

1833
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,31 +0,0 @@
{
"name": "cc-data-backend",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "nodemon ./index.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"axios": "^0.19.2",
"body-parser": "^1.19.0",
"cors": "^2.8.5",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"httpolyglot": "^0.1.2",
"lodash": "^4.17.15",
"mariadb": "^2.4.0",
"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"
},
"devDependencies": {
"nodemon": "^2.0.4"
}
}

View File

@ -1,54 +0,0 @@
/**
* @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);
res.json(await getCountryById(dbConn, id))
});
return router;
};

View File

@ -1,95 +0,0 @@
/**
* @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)
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)
res.json(photo)
});
return router;
};

View File

@ -1,112 +0,0 @@
/**
* @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))
} 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`))
} else {
console.log("NOT EXISTS")
res.status(404).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))
});
return router;
};

View File

@ -1,182 +0,0 @@
/**
* @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 base64 = require("../util/base64.js")
const scoreAndSearch = require("../util/scoreAndSearch.js");
const oldToNewQuerySyntax = require("../util/oldToNewQuerySyntax.js")
const { getUniqueTags } = require("../models/getTags.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 => {
res.json(presets)
}).catch(error => {
// TODO error handling
})
}
}
function searchHandler(dbConn) {
return async function (req, res) {
let response = {}
response.meta = {
params: req.params,
query: req.query,
headers: req.headers
}
// SWITCH TO SUPPORT OLD AND NEW BASE64 BASED QUERY SYNTAX
let q = req.query.q ? base64.base64ToObj(req.query.q) : req.query
console.log('Q:', q)
// transform syntax and seperate climate queries from price queries
if (!req.query.q) {
q = oldToNewQuerySyntax(q)
}
// 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,
}
// FILTER if query contains filterString
if (q.textfilter) {
data.regions = filterByString(data.regions, q.textfilter, q.fulltext)
}
scoreAndSearch(data, q.from, q.to, scoreQueryObj).then(searchResults => {
// only dev:
if (process.env.SHOW_MATCH_VALUE === '1') searchResults.forEach(reg => reg.name = `${reg.name} (${_.round(reg.score * 10, 1)}% match)`)
// FILTER NULLSCORES
if (!_.get(q, 'showRegionsWithNullScore', false)) {
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))
}
// SEND RESPONSE
res.json(searchResults)
}).catch(e => {
// TODO error handling
console.log(e)
res.status(400).send(e.message)
})
}
}
function filterByString(searchResults, filterString, boolFulltext) {
return _.filter(searchResults, region => {
// console.log("filtering: filterString, boolFulltext");
// console.log(filterString, boolFulltext);
filterString = filterString.toLowerCase()
let name = region.name.toLowerCase()
let country = region.country.toLowerCase()
if (boolFulltext) {
let desc = region.description.toLowerCase()
return name.includes(filterString) || country.includes(filterString) || desc.includes(filterString)
}
return name.includes(filterString) || country.includes(filterString)
})
}
function prepareQueries(queries) {
let q = {
climate: {},
costs: {},
others: {}
}
// climate
if (queries.temperature_mean_max) q.climate.temperature_mean_max = queries.temperature_mean_max
if (queries.precipitation) q.climate.precipitation = queries.precipitation
if (queries.rain_days) q.climate.rain_days = queries.rain_days
if (queries.sun_hours) q.climate.sun_hours = queries.sun_hours
// costs
if (queries.accommodation_costs) q.costs.accommodation_costs = queries.accommodation_costs
if (queries.food_costs) q.costs.food_costs = queries.food_costs
if (queries.alcohol_costs) q.costs.alcohol_costs = queries.alcohol_costs
if (queries.water_costs) q.costs.water_costs = queries.water_costs
if (queries.local_transportation_costs) q.costs.local_transportation_costs = queries.local_transportation_costs
if (queries.entertainment_costs) q.costs.entertainment_costs = queries.entertainment_costs
if (queries.average_per_day_costs) q.costs.average_per_day_costs = queries.average_per_day_costs
// 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
}
}
}
}

View File

@ -1,33 +0,0 @@
/**
* Encodes an object as base64 string..
* @param obj The object to encode
*/
exports.objToBase64 = function(obj) {
return btoa(JSON.stringify(obj));
}
/**
* Decodes a base64 encoded object.
* @param base64 Encoded object
*/
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,15 +0,0 @@
/**
* 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
array = value.split(",");
for (i = 0; i < array.length; i++) {
array[i] = parseFloat(array[i])
}
}
return array;
}

View File

@ -1,47 +0,0 @@
const mariadb = require("mariadb");
let dbConn;
let conPool;
// mariadb doc: https://github.com/MariaDB/mariadb-connector-nodejs/blob/master/documentation/promise-api.md
async function reconnect() {
try {
dbConn = await conPool.getConnection();
} catch (e) {
if (e.code === "ECONNREFUSED") {
let err = new Error("Lost connection to the database");
err.code = "ERR_DB_CONN_LOST";
throw err;
} else {
throw e;
}
}
}
module.exports = async config => {
conPool = mariadb.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
port: process.env.DB_PORT,
database: process.env.DATABASE,
connectionLimit: 10
});
dbConn = await conPool.getConnection();
return {
async query(q, p) {
let res;
try {
res = await dbConn.query(q, p);
} catch (e) {
if (e.code === "ER_CMD_CONNECTION_CLOSED") {
await reconnect();
await this.query(q, p);
} else {
throw e;
}
}
return res;
}
};
};

View File

@ -1,24 +0,0 @@
module.exports = function (dbConn) {
return async function getAllRegionsWithClimatePerMonth(month) {
console.log('getAllRegionsWithClimatePerMonth')
const sql = `SELECT
region_climate.region_id AS region_id,
region_climate.month AS month,
countries.country AS country,
regions.region AS name,
ROUND(AVG(region_climate.temperature_mean), 1) AS temperature_mean,
ROUND(AVG(region_climate.temperature_mean_min), 1) AS temperature_mean_min,
ROUND(AVG(region_climate.temperature_mean_max), 1) AS temperature_mean_max,
ROUND(AVG(region_climate.precipitation), 1) AS precipitation,
ROUND(AVG(region_climate.raindays), 1) AS rain_days,
ROUND(AVG(region_climate.sunshine), 1) AS sun_hours
FROM region_climate
JOIN regions ON region_climate.region_id = regions.id
JOIN countries ON regions.country_id = countries.id
WHERE region_climate.month = ${month} GROUP BY region_id`
let response = await dbConn.query(sql)
console.log(response[0]);
return response
}
}

View File

@ -1,22 +0,0 @@
exports.getClimateMinMax = async function (dbConn) {
console.log('getClimateMinMax')
const sqlMin = `SELECT
MIN(temperature_mean) AS temperature_mean,
MIN(temperature_mean_min) AS temperature_mean_min,
MIN(temperature_mean_max) AS temperature_mean_max,
MIN(precipitation) AS precipitation,
MIN(rain_days) AS rain_days,
MIN(sun_hours) AS sun_hours
FROM region_climate`
const sqlMax = `SELECT
MAX(temperature_mean) AS temperature_mean,
MAX(temperature_mean_min) AS temperature_mean_min,
MAX(temperature_mean_max) AS temperature_mean_max,
MAX(precipitation) AS precipitation,
MAX(rain_days) AS rain_days,
MAX(sun_hours) AS sun_hours
FROM region_climate`
const [qResMin, qResMax] = await Promise.all([dbConn.query(sqlMin), dbConn.query(sqlMax)])
// console.log(qResMin)
return { min: qResMin[0], max: qResMax[0] }
}

View File

@ -1,5 +0,0 @@
function getClimatePerRegionAndMonth(regionId, month) {
console.log('getClimatePerRegionAndMonth')
const sql = `SELECT region_id, AVG(temperature_mean), AVG(temperature_mean_min), AVG(temperature_mean_max), AVG(precipitation), AVG(sunshine) FROM region_climate WHERE month = ${month} AND region_id = ${regionId}`
return getQueryRows(sql)
}

View File

@ -1,38 +0,0 @@
const _ = require('lodash')
module.exports = function (queries) {
let res = _.clone(queries)
console.log(res);
// try {
if (queries.temperature_mean_max) res.temperature_mean_max = [Number(queries.temperature_mean_max.split(',')[0]), Number(queries.temperature_mean_max.split(',')[1])]
if (queries.precipitation) res.precipitation = [Number(queries.precipitation.split(',')[0]), Number(queries.precipitation.split(',')[1])]
if (queries.rain_days) res.rain_days = [Number(queries.rain_days.split(',')[0]), Number(queries.rain_days.split(',')[1])]
if (queries.sun_hours) res.sun_hours = [Number(queries.sun_hours.split(',')[0]), Number(queries.sun_hours.split(',')[1])]
if (queries.accommodation_costs) res.accommodation_costs = [Number(queries.accommodation_costs.split(',')[0]), Number(queries.accommodation_costs.split(',')[1])]
if (queries.food_costs) res.food_costs = [Number(queries.food_costs.split(',')[0]), Number(queries.food_costs.split(',')[1])]
if (queries.alcohol_costs) res.alcohol_costs = [Number(queries.alcohol_costs.split(',')[0]), Number(queries.alcohol_costs.split(',')[1])]
if (queries.water_costs) res.water_costs = [Number(queries.water_costs.split(',')[0]), Number(queries.water_costs.split(',')[1])]
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');
// return queries
// }
return res
}

View File

@ -1,66 +0,0 @@
const _ = require('lodash')
exports.calculateAvgScore = (...scores) => {
return avgScore = scores.reduce((total, score) => total += score) / scores.length;
}
exports.calculateScoreRange = (transitionRange, 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);
}
exports.calculateScore = (transitionRange, regionVal, searchVal) => {
let score = 1 - (Math.abs(searchVal - regionVal) / transitionRange);
return (score) * 10;
//return score <= 0 ? 0 : score * 10;
}
exports.linear = function (x, exponent) {
if (x < 0) return 0
if (x > 10) return 10
return x
}
exports.easeOut = function (x, exponent) {
if (x < 0) return 0
if (x > 10) return 10
return (1 - Math.pow(1 - (x / 10), exponent)) * 10
}
exports.easeInOut = function (sc, exponent) {
const x = (sc ) / 10
// console.log(sc, x);
if (x<0) return 0
if (x>1) return 10
return x < 0.5 ? Math.pow(2, exponent-1) * Math.pow(x,exponent) * 10 : (1 - Math.pow(-2 * x + 2, exponent)/2) * 10
}
exports.easeInOutAsymmetric = function (sc, exponent) {
const x = (sc ) / 10
// console.log(sc, x);
if (x<0) return 0
if (x>1) return 10
return x < 0.5 ? (2 * x) - 0.5 * 10 : (1 - Math.pow(-2 * x + 2, exponent)/2) * 10
}
exports.sigmoid = function (x, exponent) {
// const sigm = (1 / (1 + Math.pow(Math.E, 5 * -x))) * 10 + 5
// const sigm = 10 / (1 + Math.pow(Math.E, 1.2 * -x + 6))
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
}

Some files were not shown because too many files have changed in this diff Show More