From bf0a71c63c463d8ef5adf30aa1b318c3efa96746 Mon Sep 17 00:00:00 2001 From: Timo John Date: Tue, 23 Jun 2020 02:01:13 +0200 Subject: [PATCH] Implemented Endpoints for usage of google place api --- backend/.env.sample | 3 +- backend/app.js | 275 ------------------------------- backend/index.js | 2 + backend/models/getPlace.js | 18 ++ backend/models/getPlaceNearby.js | 19 +++ backend/models/getPlacePhoto.js | 10 ++ backend/package.json | 2 +- backend/routes/place.js | 23 +++ 8 files changed, 75 insertions(+), 277 deletions(-) delete mode 100644 backend/app.js create mode 100644 backend/models/getPlace.js create mode 100644 backend/models/getPlaceNearby.js create mode 100644 backend/models/getPlacePhoto.js create mode 100644 backend/routes/place.js diff --git a/backend/.env.sample b/backend/.env.sample index aed09fd..3e995bc 100644 --- a/backend/.env.sample +++ b/backend/.env.sample @@ -4,4 +4,5 @@ DB_HOST=lhinderberger.dev DB_USER=root DB_PASSWORD=devtest DB_PORT=3306 -DATABASE=travopti \ No newline at end of file +DATABASE=travopti +GOOGLE_CLOUD_APIS=AIzaSyCeMBLfpqTp0IVB7Xipx6ekRQFUBjPacQc \ No newline at end of file diff --git a/backend/app.js b/backend/app.js deleted file mode 100644 index f0f66a7..0000000 --- a/backend/app.js +++ /dev/null @@ -1,275 +0,0 @@ -const express = require('express') -const moment = require('moment') -const _ = require('lodash') -const score = require('./util/score') -const transformer = require('./util/transformer') -const base = require('./util/base64') - -const app = express() - -const port = 3000 -//const multiplier_temp = 5 -const multiplier = { - temperature_mean_max: 5, - precipitation: 3.5, - raindays: 3, - sunhours: 2.5, -} - -const samplePresets = [ - { - id: 29837, - parameter: "temperature", - label: "warm", - values: [22, 25] - } -] - -app.get('/', (req, res) => res.send('Hello Timo!')) -app.get('/v1/regions', (req, res) => getAllRegions().then(x => res.json({ data: x }))) -app.get('/v1/presets', (req, res) => res.json({ data: samplePresets })) -app.get('/v1/search', searchHandler) -app.get('/v1/climate/update', climateUpdateHandler) - -app.listen(port, () => console.log(`Travopti backend listening at http://localhost:${port}`)) - -function climateUpdateHandler(req, res) { - let parameter = [] - if (req.query.startDate) parameter.push(req.query.startDate) - if (req.query.endDate) parameter.push(req.query.endDate) - climate.update(...parameter).then(x => { - res.send(x) - }).catch(e => { - let result = { - message: 'error during update process. check backend logs.', - error: e - } - res.send(result) - } - - ) -} - -function searchHandler(req, res) { - let response = {} - - response.meta = { - params: req.params, - query: req.query, - headers: req.headers - } - - let q = req.query.q ? base.base64ToObj(req.query.q) : req.query - console.log('Q:', q) - - let queryObj = {} - if (q.temperature) queryObj['temperature_mean_max'] = q.temperature - if (q.precipitation) queryObj['precipitation'] = q.precipitation - if (q.raindays) queryObj['raindays'] = q.raindays - if (q.sunhours) queryObj['sunhours'] = q.sunhours - - scoreAndSearch(q.from, q.to, queryObj).then(searchResults => { - response.data = searchResults - res.json(response) - }).catch(e => { - console.log(e) - res.json(e.message) - }) -} - -async function scoreAndSearch(from, to, queries) { - // TODO break funtion into parts when implementing non-climate queries and modularize (new file) - - console.log('search') - - // get Min and Max values for each Parameter - const minMax = await getClimateMinMax() - - // randomize if empty queries - if (_.isEmpty(queries)) { - let t = _.round(_.random(minMax.min.temperature_mean_max, minMax.max.temperature_mean_max - 5), 0) - let p = _.round(_.random(minMax.min.precipitation, minMax.max.precipitation - 50), 0) - let r = _.round(_.random(minMax.min.raindays, minMax.max.raindays - 5), 0) - let s = _.round(_.random(minMax.min.sunhours, minMax.max.sunhours - 50), 0) - queries.temperature_mean_max = `${t},${t + 5}` - queries.precipitation = `${p},${p + 50}` - queries.raindays = `${r},${r + 5}` - queries.sunhours = `${s},${s + 50}` - } - queries = oldToNewQuerySyntax(queries) - console.log(queries) - - // TODO simplify and remove support for old query syntaax - let monthFrom = 0 - let monthTo = 0 - let dayFrom = 0 - let dayTo = 0 - - if (_.isNumber(from) && _.isNumber(to)) { - let dateFrom = moment(from).toDate() - let dateTo = moment(to).toDate() - monthFrom = dateFrom.getMonth() - monthTo = dateTo.getMonth() - dayFrom = dateFrom.getDay() - dayTo = dateTo.getDay() - if (moment(dateFrom).add(23, 'hours').isAfter(moment(dateTo))) throw new Error("ERR: 'to' must be at least one day after 'from'.") - } else { - // to still support old query syntax - let re = /([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))/i; - monthFrom = Number(from.split("-")[1]) - monthTo = Number(to.split("-")[1]) - dayFrom = Number(from.split("-")[2]) - dayTo = Number(to.split("-")[2]) - if (!from.match(re) || !to.match(re)) throw new Error("ERR: invalid parameter:", from, to) - if (moment(from, 'YYYY-MM-DD').add(23, 'hours').isAfter(moment(to, 'YYYY-MM-DD'))) throw new Error("ERR: 'to' must be at least one day after 'from'.") - } - - // -- Prepare search -- - // to calculate average if traveldates are in more than one month - let travelPeriods = [] - if (monthFrom === monthTo) { - let element = { - month: monthFrom, - days: dayTo - dayFrom - } - travelPeriods.push(element) - } else { - for (let index = monthFrom; index <= monthTo; index++) { - let element = {} - if (index === monthFrom) { - element = { - month: index, - days: 32 - dayFrom - } - } else if (index === monthTo) { - element = { - month: index, - days: dayTo - } - } else { - element = { - month: index, - days: 30 - } - } - travelPeriods.push(element) - } - } - - // calculate detail scores - let detailScores = await Promise.all(travelPeriods.map(async period => { - period.climate = await getAllRegionsWithClimatePerMonth(period.month) - period.scores = {} - Object.entries(queries).forEach(([key, value]) => { - // console.log('key',key) - // console.log('val', value) - period.scores[key] = calculateScores(key, period.climate, value[0], value[1], minMax) - }); - return period - })); - - - // calculate ratio and transform into target object - return { - results: transformer.transform(detailScores), - debug: { - detailScores: detailScores, - minMax: minMax - } - } -} - -function calculateScores(type, regionDataRows, searchLowParam, searchMaxParam, minMax) { - console.log('calculateScores for', type) - let result = regionDataRows.map(x => { - const sc = Math.round(score.calculateScoreRange(minMax.min[type], minMax.max[type], multiplier[type], x[type], searchLowParam, searchMaxParam) * 100) / 100 - - return { - region_id: x.region_id, - type: type, - value: x[type], - score: x[type] === null ? null : sc - } - - }) - return result -} - -async function getClimateMinMax() { - 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(raindays) AS raindays, - MIN(sunshine) AS sunhours - 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(raindays) AS raindays, - MAX(sunshine) AS sunhours - FROM region_climate` - const [qResMin, qResMax] = await Promise.all([getQueryRows(sqlMin), getQueryRows(sqlMax)]) - //console.log(qResMin) - return { min: qResMin[0], max: qResMax[0] } -} - -async function getQueryRows(sql) { - //console.log('getQueryRows') - const [rows, fields] = await db.execute(sql) - return rows -} - -function getAllRegions() { - const sql = `SELECT - regions.id AS region_id, - regions.region AS name, - regions.country_id AS country_id, - countries.country AS country, - regions.meteostat_id AS meteostat_id - FROM regions - JOIN countries - ON regions.country_id = countries.id` - return getQueryRows(sql) -} - -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) -} - -function getAllRegionsWithClimatePerMonth(month) { - console.log('getAllRegionsWithClimatePerMonth') - const sql = `SELECT - region_climate.region_id AS region_id, - regions.country_id AS country_id, - 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 raindays, - ROUND(AVG(region_climate.sunshine), 1) AS sunhours - FROM region_climate JOIN regions ON region_climate.region_id = regions.id WHERE region_climate.month = ${month} GROUP BY region_id` - return getQueryRows(sql) -} - -function oldToNewQuerySyntax(queries) { - let res = {} - try { - if (queries.temperature_mean_max) res.temperature_mean_max = [queries.temperature_mean_max.split(',')[0], queries.temperature_mean_max.split(',')[1]] - if (queries.precipitation) res.precipitation = [queries.precipitation.split(',')[0], queries.precipitation.split(',')[1]] - if (queries.raindays) res.raindays = [queries.raindays.split(',')[0], queries.raindays.split(',')[1]] - if (queries.sunhours) res.sunhours = [queries.sunhours.split(',')[0], queries.sunhours.split(',')[1]] - console.log('queries successfully transformed'); - } catch (error) { - console.log('queries are ok'); - return queries - } - return res -} \ No newline at end of file diff --git a/backend/index.js b/backend/index.js index 88d98e0..a61a0da 100644 --- a/backend/index.js +++ b/backend/index.js @@ -15,6 +15,7 @@ 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 app = express(); @@ -33,6 +34,7 @@ const app = express(); app.use(regions(dbConn)); app.use(countries(dbConn)); app.use(climate(dbConn)); + app.use(places(dbConn)); app.use((err, req, res, next) => { // 500 diff --git a/backend/models/getPlace.js b/backend/models/getPlace.js new file mode 100644 index 0000000..8846c94 --- /dev/null +++ b/backend/models/getPlace.js @@ -0,0 +1,18 @@ +const axios = require("axios") +const getPlacePhoto = require("./getPlacePhoto.js") + +module.exports = async (q) => { + const res = await axios.get( + `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 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 +} diff --git a/backend/models/getPlaceNearby.js b/backend/models/getPlaceNearby.js new file mode 100644 index 0000000..1f370e2 --- /dev/null +++ b/backend/models/getPlaceNearby.js @@ -0,0 +1,19 @@ +const axios = require("axios") +const getPlacePhoto = require("./getPlacePhoto.js") + +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}&rankby=${rankby}&key=${process.env.GOOGLE_CLOUD_APIS}` + ); + // Photo url is not returned 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 +} diff --git a/backend/models/getPlacePhoto.js b/backend/models/getPlacePhoto.js new file mode 100644 index 0000000..3a4dd05 --- /dev/null +++ b/backend/models/getPlacePhoto.js @@ -0,0 +1,10 @@ +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 +} diff --git a/backend/package.json b/backend/package.json index 5db945e..a2d9a56 100644 --- a/backend/package.json +++ b/backend/package.json @@ -2,7 +2,7 @@ "name": "cc-data-backend", "version": "1.0.0", "description": "", - "main": "app.js", + "main": "index.js", "scripts": { "start": "nodemon ./index.js" }, diff --git a/backend/routes/place.js b/backend/routes/place.js new file mode 100644 index 0000000..fa543a0 --- /dev/null +++ b/backend/routes/place.js @@ -0,0 +1,23 @@ +const router = require("express").Router() +const getPlace = require("../models/getPlace.js") +const getPlaceNearby = require("../models/getPlaceNearby.js") +const getPlacePhoto = require("../models/getPlacePhoto.js") + +module.exports = dbConn => { + router.get("/api/v1/place", async (req, res) => { + const place = await getPlace(req.query.q) + res.json(place) + }); + + router.get("/api/v1/place/nearby", async (req, res) => { + const place = await getPlaceNearby(req.query.lat, req.query.lng) + res.json(place) + }); + + router.get("/api/v1/place/photo", async (req, res) => { + const photo = await getPlacePhoto(req.query.photoref) + res.json(photo) + }); + + return router; +};