fix: Restore GeoCoding from coordinates without API \o/

This commit is contained in:
Samuel Ortion 2024-01-08 19:41:57 +01:00
parent 07a42f4d8d
commit 94d30121d4
10 changed files with 152 additions and 71 deletions

View File

@ -3,6 +3,7 @@ const debug = require('debug')('soundbirder:api');
const debugLocale = require('debug')('soundbirder:locale'); const debugLocale = require('debug')('soundbirder:locale');
const debugResponses = require('debug')('soundbirder:api:responses'); const debugResponses = require('debug')('soundbirder:api:responses');
const quizzController = require('./quizz'); const quizzController = require('./quizz');
const { getRegion } = require('./region');
const QUIZZ_SIZE = process.env.QUIZZ_SIZE ? process.env.QUIZZ_SIZE : 5; 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) { function quizz(req, res) {
debug('Generating quizz'); debug('Generating quizz');
const { region } = req.query; const { region } = req.query;
// debug(`Coordinates: ${lat}, ${lng}`); // debug(`Coordinates: ${lat}, ${lng}`);
const locale = req.i18n.locale; const locale = req.i18n.locale;
debugLocale("Locale:", locale); debugLocale("Locale:", locale);
quizzController.generateQuizz(region, locale, QUIZZ_SIZE) quizzController.getQuizzCached(region, locale, QUIZZ_SIZE)
.then(({ species, answer, audio }) => { .then(({ species, answer, audio }) => {
req.session.answer = answer; req.session.answer = answer;
res.json({ species, audio }); res.json({ species, audio }).send();
debug("Quizz sent"); debug("Quizz sent");
quizzController.cacheQuizz(); // Prepare the next question in advance.
}) })
.catch(error => { .catch(error => {
debug("Faced error while generating quizz"); debug("Faced error while generating quizz");
res.json({ error }); debug(error)
throw error;
}); });
} }
@ -72,5 +83,6 @@ const game = {
module.exports = { module.exports = {
getHome, getHome,
game game,
region
} }

View File

@ -1,3 +1,4 @@
const { json } = require('express');
const { redisClient } = require('../redis'); const { redisClient } = require('../redis');
const debug = require('debug')('soundbirder:cache'); const debug = require('debug')('soundbirder:cache');
@ -6,6 +7,7 @@ function cacheResponse(request, response) {
redisClient.set(request, JSON.stringify(response)); redisClient.set(request, JSON.stringify(response));
} }
async function getCached(request) { async function getCached(request) {
const cached = await redisClient.get(request); const cached = await redisClient.get(request);
if (cached) { if (cached) {
@ -15,7 +17,22 @@ async function getCached(request) {
return null; 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 = { module.exports = {
cacheResponse, cacheResponse,
getCached getCached,
push,
pop
} }

View File

@ -5,20 +5,49 @@ const cache = require('./cache');
const eBird = require('@unclesamulus/ebird-api')(process.env.EBIRD_API_KEY); const eBird = require('@unclesamulus/ebird-api')(process.env.EBIRD_API_KEY);
const XenoCanto = require('@unclesamulus/xeno-canto-api'); const XenoCanto = require('@unclesamulus/xeno-canto-api');
const { choices, choice } = require('../utils/choices'); 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) { async function generateQuizz(region, locale, size) {
const quizz = {} const quizz = {}
try { try {
const speciesSelection = await getSpeciesSelection(region, size); let speciesSelection = [];
do {
speciesSelection = await getSpeciesSelection(region, size);
} while (speciesSelection == []);
debugResponses(`Species selection: ${speciesSelection}`); debugResponses(`Species selection: ${speciesSelection}`);
const speciesSelectionLocalized = await getLocalizedNames(speciesSelection, locale); const speciesSelectionLocalized = await getLocalizedNames(speciesSelection, locale);
if (!speciesSelectionLocalized) {
throw 'Not localized';
}
debugResponses('Localized species selection:', speciesSelectionLocalized); debugResponses('Localized species selection:', speciesSelectionLocalized);
quizz.species = speciesSelectionLocalized; quizz.species = speciesSelectionLocalized;
debug("Got species selection", quizz.species); debug("Got species selection", quizz.species);
let answer; let answer;
do { do {
answer = choice(speciesSelectionLocalized); answer = choice(speciesSelectionLocalized);
if (answer === undefined) {
debug('Species', speciesSelectionLocalized);
}
quizz.answer = answer; quizz.answer = answer;
quizz.audio = await getAudio(answer.sciName); quizz.audio = await getAudio(answer.sciName);
if (quizz.audio === undefined) { if (quizz.audio === undefined) {
@ -99,6 +128,7 @@ async function getSpeciesList(regionCode) {
} else { } else {
return eBird.product.spplist.in(regionCode)() return eBird.product.spplist.in(regionCode)()
.then(species => { .then(species => {
species = species.filter((sp) => !sp.includes(" x "));
cache.cacheResponse(`spplist-${regionCode}`, species); cache.cacheResponse(`spplist-${regionCode}`, species);
return species; return species;
}) })
@ -124,5 +154,7 @@ function getAudio(speciesScientificName) {
} }
module.exports = { module.exports = {
generateQuizz generateQuizz,
getQuizzCached,
cacheQuizz
} }

View File

@ -1,46 +1,41 @@
require('dotenv').config(); require("dotenv").config();
const debug = require('debug')('soundbirder:api:region'); const debug = require("debug")("soundbirder:api:region");
const cache = require('./cache'); const cache = require("./cache");
const axios = require('axios'); const axios = require("axios");
const OPENCAGE_API_KEY = process.env.OPENCAGE_API_KEY; const { lookUp } = require("geojson-places");
const OPENCAGE_API_URL = 'https://api.opencagedata.com/geocode/v1/json?q=<lat>+<lon>&key=<key>';
async function getRegion(lat, lon) { async function getRegion(lat, lon) {
const url = OPENCAGE_API_URL // Reverse geocoding to get the region info of Valladolid (Spain)
.replace('<lat>', lat) const key = `region-${lat}-${lon}`;
.replace('<lon>', lon) try {
.replace('<key>', OPENCAGE_API_KEY); const cached = await cache.getCached(key);
try { if (cached) {
const cached = await cache.getCached(`region-${lat}-${lon}`); const { region } = cached;
if (cached) { return region;
return cached;
}
} catch (error) {
throw error;
} }
return axios.get(url).then(response => { } catch (error) {
const { results } = response.data; throw error;
const region = results[0].components; }
region.country_code = region.country_code.toUpperCase(); const result = lookUp(lat, lon);
cache.cacheResponse(url, region); const region = result.country_a2;
return { country_code, country, state, county } = region; cache.cacheResponse(key, {region});
}).catch(error => { return region;
throw error;
});
} }
function getRegionCode({ country_code, country, state, county }) { function getRegionCode({ country_code, country, state, county }) {
return eBird.ref.region.list('subnational1', country_code)() return eBird.ref.region
.then(states => { .list("subnational1", country_code)()
console.log(states); .then((states) => {
const regionCode = states.find(region => region.name === state).code; console.log(states);
return regionCode; const regionCode = states.find((region) => region.name === state).code;
}).catch(error => { return regionCode;
throw error; })
}); .catch((error) => {
throw error;
});
} }
module.exports = { module.exports = {
getRegion, getRegion,
} };

20
package-lock.json generated
View File

@ -12,6 +12,7 @@
"@unclesamulus/xeno-canto-api": "^0.0.0", "@unclesamulus/xeno-canto-api": "^0.0.0",
"autoprefixer": "^10.4.8", "autoprefixer": "^10.4.8",
"axios": "^0.27.2", "axios": "^0.27.2",
"codegrid-js": "github:hlaw/codegrid-js",
"connect-redis": "^6.1.3", "connect-redis": "^6.1.3",
"cookie-parser": "~1.4.4", "cookie-parser": "~1.4.4",
"csurf": "^1.11.0", "csurf": "^1.11.0",
@ -20,6 +21,7 @@
"express": "^4.18.2", "express": "^4.18.2",
"express-session": "^1.17.3", "express-session": "^1.17.3",
"feather-icons": "^4.29.0", "feather-icons": "^4.29.0",
"geojson-places": "^1.0.8",
"http-errors": "~1.6.3", "http-errors": "~1.6.3",
"i18n-2": "^0.7.3", "i18n-2": "^0.7.3",
"leaflet": "^1.8.0", "leaflet": "^1.8.0",
@ -610,6 +612,11 @@
"node": ">=0.10.0" "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": { "node_modules/combined-stream": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@ -1264,6 +1271,14 @@
"node": ">= 4" "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": { "node_modules/get-intrinsic": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
@ -1527,6 +1542,11 @@
"promise": "^7.0.1" "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": { "node_modules/leaflet": {
"version": "1.9.4", "version": "1.9.4",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",

View File

@ -11,6 +11,7 @@
"@unclesamulus/xeno-canto-api": "^0.0.0", "@unclesamulus/xeno-canto-api": "^0.0.0",
"autoprefixer": "^10.4.8", "autoprefixer": "^10.4.8",
"axios": "^0.27.2", "axios": "^0.27.2",
"codegrid-js": "github:hlaw/codegrid-js",
"connect-redis": "^6.1.3", "connect-redis": "^6.1.3",
"cookie-parser": "~1.4.4", "cookie-parser": "~1.4.4",
"csurf": "^1.11.0", "csurf": "^1.11.0",
@ -19,6 +20,7 @@
"express": "^4.18.2", "express": "^4.18.2",
"express-session": "^1.17.3", "express-session": "^1.17.3",
"feather-icons": "^4.29.0", "feather-icons": "^4.29.0",
"geojson-places": "^1.0.8",
"http-errors": "~1.6.3", "http-errors": "~1.6.3",
"i18n-2": "^0.7.3", "i18n-2": "^0.7.3",
"leaflet": "^1.8.0", "leaflet": "^1.8.0",

View File

@ -20,27 +20,23 @@ async function query(endpoint, params) {
return await get(`${API_URL}/${endpoint}`, params); return await get(`${API_URL}/${endpoint}`, params);
} }
function getQuizz(REGION) { function getQuizz(region) {
return query('game/quizz', { region: REGION }) return query('game/quizz', { region })
.then(data => {
return data;
}).catch(error => {
throw error;
});
} }
function checkAnswer(speciesId) { function getRegion(lat, lon) {
return query(`game/check`, { return query('region', { lat, lon })
species: speciesId }
}).then(data => {
return data; function checkAnswer(species) {
}).catch(error => { return query('game/check', {
throw error; species: species
}); })
} }
const client = { const client = {
getQuizz, getQuizz,
getRegion,
checkAnswer checkAnswer
} }

View File

@ -19,24 +19,28 @@ function geolocationStep() {
if (map != undefined) if (map != undefined)
geolocationHandler(); geolocationHandler();
if (startButton != undefined) if (startButton != undefined)
startButton.addEventListener('click', quizzStep); startButton.addEventListener('click', regionCoder);
} }
const REGION = "FR"; function regionCoder() {
function quizzStep() {
// Start by disallowing geolocation step // Start by disallowing geolocation step
gameMapStep.classList.add('none'); gameMapStep.classList.add('none');
// Then allow the quizz step // Then allow the quizz step
gameQuizzStep.classList.remove('none'); gameQuizzStep.classList.remove('none');
// Retrieve coordinates from former done geolocation (TODO: fix the need of cookie) // Retrieve coordinates from former done geolocation (TODO: fix the need of cookie)
// const coordinates = getCoordinates(); const coordinates = getCoordinates();
// const [lat, lng] = coordinates; let [lat, lon] = coordinates;
// console.log("Coordinates: " + `${lat}, ${lng}`); client.getRegion(lat, lon).then(data => {
client.getQuizz(REGION) let { region } = data;
console.log(region);
quizzStep(region);
});
}
function quizzStep(region) {
client.getQuizz(region)
.then(quizz => { .then(quizz => {
// Display the quizz
displayQuizz(quizz); displayQuizz(quizz);
}).catch(error => { }).catch(error => {
console.log(error); console.log(error);

View File

@ -11,4 +11,7 @@ router.route('/game/quizz')
router.route('/game/check') router.route('/game/check')
.get(apiController.game.check); .get(apiController.game.check);
router.route('/region')
.get(apiController.region);
module.exports = router; module.exports = router;

View File

@ -1,7 +1,7 @@
.game .game
.game-map-step .game-map-step
//- #map.h-100 #map.h-100
//- button.button.geolocation-button button.button.geolocation-button
i(data-feather="map-pin") i(data-feather="map-pin")
button.button.start-button button.button.start-button
i(data-feather="play") i(data-feather="play")