commit 75dcd7df5a577f93cc110eb18d754cd5a5c31c5a Author: Gilles Soulier Date: Sun Dec 15 19:46:39 2024 +0100 Sauvegarde initiale de l'application diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..527b2cf --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Ignore le dossier node_modules +node_modules/ + +# Ignore les fichiers de logs +*.log + +# Ignore les fichiers lock +package-lock.json + +# Ignore les fichiers temporaires ou cache +*.tmp +*.cache +.DS_Store + +# Ignore les fichiers sensibles (exemple) +#config.yaml + +# Ignore les fichiers compilés +dist/ +build/ + +# Autres fichiers à ignorer +.env diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..009d234 --- /dev/null +++ b/config.yaml @@ -0,0 +1,15 @@ +port: 3000 +targetCoordinates: + latitude: 45.141916 + longitude: 4.075059 +assets: + backgroundImage: "static/img/fond.jpg" + arrowApproaching: "static/img/fleche_rouge.png" + arrowMovingAway: "static/img/fleche_bleu.png" + bart: "static/img/bart.png" +userFile: "data/baptiste.yaml" # Chemin vers le fichier user.yaml +position_update: 2 # Temps en secondes entre chaque mise à jour de la position +pin_offset: + x: -0 # Décalage horizontal (en pixels) + y: -0 # Décalage vertical (en pixels) +nb_valeur_gps_moy: 5 \ No newline at end of file diff --git a/data/baptiste.yaml b/data/baptiste.yaml new file mode 100755 index 0000000..aac2529 --- /dev/null +++ b/data/baptiste.yaml @@ -0,0 +1,74 @@ +nom: Baptiste +progression: defi 3/6 +avatar: static/img/bart.png +position_actuelle: + longitude: 4.0750673 + latitude: 45.1420026 + last_update: '2024-12-15T18:35:09.072Z' +defis: + defi_1: + geolocalisation: + longitude: 4.075068 + latitude: 45.141916 + image_1: static/img/imageaaabb01.png + text_found_1: AZERT + mode: reponse + resolu: oui + reception: 0 + contenu_reception: ceci est un test + pin: static/img/pin1.png + defi_2: + geolocalisation: + longitude: 4.076653 + latitude: 45.14198 + image_1: static/img/imageaaabb02.png + text_found_1: QWERT + mode: message + resolu: oui + reception: 1 + contenu_reception: un autre test + pin: static/img/pin2.png + defi_3: + geolocalisation: + longitude: 4.073695 + latitude: 45.139302 + image_1: static/img/imageaaabb01.png + text_found_1: AZERT + mode: reponse + resolu: non + reception: 0 + contenu_reception: ceci est un test + pin: static/img/pin3.png + defi_4: + geolocalisation: + longitude: 4.07735 + latitude: 45.141932 + image_1: static/img/imageaaabb02.jpg + text_found_1: QWERT + mode: message + resolu: non + reception: 1 + contenu_reception: un autre test + pin: static/img/pin4.png + defi_5: + geolocalisation: + longitude: 4.073903 + latitude: 45.140061 + image_1: static/img/imageaaabb01.jpg + text_found_1: AZERT + mode: reponse + resolu: non + reception: 0 + contenu_reception: ceci est un test + pin: static/img/pin5.png + defi_6: + geolocalisation: + longitude: 4.077286 + latitude: 45.142316 + image_1: static/img/imageaaabb02.jpg + text_found_1: QWERT + mode: message + resolu: non + reception: 1 + contenu_reception: un autre test + pin: static/img/pin6.png diff --git a/data/julien.yaml b/data/julien.yaml new file mode 100755 index 0000000..3ee52c9 --- /dev/null +++ b/data/julien.yaml @@ -0,0 +1,27 @@ +nom: Julien +progression: defi 2/6 +avatar: "static/img/julien.jpg" +position_actuelle: + longitude: 4.075159 + latitude: 45.141956 +defis: + defi_1: + geolocalisation: + longitude: 4.076159 + latitude: 45.142956 + image_1: "static/img/imageaaabb03.jpg" + text_found_1: "XYZ" + mode: reponse + resolu: oui + reception: 0 + contenu_reception: "réussi" + defi_2: + geolocalisation: + longitude: 4.077159 + latitude: 45.143956 + image_2: "static/img/imageaaabb04.jpg" + text_found_2: "HELLO" + mode: rien + resolu: non + reception: 0 + contenu_reception: "" diff --git a/index.html b/index.html new file mode 100644 index 0000000..5094712 --- /dev/null +++ b/index.html @@ -0,0 +1,42 @@ + + + + + + Jeu Smartphone + + + + +
+

--

+

défi n° : --

+

-- m

+
+
+

--

+

Latitude : --

+

Longitude : --

+

Distance : -- mètres

+

Progression : --

+ +
+

Défi n° : --

+

Latitude cible : --

+

Longitude cible : --

+
+
+ Flèche directionnelle +
+
+ + Vous vous rapprochez + Vous vous éloignez + Avatar + + + + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..0b74aec --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "jeu_smartphone_js", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "express": "^4.21.2", + "js-yaml": "^4.1.0" + } +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..74fe378 --- /dev/null +++ b/server.js @@ -0,0 +1,81 @@ +const express = require('express'); +const path = require('path'); +const fs = require('fs'); +const yaml = require('js-yaml'); + +const app = express(); + +// Activer le middleware JSON pour parser le corps des requêtes +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); // Si vous utilisez des requêtes POST de type formulaire + +// Charger config.yaml +const config = yaml.load(fs.readFileSync('config.yaml', 'utf8')); + +// Route pour récupérer les données utilisateur +app.get('/user/:username', (req, res) => { + const username = req.params.username; + const userFilePath = path.join(__dirname, `data/${username}.yaml`); + + try { + const userData = yaml.load(fs.readFileSync(userFilePath, 'utf8')); + res.json(userData); + } catch (error) { + res.status(404).json({ error: "Utilisateur non trouvé" }); + } +}); + +// Route pour mettre à jour la position dans baptiste.yaml +app.post('/update-position', (req, res) => { + const { longitude, latitude, last_update } = req.body; // Problème résolu ici + const userFilePath = path.join(__dirname, config.userFile); + + try { + console.log("Tentative de mise à jour des coordonnées :", longitude, latitude, last_update); + + const userData = yaml.load(fs.readFileSync(userFilePath, 'utf8')); + userData.position_actuelle.longitude = longitude; + userData.position_actuelle.latitude = latitude; + userData.position_actuelle.last_update = last_update; + + fs.writeFileSync(userFilePath, yaml.dump(userData), 'utf8'); + console.log("Mise à jour réussie de baptiste.yaml"); + + res.json({ success: true, message: "Position mise à jour", data: userData }); + } catch (error) { + console.error("Erreur lors de la mise à jour :", error.message); + res.status(500).json({ error: "Impossible de mettre à jour la position" }); + } +}); + +// Servir les fichiers statiques +app.use('/static', express.static(path.join(__dirname, 'static'))); + +// Page principale +app.get('/', (req, res) => res.sendFile(path.join(__dirname, 'index.html'))); + +app.listen(config.port, () => { + console.log(`Serveur démarré sur http://localhost:${config.port}`); +}); + +// Route pour servir la page de supervision +app.get('/supervise', (req, res) => { + res.sendFile(path.join(__dirname, 'supervise.html')); +}); +// Route pour récupérer les données de Baptiste +app.get('/data/baptiste.yaml', (req, res) => { + const userFilePath = path.join(__dirname, 'data/baptiste.yaml'); + const configFilePath = path.join(__dirname, 'config.yaml'); + + try { + const userData = yaml.load(fs.readFileSync(userFilePath, 'utf8')); + const config = yaml.load(fs.readFileSync(configFilePath, 'utf8')); +// Inclure le décalage des pins dans la réponse +userData.pin_offset = config.pin_offset; + + res.json(userData); + } catch (error) { + console.error("Erreur lors de la récupération des données :", error.message); + res.status(500).json({ error: "Impossible de récupérer les données" }); + } +}); diff --git a/static/css/img/fond.jpg b/static/css/img/fond.jpg new file mode 100644 index 0000000..aa23223 Binary files /dev/null and b/static/css/img/fond.jpg differ diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..613f786 --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,134 @@ +body { + font-family: Arial, sans-serif; + text-align: center; + margin: 0; + padding: 0; + height: 100vh; + width: 100vw; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + /* Fond d'écran */ + background: url('img/fond.jpg') no-repeat center center; + background-size: cover; /* Adapte l'image à la taille de l'écran */ +} +#content0 { + position: fixed; /* Fixe le conteneur en haut de la page */ + top: 0; /* Place l'élément tout en haut */ + margin: 0; /* Supprime les marges par défaut */ + right: 0; /* Aligne à gauche */ + width: 50%; /* Prend toute la largeur */ + background: rgba(255, 255, 255, 0.8); /* Fond semi-transparent pour un effet header */ + padding: 10px 10px; /* Espacement intérieur */ + border-radius: 15px; /* Arrondit les angles */ + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); /* Ajoute une légère ombre */ + text-align: center; /* Centrage du texte */ + border-bottom: 2px solid rgba(0, 0, 0, 0.1); /* Légère bordure en bas */ + z-index: 1000; /* Assure que le header est au-dessus des autres éléments */ + +} + +#content { + position: relative; /* Nécessaire pour positionner des éléments enfants absolument */ + background: rgba(255, 255, 255, 0.8); /* Fond semi-transparent */ + padding: 20px; + border-radius: 10px; + text-align: center; +} +#position { + font-size: 1.2em; + margin-top: 10px; +} + +#distance { + margin-top: 10px; + font-size: 1.5em; +} + +/* Style des flèches */ +.arrow { + position: absolute; + width: 100px; + height: 100px; + display: none; /* Masqué par défaut */ +} + +#arrow-approaching { + bottom: 10px; + left: 10px; +} + +#arrow-moving-away { + bottom: 10px; + right: 10px; +} + +/* Style de Avatar */ +#avatar { + position: absolute; + width: 130px; + height: 130px; + background-color: rgba(27, 28, 29, 0.6); /* Couleur de remplissage interne */ + border-radius: 50%; /* Pour arrondir l'intérieur */ + box-shadow: 0 0 20px 10px rgba(27, 28, 29, 0.6); /* Halo par défaut */ + transition: all 0.5s ease-in-out; /* Transition douce pour les changements */ +} +/* Conteneur de la flèche */ +#arrow-container { + position: relative; + width: 100px; /* Taille de la boîte de la flèche */ + height: 100px; + margin: 20px auto; /* Centre horizontalement dans le div #content */ +} + +/* Flèche noire */ +#arrow-black { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + transform-origin: center center; /* Centre de rotation */ + transform: rotate(0deg); /* Rotation initiale */ + transition: transform 0.1s linear; /* Animation fluide pour les rotations */ +} +#user-avatar { + position: absolute; /* Permet de positionner avec left et top */ + width: 100px; + height: 100px; + border-radius: 50%; /* Cercle */ + object-fit: cover; /* Ajuste l'image */ + transition: left 0.2s ease, top 0.2s ease; /* Animation fluide */ +} + +[data-debug="true"] { + display: none; +} + +#user-name { + font-weight: bold; /* Rend le texte en gras */ + margin: 0; /* Supprime les marges par défaut */ +} + +#challenge-number , #distance { + margin: 0; /* Supprime les marges par défaut */ +} + +#arrow-approaching, #arrow-moving-away { + position: fixed; /* Fixe le conteneur en haut de la page */ + bottom: 0; /* Place l'élément tout en haut */ + margin: 0; /* Supprime les marges par défaut */ + left: 0; /* Aligne à gauche */ + width: 25%; /* Prend toute la largeur */ + background: rgba(255, 255, 255, 0.8); /* Fond semi-transparent pour un effet header */ + padding: 10px 10px; /* Espacement intérieur */ + border-radius: 15px; /* Arrondit les angles */ + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); /* Ajoute une légère ombre */ + text-align: center; /* Centrage du texte */ + border-bottom: 2px solid rgba(0, 0, 0, 0.1); /* Légère bordure en bas */ + z-index: 1000; /* Assure que le header est au-dessus des autres éléments */ + + +} \ No newline at end of file diff --git a/static/css/supervise.css b/static/css/supervise.css new file mode 100644 index 0000000..79349dd --- /dev/null +++ b/static/css/supervise.css @@ -0,0 +1,106 @@ +/* Réinitialisation de base */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, body { + height: 100%; + width: 100%; + font-family: Arial, sans-serif; + background-color: #f4f4f4; +} + +/* Conteneur principal */ +#container { + display: flex; + flex-direction: row; + height: 100vh; /* Vue entière */ + gap: 10px; /* Espacement entre les sections */ + padding: 10px; +} + +/* Section principale (à gauche) */ +.large-section { + flex: 2; /* Prend plus d'espace */ + background-color: #ccc; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.5rem; + border-radius: 5px; +} + +/* Conteneur pour les sections de droite */ +.section-container { + flex: 1; + display: flex; + flex-direction: column; + gap: 10px; /* Espacement vertical */ +} + +/* Petites sections (à droite) */ +.small-section { + flex: 1; + background-color: #aaa; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.2rem; + border-radius: 5px; +} + +/* Responsivité : Pour les tablettes */ +@media (max-width: 1024px) { + #container { + flex-direction: column; /* Colonne pour iPad portrait */ + } + .large-section { + flex: 1; + } + .section-container { + flex: 1; + } +} + +#section-top-right h2, +#section-top-right p { + margin: 5px 0; + text-align: center; +} + +#user-avatar { + display: block; + margin: 10px auto; + border: 2px solid #333; +} +/* Section 2 - Organisation en colonne */ +#section-top-right { + display: flex; /* Active le flexbox */ + flex-direction: column; /* Aligne les éléments en colonne */ + align-items: center; /* Centre les éléments horizontalement */ + justify-content: center; /* Centre les éléments verticalement */ + gap: 10px; /* Espacement entre les éléments */ + font-size: 14px; /* Taille identique pour tous les caractères */ + text-align: left; /* Alignement du texte */ + padding: 10px; +} + +#section-top-right h2, +#section-top-right p { + margin: 0; /* Enlève les marges par défaut */ + font-size: 14px; /* Taille identique */ +} + +#user-avatar { + width: 50px; + height: 50px; + border-radius: 50%; /* Avatar en forme de cercle */ + object-fit: cover; + border: 2px solid #333; /* Bordure autour de l'avatar */ +} +#map { + width: 100%; + height: 100%; +} diff --git a/static/img/bart.png b/static/img/bart.png new file mode 100644 index 0000000..6fd5465 Binary files /dev/null and b/static/img/bart.png differ diff --git a/static/img/fleche_bleu.png b/static/img/fleche_bleu.png new file mode 100644 index 0000000..9306e6b Binary files /dev/null and b/static/img/fleche_bleu.png differ diff --git a/static/img/fleche_noir.png b/static/img/fleche_noir.png new file mode 100644 index 0000000..5fd8210 Binary files /dev/null and b/static/img/fleche_noir.png differ diff --git a/static/img/fleche_rouge.png b/static/img/fleche_rouge.png new file mode 100644 index 0000000..7910d0d Binary files /dev/null and b/static/img/fleche_rouge.png differ diff --git a/static/img/fleche_verte.png b/static/img/fleche_verte.png new file mode 100644 index 0000000..38079e3 Binary files /dev/null and b/static/img/fleche_verte.png differ diff --git a/static/img/imageaaabb01.png b/static/img/imageaaabb01.png new file mode 100644 index 0000000..1653e28 Binary files /dev/null and b/static/img/imageaaabb01.png differ diff --git a/static/img/pin1.png b/static/img/pin1.png new file mode 100644 index 0000000..25e0a9b Binary files /dev/null and b/static/img/pin1.png differ diff --git a/static/img/pin2.png b/static/img/pin2.png new file mode 100644 index 0000000..25e0a9b Binary files /dev/null and b/static/img/pin2.png differ diff --git a/static/img/pin3.png b/static/img/pin3.png new file mode 100644 index 0000000..25e0a9b Binary files /dev/null and b/static/img/pin3.png differ diff --git a/static/img/pin4.png b/static/img/pin4.png new file mode 100644 index 0000000..25e0a9b Binary files /dev/null and b/static/img/pin4.png differ diff --git a/static/img/pin5.png b/static/img/pin5.png new file mode 100644 index 0000000..25e0a9b Binary files /dev/null and b/static/img/pin5.png differ diff --git a/static/img/pin6.png b/static/img/pin6.png new file mode 100644 index 0000000..25e0a9b Binary files /dev/null and b/static/img/pin6.png differ diff --git a/static/js/script.js b/static/js/script.js new file mode 100644 index 0000000..e6b6482 --- /dev/null +++ b/static/js/script.js @@ -0,0 +1,387 @@ +const modeDebug = false; // Passez à true pour activer le mode debug + +document.addEventListener('DOMContentLoaded', () => { + toggleDebugMode(); // Appliquer le mode debug au chargement de la page +}); + +function toggleDebugMode() { + const debugElements = document.querySelectorAll('[data-debug="true"]'); + + debugElements.forEach(element => { + element.style.display = modeDebug ? 'block' : 'none'; + }); + + console.log(`Mode debug ${modeDebug ? 'activé' : 'désactivé'}`); +} + +document.addEventListener('DOMContentLoaded', () => { + // Votre code ici + +}); + +let wakeLock = null; + +async function enableWakeLock() { + try { + if ('wakeLock' in navigator) { + const wakeLock = await navigator.wakeLock.request('screen'); + console.log('Verrouillage de l’écran activé.'); + return wakeLock; + } else { + console.warn('API Wake Lock non prise en charge par ce navigateur.'); + } + } catch (error) { + console.error('Erreur lors de l’activation du verrouillage de l’écran :', error); + } +} + +// Fonction pour réactiver le Wake Lock si la page redevient visible +function handleVisibilityChange() { + if (wakeLock !== null && document.visibilityState === 'visible') { + enableWakeLock(); + } +} + +// Activer le Wake Lock au chargement de la page +document.addEventListener('DOMContentLoaded', () => { + enableWakeLock(); +}); + +function showPopup(challenge) { + if (!challenge) { + console.error("Aucune donnée de défi actif !"); + return; + } + + const { image, textFound, mode } = challenge; + + // Créer l'élément popup + const popup = document.createElement('div'); + popup.id = 'popup'; + popup.style.position = 'fixed'; + popup.style.top = '50%'; + popup.style.left = '50%'; + popup.style.transform = 'translate(-50%, -50%)'; + popup.style.padding = '20px'; + popup.style.backgroundColor = '#fff'; + popup.style.boxShadow = '0 0 10px rgba(0,0,0,0.5)'; + popup.style.zIndex = '1000'; + + // Ajouter l'image + const img = document.createElement('img'); + img.src = image; + img.alt = 'Défi actif'; + img.style.width = '100%'; + popup.appendChild(img); + + // Ajouter le texte trouvé + const text = document.createElement('p'); + text.textContent = `Texte à trouver : ${textFound}`; + popup.appendChild(text); + + // Ajouter une action en fonction du mode + if (mode === 'message') { + const message = document.createElement('p'); + message.textContent = "Mode : Message"; + popup.appendChild(message); + } else if (mode === 'reponse') { + const input = document.createElement('input'); + input.type = 'text'; + input.placeholder = 'Entrez votre réponse'; + popup.appendChild(input); + + const submitButton = document.createElement('button'); + submitButton.textContent = 'Envoyer'; + popup.appendChild(submitButton); + } + + // Ajouter un bouton pour fermer + const closeButton = document.createElement('button'); + closeButton.textContent = 'Fermer'; + closeButton.onclick = () => document.body.removeChild(popup); + popup.appendChild(closeButton); + + // Ajouter le popup au corps de la page + document.body.appendChild(popup); +} + +// Fonction pour charger les données utilisateur +async function fetchUserData() { + try { + const response = await fetch(`/user/${username}`); + const userData = await response.json(); + + console.log("Données utilisateur :", userData); + + // Mettre à jour l'interface avec les données + document.getElementById('user-name').textContent = userData.nom; + document.getElementById('user-avatar').src = userData.avatar; + document.getElementById('user-progression').textContent = userData.progression; + + // Trouver le premier défi non résolu + let firstUnresolvedChallenge = null; + let challengeNumber = null; + + for (let i = 1; i <= 6; i++) { + const challengeKey = `defi_${i}`; + const challenge = userData.defis[challengeKey]; + + if (challenge.resolu === "non") { + firstUnresolvedChallenge = challenge; + challengeNumber = i; + break; // On arrête dès qu'on trouve le premier défi non résolu + } + } + + if (firstUnresolvedChallenge) { + const { latitude, longitude } = firstUnresolvedChallenge.geolocalisation; + // Mettre à jour la variable globale target + target = { latitude, longitude }; + + // Extraire les informations du défi actif + const { image_1, text_found_1, mode } = firstUnresolvedChallenge; + + // Stocker les informations du défi actif dans une variable globale + window.activeChallenge = { + image: image_1, + textFound: text_found_1, + mode: mode, + }; + + // Mettre à jour l'interface avec les coordonnées du défi + document.getElementById('target-latitude').textContent = latitude.toFixed(6); + document.getElementById('target-longitude').textContent = longitude.toFixed(6); + document.getElementById('challenge-number').textContent = `défi n°${challengeNumber}`; + } else { + console.log("Tous les défis sont résolus !"); + target = null; // Réinitialisation de la cible + } + + } catch (error) { + console.error("Erreur lors du chargement des données utilisateur :", error); + } +} + +// Charger les données utilisateur dès le chargement de la page +document.addEventListener('DOMContentLoaded', () => { + fetchUserData(); // Charger les données au démarrage + setInterval(fetchUserData, 10000); // Actualiser toutes les 10 secondes +}); + + +// Coordonnées de la cible +//let target = { latitude: 45.141916, longitude: 4.075059 }; // Coordonnées de la cible +//let currentHeading = 0; // Orientation actuelle de l'appareil +let lastDistance = null; + +// Variables pour les coins de la droite virtuelle +let topLeft = { x: 2, y: 2 }; // Coin supérieur gauche en pixels +let bottomRight = { x: window.innerWidth - 100, y: window.innerHeight - 250 }; // Coin inférieur droit en pixels + +// Fonction pour recalculer les coins dynamiquement +function updateCorners() { + bottomRight = { + x: window.innerWidth - 100, + y: window.innerHeight - 250 + }; +} +window.addEventListener('resize', updateCorners); + +// Fonction utilitaire pour définir le halo +function setHaloColor(element, color) { + element.style.boxShadow = `0 0 20px 10px ${color}`; +} + +// Fonction pour mettre à jour le style de Avatar +function updateAvatarStyle(distance) { + const avatar = document.getElementById('user-avatar'); + + if (distance > 20) { + setHaloColor(avatar, 'rgba(0, 0, 255, 0.6)'); // Halo bleu + } else if (distance <= 10) { + setHaloColor(avatar, 'rgba(255, 0, 0, 0.6)'); // Halo rouge + } else { + setHaloColor(avatar, 'rgba(27, 28, 29, 0.6)'); // Halo gris + } +} + +function positionAvatar(distance) { + const avatar = document.getElementById('user-avatar'); + if (!avatar) { + console.error("L'élément 'user-avatar' n'existe pas dans le DOM."); + return; + } + + if (distance > 25) { + avatar.style.left = `${topLeft.x}px`; + avatar.style.top = `${topLeft.y}px`; + } else if (distance <= 5) { + avatar.style.left = `${bottomRight.x}px`; + avatar.style.top = `${bottomRight.y}px`; + } else { + const ratio = (25 - distance) / 20; + const newX = topLeft.x + (bottomRight.x - topLeft.x) * ratio; + const newY = topLeft.y + (bottomRight.y - topLeft.y) * ratio; + + avatar.style.left = `${newX}px`; + avatar.style.top = `${newY}px`; + } +} + +// Fonction pour gérer les flèches directionnelles +function updateArrows(distance) { + const arrowApproaching = document.getElementById('arrow-approaching'); + const arrowMovingAway = document.getElementById('arrow-moving-away'); + + if (lastDistance !== null) { + if (distance < lastDistance) { + arrowApproaching.style.display = 'block'; + arrowMovingAway.style.display = 'none'; + } else { + arrowApproaching.style.display = 'none'; + arrowMovingAway.style.display = 'block'; + } + } +} + +// Fonction pour calculer la direction vers la cible (bearing) +function calculateBearing(lat1, lon1, lat2, lon2) { + const toRad = (value) => (value * Math.PI) / 180; + const toDeg = (value) => (value * 180) / Math.PI; + + const dLon = toRad(lon2 - lon1); + const y = Math.sin(dLon) * Math.cos(toRad(lat2)); + const x = Math.cos(toRad(lat1)) * Math.sin(toRad(lat2)) - + Math.sin(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.cos(dLon); + + return (toDeg(Math.atan2(y, x)) + 360) % 360; // Angle en degrés, ajusté pour être positif +} + +// Fonction pour mettre à jour l'orientation de la flèche +function updateCompass(bearing) { + const arrow = document.getElementById('arrow-black'); + const angle = (bearing - currentHeading + 360) % 360; // Ajuste selon l'orientation actuelle + arrow.style.transform = `rotate(${angle}deg)`; // Applique la rotation +} + +// Écoute les changements d'orientation de l'appareil +window.addEventListener('deviceorientation', (event) => { + currentHeading = event.alpha || 0; // Orientation absolue en degrés +}); + +// Fonction principale pour mettre à jour les éléments +function updatePosition(position) { + const { latitude, longitude } = position.coords; + + document.getElementById('latitude').textContent = latitude.toFixed(6); + document.getElementById('longitude').textContent = longitude.toFixed(6); + + if (!target) { + console.warn("Aucune cible définie. Impossible de calculer la distance ou la direction."); + return; + } + + const distance = calculateDistance(latitude, longitude, target.latitude, target.longitude); + document.getElementById('distance').textContent = `${distance.toFixed(0)} m`; + + + const bearing = calculateBearing(latitude, longitude, target.latitude, target.longitude); + + positionAvatar(distance); + updateAvatarStyle(distance); + updateArrows(distance); + updateCompass(bearing); + + // Afficher le popup si distance < 5m + if (distance < 4) { + showPopup(window.activeChallenge); + } + + lastDistance = distance; +} + +// Fonction pour calculer la distance entre deux points GPS (Haversine Formula) +function calculateDistance(lat1, lon1, lat2, lon2) { + const R = 6371000; // Rayon de la Terre en mètres + const toRad = (value) => (value * Math.PI) / 180; + const dLat = toRad(lat2 - lat1); + const dLon = toRad(lon2 - lon1); + const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * + Math.sin(dLon / 2) * Math.sin(dLon / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + return R * c; +} + +// Gestion des erreurs de géolocalisation +function handleError(error) { + const errorMessages = { + 1: "Permission refusée", + 2: "Position indisponible", + 3: "Délai dépassé" + }; + alert("Erreur de géolocalisation : " + errorMessages[error.code] || "Erreur inconnue"); +} + +// Initialiser la géolocalisation en temps réel +function startTracking() { + if (!navigator.geolocation) { + alert("La géolocalisation n'est pas prise en charge par votre navigateur."); + return; + } + + navigator.geolocation.watchPosition(updatePosition, handleError, { + enableHighAccuracy: true, + maximumAge: 0, + timeout: 5000 + }); +} + +// Démarrer le suivi dès le chargement de la page +startTracking(); + +const positionUpdateInterval = 1000 * 5; // 5 secondes (à récupérer dynamiquement depuis config.yaml) + +// Fonction pour envoyer les nouvelles coordonnées au serveur +async function updateServerPosition(latitude, longitude) { + + const now = new Date().toISOString(); // Format ISO 8601 pour last_update + try { + const response = await fetch('/update-position', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + latitude: latitude, + longitude: longitude, + last_update: now // Inclure le champ last_update + }) + }); + const result = await response.json(); + console.log("Position mise à jour :", result.message); + } catch (error) { + console.error("Erreur lors de la mise à jour de la position :", error); + } +} + +// Mise à jour de la position toutes les X secondes +function startPositionUpdates() { + setInterval(() => { + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition(position => { + const { latitude, longitude } = position.coords; + updateServerPosition(latitude, longitude); + }); + } else { + console.error("La géolocalisation n'est pas prise en charge."); + } + }, positionUpdateInterval); +} + +// Lancer le suivi dès le chargement de la page +document.addEventListener('DOMContentLoaded', () => { + startPositionUpdates(); + startTracking(); // Fonction existante pour afficher les mises à jour côté client +}); + + + diff --git a/static/js/supervise.js b/static/js/supervise.js new file mode 100644 index 0000000..334f108 --- /dev/null +++ b/static/js/supervise.js @@ -0,0 +1,161 @@ + +function calculateDeltaInSeconds(lastUpdate) { + // Convertir la date de mise à jour en UTC + const lastUpdateTime = new Date(lastUpdate).getTime(); // En millisecondes + const currentTime = Date.now(); // Temps actuel en UTC (automatique) + + const deltaInMilliseconds = currentTime - lastUpdateTime; + + // Retourner la différence en secondes + return Math.floor(deltaInMilliseconds / 1000); +} + +// Fonction pour récupérer les données et mettre à jour l'interface +async function fetchUserData() { + try { + // Appel à l'API pour récupérer les données + const response = await fetch('/data/baptiste.yaml'); + const userData = await response.json(); + + // Calculer le temps d'inactivité + const lastUpdate = userData.position_actuelle.last_update; // Récupération du champ "last_update" + const inactivitySeconds = calculateDeltaInSeconds(lastUpdate); // Calcul du delta en secondes + + // Vérification des données reçues + console.log("Données utilisateur :", userData); + + // Mise à jour des éléments HTML + document.getElementById('user-name').textContent = `Nom : ${userData.nom}`; + document.getElementById('user-progression').textContent = `Progression : ${userData.progression}`; + document.getElementById('user-avatar').src = userData.avatar; + document.getElementById('user-longitude').textContent = userData.position_actuelle.longitude; + document.getElementById('user-latitude').textContent = userData.position_actuelle.latitude; + document.getElementById('user-inactive').textContent = `Inactif depuis : ${inactivitySeconds} s`; + + + } catch (error) { + console.error("Erreur lors de la récupération des données :", error); + } +} + +// Actualiser les données toutes les 3 secondes +document.addEventListener("DOMContentLoaded", () => { + fetchUserData(); // Appel initial pour charger les données au démarrage + setInterval(fetchUserData, 3000); // Appels périodiques toutes les 3 secondes +}); + +document.addEventListener("DOMContentLoaded", async () => { + // Coordonnées par défaut pour centrer la carte + const centerCoordinates = [45.141998, 4.0750724]; + let userMarker; // Variable pour stocker le pin de l'avatar + + // Initialiser la carte + const map = L.map('map').setView(centerCoordinates, 20); + + // Définir les couches de carte + const osmStandard = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors' + }); + + const cycleOSM = L.tileLayer('https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png', { + attribution: '© CycleOSM' + }); + + const satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { + attribution: '© ESRI World Imagery' + }); + + // Ajouter la carte OpenStreetMap par défaut + osmStandard.addTo(map); + + // Contrôle pour basculer entre les couches + const baseLayers = { + "OpenStreetMap Standard": osmStandard, + "CycleOSM": cycleOSM, + "Satellite": satellite + }; + L.control.layers(baseLayers).addTo(map); + + // Fonction pour ajouter un pin personnalisé + function addCustomPin(coordinates, iconUrl, popupText, offsetX = 20, offsetY = 40) { + const customIcon = L.icon({ + iconUrl: iconUrl, + iconSize: [40, 40], // Taille de l'icône + iconAnchor: [offsetX, offsetY], // Décalage en X et Y + popupAnchor: [0, -40] // Position du popup + }); + + return L.marker(coordinates, { icon: customIcon }).bindPopup(popupText); + } + + + + // Fonction pour mettre à jour la position de l'avatar + async function updateAvatarPosition() { + try { + const response = await fetch('/data/baptiste.yaml'); + const userData = await response.json(); + + const newCoordinates = [ + userData.position_actuelle.latitude, + userData.position_actuelle.longitude + + + ]; + + const lastUpdate = userData.position_actuelle.last_update; + const inactivitySeconds = calculateDeltaInSeconds(lastUpdate); + + // Si le pin existe déjà, on met à jour sa position + if (userMarker) { + userMarker.setLatLng(newCoordinates); + } else { + // Ajouter le pin de l'avatar pour la première fois + userMarker = addCustomPin(newCoordinates, userData.avatar, `Baptiste : ${userData.progression}`); + userMarker.addTo(map); + } + + console.log("Position de l'avatar mise à jour :", newCoordinates); + } catch (error) { + console.error("Erreur lors de la mise à jour de la position :", error); + } + } + + // Ajouter les pins pour les défis (chargement initial uniquement) + async function loadChallengePins() { + try { + const response = await fetch('/data/baptiste.yaml'); + const userData = await response.json(); + + const offsetX = userData.pin_offset?.x || 20; + const offsetY = userData.pin_offset?.y || 40; + + for (let i = 1; i <= 6; i++) { + const defiKey = `defi_${i}`; + const defi = userData.defis[defiKey]; + + const defiCoordinates = [ + defi.geolocalisation.latitude, + defi.geolocalisation.longitude + ]; + + const pinIcon = defi.pin; + const popupText = `Défi ${i} - ${defi.contenu_reception}`; + + const defiMarker = addCustomPin(defiCoordinates, pinIcon, popupText, offsetX, offsetY); + defiMarker.addTo(map); + } + + console.log("Pins des défis chargés avec succès !"); + } catch (error) { + console.error("Erreur lors du chargement des défis :", error); + } + } + + // Charger les pins des défis (une seule fois) + await loadChallengePins(); + + // Mettre à jour la position de l'avatar toutes les 3 secondes + updateAvatarPosition(); // Premier appel pour l'initialisation + setInterval(updateAvatarPosition, 3000); +}); \ No newline at end of file diff --git a/supervise.html b/supervise.html new file mode 100644 index 0000000..ca560ff --- /dev/null +++ b/supervise.html @@ -0,0 +1,42 @@ + + + + + + Supervision + + + + + +
+ +
+
+
+ + +
+
+ Avatar +

Nom : --

+

Progression : --

+

Longitude : --

+

Latitude : --

+

Inactif depuis : -- s

+
+
+

Section 3

+
+
+
+ + + +