mirror of
https://git.rezel.net/LudoTech/traque.git
synced 2026-02-09 02:10:18 +01:00
Ajout zones en pavage + fix dockefiles
This commit is contained in:
@@ -5,7 +5,7 @@ FROM node:22-alpine
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy package.json and package-lock.json to the /app directory
|
# Copy package.json and package-lock.json to the /app directory
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json* ./
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ WORKDIR /app
|
|||||||
ENV NODE_ENV=development
|
ENV NODE_ENV=development
|
||||||
|
|
||||||
# Copy package.json and package-lock.json to the /app directory
|
# Copy package.json and package-lock.json to the /app directory
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json* ./
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ This module also exposes functions to send messages via socket to all admins
|
|||||||
*/
|
*/
|
||||||
import { io } from "./index.js";
|
import { io } from "./index.js";
|
||||||
import game from "./game.js"
|
import game from "./game.js"
|
||||||
import zone from "./zone_manager.js"
|
import zoneManager from "./zone_manager.js"
|
||||||
import penaltyController from "./penalty_controller.js";
|
import penaltyController from "./penalty_controller.js";
|
||||||
import { playersBroadcast, sendUpdatedTeamInformations } from "./team_socket.js";
|
import { playersBroadcast, sendUpdatedTeamInformations } from "./team_socket.js";
|
||||||
import { createHash } from "crypto";
|
import { createHash } from "crypto";
|
||||||
@@ -57,12 +57,11 @@ export function initAdminSocketHandler() {
|
|||||||
// Other settings that need initialization
|
// Other settings that need initialization
|
||||||
socket.emit("penalty_settings", penaltyController.settings)
|
socket.emit("penalty_settings", penaltyController.settings)
|
||||||
socket.emit("game_settings", game.settings)
|
socket.emit("game_settings", game.settings)
|
||||||
socket.emit("zone_settings", zone.zoneSettings)
|
socket.emit("zone_settings", zoneManager.settings)
|
||||||
socket.emit("zone", zone.currentZone)
|
socket.emit("current_zone", {
|
||||||
socket.emit("new_zone", {
|
begin: zoneManager.getCurrentZone(),
|
||||||
begin: zone.currentStartZone,
|
end: zoneManager.getNextZone(),
|
||||||
end: zone.nextZone,
|
endDate: zoneManager.currentZoneEndDate,
|
||||||
endDate: zone.nextZoneDate,
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// Attempt unsuccessful
|
// Attempt unsuccessful
|
||||||
@@ -89,11 +88,11 @@ export function initAdminSocketHandler() {
|
|||||||
socket.emit("error", "Not logged in");
|
socket.emit("error", "Not logged in");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!game.setZoneSettings(settings)) {
|
if (!zoneManager.changeSettings(settings)) {
|
||||||
socket.emit("error", "Error changing zone");
|
socket.emit("error", "Error changing zone");
|
||||||
socket.emit("zone_settings", zone.zoneSettings) // Still broadcast the old config to the client who submited an incorrect config to keep the client up to date
|
socket.emit("zone_settings", zoneManager.settings)
|
||||||
} else {
|
} else {
|
||||||
secureAdminBroadcast("zone_settings", zone.zoneSettings)
|
secureAdminBroadcast("zone_settings", zoneManager.settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,12 +3,44 @@ This module manages the main game state, the teams, the settings and the game lo
|
|||||||
*/
|
*/
|
||||||
import { secureAdminBroadcast } from "./admin_socket.js";
|
import { secureAdminBroadcast } from "./admin_socket.js";
|
||||||
import { playersBroadcast, sendUpdatedTeamInformations } from "./team_socket.js";
|
import { playersBroadcast, sendUpdatedTeamInformations } from "./team_socket.js";
|
||||||
import { isInCircle, getDistanceFromLatLon } from "./map_utils.js";
|
|
||||||
import timeoutHandler from "./timeoutHandler.js";
|
import timeoutHandler from "./timeoutHandler.js";
|
||||||
import penaltyController from "./penalty_controller.js";
|
import penaltyController from "./penalty_controller.js";
|
||||||
import zoneManager from "./zone_manager.js";
|
import zoneManager from "./zone_manager.js";
|
||||||
import trajectory from "./trajectory.js";
|
import trajectory from "./trajectory.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the distance between two points givent their longitude and latitude
|
||||||
|
* @param {Object} pos1 The first position
|
||||||
|
* @param {Object} pos2 The second position
|
||||||
|
* @returns the distance between the two positions in meters
|
||||||
|
* @see https://gist.github.com/miguelmota/10076960
|
||||||
|
*/
|
||||||
|
function getDistanceFromLatLon({ lat: lat1, lng: lon1 }, { lat: lat2, lng: lon2 }) {
|
||||||
|
const degToRad = (deg) => deg * (Math.PI / 180);
|
||||||
|
var R = 6371; // Radius of the earth in km
|
||||||
|
var dLat = degToRad(lat2 - lat1);
|
||||||
|
var dLon = degToRad(lon2 - lon1);
|
||||||
|
var a =
|
||||||
|
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||||
|
Math.cos(degToRad(lat1)) * Math.cos(degToRad(lat2)) *
|
||||||
|
Math.sin(dLon / 2) * Math.sin(dLon / 2)
|
||||||
|
;
|
||||||
|
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||||
|
var d = R * c; // Distance in km
|
||||||
|
return d * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a GPS point is in a circle
|
||||||
|
* @param {Object} position The position to check, an object with lat and lng fields
|
||||||
|
* @param {Object} center The center of the circle, an object with lat and lng fields
|
||||||
|
* @param {Number} radius The radius of the circle in meters
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function isInCircle(position, center, radius) {
|
||||||
|
return getDistanceFromLatLon(position, center) < radius;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The possible states of the game
|
* The possible states of the game
|
||||||
*/
|
*/
|
||||||
@@ -56,7 +88,7 @@ export default {
|
|||||||
switch (newState) {
|
switch (newState) {
|
||||||
case GameState.SETUP:
|
case GameState.SETUP:
|
||||||
trajectory.stop();
|
trajectory.stop();
|
||||||
zoneManager.reset();
|
zoneManager.stop();
|
||||||
penaltyController.stop();
|
penaltyController.stop();
|
||||||
timeoutHandler.endAllSendPositionTimeout();
|
timeoutHandler.endAllSendPositionTimeout();
|
||||||
for (let team of this.teams) {
|
for (let team of this.teams) {
|
||||||
@@ -77,28 +109,38 @@ export default {
|
|||||||
this.updateTeamChasing();
|
this.updateTeamChasing();
|
||||||
break;
|
break;
|
||||||
case GameState.PLACEMENT:
|
case GameState.PLACEMENT:
|
||||||
|
if (this.teams.length < 3) {
|
||||||
|
secureAdminBroadcast("game_state", {state: this.state, startDate: this.startDate});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
trajectory.stop();
|
trajectory.stop();
|
||||||
zoneManager.reset();
|
zoneManager.stop();
|
||||||
penaltyController.stop();
|
penaltyController.stop();
|
||||||
timeoutHandler.endAllSendPositionTimeout();
|
timeoutHandler.endAllSendPositionTimeout();
|
||||||
this.startDate = null;
|
this.startDate = null;
|
||||||
break;
|
break;
|
||||||
case GameState.PLAYING:
|
case GameState.PLAYING:
|
||||||
if (!zoneManager.start()) {
|
if (this.teams.length < 3) {
|
||||||
|
secureAdminBroadcast("game_state", {state: this.state, startDate: this.startDate});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
trajectory.start();
|
trajectory.start();
|
||||||
|
zoneManager.start();
|
||||||
penaltyController.start();
|
penaltyController.start();
|
||||||
this.initLastSentLocations();
|
this.initLastSentLocations();
|
||||||
this.startDate = Date.now();
|
this.startDate = Date.now();
|
||||||
break;
|
break;
|
||||||
case GameState.FINISHED:
|
case GameState.FINISHED:
|
||||||
|
if (this.state != GameState.PLAYING) {
|
||||||
|
secureAdminBroadcast("game_state", {state: this.state, startDate: this.startDate});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
for (const team of this.teams) {
|
for (const team of this.teams) {
|
||||||
if (!team.finishDate) team.finishDate = Date.now();
|
if (!team.finishDate) team.finishDate = Date.now();
|
||||||
}
|
}
|
||||||
trajectory.stop();
|
trajectory.stop();
|
||||||
|
zoneManager.stop();
|
||||||
penaltyController.stop();
|
penaltyController.stop();
|
||||||
zoneManager.reset();
|
|
||||||
timeoutHandler.endAllSendPositionTimeout();
|
timeoutHandler.endAllSendPositionTimeout();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -379,28 +421,4 @@ export default {
|
|||||||
timeoutHandler.endSendPositionTimeout(teamId);
|
timeoutHandler.endSendPositionTimeout(teamId);
|
||||||
this.updateTeamChasing();
|
this.updateTeamChasing();
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Change the settings of the Zone manager
|
|
||||||
* The game should not be in PLAYING or FINISHED state
|
|
||||||
* @param {Object} newSettings The object containing the settings to be changed
|
|
||||||
* @returns false if failed
|
|
||||||
*/
|
|
||||||
setZoneSettings(newSettings) {
|
|
||||||
if ('min' in newSettings || 'max' in newSettings) {
|
|
||||||
const min = newSettings.min ?? zoneManager.zoneSettings.min;
|
|
||||||
const max = newSettings.max ?? zoneManager.zoneSettings.max;
|
|
||||||
// The end zone must be included in the start zone
|
|
||||||
if (!isInCircle(min.center, max.center, max.radius-min.radius)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
zoneManager.udpateSettings(newSettings);
|
|
||||||
if (this.state == GameState.PLAYING || this.state == GameState.FINISHED) {
|
|
||||||
if (!zoneManager.start()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
/**
|
|
||||||
* Convert a angle from degree to radian
|
|
||||||
* @param {Number} deg angle in degree
|
|
||||||
* @returns angle in radian
|
|
||||||
*/
|
|
||||||
function degToRad(deg) {
|
|
||||||
return deg * (Math.PI / 180);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compute the distance between two points givent their longitude and latitude
|
|
||||||
* @param {Object} pos1 The first position
|
|
||||||
* @param {Object} pos2 The second position
|
|
||||||
* @returns the distance between the two positions in meters
|
|
||||||
* @see https://gist.github.com/miguelmota/10076960
|
|
||||||
*/
|
|
||||||
export function getDistanceFromLatLon({ lat: lat1, lng: lon1 }, { lat: lat2, lng: lon2 }) {
|
|
||||||
var R = 6371; // Radius of the earth in km
|
|
||||||
var dLat = degToRad(lat2 - lat1);
|
|
||||||
var dLon = degToRad(lon2 - lon1);
|
|
||||||
var a =
|
|
||||||
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
|
||||||
Math.cos(degToRad(lat1)) * Math.cos(degToRad(lat2)) *
|
|
||||||
Math.sin(dLon / 2) * Math.sin(dLon / 2)
|
|
||||||
;
|
|
||||||
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
||||||
var d = R * c; // Distance in km
|
|
||||||
return d * 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a GPS point is in a circle
|
|
||||||
* @param {Object} position The position to check, an object with lat and lng fields
|
|
||||||
* @param {Object} center The center of the circle, an object with lat and lng fields
|
|
||||||
* @param {Number} radius The radius of the circle in meters
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function isInCircle(position, center, radius) {
|
|
||||||
return getDistanceFromLatLon(position, center) < radius;
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
/*
|
/*
|
||||||
This module manages the verification of the game rules and the penalties.
|
This module manages the verification of the game rules and the penalties.
|
||||||
*/
|
*/
|
||||||
import { isInCircle } from "./map_utils.js";
|
|
||||||
import { sendUpdatedTeamInformations, teamBroadcast } from "./team_socket.js";
|
import { sendUpdatedTeamInformations, teamBroadcast } from "./team_socket.js";
|
||||||
import { secureAdminBroadcast } from "./admin_socket.js";
|
import { secureAdminBroadcast } from "./admin_socket.js";
|
||||||
import game, { GameState } from "./game.js";
|
import game, { GameState } from "./game.js";
|
||||||
import zone from "./zone_manager.js";
|
import zoneManager from "./zone_manager.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
// Object mapping team id to the date they left the zone as a UNIX millisecond timestamp
|
// Object mapping team id to the date they left the zone as a UNIX millisecond timestamp
|
||||||
@@ -115,10 +114,10 @@ export default {
|
|||||||
game.teams.forEach((team) => {
|
game.teams.forEach((team) => {
|
||||||
if (team.captured) { return }
|
if (team.captured) { return }
|
||||||
//All the informations are not ready yet
|
//All the informations are not ready yet
|
||||||
if (team.currentLocation == null || zone.currentZone == null) {
|
if (team.currentLocation == null || !zoneManager.isRunning) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isInCircle({ lat: team.currentLocation[0], lng: team.currentLocation[1] }, zone.currentZone.center, zone.currentZone.radius)) {
|
if (!zoneManager.isInCircle({ lat: team.currentLocation[0], lng: team.currentLocation[1] })) {
|
||||||
//The team was not previously out of the zone
|
//The team was not previously out of the zone
|
||||||
if (!this.outOfBoundsSince[team.id]) {
|
if (!this.outOfBoundsSince[team.id]) {
|
||||||
this.outOfBoundsSince[team.id] = Date.now();
|
this.outOfBoundsSince[team.id] = Date.now();
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ This module also exposes functions to send messages via socket to all teams
|
|||||||
import { secureAdminBroadcast } from "./admin_socket.js";
|
import { secureAdminBroadcast } from "./admin_socket.js";
|
||||||
import { io } from "./index.js";
|
import { io } from "./index.js";
|
||||||
import game from "./game.js";
|
import game from "./game.js";
|
||||||
import zone from "./zone_manager.js";
|
import zoneManager from "./zone_manager.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a socket message to all the players of a team
|
* Send a socket message to all the players of a team
|
||||||
@@ -97,11 +97,10 @@ export function initTeamSocket() {
|
|||||||
socket.emit("login_response", true);
|
socket.emit("login_response", true);
|
||||||
socket.emit("game_state", game.state);
|
socket.emit("game_state", game.state);
|
||||||
socket.emit("game_settings", game.settings);
|
socket.emit("game_settings", game.settings);
|
||||||
socket.emit("zone", zone.currentZone);
|
socket.emit("zone", {
|
||||||
socket.emit("new_zone", {
|
begin: zoneManager.getCurrentZone(),
|
||||||
begin: zone.currentStartZone,
|
end: zoneManager.getNextZone(),
|
||||||
end: zone.nextZone,
|
endDate: zoneManager.currentZoneEndDate,
|
||||||
endDate: zone.nextZoneDate,
|
|
||||||
})
|
})
|
||||||
callback({ isLoggedIn : true, message: "Logged in"});
|
callback({ isLoggedIn : true, message: "Logged in"});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,237 +1,230 @@
|
|||||||
/*
|
|
||||||
This module manages the play area during the game, shrinking it over time based of some settings.
|
|
||||||
*/
|
|
||||||
import { randomCirclePoint } from 'random-location'
|
|
||||||
import { isInCircle } from './map_utils.js';
|
|
||||||
import { playersBroadcast } from './team_socket.js';
|
import { playersBroadcast } from './team_socket.js';
|
||||||
import { secureAdminBroadcast } from './admin_socket.js';
|
import { secureAdminBroadcast } from './admin_socket.js';
|
||||||
|
|
||||||
/**
|
|
||||||
* Scale a value that is known to be in a range to a new range
|
/* -------------------------------- Useful functions and constants -------------------------------- */
|
||||||
* for instance map(50,0,100,1000,2000) will return 1500 as 50 is halfway between 0 and 100 and 1500 is halfway through 1000 and 2000
|
|
||||||
* @param {Number} value value to map
|
const EARTH_RADIUS = 6_371_000; // Radius of the earth in m
|
||||||
* @param {Number} oldMin minimum value of the number
|
|
||||||
* @param {Number} oldMax maximum value of the number
|
function haversine_distance({ lat: lat1, lng: lon1 }, { lat: lat2, lng: lon2 }) {
|
||||||
* @param {Number} newMin minimum value of the output
|
const degToRad = (deg) => deg * (Math.PI / 180);
|
||||||
* @param {Number} newMax maximum value of the output
|
const dLat = degToRad(lat2 - lat1);
|
||||||
* @returns
|
const dLon = degToRad(lon2 - lon1);
|
||||||
*/
|
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(degToRad(lat1)) * Math.cos(degToRad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||||
function map(value, oldMin, oldMax, newMin, newMax) {
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||||
return ((value - oldMin) / (oldMax - oldMin)) * (newMax - newMin) + newMin;
|
return c * EARTH_RADIUS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function latlngEqual(latlng1, latlng2, epsilon = 1e-9) {
|
||||||
|
return Math.abs(latlng1.lat - latlng2.lat) < epsilon && Math.abs(latlng1.lng - latlng2.lng) < epsilon;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* -------------------------------- Polygon zones -------------------------------- */
|
||||||
|
|
||||||
|
const defaultPolygonSettings = { polygons: [], durations: [] };
|
||||||
|
|
||||||
|
function polygonZone(points, duration) {
|
||||||
|
return {
|
||||||
|
points: points,
|
||||||
|
duration: duration,
|
||||||
|
|
||||||
|
isInZone(location) {
|
||||||
|
const {lat: x, lng: y} = location;
|
||||||
|
let inside = false;
|
||||||
|
|
||||||
|
for (let i = 0, j = this.points.length - 1; i < this.points.length; j = i++) {
|
||||||
|
const {lat: xi, lng: yi} = this.points[i];
|
||||||
|
const {lat: xj, lng: yj} = this.points[j];
|
||||||
|
|
||||||
|
const intersects = ((yi > y) !== (yj > y)) && (x < ((xj - xi) * (y - yi)) / (yj - yi) + xi);
|
||||||
|
|
||||||
|
if (intersects) inside = !inside;
|
||||||
|
}
|
||||||
|
|
||||||
|
return inside;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergePolygons(poly1, poly2) {
|
||||||
|
// poly1 and poly2 are clockwise, not overlaping and touching polygons. If those two polygons were on a map, they would be
|
||||||
|
// one against each other, and the merge would make a new clockwise polygon out of the outer border of the two polygons.
|
||||||
|
// If it happens that poly1 and poly2 are not touching, poly1 would be returned untouched.
|
||||||
|
// Basically because polygons are clockwise, the alogorithm starts from a point A in poly1 not shared by poly2, and
|
||||||
|
// when a point is shared by poly1 and poly2, the algorithm continues in poly2, and so on until point A.
|
||||||
|
|
||||||
|
const getPointIndex = (point, array) => {
|
||||||
|
for (let i = 0; i < array.length; i++) {
|
||||||
|
if (latlngEqual(array[i], point)) return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the index of the first point of poly1 that doesn't belong to merge (it exists)
|
||||||
|
let i = 0;
|
||||||
|
while (getPointIndex(poly1[i], poly2) != -1) i++;
|
||||||
|
// Starting the merge from that point
|
||||||
|
const merge = [poly1[i]];
|
||||||
|
i = (i + 1) % poly1.length;
|
||||||
|
let currentArray = poly1;
|
||||||
|
let otherArray = poly2;
|
||||||
|
while (!latlngEqual(currentArray[i], merge[0])) {
|
||||||
|
const j = getPointIndex(currentArray[i], otherArray);
|
||||||
|
if (j != -1) {
|
||||||
|
[currentArray, otherArray] = [otherArray, currentArray];
|
||||||
|
i = j;
|
||||||
|
}
|
||||||
|
merge.push(currentArray[i]);
|
||||||
|
i = (i + 1) % currentArray.length;
|
||||||
|
}
|
||||||
|
return merge;
|
||||||
|
}
|
||||||
|
|
||||||
|
function polygonSettingsToZones(settings) {
|
||||||
|
const { polygons, durations } = settings;
|
||||||
|
const reversedPolygons = polygons.slice().reverse();
|
||||||
|
const reversedDurations = durations.slice().reverse();
|
||||||
|
|
||||||
|
const zones = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < reversedPolygons.length; i++) {
|
||||||
|
const polygon =reversedPolygons[i];
|
||||||
|
const duration = reversedDurations[i];
|
||||||
|
const length = zones.length;
|
||||||
|
|
||||||
|
if (length == 0) {
|
||||||
|
zones.push(polygonZone(
|
||||||
|
polygon,
|
||||||
|
duration
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
zones.push(polygonZone(
|
||||||
|
mergePolygons(zones[length-1].points, polygon),
|
||||||
|
duration
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return zones.slice().reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* -------------------------------- Circle zones -------------------------------- */
|
||||||
|
|
||||||
|
const defaultCircleSettings = { min: null, max: null, reductionCount: 4, duration: 1 };
|
||||||
|
|
||||||
|
function circleZone(center, radius, duration) {
|
||||||
|
return {
|
||||||
|
center: center,
|
||||||
|
radius: radius,
|
||||||
|
duration: duration,
|
||||||
|
|
||||||
|
isInZone(location) {
|
||||||
|
return haversine_distance(center, location) < this.radius;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function circleSettingsToZones(settings) {
|
||||||
|
const {min, max, reductionCount, duration} = settings;
|
||||||
|
if (haversine_distance(max.center, min.center) > max.radius - min.radius) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const zones = [circleZone(max.center, max.radius, duration)];
|
||||||
|
const radiusReductionLength = (max.radius - min.radius) / reductionCount;
|
||||||
|
let center = max.center;
|
||||||
|
let radius = max.radius;
|
||||||
|
for (let i = 1; i < reductionCount; i++) {
|
||||||
|
radius -= radiusReductionLength;
|
||||||
|
let new_center = null;
|
||||||
|
while (!new_center || haversine_distance(new_center, min.center) > radius - min.radius) {
|
||||||
|
const angle = Math.random() * 2 * Math.PI;
|
||||||
|
const angularDistance = Math.sqrt(Math.random()) * radiusReductionLength / EARTH_RADIUS;
|
||||||
|
const lat0Rad = center.lat * Math.PI / 180;
|
||||||
|
const lon0Rad = center.lng * Math.PI / 180;
|
||||||
|
const latRad = Math.asin(
|
||||||
|
Math.sin(lat0Rad) * Math.cos(angularDistance) +
|
||||||
|
Math.cos(lat0Rad) * Math.sin(angularDistance) * Math.cos(angle)
|
||||||
|
);
|
||||||
|
|
||||||
|
const lonRad = lon0Rad + Math.atan2(
|
||||||
|
Math.sin(angle) * Math.sin(angularDistance) * Math.cos(lat0Rad),
|
||||||
|
Math.cos(angularDistance) - Math.sin(lat0Rad) * Math.sin(latRad)
|
||||||
|
);
|
||||||
|
new_center = {lat: latRad * 180 / Math.PI, lng: lonRad * 180 / Math.PI};
|
||||||
|
}
|
||||||
|
center = new_center;
|
||||||
|
zones.push(circleZone(center, radius, duration))
|
||||||
|
}
|
||||||
|
zones.push(circleZone(min.center, min.radius, 0));
|
||||||
|
return zones;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* -------------------------------- Zone manager -------------------------------- */
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
//Setings storing where the zone will start, end and how it should evolve
|
isRunning: false,
|
||||||
//The zone will start by staying at its max value for reductionInterval minutes
|
zones: [], // A zone has to be connected space that doesn't contain an earth pole
|
||||||
//and then reduce during reductionDuration minutes, then wait again...
|
currentZone: { id: 0, timeoutId: null, endDate: null },
|
||||||
//The reduction factor is such that after reductionCount the zone will be the min zone
|
settings: defaultPolygonSettings,
|
||||||
zoneSettings: {
|
|
||||||
min: { center: null, radius: null },
|
|
||||||
max: { center: null, radius: null },
|
|
||||||
reductionCount: 2,
|
|
||||||
reductionDuration: 1,
|
|
||||||
reductionInterval: 1,
|
|
||||||
updateIntervalSeconds: 1,
|
|
||||||
},
|
|
||||||
|
|
||||||
nextZoneDecrement: null,
|
|
||||||
//Live location of the zone
|
|
||||||
currentZone: { center: null, radius: null },
|
|
||||||
|
|
||||||
//If the zone is shrinking, this is the target of the current shrinking
|
|
||||||
//If the zone is not shrinking, this will be the target of the next shrinking
|
|
||||||
nextZone: { center: null, radius: null },
|
|
||||||
|
|
||||||
//Zone at the begining of the shrinking
|
|
||||||
currentStartZone: { center: null, radius: null },
|
|
||||||
|
|
||||||
startDate: null,
|
|
||||||
started: false,
|
|
||||||
updateIntervalId: null,
|
|
||||||
nextZoneTimeoutId: null,
|
|
||||||
|
|
||||||
nextZoneDate: null,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test if a given configuration object is valid, i.e if all needed values are well defined
|
|
||||||
* @param {Object} settings Settings object describing a config of a zone manager
|
|
||||||
* @returns if the config is correct
|
|
||||||
*/
|
|
||||||
validateSettings(settings) {
|
|
||||||
if (settings.reductionCount && (typeof settings.reductionCount != "number" || settings.reductionCount <= 0)) { return false }
|
|
||||||
if (settings.reductionDuration && (typeof settings.reductionDuration != "number" || settings.reductionDuration < 0)) { return false }
|
|
||||||
if (settings.reductionInterval && (typeof settings.reductionInterval != "number" || settings.reductionInterval < 0)) { return false }
|
|
||||||
if (settings.updateIntervalSeconds && (typeof settings.updateIntervalSeconds != "number" || settings.updateIntervalSeconds <= 0)) { return false }
|
|
||||||
if (settings.max && (typeof settings.max.radius != "number" || typeof settings.max.center.lat != "number" || typeof settings.max.center.lng != "number")) { return false }
|
|
||||||
if (settings.min && (typeof settings.min.radius != "number" || typeof settings.min.center.lat != "number" || typeof settings.min.center.lng != "number")) { return false }
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Test if the zone manager is ready to start
|
|
||||||
* @returns true if the zone manager is ready to be started, false otherwise
|
|
||||||
*/
|
|
||||||
ready() {
|
|
||||||
return this.validateSettings(this.zoneSettings);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the settings of the zone, this can be done by passing an object containing the settings to change.
|
|
||||||
* Unless specified, the durations are in minutes
|
|
||||||
* Default config :
|
|
||||||
* `this.zoneSettings = {
|
|
||||||
* min: {center: null, radius: null},
|
|
||||||
* max: {center: null, radius: null},
|
|
||||||
* reductionCount: 2,
|
|
||||||
* reductionDuration: 10,
|
|
||||||
* reductionInterval: 10,
|
|
||||||
* updateIntervalSeconds: 10,
|
|
||||||
* }`
|
|
||||||
* @param {Object} newSettings The fields of the settings to udpate
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
udpateSettings(newSettings) {
|
|
||||||
//validate settings
|
|
||||||
this.zoneSettings = { ...this.zoneSettings, ...newSettings };
|
|
||||||
this.nextZoneDecrement = (this.zoneSettings.max.radius - this.zoneSettings.min.radius) / this.zoneSettings.reductionCount;
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reinitialize the object and stop all the tasks
|
|
||||||
*/
|
|
||||||
reset() {
|
|
||||||
this.currentZoneCount = 0;
|
|
||||||
this.started = false;
|
|
||||||
if (this.updateIntervalId != null) {
|
|
||||||
clearInterval(this.updateIntervalId);
|
|
||||||
this.updateIntervalId = null;
|
|
||||||
}
|
|
||||||
if (this.nextZoneTimeoutId != null) {
|
|
||||||
clearTimeout(this.nextZoneTimeoutId);
|
|
||||||
this.nextZoneTimeoutId = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start the zone reduction sequence
|
|
||||||
*/
|
|
||||||
start() {
|
start() {
|
||||||
if (!this.ready()) return false;
|
this.isRunning = true;
|
||||||
this.reset();
|
this.currentZone.id = -1;
|
||||||
this.started = true;
|
this.goNextZone();
|
||||||
this.startDate = new Date();
|
|
||||||
//initialize the zone to its max value
|
|
||||||
this.nextZone = JSON.parse(JSON.stringify(this.zoneSettings.max));
|
|
||||||
this.currentStartZone = JSON.parse(JSON.stringify(this.zoneSettings.max));
|
|
||||||
this.currentZone = JSON.parse(JSON.stringify(this.zoneSettings.max));
|
|
||||||
return this.setNextZone();
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
stop() {
|
||||||
* Get the center of the next zone, this center need to satisfy two properties
|
this.isRunning = false;
|
||||||
* - it needs to be included in the current zone, this means that this new point should lie in the circle of center currentZone.center and radius currentZone.radius - newRadius
|
clearTimeout(this.currentZone.timeoutId);
|
||||||
* - it needs to include the last zone, which means that the center must lie in the center of center min.center and of radius newRadius - min.radius
|
},
|
||||||
* @param newRadius the radius that the new zone will have
|
|
||||||
* @returns the coordinates of the new center as an object with lat and long fields
|
goNextZone() {
|
||||||
*/
|
this.currentZone.id++;
|
||||||
getRandomNextCenter(newRadius) {
|
if (this.currentZone.id >= this.zones.length) return;
|
||||||
let ok = false;
|
this.currentZone.timeoutId = setTimeout(() => this.goNextZone(), this.getCurrentZone().duration * 60 * 1000);
|
||||||
let res = null
|
this.currentZone.endDate = Date.now() + this.getCurrentZone().duration * 60 * 1000;
|
||||||
let tries = 0;
|
this.zoneBroadcast();
|
||||||
const MAX_TRIES = 100000
|
},
|
||||||
//take a random point satisfying both conditions
|
|
||||||
while (tries++ < MAX_TRIES && !ok) {
|
getCurrentZone() {
|
||||||
res = randomCirclePoint({ latitude: this.currentZone.center.lat, longitude: this.currentZone.center.lng }, this.currentZone.radius - newRadius);
|
return this.zones[this.currentZone.id];
|
||||||
ok = (isInCircle({ lat: res.latitude, lng: res.longitude }, this.zoneSettings.min.center, newRadius - this.zoneSettings.min.radius))
|
},
|
||||||
}
|
|
||||||
if (tries >= MAX_TRIES) {
|
getNextZone() {
|
||||||
return false;
|
if (this.currentZone.id + 1 < this.zones.length) {
|
||||||
}
|
return this.zones[this.currentZone.id + 1];
|
||||||
return {
|
} else {
|
||||||
lat: res.latitude,
|
return this.zones[this.currentZone.id];
|
||||||
lng: res.longitude
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
isInZone(location) {
|
||||||
* Compute the next zone satifying the given settings, update the nextZone and currentStartZone
|
if (this.zones.length == 0) {
|
||||||
* Wait for the appropriate duration before starting a new zone reduction if needed
|
return true;
|
||||||
*/
|
} else {
|
||||||
setNextZone() {
|
return this.getCurrentZone().isInZone(location);
|
||||||
this.nextZoneDate = Date.now() + this.zoneSettings.reductionInterval * 60 * 1000;
|
|
||||||
//At this point, nextZone == currentZone, we need to update the next zone, the raidus decrement, and start a timer before the next shrink
|
|
||||||
//last zone
|
|
||||||
if (this.currentZoneCount == this.zoneSettings.reductionCount) {
|
|
||||||
this.nextZone = JSON.parse(JSON.stringify(this.zoneSettings.min))
|
|
||||||
this.currentStartZone = JSON.parse(JSON.stringify(this.zoneSettings.min))
|
|
||||||
} else if (this.currentZoneCount == this.zoneSettings.reductionCount - 1) {
|
|
||||||
this.currentStartZone = JSON.parse(JSON.stringify(this.currentZone))
|
|
||||||
this.nextZone = JSON.parse(JSON.stringify(this.zoneSettings.min))
|
|
||||||
this.nextZoneTimeoutId = setTimeout(() => this.startShrinking(), 1000 * 60 * this.zoneSettings.reductionInterval)
|
|
||||||
this.currentZoneCount++;
|
|
||||||
} else if (this.currentZoneCount < this.zoneSettings.reductionCount) {
|
|
||||||
this.nextZone.center = this.getRandomNextCenter(this.nextZone.radius - this.nextZoneDecrement)
|
|
||||||
//Next center cannot be found
|
|
||||||
if (this.nextZone.center === false) {
|
|
||||||
console.log("no center")
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this.nextZone.radius -= this.nextZoneDecrement;
|
|
||||||
this.currentStartZone = JSON.parse(JSON.stringify(this.currentZone))
|
|
||||||
this.nextZoneTimeoutId = setTimeout(() => this.startShrinking(), 1000 * 60 * this.zoneSettings.reductionInterval)
|
|
||||||
this.currentZoneCount++;
|
|
||||||
}
|
}
|
||||||
this.onZoneUpdate(JSON.parse(JSON.stringify(this.currentStartZone)))
|
},
|
||||||
this.onNextZoneUpdate({
|
|
||||||
begin: JSON.parse(JSON.stringify(this.currentStartZone)),
|
changeSettings(settings) {
|
||||||
end: JSON.parse(JSON.stringify(this.nextZone)),
|
const zones = polygonSettingsToZones(settings);
|
||||||
endDate: JSON.parse(JSON.stringify(this.nextZoneDate)),
|
if (!zones) return false;
|
||||||
})
|
this.zones = zones;
|
||||||
|
this.settings = settings;
|
||||||
|
this.zoneBroadcast();
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
/*
|
zoneBroadcast() {
|
||||||
* Start a task that will run periodically updatinng the zone size, and calling the onZoneUpdate callback
|
const zone = {
|
||||||
* This will also periodically check if the reduction is over or not
|
begin: this.getCurrentZone(),
|
||||||
* If the reduction is over this function will call setNextZone
|
end: this.getNextZone(),
|
||||||
*/
|
endDate:this.currentZone.endDate,
|
||||||
startShrinking() {
|
};
|
||||||
this.nextZoneDate = Date.now() + this.zoneSettings.reductionDuration * 60 * 1000;
|
playersBroadcast("current_zone", zone);
|
||||||
this.onZoneUpdateStart(JSON.parse(JSON.stringify(this.nextZoneDate)));
|
secureAdminBroadcast("current_zone", zone);
|
||||||
const startTime = new Date();
|
|
||||||
this.updateIntervalId = setInterval(() => {
|
|
||||||
const completed = ((new Date() - startTime) / (1000 * 60)) / this.zoneSettings.reductionDuration;
|
|
||||||
this.currentZone.radius = map(completed, 0, 1, this.currentStartZone.radius, this.nextZone.radius)
|
|
||||||
this.currentZone.center.lat = map(completed, 0, 1, this.currentStartZone.center.lat, this.nextZone.center.lat)
|
|
||||||
this.currentZone.center.lng = map(completed, 0, 1, this.currentStartZone.center.lng, this.nextZone.center.lng)
|
|
||||||
this.onZoneUpdate(JSON.parse(JSON.stringify(this.currentZone)))
|
|
||||||
//Zone shrinking is over
|
|
||||||
if (completed >= 1) {
|
|
||||||
clearInterval(this.updateIntervalId);
|
|
||||||
this.updateIntervalId = null;
|
|
||||||
this.setNextZone();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}, this.zoneSettings.updateIntervalSeconds * 1000);
|
|
||||||
},
|
},
|
||||||
|
}
|
||||||
//a call to onNextZoneUpdate will be made when the zone reduction ends and a new next zone is announced
|
|
||||||
onNextZoneUpdate(newZone) {
|
|
||||||
playersBroadcast("new_zone", newZone)
|
|
||||||
secureAdminBroadcast("new_zone", newZone)
|
|
||||||
},
|
|
||||||
|
|
||||||
//a call to onZoneUpdateStart will be made when the zone reduction starts
|
|
||||||
onZoneUpdateStart(date) {
|
|
||||||
playersBroadcast("zone_start", date)
|
|
||||||
secureAdminBroadcast("zone_start", date)
|
|
||||||
},
|
|
||||||
|
|
||||||
//a call to onZoneUpdate will be made every updateIntervalSeconds when the zone is changing
|
|
||||||
onZoneUpdate(zone) {
|
|
||||||
playersBroadcast("zone", zone)
|
|
||||||
secureAdminBroadcast("zone", zone)
|
|
||||||
},
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { AdminConnexionProvider} from "@/context/adminConnexionContext";
|
import { AdminConnexionProvider } from "@/context/adminConnexionContext";
|
||||||
import { AdminProvider } from "@/context/adminContext";
|
import { AdminProvider } from "@/context/adminContext";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
export default function AdminLayout({ children}) {
|
export default function AdminLayout({ children }) {
|
||||||
return (
|
return (
|
||||||
<AdminConnexionProvider>
|
<AdminConnexionProvider>
|
||||||
<AdminProvider>
|
<AdminProvider>
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { TeamReady } from "@/components/admin/teamReady";
|
import TeamReady from "@/components/admin/teamReady";
|
||||||
import BlueButton, { GreenButton, RedButton } from "@/components/util/button";
|
import { BlueButton, GreenButton, RedButton } from "@/components/util/button";
|
||||||
import { useAdminConnexion } from "@/context/adminConnexionContext";
|
import { useAdminConnexion } from "@/context/adminConnexionContext";
|
||||||
import useAdmin from "@/hook/useAdmin";
|
import useAdmin from "@/hook/useAdmin";
|
||||||
import { GameState } from "@/util/gameState";
|
import { GameState } from "@/util/gameState";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { TeamListFixed } from '@/components/admin/teamList';
|
|
||||||
|
|
||||||
const LiveMap = dynamic(() => import('@/components/admin/mapPicker').then((mod) => mod.LiveMap), {
|
// Imported at runtime and not at compile time
|
||||||
ssr: false
|
const LiveMap = dynamic(() => import('@/components/admin/liveMap'), { ssr: false });
|
||||||
});
|
|
||||||
export default function AdminPage() {
|
export default function AdminPage() {
|
||||||
const { useProtect } = useAdminConnexion();
|
const { useProtect } = useAdminConnexion();
|
||||||
const { gameState, changeState } = useAdmin();
|
const { gameState, changeState } = useAdmin();
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { GameSettings } from "@/components/admin/gameSettings";
|
import GameSettings from "@/components/admin/gameSettings";
|
||||||
import { PenaltySettings } from "@/components/admin/penaltySettings";
|
import PenaltySettings from "@/components/admin/penaltySettings";
|
||||||
import { useAdminConnexion } from "@/context/adminConnexionContext";
|
import { useAdminConnexion } from "@/context/adminConnexionContext";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
const ZoneSelector = dynamic(() => import('@/components/admin/zoneSelector').then((mod) => mod.ZoneSelector), {
|
// Imported at runtime and not at compile time
|
||||||
ssr: false
|
const ZoneSelector = dynamic(() => import('@/components/admin/polygonZoneMap'), { ssr: false });
|
||||||
});
|
|
||||||
export default function AdminPage() {
|
export default function AdminPage() {
|
||||||
const { useProtect } = useAdminConnexion();
|
const { useProtect } = useAdminConnexion();
|
||||||
useProtect();
|
useProtect();
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import ActionDrawer from '@/components/team/actionDrawer';
|
import ActionDrawer from '@/components/team/actionDrawer';
|
||||||
import { Notification } from '@/components/team/notification';
|
import Notification from '@/components/team/notification';
|
||||||
import PlacementOverlay from '@/components/team/placementOverlay';
|
import PlacementOverlay from '@/components/team/placementOverlay';
|
||||||
import { WaitingScreen } from '@/components/team/waitingScreen';
|
import WaitingScreen from '@/components/team/waitingScreen';
|
||||||
import { LogoutButton } from '@/components/util/button';
|
import { LogoutButton } from '@/components/util/button';
|
||||||
import { useSocket } from '@/context/socketContext';
|
import { useSocket } from '@/context/socketContext';
|
||||||
import { useTeamConnexion } from '@/context/teamConnexionContext';
|
import { useTeamConnexion } from '@/context/teamConnexionContext';
|
||||||
|
|||||||
110
traque-front/components/admin/circleZoneMap.jsx
Normal file
110
traque-front/components/admin/circleZoneMap.jsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { BlueButton, GreenButton, RedButton } from "../util/button";
|
||||||
|
import { TextInput } from "../util/textInput";
|
||||||
|
import useAdmin from "@/hook/useAdmin";
|
||||||
|
import useLocation from "@/hook/useLocation";
|
||||||
|
import "leaflet/dist/leaflet.css";
|
||||||
|
import { Circle, MapContainer, TileLayer } from "react-leaflet";
|
||||||
|
import useMapCircleDraw from "@/hook/useMapCircleDraw";
|
||||||
|
import { MapPan, MapEventListener } from "./mapUtils";
|
||||||
|
|
||||||
|
const DEFAULT_ZOOM = 14;
|
||||||
|
const EditMode = {
|
||||||
|
MIN: 0,
|
||||||
|
MAX: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function CircleDrawings({ minZone, setMinZone, maxZone, setMaxZone, editMode }) {
|
||||||
|
const { handleClick: maxClick, handleMouseMove: maxHover, center: maxCenter, radius: maxRadius } = useMapCircleDraw(minZone, setMinZone);
|
||||||
|
const { handleClick: minClick, handleMouseMove: minHover, center: minCenter, radius: minRadius } = useMapCircleDraw(maxZone, setMaxZone);
|
||||||
|
|
||||||
|
function handleLeftClick(e) {
|
||||||
|
if (editMode == EditMode.MAX) {
|
||||||
|
maxClick(e);
|
||||||
|
} else {
|
||||||
|
minClick(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseMove(e) {
|
||||||
|
if (editMode == EditMode.MAX) {
|
||||||
|
maxHover(e);
|
||||||
|
} else {
|
||||||
|
minHover(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{minCenter && minRadius && <Circle center={minCenter} radius={minRadius} color="blue" fillColor="blue" />}
|
||||||
|
{maxCenter && maxRadius && <Circle center={maxCenter} radius={maxRadius} color="red" fillColor="red" />}
|
||||||
|
<MapEventListener onLeftClick={handleLeftClick} onRightClick={() => {}} onMouseMove={handleMouseMove} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CircleZonePicker({ minZone, maxZone, editMode, setMinZone, setMaxZone, ...props }) {
|
||||||
|
const location = useLocation(Infinity);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='h-96'>
|
||||||
|
<MapContainer {...props} className='min-h-full w-full' center={location} zoom={DEFAULT_ZOOM} scrollWheelZoom={true}>
|
||||||
|
<TileLayer
|
||||||
|
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||||
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
|
/>
|
||||||
|
<MapPan center={location} zoom={DEFAULT_ZOOM} />
|
||||||
|
<CircleDrawings minZone={minZone} maxZone={maxZone} editMode={editMode} setMinZone={setMinZone} setMaxZone={setMaxZone} />
|
||||||
|
</MapContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CircleZoneMap() {
|
||||||
|
const [editMode, setEditMode] = useState(EditMode.MIN);
|
||||||
|
const [minZone, setMinZone] = useState(null);
|
||||||
|
const [maxZone, setMaxZone] = useState(null);
|
||||||
|
const [reductionCount, setReductionCount] = useState("");
|
||||||
|
const [duration, setDuration] = useState("");
|
||||||
|
const {zoneSettings, changeZoneSettings} = useAdmin();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (zoneSettings) {
|
||||||
|
setMinZone(zoneSettings.min);
|
||||||
|
setMaxZone(zoneSettings.max);
|
||||||
|
setReductionCount(zoneSettings.reductionCount.toString());
|
||||||
|
setDuration(zoneSettings.duration.toString());
|
||||||
|
}
|
||||||
|
}, [zoneSettings]);
|
||||||
|
|
||||||
|
function handleSettingsSubmit() {
|
||||||
|
const newSettings = {min:minZone, max:maxZone, reductionCount: Number(reductionCount), duration: Number(duration)};
|
||||||
|
changeZoneSettings(newSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the user set one zone, switch to the other
|
||||||
|
useEffect(() => {
|
||||||
|
if(editMode == EditMode.MIN) {
|
||||||
|
setEditMode(EditMode.MAX);
|
||||||
|
}else {
|
||||||
|
setEditMode(EditMode.MIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [minZone, maxZone]);
|
||||||
|
|
||||||
|
return <div className='w-2/5 h-full gap-1 bg-white p-10 flex flex-col text-center shadow-2xl overflow-y-scroll'>
|
||||||
|
<h2 className="text-2xl">Edit zones</h2>
|
||||||
|
{editMode == EditMode.MIN && <BlueButton onClick={() => setEditMode(EditMode.MAX)}>Click to edit first zone</BlueButton>}
|
||||||
|
{editMode == EditMode.MAX && <RedButton onClick={() => setEditMode(EditMode.MIN)}>Click to edit last zone</RedButton>}
|
||||||
|
<CircleZonePicker minZone={minZone} maxZone={maxZone} editMode={editMode} setMinZone={setMinZone} setMaxZone={setMaxZone} />
|
||||||
|
<div>
|
||||||
|
<p>Number of zones</p>
|
||||||
|
<TextInput value={reductionCount} onChange={(e) => setReductionCount(e.target.value)}></TextInput>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p>Duration of a zone</p>
|
||||||
|
<TextInput value={duration} onChange={(e) => setDuration(e.target.value)}></TextInput>
|
||||||
|
</div>
|
||||||
|
<GreenButton onClick={handleSettingsSubmit}>Apply</GreenButton>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ import { TextArea } from "../util/textInput";
|
|||||||
import { GreenButton } from "../util/button";
|
import { GreenButton } from "../util/button";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export const GameSettings = () => {
|
export default function GameSettings() {
|
||||||
const {gameSettings, changeGameSettings} = useAdmin();
|
const {gameSettings, changeGameSettings} = useAdmin();
|
||||||
const [capturedMessage, setCapturedMessage] = useState("");
|
const [capturedMessage, setCapturedMessage] = useState("");
|
||||||
const [winnerEndMessage, setWinnerEndMessage] = useState("");
|
const [winnerEndMessage, setWinnerEndMessage] = useState("");
|
||||||
@@ -11,7 +11,6 @@ export const GameSettings = () => {
|
|||||||
const [waitingMessage, setWaitingMessage] = useState("");
|
const [waitingMessage, setWaitingMessage] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log({gameSettings})
|
|
||||||
if (gameSettings) {
|
if (gameSettings) {
|
||||||
setCapturedMessage(gameSettings.capturedMessage);
|
setCapturedMessage(gameSettings.capturedMessage);
|
||||||
setWinnerEndMessage(gameSettings.winnerEndGameMessage);
|
setWinnerEndMessage(gameSettings.winnerEndGameMessage);
|
||||||
@@ -46,4 +45,4 @@ export const GameSettings = () => {
|
|||||||
<GreenButton onClick={applySettings}>Apply</GreenButton>
|
<GreenButton onClick={applySettings}>Apply</GreenButton>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
75
traque-front/components/admin/liveMap.jsx
Normal file
75
traque-front/components/admin/liveMap.jsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import useLocation from "@/hook/useLocation";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import "leaflet/dist/leaflet.css";
|
||||||
|
import { MapContainer, Marker, TileLayer, Tooltip, Polyline, Polygon } from "react-leaflet";
|
||||||
|
import useAdmin from "@/hook/useAdmin";
|
||||||
|
import { GameState } from "@/util/gameState";
|
||||||
|
import { MapPan } from "./mapUtils";
|
||||||
|
|
||||||
|
const DEFAULT_ZOOM = 14;
|
||||||
|
const positionIcon = new L.Icon({
|
||||||
|
iconUrl: '/icons/location.png',
|
||||||
|
iconSize: [30, 30],
|
||||||
|
iconAnchor: [15, 15],
|
||||||
|
popupAnchor: [0, -15],
|
||||||
|
shadowSize: [30, 30],
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function LiveMap() {
|
||||||
|
const location = useLocation(Infinity);
|
||||||
|
const [timeLeftNextZone, setTimeLeftNextZone] = useState(null);
|
||||||
|
const { zoneExtremities, teams, nextZoneDate, getTeam, gameState } = useAdmin();
|
||||||
|
|
||||||
|
// Remaining time before sending position
|
||||||
|
useEffect(() => {
|
||||||
|
if (nextZoneDate) {
|
||||||
|
const updateTime = () => {
|
||||||
|
setTimeLeftNextZone(Math.max(0, Math.floor((nextZoneDate - Date.now()) / 1000)));
|
||||||
|
};
|
||||||
|
|
||||||
|
updateTime();
|
||||||
|
const interval = setInterval(updateTime, 1000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}
|
||||||
|
}, [nextZoneDate]);
|
||||||
|
|
||||||
|
function formatTime(time) {
|
||||||
|
// time is in seconds
|
||||||
|
if (time < 0) return "00:00";
|
||||||
|
const minutes = Math.floor(time / 60);
|
||||||
|
const seconds = Math.floor(time % 60);
|
||||||
|
return String(minutes).padStart(2,"0") + ":" + String(seconds).padStart(2,"0");
|
||||||
|
}
|
||||||
|
|
||||||
|
function Arrow({pos1, pos2}) {
|
||||||
|
if (pos1 && pos2) {
|
||||||
|
return (
|
||||||
|
<Polyline positions={[pos1, pos2]} pathOptions={{ color: 'black', weight: 3 }}/>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='min-h-full w-full'>
|
||||||
|
{gameState == GameState.PLAYING && timeLeftNextZone && <p>{`Next zone in : ${formatTime(timeLeftNextZone)}`}</p>}
|
||||||
|
<MapContainer className='min-h-full w-full' center={location} zoom={DEFAULT_ZOOM} scrollWheelZoom={true}>
|
||||||
|
<TileLayer
|
||||||
|
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||||
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
|
/>
|
||||||
|
<MapPan center={location} zoom={DEFAULT_ZOOM} />
|
||||||
|
{gameState == GameState.PLAYING && zoneExtremities.begin && <Polygon positions={zoneExtremities.begin.points} pathOptions={{ color: 'blue', fillColor: 'blue', fillOpacity: '0.2', weight: 3 }} />}
|
||||||
|
{gameState == GameState.PLAYING && zoneExtremities.end && <Polygon positions={zoneExtremities.end.points} pathOptions={{ color: 'red', fillColor: 'red', fillOpacity: '0', weight: 3 }} />}
|
||||||
|
{teams.map((team) => team.currentLocation && !team.captured &&
|
||||||
|
<Marker key={team.id} position={team.currentLocation} icon={positionIcon}>
|
||||||
|
<Tooltip permanent direction="top" offset={[0, -5]} className="custom-tooltip">{team.name}</Tooltip>
|
||||||
|
<Arrow pos1={team.currentLocation} pos2={getTeam(team.chasing).currentLocation}/>
|
||||||
|
</Marker>
|
||||||
|
)}
|
||||||
|
</MapContainer>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import { useLocation } from "@/hook/useLocation";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import "leaflet/dist/leaflet.css";
|
|
||||||
import { Circle, MapContainer, Marker, TileLayer, useMap, Tooltip, Polyline } from "react-leaflet";
|
|
||||||
import { useMapCircleDraw } from "@/hook/mapDrawing";
|
|
||||||
import useAdmin from "@/hook/useAdmin";
|
|
||||||
import { GameState } from "@/util/gameState";
|
|
||||||
|
|
||||||
const positionIcon = new L.Icon({
|
|
||||||
iconUrl: '/icons/location.png',
|
|
||||||
iconSize: [30, 30],
|
|
||||||
iconAnchor: [15, 15],
|
|
||||||
popupAnchor: [0, -15],
|
|
||||||
shadowSize: [30, 30],
|
|
||||||
});
|
|
||||||
|
|
||||||
function MapPan(props) {
|
|
||||||
const map = useMap();
|
|
||||||
const [initialized, setInitialized] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!initialized && props.center) {
|
|
||||||
map.flyTo(props.center, props.zoom, { animate: false });
|
|
||||||
setInitialized(true)
|
|
||||||
}
|
|
||||||
}, [props.center]);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function MapEventListener({ onClick, onMouseMove }) {
|
|
||||||
const map = useMap();
|
|
||||||
useEffect(() => {
|
|
||||||
map.on('click', onClick);
|
|
||||||
return () => {
|
|
||||||
map.off('click', onClick);
|
|
||||||
}
|
|
||||||
}, [onClick]);
|
|
||||||
useEffect(() => {
|
|
||||||
map.on('mousemove', onMouseMove);
|
|
||||||
return () => {
|
|
||||||
map.off('mousemove', onMouseMove);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_ZOOM = 14;
|
|
||||||
export function CircularAreaPicker({ area, setArea, markerPosition, ...props }) {
|
|
||||||
const location = useLocation(Infinity);
|
|
||||||
const { handleClick, handleMouseMove, center, radius } = useMapCircleDraw(area, setArea);
|
|
||||||
return (
|
|
||||||
<MapContainer {...props} className='min-h-full w-full ' center={location} zoom={DEFAULT_ZOOM} scrollWheelZoom={true}>
|
|
||||||
<TileLayer
|
|
||||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
|
||||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
|
||||||
/>
|
|
||||||
{center && radius && <Circle center={center} radius={radius} fillColor="blue" />}
|
|
||||||
{markerPosition && <Marker position={markerPosition} icon={positionIcon}>
|
|
||||||
</Marker>}
|
|
||||||
<MapPan center={location} zoom={DEFAULT_ZOOM} />
|
|
||||||
<MapEventListener onClick={handleClick} onMouseMove={handleMouseMove} />
|
|
||||||
</MapContainer>)
|
|
||||||
}
|
|
||||||
export const EditMode = {
|
|
||||||
MIN: 0,
|
|
||||||
MAX: 1
|
|
||||||
}
|
|
||||||
export function ZonePicker({ minZone, setMinZone, maxZone, setMaxZone, editMode, ...props }) {
|
|
||||||
const location = useLocation(Infinity);
|
|
||||||
const { handleClick: maxClick, handleMouseMove: maxHover, center: maxCenter, radius: maxRadius } = useMapCircleDraw(minZone, setMinZone);
|
|
||||||
const { handleClick: minClick, handleMouseMove: minHover, center: minCenter, radius: minRadius } = useMapCircleDraw(maxZone, setMaxZone);
|
|
||||||
function handleClick(e) {
|
|
||||||
if (editMode == EditMode.MAX) {
|
|
||||||
maxClick(e);
|
|
||||||
} else {
|
|
||||||
minClick(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function handleMouseMove(e) {
|
|
||||||
if (editMode == EditMode.MAX) {
|
|
||||||
maxHover(e);
|
|
||||||
} else {
|
|
||||||
minHover(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className='h-96'>
|
|
||||||
<MapContainer {...props} className='min-h-full w-full ' center={location} zoom={DEFAULT_ZOOM} scrollWheelZoom={true}>
|
|
||||||
<TileLayer
|
|
||||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
|
||||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
|
||||||
/>
|
|
||||||
{minCenter && minRadius && <Circle center={minCenter} radius={minRadius} color="blue" fillColor="blue" />}
|
|
||||||
{maxCenter && maxRadius && <Circle center={maxCenter} radius={maxRadius} color="red" fillColor="red" />}
|
|
||||||
<MapPan center={location} zoom={DEFAULT_ZOOM} />
|
|
||||||
<MapEventListener onClick={handleClick} onMouseMove={handleMouseMove} />
|
|
||||||
</MapContainer>
|
|
||||||
</div>
|
|
||||||
{ maxCenter && minCenter && typeof maxCenter.distanceTo === 'function'
|
|
||||||
&& maxRadius + maxCenter.distanceTo(minCenter) >= minRadius
|
|
||||||
&& <p className="text-red-500">La zone de fin doit être incluse dans celle de départ</p>}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function LiveMap() {
|
|
||||||
const location = useLocation(Infinity);
|
|
||||||
const [timeLeftNextZone, setTimeLeftNextZone] = useState(null);
|
|
||||||
const { zone, zoneExtremities, teams, nextZoneDate, isShrinking , getTeam, gameState } = useAdmin();
|
|
||||||
|
|
||||||
// Remaining time before sending position
|
|
||||||
useEffect(() => {
|
|
||||||
const updateTime = () => {
|
|
||||||
setTimeLeftNextZone(Math.max(0, Math.floor((nextZoneDate - Date.now()) / 1000)));
|
|
||||||
};
|
|
||||||
|
|
||||||
updateTime();
|
|
||||||
const interval = setInterval(updateTime, 1000);
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, [nextZoneDate]);
|
|
||||||
|
|
||||||
function formatTime(time) {
|
|
||||||
// time is in seconds
|
|
||||||
if (time < 0) return "00:00";
|
|
||||||
const minutes = Math.floor(time / 60);
|
|
||||||
const seconds = Math.floor(time % 60);
|
|
||||||
return String(minutes).padStart(2,"0") + ":" + String(seconds).padStart(2,"0");
|
|
||||||
}
|
|
||||||
|
|
||||||
function Arrow({pos1, pos2}) {
|
|
||||||
if (pos1 && pos2) {
|
|
||||||
return (
|
|
||||||
<Polyline positions={[pos1, pos2]} pathOptions={{ color: 'black', weight: 3 }}/>
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='min-h-full w-full'>
|
|
||||||
{gameState == GameState.PLAYING && <p>{`${isShrinking ? "Fin" : "Début"} du rétrécissement de la zone dans : ${formatTime(timeLeftNextZone)}`}</p>}
|
|
||||||
<MapContainer className='min-h-full w-full' center={location} zoom={DEFAULT_ZOOM} scrollWheelZoom={true}>
|
|
||||||
<TileLayer
|
|
||||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
|
||||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
|
||||||
/>
|
|
||||||
<MapPan center={location} zoom={DEFAULT_ZOOM} />
|
|
||||||
{gameState == GameState.PLAYING && zone && <Circle center={zone.center} radius={zone.radius} color="blue" />}
|
|
||||||
{gameState == GameState.PLAYING && zoneExtremities && <Circle center={zoneExtremities.begin.center} radius={zoneExtremities.begin.radius} color='black' fill={false} />}
|
|
||||||
{gameState == GameState.PLAYING && zoneExtremities && <Circle center={zoneExtremities.end.center} radius={zoneExtremities.end.radius} color='red' fill={false} />}
|
|
||||||
{teams.map((team) => team.currentLocation && !team.captured &&
|
|
||||||
<Marker key={team.id} position={team.currentLocation} icon={positionIcon}>
|
|
||||||
<Tooltip permanent direction="top" offset={[0, -5]} className="custom-tooltip">{team.name}</Tooltip>
|
|
||||||
<Arrow pos1={team.currentLocation} pos2={getTeam(team.chasing).currentLocation}/>
|
|
||||||
</Marker>
|
|
||||||
)}
|
|
||||||
</MapContainer>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
88
traque-front/components/admin/mapUtils.jsx
Normal file
88
traque-front/components/admin/mapUtils.jsx
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import "leaflet/dist/leaflet.css";
|
||||||
|
import { useMap } from "react-leaflet";
|
||||||
|
|
||||||
|
export function MapPan(props) {
|
||||||
|
const map = useMap();
|
||||||
|
const [initialized, setInitialized] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!initialized && props.center) {
|
||||||
|
map.flyTo(props.center, props.zoom, { animate: false });
|
||||||
|
setInitialized(true)
|
||||||
|
}
|
||||||
|
}, [props.center]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MapEventListener({ onLeftClick, onRightClick, onMouseMove }) {
|
||||||
|
const map = useMap();
|
||||||
|
|
||||||
|
// Handle the mouse click left
|
||||||
|
useEffect(() => {
|
||||||
|
let moved = false;
|
||||||
|
let downButton = null;
|
||||||
|
|
||||||
|
const handleMouseDown = (e) => {
|
||||||
|
moved = false;
|
||||||
|
downButton = e.originalEvent.button;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseMove = () => {
|
||||||
|
moved = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = (e) => {
|
||||||
|
if (!moved) {
|
||||||
|
if (downButton == 0) {
|
||||||
|
onLeftClick(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
downButton = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
map.on('mousedown', handleMouseDown);
|
||||||
|
map.on('mousemove', handleMouseMove);
|
||||||
|
map.on('mouseup', handleMouseUp);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
map.off('mousedown', handleMouseDown);
|
||||||
|
map.off('mousemove', handleMouseMove);
|
||||||
|
map.off('mouseup', handleMouseUp);
|
||||||
|
};
|
||||||
|
}, [onLeftClick, onRightClick]);
|
||||||
|
|
||||||
|
// Handle the right click
|
||||||
|
useEffect(() => {
|
||||||
|
|
||||||
|
const handleMouseDown = (e) => {
|
||||||
|
if (e.originalEvent.button == 2) {
|
||||||
|
onRightClick(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
map.on('mousedown', handleMouseDown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
map.off('mousedown', handleMouseDown);
|
||||||
|
}
|
||||||
|
}, [onRightClick]);
|
||||||
|
|
||||||
|
// Handle the mouse move
|
||||||
|
useEffect(() => {
|
||||||
|
map.on('mousemove', onMouseMove);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
map.off('mousemove', onMouseMove);
|
||||||
|
}
|
||||||
|
}, [onMouseMove]);
|
||||||
|
|
||||||
|
// Prevent right click context menu
|
||||||
|
useEffect(() => {
|
||||||
|
const container = map.getContainer();
|
||||||
|
const preventContextMenu = (e) => e.preventDefault();
|
||||||
|
container.addEventListener('contextmenu', preventContextMenu);
|
||||||
|
return () => container.removeEventListener('contextmenu', preventContextMenu);
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import useAdmin from "@/hook/useAdmin";
|
import useAdmin from "@/hook/useAdmin";
|
||||||
import TextInput from "../util/textInput";
|
import { TextInput } from "../util/textInput";
|
||||||
import { GreenButton } from "../util/button";
|
import { GreenButton } from "../util/button";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export const PenaltySettings = () => {
|
export default function PenaltySettings() {
|
||||||
const {penaltySettings, changePenaltySettings} = useAdmin();
|
const {penaltySettings, changePenaltySettings} = useAdmin();
|
||||||
const [maxPenalties, setMaxPenalties] = useState("");
|
const [maxPenalties, setMaxPenalties] = useState("");
|
||||||
const [allowedTimeOutOfZone, setAllowedTimeOutOfZone] = useState("");
|
const [allowedTimeOutOfZone, setAllowedTimeOutOfZone] = useState("");
|
||||||
@@ -46,4 +46,4 @@ export const PenaltySettings = () => {
|
|||||||
<GreenButton onClick={applySettings}>Apply</GreenButton>
|
<GreenButton onClick={applySettings}>Apply</GreenButton>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
33
traque-front/components/admin/placementMap.jsx
Normal file
33
traque-front/components/admin/placementMap.jsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import useLocation from "@/hook/useLocation";
|
||||||
|
import "leaflet/dist/leaflet.css";
|
||||||
|
import { Circle, MapContainer, Marker, TileLayer } from "react-leaflet";
|
||||||
|
import useMapCircleDraw from "@/hook/useMapCircleDraw";
|
||||||
|
import { MapPan, MapEventListener } from "./mapUtils";
|
||||||
|
|
||||||
|
const DEFAULT_ZOOM = 14;
|
||||||
|
const positionIcon = new L.Icon({
|
||||||
|
iconUrl: '/icons/location.png',
|
||||||
|
iconSize: [30, 30],
|
||||||
|
iconAnchor: [15, 15],
|
||||||
|
popupAnchor: [0, -15],
|
||||||
|
shadowSize: [30, 30],
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function CircularAreaPicker({ area, setArea, markerPosition, ...props }) {
|
||||||
|
const location = useLocation(Infinity);
|
||||||
|
const { handleClick, handleMouseMove, center, radius } = useMapCircleDraw(area, setArea);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MapContainer {...props} className='min-h-full w-full ' center={location} zoom={DEFAULT_ZOOM} scrollWheelZoom={true}>
|
||||||
|
<TileLayer
|
||||||
|
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||||
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
|
/>
|
||||||
|
{center && radius && <Circle center={center} radius={radius} fillColor="blue" />}
|
||||||
|
{markerPosition && <Marker position={markerPosition} icon={positionIcon}>
|
||||||
|
</Marker>}
|
||||||
|
<MapPan center={location} zoom={DEFAULT_ZOOM} />
|
||||||
|
<MapEventListener onLeftClick={handleClick} onRightClick={() => {}} onMouseMove={handleMouseMove} />
|
||||||
|
</MapContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
142
traque-front/components/admin/polygonZoneMap.jsx
Normal file
142
traque-front/components/admin/polygonZoneMap.jsx
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { GreenButton } from "../util/button";
|
||||||
|
import { TextInput } from "../util/textInput";
|
||||||
|
import useAdmin from "@/hook/useAdmin";
|
||||||
|
import useLocation from "@/hook/useLocation";
|
||||||
|
import "leaflet/dist/leaflet.css";
|
||||||
|
import { MapContainer, TileLayer, Polyline, Polygon, CircleMarker } from "react-leaflet";
|
||||||
|
import useMapPolygonDraw from "@/hook/useMapPolygonDraw";
|
||||||
|
import { MapPan, MapEventListener } from "./mapUtils";
|
||||||
|
|
||||||
|
const DEFAULT_ZOOM = 14;
|
||||||
|
|
||||||
|
function PolygonDrawings({ polygons, addPolygon, removePolygon }) {
|
||||||
|
const { currentPolygon, highlightNodes, handleLeftClick, handleRightClick, handleMouseMove } = useMapPolygonDraw(polygons, addPolygon, removePolygon);
|
||||||
|
const nodeSize = 5; // px
|
||||||
|
const lineThickness = 3; // px
|
||||||
|
|
||||||
|
function DrawNode({pos, color}) {
|
||||||
|
return (
|
||||||
|
<CircleMarker center={pos} radius={nodeSize} pathOptions={{ color: color, fillColor: color, fillOpacity: 1 }} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawLine({pos1, pos2, color}) {
|
||||||
|
return (
|
||||||
|
<Polyline positions={[pos1, pos2]} pathOptions={{ color: color, weight: lineThickness }} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawUnfinishedPolygon({polygon}) {
|
||||||
|
const length = polygon.length;
|
||||||
|
if (length > 0) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<DrawNode pos={polygon[0]} color={"red"} zIndexOffset={1000} />
|
||||||
|
{polygon.map((_, i) => {
|
||||||
|
if (i < length-1) {
|
||||||
|
return <DrawLine key={i} pos1={polygon[i]} pos2={polygon[i+1]} color={"red"} />;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawPolygon({polygon}) {
|
||||||
|
const length = polygon.length;
|
||||||
|
|
||||||
|
if (length > 2) {
|
||||||
|
return (
|
||||||
|
<Polygon positions={polygon} pathOptions={{ color: 'black', fillColor: 'black', fillOpacity: '0.5', weight: lineThickness }} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<MapEventListener onLeftClick={handleLeftClick} onRightClick={handleRightClick} onMouseMove={handleMouseMove} />
|
||||||
|
{polygons.map((polygon, i) => <DrawPolygon key={i} polygon={polygon} />)}
|
||||||
|
<DrawUnfinishedPolygon polygon={currentPolygon} />
|
||||||
|
{highlightNodes.map((node, i) => <DrawNode key={i} pos={node} color={"black"} />)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PolygonZonePicker({ polygons, addPolygon, removePolygon, ...props }) {
|
||||||
|
const location = useLocation(Infinity);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='h-96'>
|
||||||
|
<MapContainer {...props} className='min-h-full w-full' center={location} zoom={DEFAULT_ZOOM} scrollWheelZoom={true}>
|
||||||
|
<TileLayer
|
||||||
|
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||||
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
|
/>
|
||||||
|
<MapPan center={location} zoom={DEFAULT_ZOOM} />
|
||||||
|
<PolygonDrawings polygons={polygons} addPolygon={addPolygon} removePolygon={removePolygon} />
|
||||||
|
</MapContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PolygonZoneMap() {
|
||||||
|
const defaultDuration = 10;
|
||||||
|
const [polygons, setPolygons] = useState([]);
|
||||||
|
const [durations, setDurations] = useState([]);
|
||||||
|
const {zoneSettings, changeZoneSettings} = useAdmin();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (zoneSettings) {
|
||||||
|
setPolygons(zoneSettings.polygons);
|
||||||
|
setDurations(zoneSettings.durations);
|
||||||
|
}
|
||||||
|
}, [zoneSettings]);
|
||||||
|
|
||||||
|
function addPolygon(polygon) {
|
||||||
|
// Polygons
|
||||||
|
setPolygons([...polygons, polygon]);
|
||||||
|
// Durations
|
||||||
|
setDurations([...durations, defaultDuration]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removePolygon(i) {
|
||||||
|
// Polygons
|
||||||
|
const newPolygons = [...polygons];
|
||||||
|
newPolygons.splice(i, 1);
|
||||||
|
setPolygons(newPolygons);
|
||||||
|
// Durations
|
||||||
|
const newDurations = [...durations];
|
||||||
|
newDurations.splice(i, 1);
|
||||||
|
setDurations(newDurations);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDuration(i, duration) {
|
||||||
|
const newDurations = [...durations];
|
||||||
|
newDurations[i] = duration;
|
||||||
|
setDurations(newDurations);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSettingsSubmit() {
|
||||||
|
const newSettings = {polygons: polygons, durations: durations};
|
||||||
|
changeZoneSettings(newSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-2/5 h-full gap-1 bg-white p-10 flex flex-col text-center shadow-2xl overflow-y-scroll'>
|
||||||
|
<h2 className="text-2xl">Edit zones</h2>
|
||||||
|
<PolygonZonePicker polygons={polygons} addPolygon={addPolygon} removePolygon={removePolygon} />
|
||||||
|
<ul>
|
||||||
|
{durations.map((duration, i) => (
|
||||||
|
<li key={i}>
|
||||||
|
<p>Zone {i+1}</p>
|
||||||
|
<TextInput value={duration} onChange={(e) => updateDuration(i, e.target.value)}/>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<GreenButton onClick={handleSettingsSubmit}>Apply</GreenButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import TextInput from '../util/textInput'
|
import { TextInput } from '../util/textInput'
|
||||||
import BlueButton from '../util/button'
|
import { BlueButton } from '../util/button'
|
||||||
|
|
||||||
export default function TeamAddForm({onAddTeam}) {
|
export default function TeamAddForm({onAddTeam}) {
|
||||||
const [teamName, setTeamName] = React.useState('');
|
const [teamName, setTeamName] = React.useState('');
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react'
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
import TextInput from '../util/textInput'
|
import { TextInput } from '../util/textInput'
|
||||||
import BlueButton, { RedButton } from '../util/button';
|
import { BlueButton, RedButton } from '../util/button';
|
||||||
import useAdmin from '@/hook/useAdmin';
|
import useAdmin from '@/hook/useAdmin';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
import { env } from 'next-runtime-env';
|
import { env } from 'next-runtime-env';
|
||||||
import { GameState } from '@/util/gameState';
|
import { GameState } from '@/util/gameState';
|
||||||
|
|
||||||
const CircularAreaPicker = dynamic(() => import('./mapPicker').then((mod) => mod.CircularAreaPicker), {
|
// Imported at runtime and not at compile time
|
||||||
ssr: false
|
const PlacementMap = dynamic(() => import('./placementMap'), { ssr: false });
|
||||||
});
|
|
||||||
|
|
||||||
export default function TeamEdit({ selectedTeamId, setSelectedTeamId }) {
|
export default function TeamEdit({ selectedTeamId, setSelectedTeamId }) {
|
||||||
const teamImage = useRef(null);
|
const teamImage = useRef(null);
|
||||||
@@ -84,7 +82,7 @@ export default function TeamEdit({ selectedTeamId, setSelectedTeamId }) {
|
|||||||
<RedButton onClick={handleRemove}>Remove</RedButton>
|
<RedButton onClick={handleRemove}>Remove</RedButton>
|
||||||
</div>
|
</div>
|
||||||
<p className='text-2xl text-center w-full'>Starting zone</p>
|
<p className='text-2xl text-center w-full'>Starting zone</p>
|
||||||
<CircularAreaPicker area={team.startingArea} setArea={(startingArea) => updateTeam(team.id, { startingArea })} markerPosition={team?.currentLocation} />
|
<PlacementMap area={team.startingArea} setArea={(startingArea) => updateTeam(team.id, { startingArea })} markerPosition={team?.currentLocation} />
|
||||||
</div>
|
</div>
|
||||||
<div className='flex w-1/2 flex-col h-min gap-2 items-center'>
|
<div className='flex w-1/2 flex-col h-min gap-2 items-center'>
|
||||||
<h2 className='text-2xl text-center'>Team details</h2>
|
<h2 className='text-2xl text-center'>Team details</h2>
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
"use client";
|
|
||||||
import useAdmin from '@/hook/useAdmin';
|
import useAdmin from '@/hook/useAdmin';
|
||||||
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
|
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
const reorder = (list, startIndex, endIndex) => {
|
function reorder(list, startIndex, endIndex) {
|
||||||
const result = Array.from(list);
|
const result = Array.from(list);
|
||||||
const [removed] = result.splice(startIndex, 1);
|
const [removed] = result.splice(startIndex, 1);
|
||||||
result.splice(endIndex, 0, removed);
|
result.splice(endIndex, 0, removed);
|
||||||
|
return result;
|
||||||
return result;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function TeamListItem({ team, index, onSelected, itemSelected }) {
|
function TeamListItem({ team, index, onSelected, itemSelected }) {
|
||||||
@@ -24,11 +22,12 @@ function TeamListItem({ team, index, onSelected, itemSelected }) {
|
|||||||
|
|
||||||
)}
|
)}
|
||||||
</Draggable>
|
</Draggable>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TeamList({selectedTeamId, onSelected}) {
|
export default function TeamList({selectedTeamId, onSelected}) {
|
||||||
const {teams, reorderTeams} = useAdmin();
|
const {teams, reorderTeams} = useAdmin();
|
||||||
|
|
||||||
function onDragEnd(result) {
|
function onDragEnd(result) {
|
||||||
if (!result.destination) {
|
if (!result.destination) {
|
||||||
return;
|
return;
|
||||||
@@ -46,6 +45,7 @@ export default function TeamList({selectedTeamId, onSelected}) {
|
|||||||
|
|
||||||
reorderTeams(newTeams);
|
reorderTeams(newTeams);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DragDropContext onDragEnd={onDragEnd} >
|
<DragDropContext onDragEnd={onDragEnd} >
|
||||||
<Droppable droppableId='team-list'>
|
<Droppable droppableId='team-list'>
|
||||||
@@ -61,5 +61,5 @@ export default function TeamList({selectedTeamId, onSelected}) {
|
|||||||
)}
|
)}
|
||||||
</Droppable>
|
</Droppable>
|
||||||
</DragDropContext>
|
</DragDropContext>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import useAdmin from "@/hook/useAdmin"
|
import useAdmin from "@/hook/useAdmin"
|
||||||
|
|
||||||
export function TeamReady() {
|
export default function TeamReady() {
|
||||||
const {teams} = useAdmin();
|
const {teams} = useAdmin();
|
||||||
return <div className='w-full h-full gap-1 bg-white p-10 flex flex-col text-center shadow-2xl overflow-y-scroll'>
|
return (
|
||||||
<h2 className="text-2xl">Teams ready status</h2>
|
<div className='w-full h-full gap-1 bg-white p-10 flex flex-col text-center shadow-2xl overflow-y-scroll'>
|
||||||
{teams.map((team) => team.ready ? (
|
<h2 className="text-2xl">Teams ready status</h2>
|
||||||
<div key={team.id} className="p-2 text-white bg-green-500 shadow-md text-xl rounded flex flex-row">
|
{teams.map((team) => team.ready ? (
|
||||||
<div>{team.name} : Ready</div>
|
<div key={team.id} className="p-2 text-white bg-green-500 shadow-md text-xl rounded flex flex-row">
|
||||||
</div>) : (
|
<div>{team.name} : Ready</div>
|
||||||
<div key={team.id} className="p-2 text-white bg-red-500 shadow-md text-xl rounded flex flex-row">
|
</div>) : (
|
||||||
<div>{team.name} : Not ready</div>
|
<div key={team.id} className="p-2 text-white bg-red-500 shadow-md text-xl rounded flex flex-row">
|
||||||
</div>
|
<div>{team.name} : Not ready</div>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import BlueButton, { GreenButton, RedButton } from "../util/button";
|
|
||||||
import { EditMode, ZonePicker } from "./mapPicker";
|
|
||||||
import TextInput from "../util/textInput";
|
|
||||||
import useAdmin from "@/hook/useAdmin";
|
|
||||||
|
|
||||||
export function ZoneSelector() {
|
|
||||||
const [editMode, setEditMode] = useState(EditMode.MIN);
|
|
||||||
const [minZone, setMinZone] = useState(null);
|
|
||||||
const [maxZone, setMaxZone] = useState(null);
|
|
||||||
const [reductionCount, setReductionCount] = useState("");
|
|
||||||
const [reductionDuration, setReductionDuration] = useState("");
|
|
||||||
const [reductionInterval, setReductionInterval] = useState("");
|
|
||||||
const {zoneSettings, changeZoneSettings} = useAdmin();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (zoneSettings) {
|
|
||||||
setMinZone(zoneSettings.min);
|
|
||||||
setMaxZone(zoneSettings.max);
|
|
||||||
setReductionCount(zoneSettings.reductionCount.toString());
|
|
||||||
setReductionDuration(zoneSettings.reductionDuration.toString());
|
|
||||||
setReductionInterval(zoneSettings.reductionInterval.toString());
|
|
||||||
}
|
|
||||||
}, [zoneSettings]);
|
|
||||||
|
|
||||||
function handleSettingsSubmit() {
|
|
||||||
const newSettings = {min:minZone, max:maxZone, reductionCount: Number(reductionCount), reductionDuration: Number(reductionDuration), reductionInterval: Number(reductionInterval)};
|
|
||||||
const changingSettings = {};
|
|
||||||
for (const key in newSettings) {
|
|
||||||
if (newSettings[key] != zoneSettings[key]) {
|
|
||||||
changingSettings[key] = newSettings[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
changeZoneSettings(changingSettings);
|
|
||||||
}
|
|
||||||
|
|
||||||
//When the user set one zone, switch to the other
|
|
||||||
useEffect(() => {
|
|
||||||
if(editMode == EditMode.MIN) {
|
|
||||||
setEditMode(EditMode.MAX);
|
|
||||||
}else {
|
|
||||||
setEditMode(EditMode.MIN);
|
|
||||||
}
|
|
||||||
|
|
||||||
}, [minZone, maxZone]);
|
|
||||||
|
|
||||||
return <div className='w-2/5 h-full gap-1 bg-white p-10 flex flex-col text-center shadow-2xl overflow-y-scroll'>
|
|
||||||
<h2 className="text-2xl">Edit zones</h2>
|
|
||||||
{editMode == EditMode.MIN && <BlueButton onClick={() => setEditMode(EditMode.MAX)}>Click to edit first zone</BlueButton>}
|
|
||||||
{editMode == EditMode.MAX && <RedButton onClick={() => setEditMode(EditMode.MIN)}>Click to edit last zone</RedButton>}
|
|
||||||
<ZonePicker minZone={minZone} maxZone={maxZone} editMode={editMode} setMinZone={setMinZone} setMaxZone={setMaxZone} />
|
|
||||||
<div>
|
|
||||||
<p>Number of reductions</p>
|
|
||||||
<TextInput value={reductionCount} onChange={(e) => setReductionCount(e.target.value)}></TextInput>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p>Duration of each reduction</p>
|
|
||||||
<TextInput value={reductionDuration} onChange={(e) => setReductionDuration(e.target.value)}></TextInput>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p>Interval between reductions</p>
|
|
||||||
<TextInput value={reductionInterval} onChange={(e) => setReductionInterval(e.target.value)}></TextInput>
|
|
||||||
</div>
|
|
||||||
<GreenButton onClick={handleSettingsSubmit}>Apply</GreenButton>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
import useGame from "@/hook/useGame";
|
import useGame from "@/hook/useGame";
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import BlueButton, { GreenButton, RedButton } from "../util/button";
|
import { BlueButton, GreenButton } from "../util/button";
|
||||||
import TextInput from "../util/textInput";
|
import { TextInput } from "../util/textInput";
|
||||||
import { useTeamConnexion } from "@/context/teamConnexionContext";
|
import useTeamConnexion from "@/context/teamConnexionContext";
|
||||||
import { EnemyTeamModal } from "./enemyTeamModal";
|
import EnemyTeamModal from "./enemyTeamModal";
|
||||||
import Image from "next/image";
|
|
||||||
|
|
||||||
export default function ActionDrawer() {
|
export default function ActionDrawer() {
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
@@ -73,4 +72,4 @@ export default function ActionDrawer() {
|
|||||||
<EnemyTeamModal visible={enemyModalVisible} onClose={() => setEnemyModalVisible(false)} />
|
<EnemyTeamModal visible={enemyModalVisible} onClose={() => setEnemyModalVisible(false)} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import useGame from "@/hook/useGame";
|
import useGame from "@/hook/useGame";
|
||||||
import { RedButton } from "../util/button";
|
import { RedButton } from "../util/button";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import Image from "next/image";
|
|
||||||
|
|
||||||
import { env } from 'next-runtime-env';
|
import { env } from 'next-runtime-env';
|
||||||
|
|
||||||
export function EnemyTeamModal({ visible, onClose }) {
|
export default function EnemyTeamModal({ visible, onClose }) {
|
||||||
const { teamId, enemyName } = useGame();
|
const { teamId, enemyName } = useGame();
|
||||||
const imageRef = useRef(null);
|
const imageRef = useRef(null);
|
||||||
|
|
||||||
@@ -38,4 +36,4 @@ export function EnemyTeamModal({ visible, onClose }) {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"use client";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import BlueButton from "../util/button";
|
import { BlueButton } from "../util/button";
|
||||||
import TextInput from "../util/textInput";
|
import { TextInput } from "../util/textInput";
|
||||||
|
|
||||||
export default function LoginForm({ onSubmit, title, placeholder, buttonText}) {
|
export default function LoginForm({ onSubmit, title, placeholder, buttonText}) {
|
||||||
const [value, setValue] = useState("");
|
const [value, setValue] = useState("");
|
||||||
@@ -17,4 +16,4 @@ export default function LoginForm({ onSubmit, title, placeholder, buttonText}) {
|
|||||||
<BlueButton type="submit">{buttonText}</BlueButton>
|
<BlueButton type="submit">{buttonText}</BlueButton>
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
'use client';
|
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { Circle, MapContainer, Marker, Popup, TileLayer, useMap } from 'react-leaflet'
|
import { Circle, MapContainer, Marker, Popup, TileLayer, useMap } from 'react-leaflet'
|
||||||
import 'leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css'
|
import 'leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css'
|
||||||
import "leaflet-defaulticon-compatibility";
|
import "leaflet-defaulticon-compatibility";
|
||||||
import "leaflet/dist/leaflet.css";
|
import "leaflet/dist/leaflet.css";
|
||||||
import useGame from '@/hook/useGame';
|
import useGame from '@/hook/useGame';
|
||||||
import { useTeamContext } from '@/context/teamContext';
|
import useTeamContext from '@/context/teamContext';
|
||||||
|
|
||||||
const DEFAULT_ZOOM = 14;
|
const DEFAULT_ZOOM = 14;
|
||||||
|
|
||||||
|
|
||||||
// Pan to the center of the map when the position of the user is updated for the first time
|
// Pan to the center of the map when the position of the user is updated for the first time
|
||||||
function MapPan(props) {
|
function MapPan(props) {
|
||||||
const map = useMap();
|
const map = useMap();
|
||||||
|
|||||||
@@ -1,26 +1,29 @@
|
|||||||
import { useSocketListener } from "@/hook/useSocketListener";
|
import { useSocketListener } from "@/hook/useSocketListener";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export function Notification({ socket }) {
|
export default function Notification({ socket }) {
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const [timeoutId, setTimeoutId] = useState(null);
|
const [timeoutId, setTimeoutId] = useState(null);
|
||||||
|
|
||||||
const [notification, setNotification] = useState(null);
|
const [notification, setNotification] = useState(null);
|
||||||
|
|
||||||
useSocketListener(socket, "error", (notification) => {
|
useSocketListener(socket, "error", (notification) => {
|
||||||
console.log("error", notification);
|
console.log("error", notification);
|
||||||
setNotification({ type: "error", text: notification });
|
setNotification({ type: "error", text: notification });
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
useSocketListener(socket, "success", (notification) => {
|
useSocketListener(socket, "success", (notification) => {
|
||||||
console.log("success", notification);
|
console.log("success", notification);
|
||||||
setNotification({ type: "success", text: notification });
|
setNotification({ type: "success", text: notification });
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
useSocketListener(socket, "warning", (notification) => {
|
useSocketListener(socket, "warning", (notification) => {
|
||||||
console.log("warning", notification);
|
console.log("warning", notification);
|
||||||
setNotification({ type: "warning", text: notification });
|
setNotification({ type: "warning", text: notification });
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Hide the notification after 5 seconds
|
// Hide the notification after 5 seconds
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log({ visible });
|
console.log({ visible });
|
||||||
@@ -34,12 +37,14 @@ export function Notification({ socket }) {
|
|||||||
}
|
}
|
||||||
}, [visible]);
|
}, [visible]);
|
||||||
|
|
||||||
let bgColorMap = {
|
const bgColorMap = {
|
||||||
error: "bg-red-500 text-white",
|
error: "bg-red-500 text-white",
|
||||||
success: "bg-green-500",
|
success: "bg-green-500",
|
||||||
warning: "bg-yellow-500"
|
warning: "bg-yellow-500"
|
||||||
}
|
}
|
||||||
|
|
||||||
const classNames = 'fixed relative w-11/12 p-5 z-30 mx-auto inset-x-0 flex justify-center rounded-xl transition-all shadow-xl ' + (visible ? "top-5 " : "-translate-y-full ");
|
const classNames = 'fixed relative w-11/12 p-5 z-30 mx-auto inset-x-0 flex justify-center rounded-xl transition-all shadow-xl ' + (visible ? "top-5 " : "-translate-y-full ");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
Object.keys(bgColorMap).map((key) =>
|
Object.keys(bgColorMap).map((key) =>
|
||||||
notification?.type == key &&
|
notification?.type == key &&
|
||||||
@@ -47,5 +52,5 @@ export function Notification({ socket }) {
|
|||||||
<p className="absolute top-2 right-2 p-2 rounded-l text-3xl bg-white">x</p>
|
<p className="absolute top-2 right-2 p-2 rounded-l text-3xl bg-white">x</p>
|
||||||
<p className='text-center text-xl'>{notification?.text}</p>
|
<p className='text-center text-xl'>{notification?.text}</p>
|
||||||
</div>
|
</div>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useTeamConnexion } from "@/context/teamConnexionContext";
|
import useTeamConnexion from "@/context/teamConnexionContext";
|
||||||
import useGame from "@/hook/useGame"
|
import useGame from "@/hook/useGame"
|
||||||
import Image from "next/image";
|
|
||||||
|
|
||||||
export default function PlacementOverlay() {
|
export default function PlacementOverlay() {
|
||||||
const { name, ready } = useGame();
|
const { name, ready } = useGame();
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import useGame from "@/hook/useGame"
|
import useGame from "@/hook/useGame"
|
||||||
import { GreenButton, LogoutButton } from "../util/button";
|
import { GreenButton, LogoutButton } from "../util/button";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import Image from "next/image";
|
import useTeamContext from "@/context/teamContext";
|
||||||
import { useTeamContext } from "@/context/teamContext";
|
|
||||||
|
|
||||||
import { env } from 'next-runtime-env';
|
import { env } from 'next-runtime-env';
|
||||||
|
|
||||||
export function WaitingScreen() {
|
export default function WaitingScreen() {
|
||||||
const { name, teamId } = useGame();
|
const { name, teamId } = useGame();
|
||||||
const { gameSettings } = useTeamContext();
|
const { gameSettings } = useTeamContext();
|
||||||
const imageRef = useRef(null);
|
const imageRef = useRef(null);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useTeamConnexion } from "@/context/teamConnexionContext";
|
import { useTeamConnexion } from "@/context/teamConnexionContext";
|
||||||
import Image from "next/image";
|
|
||||||
|
|
||||||
export default function BlueButton({ children, ...props }) {
|
export function BlueButton({ children, ...props }) {
|
||||||
return (<button {...props} className="bg-blue-600 hover:bg-blue-500 text-lg ease-out duration-200 text-white w-full h-full p-4 shadow-sm rounded">
|
return (<button {...props} className="bg-blue-600 hover:bg-blue-500 text-lg ease-out duration-200 text-white w-full h-full p-4 shadow-sm rounded">
|
||||||
{children}
|
{children}
|
||||||
</button>)
|
</button>)
|
||||||
@@ -20,6 +19,6 @@ export function GreenButton({ children, ...props }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function LogoutButton() {
|
export function LogoutButton() {
|
||||||
const { logout } = useTeamConnexion();
|
const { logout } = useTeamConnexion();
|
||||||
return <img src="/icons/logout.png" onClick={logout} className='w-12 h-12 bg-red-500 p-2 top-1 right-1 rounded-lg cursor-pointer bg-red fixed z-20' />
|
return <img src="/icons/logout.png" onClick={logout} className='w-12 h-12 bg-red-500 p-2 top-1 right-1 rounded-lg cursor-pointer bg-red fixed z-20' />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
export default function TextInput({...props}) {
|
export function TextInput({...props}) {
|
||||||
return (
|
return (
|
||||||
<input {...props} type="text" className="block w-full h-full p-4 rounded text-center ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600" />
|
<input {...props} type="text" className="block w-full h-full p-4 rounded text-center ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600" />
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { createContext, useContext, useMemo, } from "react";
|
import { createContext, useContext, useMemo } from "react";
|
||||||
import { useSocket } from "./socketContext";
|
import { useSocket } from "./socketContext";
|
||||||
import { useSocketAuth } from "@/hook/useSocketAuth";
|
import useSocketAuth from "@/hook/useSocketAuth";
|
||||||
import { usePasswordProtect } from "@/hook/usePasswordProtect";
|
import usePasswordProtect from "@/hook/usePasswordProtect";
|
||||||
|
|
||||||
const adminConnexionContext = createContext();
|
const adminConnexionContext = createContext();
|
||||||
const AdminConnexionProvider = ({ children }) => {
|
|
||||||
|
export function AdminConnexionProvider({ children }) {
|
||||||
const { adminSocket } = useSocket();
|
const { adminSocket } = useSocket();
|
||||||
const { login, loggedIn, loading } = useSocketAuth(adminSocket, "admin_password");
|
const { login, loggedIn, loading } = useSocketAuth(adminSocket, "admin_password");
|
||||||
const useProtect = () => usePasswordProtect("/admin/login", "/admin", loading, loggedIn);
|
const useProtect = () => usePasswordProtect("/admin/login", "/admin", loading, loggedIn);
|
||||||
@@ -19,9 +20,6 @@ const AdminConnexionProvider = ({ children }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function useAdminConnexion() {
|
export function useAdminConnexion() {
|
||||||
return useContext(adminConnexionContext);
|
return useContext(adminConnexionContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { AdminConnexionProvider, useAdminConnexion };
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,53 +1,43 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { createContext, useContext, useEffect, useMemo, useState } from "react";
|
import { createContext, useContext, useEffect, useMemo, useState } from "react";
|
||||||
import { useSocket } from "./socketContext";
|
import { useSocket } from "./socketContext";
|
||||||
import { useSocketListener } from "@/hook/useSocketListener";
|
import useSocketListener from "@/hook/useSocketListener";
|
||||||
import { useAdminConnexion } from "./adminConnexionContext";
|
import { useAdminConnexion } from "./adminConnexionContext";
|
||||||
import { GameState } from "@/util/gameState";
|
import { GameState } from "@/util/gameState";
|
||||||
|
|
||||||
const adminContext = createContext();
|
const adminContext = createContext();
|
||||||
|
|
||||||
function AdminProvider({ children }) {
|
export function AdminProvider({ children }) {
|
||||||
const [teams, setTeams] = useState([]);
|
const [teams, setTeams] = useState([]);
|
||||||
const [zoneSettings, setZoneSettings] = useState(null)
|
const [zoneSettings, setZoneSettings] = useState(null)
|
||||||
const [penaltySettings, setPenaltySettings] = useState(null);
|
const [penaltySettings, setPenaltySettings] = useState(null);
|
||||||
const [gameSettings, setGameSettings] = useState(null);
|
const [gameSettings, setGameSettings] = useState(null);
|
||||||
const [zone, setZone] = useState(null);
|
|
||||||
const [zoneExtremities, setZoneExtremities] = useState(null);
|
const [zoneExtremities, setZoneExtremities] = useState(null);
|
||||||
const [nextZoneDate, setNextZoneDate] = useState(null);
|
const [nextZoneDate, setNextZoneDate] = useState(null);
|
||||||
const [isShrinking, setIsShrinking] = useState(false);
|
|
||||||
const { adminSocket } = useSocket();
|
const { adminSocket } = useSocket();
|
||||||
const { loggedIn } = useAdminConnexion();
|
const { loggedIn } = useAdminConnexion();
|
||||||
const [gameState, setGameState] = useState(GameState.SETUP);
|
const [gameState, setGameState] = useState(GameState.SETUP);
|
||||||
const [startDate, setStartDate] = useState(null);
|
const [startDate, setStartDate] = useState(null);
|
||||||
|
|
||||||
useSocketListener(adminSocket, "game_state", (data) => {setGameState(data.state); setStartDate(data.startDate)});
|
useSocketListener(adminSocket, "game_state", (data) => {setGameState(data.state); setStartDate(data.startDate)});
|
||||||
//Send a request to get the teams when the user logs in
|
// Send a request to get the teams when the user logs in
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
adminSocket.emit("get_teams");
|
adminSocket.emit("get_teams");
|
||||||
}, [loggedIn]);
|
}, [loggedIn]);
|
||||||
|
|
||||||
function waiting(data) {
|
function setCurrent_zone(data) {
|
||||||
setIsShrinking(false);
|
|
||||||
setZoneExtremities({begin: data.begin, end: data.end});
|
setZoneExtremities({begin: data.begin, end: data.end});
|
||||||
setNextZoneDate(data.endDate);
|
setNextZoneDate(data.endDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
function shrinking(data) {
|
// Bind listeners to update the team list and the game status on socket message
|
||||||
setIsShrinking(true);
|
|
||||||
setNextZoneDate(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Bind listeners to update the team list and the game status on socket message
|
|
||||||
useSocketListener(adminSocket, "teams", setTeams);
|
useSocketListener(adminSocket, "teams", setTeams);
|
||||||
useSocketListener(adminSocket, "zone_settings", setZoneSettings);
|
useSocketListener(adminSocket, "zone_settings", setZoneSettings);
|
||||||
useSocketListener(adminSocket, "game_settings", setGameSettings);
|
useSocketListener(adminSocket, "game_settings", setGameSettings);
|
||||||
useSocketListener(adminSocket, "penalty_settings", setPenaltySettings);
|
useSocketListener(adminSocket, "penalty_settings", setPenaltySettings);
|
||||||
useSocketListener(adminSocket, "zone", setZone);
|
useSocketListener(adminSocket, "current_zone", setCurrent_zone);
|
||||||
useSocketListener(adminSocket, "zone_start", shrinking);
|
|
||||||
useSocketListener(adminSocket, "new_zone", waiting);
|
|
||||||
|
|
||||||
const value = useMemo(() => ({ zone, zoneExtremities, teams, zoneSettings, penaltySettings, gameSettings, gameState, nextZoneDate, isShrinking, startDate }), [zoneSettings, teams, gameState, zone, zoneExtremities, penaltySettings, gameSettings, nextZoneDate, isShrinking, startDate]);
|
const value = useMemo(() => ({ zoneExtremities, teams, zoneSettings, penaltySettings, gameSettings, gameState, nextZoneDate, startDate }), [zoneSettings, teams, gameState, zoneExtremities, penaltySettings, gameSettings, nextZoneDate, startDate]);
|
||||||
return (
|
return (
|
||||||
<adminContext.Provider value={value}>
|
<adminContext.Provider value={value}>
|
||||||
{children}
|
{children}
|
||||||
@@ -55,8 +45,6 @@ function AdminProvider({ children }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function useAdminContext() {
|
export function useAdminContext() {
|
||||||
return useContext(adminContext);
|
return useContext(adminContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { AdminProvider, useAdminContext };
|
|
||||||
@@ -1,22 +1,17 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { createContext, useContext, useMemo } from "react";
|
import { createContext, useContext, useMemo } from "react";
|
||||||
|
|
||||||
import { env } from 'next-runtime-env';
|
import { env } from 'next-runtime-env';
|
||||||
|
import { io } from 'socket.io-client';
|
||||||
|
|
||||||
const { io } = require("socket.io-client");
|
|
||||||
|
|
||||||
var proto = "wss://";
|
|
||||||
const NEXT_PUBLIC_SOCKET_HOST = env("NEXT_PUBLIC_SOCKET_HOST");
|
const NEXT_PUBLIC_SOCKET_HOST = env("NEXT_PUBLIC_SOCKET_HOST");
|
||||||
if (NEXT_PUBLIC_SOCKET_HOST == "localhost") {
|
const SOCKET_URL = (NEXT_PUBLIC_SOCKET_HOST == "localhost" ? "ws://" : "wss://") + NEXT_PUBLIC_SOCKET_HOST;
|
||||||
proto = "ws://";
|
|
||||||
}
|
|
||||||
const SOCKET_URL = proto + NEXT_PUBLIC_SOCKET_HOST;
|
|
||||||
const USER_SOCKET_URL = SOCKET_URL + "/player";
|
const USER_SOCKET_URL = SOCKET_URL + "/player";
|
||||||
const ADMIN_SOCKET_URL = SOCKET_URL + "/admin";
|
const ADMIN_SOCKET_URL = SOCKET_URL + "/admin";
|
||||||
|
|
||||||
export const teamSocket = io(USER_SOCKET_URL, {
|
export const teamSocket = io(USER_SOCKET_URL, {
|
||||||
path: "/back/socket.io",
|
path: "/back/socket.io",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const adminSocket = io(ADMIN_SOCKET_URL, {
|
export const adminSocket = io(ADMIN_SOCKET_URL, {
|
||||||
path: "/back/socket.io",
|
path: "/back/socket.io",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { createContext, useContext, useMemo } from "react";
|
import { createContext, useContext, useMemo } from "react";
|
||||||
import { useSocket } from "./socketContext";
|
import { useSocket } from "./socketContext";
|
||||||
import { useSocketAuth } from "@/hook/useSocketAuth";
|
import useSocketAuth from "@/hook/useSocketAuth";
|
||||||
import { usePasswordProtect } from "@/hook/usePasswordProtect";
|
import usePasswordProtect from "@/hook/usePasswordProtect";
|
||||||
|
|
||||||
const teamConnexionContext = createContext();
|
const teamConnexionContext = createContext();
|
||||||
const TeamConnexionProvider = ({ children }) => {
|
|
||||||
|
export function TeamConnexionProvider({ children }) {
|
||||||
const { teamSocket } = useSocket();
|
const { teamSocket } = useSocket();
|
||||||
const { login, password: teamId, loggedIn, loading, logout } = useSocketAuth(teamSocket, "team_password");
|
const { login, password: teamId, loggedIn, loading, logout } = useSocketAuth(teamSocket, "team_password");
|
||||||
const useProtect = () => usePasswordProtect("/team", "/team/track", loading, loggedIn);
|
const useProtect = () => usePasswordProtect("/team", "/team/track", loading, loggedIn);
|
||||||
|
|
||||||
const value = useMemo(() => ({ teamId, login, logout, loggedIn, loading, useProtect}), [teamId, login, loggedIn, loading]);
|
const value = useMemo(() => ({ teamId, login, logout, loggedIn, loading, useProtect}), [teamId, login, loggedIn, loading]);
|
||||||
@@ -19,9 +20,6 @@ const TeamConnexionProvider = ({ children }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function useTeamConnexion() {
|
export function useTeamConnexion() {
|
||||||
return useContext(teamConnexionContext);
|
return useContext(teamConnexionContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { TeamConnexionProvider, useTeamConnexion };
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useLocation } from "@/hook/useLocation";
|
import useLocation from "@/hook/useLocation";
|
||||||
import { useSocketListener } from "@/hook/useSocketListener";
|
import useSocketListener from "@/hook/useSocketListener";
|
||||||
import { createContext, use, useContext, useEffect, useMemo, useRef, useState } from "react";
|
import { createContext, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useSocket } from "./socketContext";
|
import { useSocket } from "./socketContext";
|
||||||
import { useTeamConnexion } from "./teamConnexionContext";
|
import { useTeamConnexion } from "./teamConnexionContext";
|
||||||
import { GameState } from "@/util/gameState";
|
import { GameState } from "@/util/gameState";
|
||||||
|
|
||||||
|
const teamContext = createContext();
|
||||||
|
|
||||||
const teamContext = createContext()
|
export function TeamProvider({children}) {
|
||||||
function TeamProvider({children}) {
|
|
||||||
const [teamInfos, setTeamInfos] = useState({});
|
const [teamInfos, setTeamInfos] = useState({});
|
||||||
const [gameState, setGameState] = useState(GameState.SETUP);
|
const [gameState, setGameState] = useState(GameState.SETUP);
|
||||||
const [gameSettings, setGameSettings] = useState(null);
|
const [gameSettings, setGameSettings] = useState(null);
|
||||||
@@ -21,17 +21,12 @@ function TeamProvider({children}) {
|
|||||||
|
|
||||||
teamInfosRef.current = teamInfos;
|
teamInfosRef.current = teamInfos;
|
||||||
|
|
||||||
useSocketListener(teamSocket, "update_team", (newTeamInfos) => {
|
useSocketListener(teamSocket, "update_team", (newTeamInfos) => setTeamInfos({...teamInfosRef.current, ...newTeamInfos}) );
|
||||||
setTeamInfos({...teamInfosRef.current, ...newTeamInfos});
|
|
||||||
});
|
|
||||||
|
|
||||||
useSocketListener(teamSocket, "game_state", setGameState);
|
useSocketListener(teamSocket, "game_state", setGameState);
|
||||||
useSocketListener(teamSocket, "zone", setZone);
|
useSocketListener(teamSocket, "zone", setZone);
|
||||||
useSocketListener(teamSocket, "new_zone", setZoneExtremities);
|
useSocketListener(teamSocket, "new_zone", setZoneExtremities);
|
||||||
useSocketListener(teamSocket, "game_settings", setGameSettings);
|
useSocketListener(teamSocket, "game_settings", setGameSettings);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//Send the current position to the server when the user is logged in
|
//Send the current position to the server when the user is logged in
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("sending position", measuredLocation);
|
console.log("sending position", measuredLocation);
|
||||||
@@ -48,8 +43,6 @@ function TeamProvider({children}) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function useTeamContext() {
|
export function useTeamContext() {
|
||||||
return useContext(teamContext);
|
return useContext(teamContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { TeamProvider, useTeamContext };
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
"use client";
|
||||||
import { useAdminContext } from "@/context/adminContext";
|
import { useAdminContext } from "@/context/adminContext";
|
||||||
import { useSocket } from "@/context/socketContext";
|
import { useSocket } from "@/context/socketContext";
|
||||||
|
|
||||||
@@ -52,4 +53,4 @@ export default function useAdmin() {
|
|||||||
}
|
}
|
||||||
return { ...adminContext, changeGameSettings, changeZoneSettings, changePenaltySettings, pollTeams, getTeam, getTeamName, reorderTeams, addTeam, removeTeam, changeState, updateTeam };
|
return { ...adminContext, changeGameSettings, changeZoneSettings, changePenaltySettings, pollTeams, getTeam, getTeamName, reorderTeams, addTeam, removeTeam, changeState, updateTeam };
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useSocket } from "@/context/socketContext";
|
import { useSocket } from "@/context/socketContext";
|
||||||
import { useTeamConnexion } from "@/context/teamConnexionContext";
|
import { useTeamConnexion } from "@/context/teamConnexionContext";
|
||||||
import { useTeamContext } from "@/context/teamContext";
|
import { useTeamContext } from "@/context/teamContext";
|
||||||
@@ -33,4 +32,4 @@ export default function useGame() {
|
|||||||
teamId,
|
teamId,
|
||||||
gameState,
|
gameState,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export function useLocalStorage(key, initialValue) {
|
export default function useLocalStorage(key, initialValue) {
|
||||||
const [storedValue, setStoredValue] = useState(initialValue);
|
const [storedValue, setStoredValue] = useState(initialValue);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,10 @@ import { useEffect, useState } from "react";
|
|||||||
* A hook that returns the location of the user and updates it periodically
|
* A hook that returns the location of the user and updates it periodically
|
||||||
* @returns {Object} The location of the user
|
* @returns {Object} The location of the user
|
||||||
*/
|
*/
|
||||||
export function useLocation(interval) {
|
export default function useLocation(interval) {
|
||||||
const [location, setLocation] = useState();
|
const [location, setLocation] = useState();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function update() {
|
function update() {
|
||||||
console.log('Updating location');
|
|
||||||
navigator.geolocation.getCurrentPosition((position) => {
|
navigator.geolocation.getCurrentPosition((position) => {
|
||||||
setLocation([position.coords.latitude, position.coords.longitude]);
|
setLocation([position.coords.latitude, position.coords.longitude]);
|
||||||
if(interval != Infinity) {
|
if(interval != Infinity) {
|
||||||
@@ -21,4 +20,4 @@ export function useLocation(interval) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return location;
|
return location;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
"use client";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export function useMapCircleDraw(area, setArea) {
|
export default function useMapCircleDraw(area, setArea) {
|
||||||
const [drawing, setDrawing] = useState(false);
|
const [drawing, setDrawing] = useState(false);
|
||||||
const [center, setCenter] = useState(area?.center || null);
|
const [center, setCenter] = useState(area?.center || null);
|
||||||
const [radius, setRadius] = useState(area?.radius || null);
|
const [radius, setRadius] = useState(area?.radius || null);
|
||||||
@@ -12,7 +13,7 @@ export function useMapCircleDraw(area, setArea) {
|
|||||||
}, [area])
|
}, [area])
|
||||||
|
|
||||||
function handleClick(e) {
|
function handleClick(e) {
|
||||||
if(!drawing) {
|
if (!drawing) {
|
||||||
setCenter(e.latlng);
|
setCenter(e.latlng);
|
||||||
setRadius(null);
|
setRadius(null);
|
||||||
setDrawing(true);
|
setDrawing(true);
|
||||||
@@ -23,14 +24,10 @@ export function useMapCircleDraw(area, setArea) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleMouseMove(e) {
|
function handleMouseMove(e) {
|
||||||
if(drawing) {
|
if (drawing) {
|
||||||
setRadius(e.latlng.distanceTo(center));
|
setRadius(e.latlng.distanceTo(center));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
|
||||||
handleClick,
|
return { handleClick, handleMouseMove, center, radius };
|
||||||
handleMouseMove,
|
}
|
||||||
center,
|
|
||||||
radius,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
214
traque-front/hook/useMapPolygonDraw.jsx
Normal file
214
traque-front/hook/useMapPolygonDraw.jsx
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
"use client";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useMap } from "react-leaflet";
|
||||||
|
|
||||||
|
export default function useMapPolygonDraw(polygons, addPolygon, removePolygon) {
|
||||||
|
const map = useMap();
|
||||||
|
const nodeCatchDistance = 30; // px
|
||||||
|
const nodeHighlightDistance = 30; // px
|
||||||
|
const [currentPolygon, setCurrentPolygon] = useState([]);
|
||||||
|
const [highlightNodes, setHighlightNodes] = useState([]);
|
||||||
|
|
||||||
|
function latlngEqual(latlng1, latlng2, epsilon = 1e-9) {
|
||||||
|
return Math.abs(latlng1.lat - latlng2.lat) < epsilon && Math.abs(latlng1.lng - latlng2.lng) < epsilon;
|
||||||
|
}
|
||||||
|
|
||||||
|
function layerDistance(latlng1, latlng2) {
|
||||||
|
// Return the pixel distance between latlng1 and latlng2 as they appear on the map
|
||||||
|
const {x: x1, y: y1} = map.latLngToLayerPoint(latlng1);
|
||||||
|
const {x: x2, y: y2} = map.latLngToLayerPoint(latlng2);
|
||||||
|
return Math.sqrt((x1 - x2)**2 + (y1 - y2)**2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDrawing() {
|
||||||
|
return currentPolygon.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function areSegmentsIntersecting(p1, p2, p3, p4) {
|
||||||
|
// Return true if the segments (p1, p2) and (p3, p4) are strictly intersecting, else false
|
||||||
|
const direction = (a, b, c) => {
|
||||||
|
return (c.lng - a.lng) * (b.lat - a.lat) - (b.lng - a.lng) * (c.lat - a.lat);
|
||||||
|
};
|
||||||
|
|
||||||
|
const d1 = direction(p3, p4, p1);
|
||||||
|
const d2 = direction(p3, p4, p2);
|
||||||
|
const d3 = direction(p1, p2, p3);
|
||||||
|
const d4 = direction(p1, p2, p4);
|
||||||
|
|
||||||
|
return ((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) && ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isIntersecting(segment, pointArray, isPolygon) {
|
||||||
|
// Return true if segment intersects one of the pointArray segments according to areSegmentsIntersecting
|
||||||
|
// Moreover if isPolygon, then it verifies if segment intersects the segment closing pointArray
|
||||||
|
const length = pointArray.length;
|
||||||
|
|
||||||
|
for (let i = 0; i < length-1; i++) {
|
||||||
|
if (areSegmentsIntersecting(segment[0], segment[1], pointArray[i], pointArray[i+1])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPolygon && length > 2) {
|
||||||
|
return areSegmentsIntersecting(segment[0], segment[1], pointArray[length-1], pointArray[0]);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInPolygon(latlng, polygon) {
|
||||||
|
// Return true if latlng is strictly inside polygon
|
||||||
|
// Return false if latlng is outside polygon or on a vertex of the polygon
|
||||||
|
// Return true or false if latlng is on the border
|
||||||
|
if (latlngEqual(latlng, polygon[0])) return false;
|
||||||
|
|
||||||
|
const length = polygon.length;
|
||||||
|
const {lat: x, lng: y} = latlng;
|
||||||
|
let inside = false;
|
||||||
|
|
||||||
|
for (let i = 0, j = length - 1; i < length; j = i++) {
|
||||||
|
if (latlngEqual(latlng, polygon[j])) return false;
|
||||||
|
|
||||||
|
const {lat: xi, lng: yi} = polygon[i];
|
||||||
|
const {lat: xj, lng: yj} = polygon[j];
|
||||||
|
const intersects = ((yi > y) !== (yj > y)) && (x < ((xj - xi) * (y - yi)) / (yj - yi) + xi);
|
||||||
|
|
||||||
|
if (intersects) inside = !inside;
|
||||||
|
}
|
||||||
|
|
||||||
|
return inside;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isClockwise(points) {
|
||||||
|
// Return true if the tab describes a clockwise polygon (Shoelace formula)
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < points.length; i++) {
|
||||||
|
const curr = points[i];
|
||||||
|
const next = points[(i + 1) % points.length];
|
||||||
|
sum += (next.lng - curr.lng) * (next.lat + curr.lat);
|
||||||
|
}
|
||||||
|
return sum > 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getZoneIndex(latlng) {
|
||||||
|
// Return the index of the polygon where latlng is according to isInPolygon
|
||||||
|
for (let iPolygon = 0; iPolygon < polygons.length; iPolygon++) {
|
||||||
|
if (isInPolygon(latlng, polygons[iPolygon])) {
|
||||||
|
return iPolygon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEventLatLng(e) {
|
||||||
|
// Return the closest latlng to e.latlng among the existing nodes including the first node of currentPolygon
|
||||||
|
// If the closest distance is superior to nodeCatchDistance, then e.latlng is returned
|
||||||
|
const closeNodes = [];
|
||||||
|
// Existing nodes
|
||||||
|
for (const polygon of polygons) {
|
||||||
|
for (const node of polygon) {
|
||||||
|
const d = layerDistance(e.latlng, node);
|
||||||
|
if (d < nodeCatchDistance) {
|
||||||
|
closeNodes.push([d, node]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// First node of currentPolygon
|
||||||
|
if (isDrawing()) {
|
||||||
|
const d = layerDistance(e.latlng, currentPolygon[0]);
|
||||||
|
if (d < nodeCatchDistance) {
|
||||||
|
closeNodes.push([d, currentPolygon[0]]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If there is no close node
|
||||||
|
if (closeNodes.length == 0) {
|
||||||
|
return e.latlng;
|
||||||
|
// Else return the closest close node
|
||||||
|
} else {
|
||||||
|
return closeNodes.reduce( (min, current) => { return current[0] < min[0] ? current : min } )[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLeftClick(e) {
|
||||||
|
setHighlightNodes([]);
|
||||||
|
const latlng = getEventLatLng(e);
|
||||||
|
const length = currentPolygon.length;
|
||||||
|
|
||||||
|
// If it is the first node
|
||||||
|
if (!isDrawing()) {
|
||||||
|
// If the point is not in an existing polygon
|
||||||
|
if (getZoneIndex(latlng) == -1) {
|
||||||
|
setCurrentPolygon([latlng]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it is the last node
|
||||||
|
} else if (latlngEqual(latlng, currentPolygon[0])) {
|
||||||
|
// If the current polygon is a polygon (at least 3 points)
|
||||||
|
if (length >= 3) {
|
||||||
|
// If the current polygon is not circling an existing polygon
|
||||||
|
for (const polygon of polygons) {
|
||||||
|
// meanPoint exists and is strictly inside polygon
|
||||||
|
const meanPoint = {
|
||||||
|
lat: (polygon[0].lat + polygon[1].lat + polygon[2].lat) / 3,
|
||||||
|
lng: (polygon[0].lng + polygon[1].lng + polygon[2].lng) / 3
|
||||||
|
};
|
||||||
|
if (isInPolygon(meanPoint, currentPolygon)) return;
|
||||||
|
}
|
||||||
|
// Making the new polygon clockwise to simplify some algorithms
|
||||||
|
if (!isClockwise(currentPolygon)) currentPolygon.reverse();
|
||||||
|
addPolygon(currentPolygon);
|
||||||
|
setCurrentPolygon([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it is an intermediate node
|
||||||
|
} else {
|
||||||
|
// Is the polygon closing to early ?
|
||||||
|
for (const point of currentPolygon) if (latlngEqual(point, latlng)) return;
|
||||||
|
// Is the new point making the current polygon intersect with itself ?
|
||||||
|
if (isIntersecting([latlng, currentPolygon[length-1]], currentPolygon, false)) return;
|
||||||
|
// Is the new point inside a polygon ?
|
||||||
|
if (getZoneIndex(latlng) != -1) return;
|
||||||
|
// Is the new point making the current polygon intersect with another polygon ?
|
||||||
|
for (const polygon of polygons) {
|
||||||
|
// Strict intersection
|
||||||
|
if (isIntersecting([latlng, currentPolygon[length-1]], polygon, true)) return;
|
||||||
|
// Intersection by joining two non adjacent nodes of polygon
|
||||||
|
let tab = [-1, -1];
|
||||||
|
for (let i = 0; i < polygon.length; i++) {
|
||||||
|
if (latlngEqual(latlng, polygon[i])) tab[0] = i;
|
||||||
|
if (latlngEqual(currentPolygon[length-1], polygon[i])) tab[1] = i;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
tab[0] != -1 && tab[1] != -1 &&
|
||||||
|
(tab[0] != (tab[1] + 1) % polygon.length) &&
|
||||||
|
(tab[1] != (tab[0] + 1) % polygon.length)
|
||||||
|
) return;
|
||||||
|
}
|
||||||
|
setCurrentPolygon([...currentPolygon, latlng]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRightClick(e) {
|
||||||
|
setHighlightNodes([]);
|
||||||
|
// If isDrawing, cancel the currentPolygon
|
||||||
|
if (isDrawing()) {
|
||||||
|
setCurrentPolygon([]);
|
||||||
|
// If not isDrawing, remove the clicked polygon
|
||||||
|
} else {
|
||||||
|
const i = getZoneIndex(e.latlng);
|
||||||
|
if (i != -1) removePolygon(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseMove(e) {
|
||||||
|
const nodes = [];
|
||||||
|
for (const polygon of polygons) {
|
||||||
|
for (const node of polygon) {
|
||||||
|
if (layerDistance(node, e.latlng) < nodeHighlightDistance && node != currentPolygon[0]) nodes.push(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setHighlightNodes(nodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { currentPolygon, highlightNodes, handleLeftClick, handleRightClick, handleMouseMove };
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
import { redirect, usePathname } from "next/navigation";
|
import { redirect, usePathname } from "next/navigation";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
export function usePasswordProtect(loginPath, redirectPath, loading, loggedIn) {
|
export default function usePasswordProtect(loginPath, redirectPath, loading, loggedIn) {
|
||||||
const path = usePathname();
|
const path = usePathname();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loggedIn && !loading && path !== loginPath) {
|
if (!loggedIn && !loading && path !== loginPath) {
|
||||||
@@ -12,4 +12,4 @@ export function usePasswordProtect(loginPath, redirectPath, loading, loggedIn) {
|
|||||||
redirect(redirectPath)
|
redirect(redirectPath)
|
||||||
}
|
}
|
||||||
}, [loggedIn, loading, path]);
|
}, [loggedIn, loading, path]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import {useEffect, useState} from 'react';
|
"use client";
|
||||||
import { useSocketListener } from './useSocketListener';
|
import { useEffect, useState } from 'react';
|
||||||
import { useLocalStorage } from './useLocalStorage';
|
import useSocketListener from './useSocketListener';
|
||||||
import { usePathname } from 'next/navigation';
|
import useLocalStorage from './useLocalStorage';
|
||||||
|
|
||||||
const LOGIN_MESSAGE = "login";
|
const LOGIN_MESSAGE = "login";
|
||||||
const LOGOUT_MESSAGE = "logout";
|
const LOGOUT_MESSAGE = "logout";
|
||||||
const LOGIN_RESPONSE_MESSAGE = "login_response";
|
const LOGIN_RESPONSE_MESSAGE = "login_response";
|
||||||
|
|
||||||
export function useSocketAuth(socket, passwordName) {
|
export default function useSocketAuth(socket, passwordName) {
|
||||||
const [loggedIn, setLoggedIn] = useState(false);
|
const [loggedIn, setLoggedIn] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [waitingForResponse, setWaitingForResponse] = useState(true);
|
const [waitingForResponse, setWaitingForResponse] = useState(true);
|
||||||
@@ -50,4 +50,4 @@ export function useSocketAuth(socket, passwordName) {
|
|||||||
|
|
||||||
|
|
||||||
return {login,logout,password: savedPassword, loggedIn, loading};
|
return {login,logout,password: savedPassword, loggedIn, loading};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { useEffect} from "react";
|
"use client";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
export function useSocketListener(socket, event, callback) {
|
export default function useSocketListener(socket, event, callback) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
socket.on(event,callback);
|
socket.on(event,callback);
|
||||||
return () => {
|
return () => {
|
||||||
socket.off(event, callback);
|
socket.off(event, callback);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user