From 94d30121d4680c4c703ae763616791a0782249a6 Mon Sep 17 00:00:00 2001 From: Samuel Ortion Date: Mon, 8 Jan 2024 19:41:57 +0100 Subject: [PATCH] fix: Restore GeoCoding from coordinates without API \o/ --- controllers/api.js | 22 ++++++++--- controllers/cache.js | 19 ++++++++- controllers/quizz.js | 38 ++++++++++++++++-- controllers/region.js | 67 +++++++++++++++----------------- package-lock.json | 20 ++++++++++ package.json | 2 + public/javascripts/api-client.js | 26 ++++++------- public/javascripts/game.js | 22 ++++++----- routes/api.js | 3 ++ views/game.pug | 4 +- 10 files changed, 152 insertions(+), 71 deletions(-) diff --git a/controllers/api.js b/controllers/api.js index d1d4a34..d9b9e84 100755 --- a/controllers/api.js +++ b/controllers/api.js @@ -3,6 +3,7 @@ const debug = require('debug')('soundbirder:api'); const debugLocale = require('debug')('soundbirder:locale'); const debugResponses = require('debug')('soundbirder:api:responses'); const quizzController = require('./quizz'); +const { getRegion } = require('./region'); const QUIZZ_SIZE = process.env.QUIZZ_SIZE ? process.env.QUIZZ_SIZE : 5; @@ -13,22 +14,32 @@ function getHome(req, res) { }); } +async function region(req, res) { + let {lat, lon} = req.query; + lat = parseFloat(lat); + lon = parseFloat(lon); + const region = await getRegion(lat, lon); + res.json({ + region + }) +} + function quizz(req, res) { debug('Generating quizz'); const { region } = req.query; // debug(`Coordinates: ${lat}, ${lng}`); const locale = req.i18n.locale; debugLocale("Locale:", locale); - quizzController.generateQuizz(region, locale, QUIZZ_SIZE) + quizzController.getQuizzCached(region, locale, QUIZZ_SIZE) .then(({ species, answer, audio }) => { req.session.answer = answer; - res.json({ species, audio }); + res.json({ species, audio }).send(); debug("Quizz sent"); + quizzController.cacheQuizz(); // Prepare the next question in advance. }) .catch(error => { debug("Faced error while generating quizz"); - res.json({ error }); - throw error; + debug(error) }); } @@ -72,5 +83,6 @@ const game = { module.exports = { getHome, - game + game, + region } \ No newline at end of file diff --git a/controllers/cache.js b/controllers/cache.js index 9bef8bc..8e1b94a 100755 --- a/controllers/cache.js +++ b/controllers/cache.js @@ -1,3 +1,4 @@ +const { json } = require('express'); const { redisClient } = require('../redis'); const debug = require('debug')('soundbirder:cache'); @@ -6,6 +7,7 @@ function cacheResponse(request, response) { redisClient.set(request, JSON.stringify(response)); } + async function getCached(request) { const cached = await redisClient.get(request); if (cached) { @@ -15,7 +17,22 @@ async function getCached(request) { return null; } +function push(key, value) { + redisClient.lpush(key, JSON.stringify(value)); +} + +async function pop(key) { + const cached = await redisClient.lpop(key); + debug("Pop cached", cached); + if (cached) { + return JSON.parse(cached); + } + return null; +} + module.exports = { cacheResponse, - getCached + getCached, + push, + pop } \ No newline at end of file diff --git a/controllers/quizz.js b/controllers/quizz.js index 1675983..def71ed 100755 --- a/controllers/quizz.js +++ b/controllers/quizz.js @@ -5,20 +5,49 @@ const cache = require('./cache'); const eBird = require('@unclesamulus/ebird-api')(process.env.EBIRD_API_KEY); const XenoCanto = require('@unclesamulus/xeno-canto-api'); const { choices, choice } = require('../utils/choices'); -// const { getRegion } = require('./region'); +const { getRegion } = require('./region'); + +function quizzKey(region, locale, size) { + return `${region}:${locale}#${size}`; +} + +async function getQuizzCached(region, locale, size) { + let key = quizzKey(region, locale, size); + let quizz = await cache.pop(key); + if (!quizz) { + quizz = generateQuizz(region, locale, size); + cache.push(key, quizz); + } + return quizz; +} + +async function cacheQuizz(region, locale, size) { + let quizz = generateQuizz(region, locale, size); + let key = quizzKey(region, locale, size); + cache.push(key, quizz); +} async function generateQuizz(region, locale, size) { const quizz = {} try { - const speciesSelection = await getSpeciesSelection(region, size); + let speciesSelection = []; + do { + speciesSelection = await getSpeciesSelection(region, size); + } while (speciesSelection == []); debugResponses(`Species selection: ${speciesSelection}`); const speciesSelectionLocalized = await getLocalizedNames(speciesSelection, locale); + if (!speciesSelectionLocalized) { + throw 'Not localized'; + } debugResponses('Localized species selection:', speciesSelectionLocalized); quizz.species = speciesSelectionLocalized; debug("Got species selection", quizz.species); let answer; do { answer = choice(speciesSelectionLocalized); + if (answer === undefined) { + debug('Species', speciesSelectionLocalized); + } quizz.answer = answer; quizz.audio = await getAudio(answer.sciName); if (quizz.audio === undefined) { @@ -99,6 +128,7 @@ async function getSpeciesList(regionCode) { } else { return eBird.product.spplist.in(regionCode)() .then(species => { + species = species.filter((sp) => !sp.includes(" x ")); cache.cacheResponse(`spplist-${regionCode}`, species); return species; }) @@ -124,5 +154,7 @@ function getAudio(speciesScientificName) { } module.exports = { - generateQuizz + generateQuizz, + getQuizzCached, + cacheQuizz } \ No newline at end of file diff --git a/controllers/region.js b/controllers/region.js index 2236cf3..3b69e04 100755 --- a/controllers/region.js +++ b/controllers/region.js @@ -1,46 +1,41 @@ -require('dotenv').config(); -const debug = require('debug')('soundbirder:api:region'); -const cache = require('./cache'); -const axios = require('axios'); +require("dotenv").config(); +const debug = require("debug")("soundbirder:api:region"); +const cache = require("./cache"); +const axios = require("axios"); -const OPENCAGE_API_KEY = process.env.OPENCAGE_API_KEY; -const OPENCAGE_API_URL = 'https://api.opencagedata.com/geocode/v1/json?q=+&key='; +const { lookUp } = require("geojson-places"); async function getRegion(lat, lon) { - const url = OPENCAGE_API_URL - .replace('', lat) - .replace('', lon) - .replace('', OPENCAGE_API_KEY); - try { - const cached = await cache.getCached(`region-${lat}-${lon}`); - if (cached) { - return cached; - } - } catch (error) { - throw error; + // Reverse geocoding to get the region info of Valladolid (Spain) + const key = `region-${lat}-${lon}`; + try { + const cached = await cache.getCached(key); + if (cached) { + const { region } = cached; + return region; } - return axios.get(url).then(response => { - const { results } = response.data; - const region = results[0].components; - region.country_code = region.country_code.toUpperCase(); - cache.cacheResponse(url, region); - return { country_code, country, state, county } = region; - }).catch(error => { - throw error; - }); + } catch (error) { + throw error; + } + const result = lookUp(lat, lon); + const region = result.country_a2; + cache.cacheResponse(key, {region}); + return region; } function getRegionCode({ country_code, country, state, county }) { - return eBird.ref.region.list('subnational1', country_code)() - .then(states => { - console.log(states); - const regionCode = states.find(region => region.name === state).code; - return regionCode; - }).catch(error => { - throw error; - }); + return eBird.ref.region + .list("subnational1", country_code)() + .then((states) => { + console.log(states); + const regionCode = states.find((region) => region.name === state).code; + return regionCode; + }) + .catch((error) => { + throw error; + }); } module.exports = { - getRegion, -} \ No newline at end of file + getRegion, +}; diff --git a/package-lock.json b/package-lock.json index ac88d9e..4a8e4fe 100755 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@unclesamulus/xeno-canto-api": "^0.0.0", "autoprefixer": "^10.4.8", "axios": "^0.27.2", + "codegrid-js": "github:hlaw/codegrid-js", "connect-redis": "^6.1.3", "cookie-parser": "~1.4.4", "csurf": "^1.11.0", @@ -20,6 +21,7 @@ "express": "^4.18.2", "express-session": "^1.17.3", "feather-icons": "^4.29.0", + "geojson-places": "^1.0.8", "http-errors": "~1.6.3", "i18n-2": "^0.7.3", "leaflet": "^1.8.0", @@ -610,6 +612,11 @@ "node": ">=0.10.0" } }, + "node_modules/codegrid-js": { + "version": "0.0.2", + "resolved": "git+ssh://git@github.com/hlaw/codegrid-js.git#38decfe10dced9006c6b722cc403d7c1217abe98", + "license": "WTFPL" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1264,6 +1271,14 @@ "node": ">= 4" } }, + "node_modules/geojson-places": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/geojson-places/-/geojson-places-1.0.8.tgz", + "integrity": "sha512-4jqzFgpz9S3QZ9KsDTvokuf3qCygfhgYlBaVHhGt27bI9uzvbxacFz19qxGui1WUAwYXj39IZdtATsQ1ggtlOQ==", + "dependencies": { + "just-clone": "^6.2.0" + } + }, "node_modules/get-intrinsic": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", @@ -1527,6 +1542,11 @@ "promise": "^7.0.1" } }, + "node_modules/just-clone": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-clone/-/just-clone-6.2.0.tgz", + "integrity": "sha512-1IynUYEc/HAwxhi3WDpIpxJbZpMCvvrrmZVqvj9EhpvbH8lls7HhdhiByjL7DkAaWlLIzpC0Xc/VPvy/UxLNjA==" + }, "node_modules/leaflet": { "version": "1.9.4", "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", diff --git a/package.json b/package.json index 0b1cf4b..ce049bc 100755 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "@unclesamulus/xeno-canto-api": "^0.0.0", "autoprefixer": "^10.4.8", "axios": "^0.27.2", + "codegrid-js": "github:hlaw/codegrid-js", "connect-redis": "^6.1.3", "cookie-parser": "~1.4.4", "csurf": "^1.11.0", @@ -19,6 +20,7 @@ "express": "^4.18.2", "express-session": "^1.17.3", "feather-icons": "^4.29.0", + "geojson-places": "^1.0.8", "http-errors": "~1.6.3", "i18n-2": "^0.7.3", "leaflet": "^1.8.0", diff --git a/public/javascripts/api-client.js b/public/javascripts/api-client.js index c1f7745..8d161a8 100755 --- a/public/javascripts/api-client.js +++ b/public/javascripts/api-client.js @@ -20,27 +20,23 @@ async function query(endpoint, params) { return await get(`${API_URL}/${endpoint}`, params); } -function getQuizz(REGION) { - return query('game/quizz', { region: REGION }) - .then(data => { - return data; - }).catch(error => { - throw error; - }); +function getQuizz(region) { + return query('game/quizz', { region }) } -function checkAnswer(speciesId) { - return query(`game/check`, { - species: speciesId - }).then(data => { - return data; - }).catch(error => { - throw error; - }); +function getRegion(lat, lon) { + return query('region', { lat, lon }) +} + +function checkAnswer(species) { + return query('game/check', { + species: species + }) } const client = { getQuizz, + getRegion, checkAnswer } diff --git a/public/javascripts/game.js b/public/javascripts/game.js index ee53c87..4614e6f 100755 --- a/public/javascripts/game.js +++ b/public/javascripts/game.js @@ -19,24 +19,28 @@ function geolocationStep() { if (map != undefined) geolocationHandler(); if (startButton != undefined) - startButton.addEventListener('click', quizzStep); + startButton.addEventListener('click', regionCoder); } -const REGION = "FR"; - -function quizzStep() { +function regionCoder() { // Start by disallowing geolocation step gameMapStep.classList.add('none'); // Then allow the quizz step gameQuizzStep.classList.remove('none'); // Retrieve coordinates from former done geolocation (TODO: fix the need of cookie) - // const coordinates = getCoordinates(); - // const [lat, lng] = coordinates; - // console.log("Coordinates: " + `${lat}, ${lng}`); - client.getQuizz(REGION) + const coordinates = getCoordinates(); + let [lat, lon] = coordinates; + client.getRegion(lat, lon).then(data => { + let { region } = data; + console.log(region); + quizzStep(region); + }); +} + +function quizzStep(region) { + client.getQuizz(region) .then(quizz => { - // Display the quizz displayQuizz(quizz); }).catch(error => { console.log(error); diff --git a/routes/api.js b/routes/api.js index 391dd29..6ac7d66 100755 --- a/routes/api.js +++ b/routes/api.js @@ -11,4 +11,7 @@ router.route('/game/quizz') router.route('/game/check') .get(apiController.game.check); +router.route('/region') + .get(apiController.region); + module.exports = router; \ No newline at end of file diff --git a/views/game.pug b/views/game.pug index 137031e..7b78abd 100755 --- a/views/game.pug +++ b/views/game.pug @@ -1,7 +1,7 @@ .game .game-map-step - //- #map.h-100 - //- button.button.geolocation-button + #map.h-100 + button.button.geolocation-button i(data-feather="map-pin") button.button.start-button i(data-feather="play")