www: Add redis cache and api cookie-based i18n

This commit is contained in:
Samuel Ortion 2022-08-29 06:44:58 +02:00
parent 4bfa6cfe16
commit d590582a85
9 changed files with 382 additions and 138 deletions

27
app.js
View File

@ -1,3 +1,4 @@
require('dotenv').config();
const createError = require('http-errors');
const express = require('express');
const session = require('express-session');
@ -6,6 +7,13 @@ const path = require('path');
const cookieParser = require('cookie-parser');
const logger = require('morgan');
const i18n = require('i18n-2');
const redis = require('redis');
const [redisHost, redisPort] = [process.env.REDIS_HOST, process.env.REDIS_PORT];
const redisClient = redis.createClient(redisPort, redisHost);
redisClient.on('error', (error) => {
console.error('Redis error:', error);
});
const indexRouter = require('./routes/index');
const apiRouter = require('./routes/api');
@ -45,18 +53,15 @@ i18n.expressBind(app, {
app.use(function (req, res, next) {
req.i18n.setLocaleFromQuery();
req.i18n.setLocaleFromCookie();
next();
});
app.all('*', function(req, res, next) {
// set locale
// set locale from url prefix
var rxLocale = /^\/(fr|en)/i;
if(rxLocale.test(req.url)){
const arr = rxLocale.exec(req.url);
const locale=arr[1];
req.i18n.setLocale(locale);
} else {
req.i18n.setLocale('en');
if (rxLocale.test(req.url)) {
const arr = rxLocale.exec(req.url);
const locale = arr[1];
req.i18n.setLocale(locale);
}
if (req.cookies.locale === undefined) {
res.cookie('locale', req.i18n.locale, { maxAge: 90000 });
}
// add extra logic
next();

View File

@ -1,23 +1,20 @@
const debug = require('debug')('soundbirder:api');
const debugResponses = require('debug')('soundbirder:api:responses');
require('dotenv').config();
const axios = require('axios');
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 debug = require('debug')('soundbirder:api');
const debugLocale = require('debug')('soundbirder:locale');
const debugResponses = require('debug')('soundbirder:api:responses');
const quizzController = require('./quizz');
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 QUIZZ_SIZE = process.env.QUIZZ_SIZE ? process.env.QUIZZ_SIZE : 5;
function check(req, res) {
}
function quizz(req, res) {
debug('Generating quizz');
const { lat, lng } = req.body;
generateQuizz({ lat, lng }, req.locale)
const locale = req.i18n.locale;
debugLocale("Locale:", locale);
quizzController.generateQuizz({ lat, lng }, locale, QUIZZ_SIZE)
.then(({ species, correct, audio }) => {
req.session.correct = correct;
res.json({ species, audio });
@ -30,118 +27,11 @@ function quizz(req, res) {
});
}
async function generateQuizz(coordinates, locale) {
const quizz = {}
try {
const speciesSelection = await getSpeciesSelection(coordinates);
const speciesSelectionLocalized = await getLocalizedNames(speciesSelection, locale);
quizz.species = await speciesSelectionLocalized;
debug("Got species selection", quizz.species);
const answer = await choice(speciesSelectionLocalized);
debug("Got answer", answer);
quizz.correct = answer.code;
quizz.audio = await getAudio(answer.sciName);
} catch (error) {
debug("Error raised while generating quizz");
console.error(error);
}
return quizz;
}
async function getSpeciesSelection(coordinates) {
const { lat, lng } = coordinates;
const region = await getRegion(lat, lng);
region.country_code = region.country_code.toUpperCase();
// const regionCode = await getRegionCode(region);
const regionCode = region.country_code;
const speciesList = await getSpeciesList(regionCode);
const speciesSelection = choices(speciesList, QUIZZ_SIZE);
debug("Species proposals:", speciesSelection)
return speciesSelection;
}
function getHome(req, res) {
res.render('api', {
title: "SoundBirder api",
version: 0
}
);
}
function getRegion(lat, lon) {
const url = OPENCAGE_API_URL
.replace('<lat>', lat)
.replace('<lon>', lon)
.replace('<key>', OPENCAGE_API_KEY);
return axios.get(url).then(response => {
const { results } = response.data;
const { country_code, country, state, county } = results[0].components;
return { country_code, country, state, county };
}).catch(error => {
throw error;
});
}
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;
});
}
async function getLocalizedNames(speciesCodes, locale) {
let names = [];
for (let i = 0; i < speciesCodes.length; i++) {
const code = speciesCodes[i];
const { sciName, comName } = await getLocalizedName(code, locale);
names.push({ code, sciName, comName });
}
return names;
}
function getLocalizedName(speciesCode, locale) {
return eBird.ref.taxonomy.ebird({
fmt: 'json',
locale: locale,
species: speciesCode
}).then(
response => {
const names = response[0];
debug("Got localized species names");
debugResponses(names);
return names;
}
).catch(error => {
throw error;
});
}
function getSpeciesList(regionCode) {
return eBird.product.spplist.in(regionCode)()
.then(species => {
return species;
})
.catch(error => {
throw error;
});
}
function getAudio(speciesScientificName) {
return XenoCanto.search({
name: speciesScientificName,
quality: 'A'
}).then(response => {
debugResponses(response);
const { recordings } = response;
const randomRecord = choice(recordings);
const audio = randomRecord.file;
return audio;
}).catch(error => {
throw error;
});
}

28
controllers/cache.js Normal file
View File

@ -0,0 +1,28 @@
require('dotenv').config();
const debug = require('debug')('soundbirder:cache');
const redis = require('redis');
[redisHost, redisPort] = [process.env.REDIS_HOST, process.env.REDIS_PORT];
const redisClient = redis.createClient(redisPort, redisHost);
(async () => {
redisClient.connect();
})();
function cacheResponse(request, response) {
debug("Caching response", request);
redisClient.set(request, JSON.stringify(response));
}
async function getCached(request) {
debug("Getting cached response", request);
const cached = await redisClient.get(request);
if (cached) {
return JSON.parse(cached);
}
return null;
}
module.exports = {
cacheResponse,
getCached
}

120
controllers/quizz.js Normal file
View File

@ -0,0 +1,120 @@
require('dotenv').config();
const debug = require('debug')('soundbirder:api:quizz');
const debugResponses = require('debug')('soundbirder:api:responses');
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');
async function generateQuizz(coordinates, locale, size) {
const quizz = {}
try {
const speciesSelection = await getSpeciesSelection(coordinates, size);
debugResponses(`Species selection: ${speciesSelection}`);
const speciesSelectionLocalized = await getLocalizedNames(speciesSelection, locale);
debugResponses('Localized species selection:', speciesSelectionLocalized);
quizz.species = speciesSelectionLocalized;
debug("Got species selection", quizz.species);
const answer = await choice(speciesSelectionLocalized);
debug("Got answer", answer);
quizz.correct = answer.speciesCode;
quizz.audio = await getAudio(answer.sciName);
debug("Got audio", quizz.audio);
} catch (error) {
debug("Error raised while generating quizz");
console.error(error);
}
return quizz;
}
async function getSpeciesSelection(coordinates, number) {
const { lat, lng } = coordinates;
const region = await getRegion(lat, lng);
// const regionCode = await getRegionCode(region);
const regionCode = region.country_code;
const speciesList = await getSpeciesList(regionCode);
const speciesSelection = choices(speciesList, number);
debug("Species proposals:", speciesSelection)
return speciesSelection;
}
async function getLocalizedNames(speciesCodes, locale) {
let allNames = [];
for (let i = 0; i < speciesCodes.length; i++) {
const code = speciesCodes[i];
let flag = false;
try {
names = await cache.getCached(`${code}-${locale}`);
if (names) {
allNames.push(names);
flag = true;
}
} catch (error) {
console.error(error);
}
if (!flag) {
try {
const names = { speciesCode, sciName, comName } = await getLocalizedName(code, locale);
cache.cacheResponse(`${code}-${locale}`, names);
allNames.push(names);
} catch (error) {
console.error(error);
}
}
}
return allNames;
}
function getLocalizedName(speciesCode, locale) {
return eBird.ref.taxonomy.ebird({
fmt: 'json',
locale: locale,
species: speciesCode
}).then(
response => {
const names = response[0];
debug("Got localized species names");
debugResponses(names);
return names;
}
).catch(error => {
throw error;
});
}
async function getSpeciesList(regionCode) {
const cached = await cache.getCached(`spplist-${regionCode}`);
if (cached) {
return cached;
} else {
return eBird.product.spplist.in(regionCode)()
.then(species => {
cache.cacheResponse(`spplist-${regionCode}`, species);
return species;
})
.catch(error => {
throw error;
});
}
}
function getAudio(speciesScientificName) {
return XenoCanto.search({
name: speciesScientificName,
quality: 'A'
}).then(response => {
debugResponses(response);
const { recordings } = response;
const randomRecord = choice(recordings);
const audio = randomRecord.file;
return audio;
}).catch(error => {
throw error;
});
}
module.exports = {
generateQuizz
}

46
controllers/region.js Normal file
View File

@ -0,0 +1,46 @@
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>';
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;
}
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;
});
}
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;
});
}
module.exports = {
getRegion,
}

158
package-lock.json generated
View File

@ -23,7 +23,8 @@
"leaflet": "^1.8.0",
"lodash": "^4.17.21",
"morgan": "~1.9.1",
"pug": "^3.0.2"
"pug": "^3.0.2",
"redis": "^4.3.0"
}
},
"node_modules/@babel/helper-string-parser": {
@ -66,6 +67,59 @@
"node": ">=6.9.0"
}
},
"node_modules/@redis/bloom": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.0.2.tgz",
"integrity": "sha512-EBw7Ag1hPgFzdznK2PBblc1kdlj5B5Cw3XwI9/oG7tSn85/HKy3X9xHy/8tm/eNXJYHLXHJL/pkwBpFMVVefkw==",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/client": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.3.0.tgz",
"integrity": "sha512-XCFV60nloXAefDsPnYMjHGtvbtHR8fV5Om8cQ0JYqTNbWcQo/4AryzJ2luRj4blveWazRK/j40gES8M7Cp6cfQ==",
"dependencies": {
"cluster-key-slot": "1.1.0",
"generic-pool": "3.8.2",
"yallist": "4.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@redis/graph": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.0.1.tgz",
"integrity": "sha512-oDE4myMCJOCVKYMygEMWuriBgqlS5FqdWerikMoJxzmmTUErnTRRgmIDa2VcgytACZMFqpAOWDzops4DOlnkfQ==",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/json": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.3.tgz",
"integrity": "sha512-4X0Qv0BzD9Zlb0edkUoau5c1bInWSICqXAGrpwEltkncUwcxJIGEcVryZhLgb0p/3PkKaLIWkjhHRtLe9yiA7Q==",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/search": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.0.tgz",
"integrity": "sha512-NyFZEVnxIJEybpy+YskjgOJRNsfTYqaPbK/Buv6W2kmFNaRk85JiqjJZA5QkRmWvGbyQYwoO5QfDi2wHskKrQQ==",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/time-series": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.3.tgz",
"integrity": "sha512-OFp0q4SGrTH0Mruf6oFsHGea58u8vS/iI5+NpYdicaM+7BgqBZH8FFvNZ8rYYLrUO/QRqMq72NpXmxLVNcdmjA==",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@unclesamulus/ebird-api": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/@unclesamulus/ebird-api/-/ebird-api-0.0.0.tgz",
@ -209,6 +263,14 @@
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
"integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
},
"node_modules/cluster-key-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz",
"integrity": "sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@ -596,6 +658,14 @@
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
},
"node_modules/generic-pool": {
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.8.2.tgz",
"integrity": "sha512-nGToKy6p3PAbYQ7p1UlWl6vSPwfwU6TMSWK7TTu+WUY4ZjyZQGniGGt2oNVvyNSpyZYSB43zMXVLcBm08MTMkg==",
"engines": {
"node": ">= 4"
}
},
"node_modules/get-intrinsic": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz",
@ -1063,6 +1133,19 @@
"node": ">= 0.8"
}
},
"node_modules/redis": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/redis/-/redis-4.3.0.tgz",
"integrity": "sha512-RXRUor0iU1vizu4viHoUyLpe1ZO/RngZp0V9DyXBHTI+7tC7rEz6Wzn4Sv9v0tTJeqGAzdJ+q5YVbNKKQ5hX9A==",
"dependencies": {
"@redis/bloom": "1.0.2",
"@redis/client": "1.3.0",
"@redis/graph": "1.0.1",
"@redis/json": "1.0.3",
"@redis/search": "1.1.0",
"@redis/time-series": "1.0.3"
}
},
"node_modules/resolve": {
"version": "1.22.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
@ -1257,6 +1340,11 @@
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
}
},
"dependencies": {
@ -1285,6 +1373,46 @@
"to-fast-properties": "^2.0.0"
}
},
"@redis/bloom": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.0.2.tgz",
"integrity": "sha512-EBw7Ag1hPgFzdznK2PBblc1kdlj5B5Cw3XwI9/oG7tSn85/HKy3X9xHy/8tm/eNXJYHLXHJL/pkwBpFMVVefkw==",
"requires": {}
},
"@redis/client": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.3.0.tgz",
"integrity": "sha512-XCFV60nloXAefDsPnYMjHGtvbtHR8fV5Om8cQ0JYqTNbWcQo/4AryzJ2luRj4blveWazRK/j40gES8M7Cp6cfQ==",
"requires": {
"cluster-key-slot": "1.1.0",
"generic-pool": "3.8.2",
"yallist": "4.0.0"
}
},
"@redis/graph": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.0.1.tgz",
"integrity": "sha512-oDE4myMCJOCVKYMygEMWuriBgqlS5FqdWerikMoJxzmmTUErnTRRgmIDa2VcgytACZMFqpAOWDzops4DOlnkfQ==",
"requires": {}
},
"@redis/json": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.3.tgz",
"integrity": "sha512-4X0Qv0BzD9Zlb0edkUoau5c1bInWSICqXAGrpwEltkncUwcxJIGEcVryZhLgb0p/3PkKaLIWkjhHRtLe9yiA7Q==",
"requires": {}
},
"@redis/search": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.0.tgz",
"integrity": "sha512-NyFZEVnxIJEybpy+YskjgOJRNsfTYqaPbK/Buv6W2kmFNaRk85JiqjJZA5QkRmWvGbyQYwoO5QfDi2wHskKrQQ==",
"requires": {}
},
"@redis/time-series": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.3.tgz",
"integrity": "sha512-OFp0q4SGrTH0Mruf6oFsHGea58u8vS/iI5+NpYdicaM+7BgqBZH8FFvNZ8rYYLrUO/QRqMq72NpXmxLVNcdmjA==",
"requires": {}
},
"@unclesamulus/ebird-api": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/@unclesamulus/ebird-api/-/ebird-api-0.0.0.tgz",
@ -1404,6 +1532,11 @@
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
"integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
},
"cluster-key-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz",
"integrity": "sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw=="
},
"combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@ -1692,6 +1825,11 @@
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
},
"generic-pool": {
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.8.2.tgz",
"integrity": "sha512-nGToKy6p3PAbYQ7p1UlWl6vSPwfwU6TMSWK7TTu+WUY4ZjyZQGniGGt2oNVvyNSpyZYSB43zMXVLcBm08MTMkg=="
},
"get-intrinsic": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz",
@ -2074,6 +2212,19 @@
"unpipe": "1.0.0"
}
},
"redis": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/redis/-/redis-4.3.0.tgz",
"integrity": "sha512-RXRUor0iU1vizu4viHoUyLpe1ZO/RngZp0V9DyXBHTI+7tC7rEz6Wzn4Sv9v0tTJeqGAzdJ+q5YVbNKKQ5hX9A==",
"requires": {
"@redis/bloom": "1.0.2",
"@redis/client": "1.3.0",
"@redis/graph": "1.0.1",
"@redis/json": "1.0.3",
"@redis/search": "1.1.0",
"@redis/time-series": "1.0.3"
}
},
"resolve": {
"version": "1.22.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
@ -2217,6 +2368,11 @@
"assert-never": "^1.2.1",
"babel-walk": "3.0.0-canary-5"
}
},
"yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
}
}
}

View File

@ -21,6 +21,7 @@
"leaflet": "^1.8.0",
"lodash": "^4.17.21",
"morgan": "~1.9.1",
"pug": "^3.0.2"
"pug": "^3.0.2",
"redis": "^4.3.0"
}
}

View File

@ -24,7 +24,6 @@ async function query(endpoint, params) {
function getQuizz(lat, lng) {
return query('game/quizz', { lat, lng })
.then(data => {
console.log(data);
return data;
}).catch(error => {
throw error;

View File

@ -17,7 +17,7 @@ function updateLocationCookies([lat, lng]) {
}
function geolocationHandler() {
let location = getCoordinates();
let message = document.querySelector('.message');
// Init map
@ -26,7 +26,10 @@ function geolocationHandler() {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(map);
let marker = undefined;
let marker = L.marker(location, { draggable: true }).addTo(map);
marker.on('dragend', updateMarker);
marker.addTo(map);
// Get geolocation
function getLocation() {
if (navigator.geolocation) {
@ -38,11 +41,7 @@ function geolocationHandler() {
function setLocation(position) {
location = [position.coords.latitude, position.coords.longitude];
if (marker === undefined) {
marker = new L.marker(location, { draggable: 'true' });
marker.on('dragend', updateMarker);
marker.addTo(map);
} else {
if (marker !== undefined) {
marker.setLatLng(location);
}
map.setView(location, 15);