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 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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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=<lat>+<lon>&key=<key>';
const { lookUp } = require("geojson-places");
async function getRegion(lat, lon) {
const url = OPENCAGE_API_URL
.replace('<lat>', lat)
.replace('<lon>', lon)
.replace('<key>', 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,
}
getRegion,
};

20
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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
}

View File

@ -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);

View File

@ -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;

View File

@ -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")