Sauvegarde initiale de l'application
23
.gitignore
vendored
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
After Width: | Height: | Size: 106 KiB |
134
static/css/style.css
Normal 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
@@ -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
|
After Width: | Height: | Size: 68 KiB |
BIN
static/img/fleche_bleu.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
static/img/fleche_noir.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
static/img/fleche_rouge.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
static/img/fleche_verte.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
static/img/imageaaabb01.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
static/img/pin1.png
Normal file
|
After Width: | Height: | Size: 188 KiB |
BIN
static/img/pin2.png
Normal file
|
After Width: | Height: | Size: 188 KiB |
BIN
static/img/pin3.png
Normal file
|
After Width: | Height: | Size: 188 KiB |
BIN
static/img/pin4.png
Normal file
|
After Width: | Height: | Size: 188 KiB |
BIN
static/img/pin5.png
Normal file
|
After Width: | Height: | Size: 188 KiB |
BIN
static/img/pin6.png
Normal file
|
After Width: | Height: | Size: 188 KiB |
387
static/js/script.js
Normal 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 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
|
||||
});
|
||||
|
||||
|
||||
|
||||
161
static/js/supervise.js
Normal 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: '© <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: '© <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: '© <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
@@ -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>
|
||||