Sauvegarde initiale de l'application

This commit is contained in:
2024-12-15 19:46:39 +01:00
commit 75dcd7df5a
25 changed files with 1108 additions and 0 deletions

23
.gitignore vendored Normal file
View File

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

15
config.yaml Normal file
View File

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

74
data/baptiste.yaml Executable file
View File

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

27
data/julien.yaml Executable file
View File

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

42
index.html Normal file
View File

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Jeu Smartphone</title>
<link rel="stylesheet" href="static/css/style.css">
<script>
const username = 'baptiste'; // Charge les données de Baptiste
</script>
</head>
<body>
<div id="content0">
<p id="user-name">--</p>
<p id="challenge-number">défi n° : --</p>
<p id="distance">-- m</p>
</div>
<div data-debug="true" id="content">
<h1> <span id="user-name">--</span></h1>
<p data-debug="true">Latitude : <span id="latitude">--</span></p>
<p data-debug="true">Longitude : <span id="longitude">--</span></p>
<p>Distance : <span id="distance">--</span> mètres</p>
<p data-debug="true">Progression : <span id="user-progression">--</span></p>
<div data-debug="true" id="challenge-info">
<p id="challenge-number">Défi n° : --</p>
<p data-debug="true">Latitude cible : <span id="target-latitude">--</span></p>
<p data-debug="true">Longitude cible : <span id="target-longitude">--</span></p>
</div>
<div id="arrow-container">
<img id="arrow-black" src="static/img/fleche_noir.png" alt="Flèche directionnelle" data-debug="true" >
</div>
</div>
<img id="arrow-approaching" src="static/img/fleche_rouge.png" alt="Vous vous rapprochez">
<img id="arrow-moving-away" src="static/img/fleche_bleu.png" alt="Vous vous éloignez">
<img id="user-avatar" src="" alt="Avatar" style="width: 100px; border-radius: 50%;">
<script src="static/js/script.js" defer></script>
</body>
</html>

16
package.json Normal file
View File

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

81
server.js Normal file
View File

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

BIN
static/css/img/fond.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

134
static/css/style.css Normal file
View File

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

106
static/css/supervise.css Normal file
View File

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

BIN
static/img/bart.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
static/img/fleche_bleu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
static/img/fleche_noir.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
static/img/fleche_rouge.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
static/img/fleche_verte.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
static/img/imageaaabb01.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

BIN
static/img/pin1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

BIN
static/img/pin2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

BIN
static/img/pin3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

BIN
static/img/pin4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

BIN
static/img/pin5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

BIN
static/img/pin6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

387
static/js/script.js Normal file
View File

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

161
static/js/supervise.js Normal file
View File

@@ -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: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
});
const cycleOSM = L.tileLayer('https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.cyclosm.org/">CycleOSM</a>'
});
const satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: '&copy; <a href="https://www.esri.com/">ESRI</a> 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);
});

42
supervise.html Normal file
View File

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Supervision</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<link rel="stylesheet" href="static/css/supervise.css">
<style>
#map {
width: 100%;
height: 100%;
border-radius: 5px;
}
</style>
</head>
<body>
<div id="container">
<!-- Section principale (à gauche) -->
<div class="section large-section" id="section-left">
<div id="map"></div>
</div>
<!-- Deux sections à droite -->
<div class="section-container">
<div class="section small-section" id="section-top-right">
<img id="user-avatar" src="" alt="Avatar" style="width: 50px; height: 50px; border-radius: 50%;">
<h2 id="user-name">Nom : --</h2>
<p id="user-progression">Progression : --</p>
<p>Longitude : <span id="user-longitude">--</span></p>
<p>Latitude : <span id="user-latitude">--</span></p>
<p id="user-inactive">Inactif depuis : -- s</p>
</div>
<div class="section small-section" id="section-bottom-right">
<p>Section 3</p>
</div>
</div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="static/js/supervise.js"></script>
</body>
</html>