Polygonal zone fix + zones redesing + cleaning

This commit is contained in:
Sebastien Riviere
2026-02-16 01:28:31 +01:00
parent 0768609ada
commit e0aaeb63f7
8 changed files with 278 additions and 121 deletions

View File

@@ -2,7 +2,7 @@ compte rendu traque 14-12-2025
Contexte
Cette traque s'est déroulé avec le même site et la même application que la traque de septembre.
Cette traque s'est déroulé avec le même serveur et la même application que la traque de septembre.
Météo : 8°C, pas de pluie, brouillard (visibilité <400m), vent 10km/h
@@ -16,7 +16,9 @@ automatique.
[x] Il est pas évident de voir qu'elles équipes sont encore en jeu sur la page principale admin. Notamment, on voudrait que les
voyants des équipes encore en jeu soit mis en avant.
[x] La visibilité des éléments de la map en calque satellite est mauvaise. Il faut revoir les couleurs.
[ ] Les zones polygonales s'affichent mal sur la version prod de l'app mobile. Les anciennes n'ont pas l'air de disparaitre.
[x] Les zones polygonales s'affichent mal sur la version prod de l'app mobile. Les anciennes n'ont pas l'air de disparaitre.
[x] La zone de jeu (en rouge) est mal comprise, les joueurs évitent de rester dedans alors qu'ils ont le droit.
[ ] L'application n'est pas pleinement intuitive, par exemple le bouton pour actualiser sa position ou alors les icônes des stats.
À faire

View File

@@ -14,14 +14,14 @@ sans raisons apparentes
[ ] Une équipe avait deux téléphones connectés sauf que le premier ne marchait pas bien. Par ailleurs, l'équipe disaient n'avoir
qu'un seul téléphone connecté.
[ ] La photo d'une des équipes ne parvenait pas jusqu'au serveur. Tout semblait normal pour l'équipe.
[ ] Il y a apparement eu un problème de synchronisation de l'affichage entre admin concernant les zones de départs. Il s'est résolu
[x] Il y a apparement eu un problème de synchronisation de l'affichage entre admin concernant les zones de départs. Il s'est résolu
de lui même. Peut être un problème passager de connection.
[x] Le focus sur une équipe dans la page principale des admins est à revoir. Par exemple, le zoom seul devrait désactiver le focus
automatique.
[x] Il est pas évident de voir qu'elles équipes sont encore en jeu sur la page principale admin. Notamment, on voudrait que les
voyants des équipes encore en jeu soit mis en avant.
[x] La visibilité des éléments de la map en calque satellite est mauvaise. Il faut revoir les couleurs.
[ ] Les zones polygonales s'affichent mal sur la version prod de l'app mobile. Les anciennes n'ont pas l'air de disparaitre.
[x] Les zones polygonales s'affichent mal sur la version prod de l'app mobile. Les anciennes n'ont pas l'air de disparaitre.
À faire

View File

@@ -1,5 +1,5 @@
// React
import { useState, useEffect, Fragment } from 'react';
import { useState, useEffect, useMemo, Fragment } from 'react';
import { View, Text, Image, Alert, StyleSheet, TouchableOpacity } from 'react-native';
// Expo
import { useRouter } from 'expo-router';
@@ -15,19 +15,37 @@ import { useTimeDifference } from '../hook/useTimeDifference';
// Util
import { GameState } from '../util/gameState';
import { TimerMMSS } from '../components/timer';
import { secondsToMMSS } from '../util/format';
import { secondsToMMSS } from '../util/functions';
import { Colors } from '../util/colors';
const Interface = () => {
const router = useRouter();
const {messages, nextZoneDate, isShrinking, startLocationTracking, stopLocationTracking, gameState} = useTeamContext();
const {loggedIn, logout, loading} = useTeamConnexion();
const {name, ready, captured, locationSendDeadline, outOfZone, outOfZoneDeadline, hasHandicap, enemyHasHandicap} = useGame();
const {name, ready, captured, locationSendDeadline, sendCurrentPosition, outOfZone, outOfZoneDeadline, hasHandicap, enemyHasHandicap} = useGame();
const [timeLeftSendLocation] = useTimeDifference(locationSendDeadline, 1000);
const [timeLeftNextZone] = useTimeDifference(nextZoneDate, 1000);
const [timeLeftOutOfZone] = useTimeDifference(outOfZoneDeadline, 1000);
const [bottomContainerHeight, setBottomContainerHeight] = useState(0);
const statusMessage = useMemo(() => {
switch (gameState) {
case GameState.SETUP:
return messages?.waiting || "Préparation de la partie";
case GameState.PLACEMENT:
return "Phase de placement";
case GameState.PLAYING:
if (captured) return messages?.captured || "Vous avez été éliminé...";
if (!outOfZone) return "La partie est en cours";
if (!hasHandicap) return `Veuillez retourner dans la zone\nHandicap dans ${secondsToMMSS(-timeLeftOutOfZone)}`;
else return `Veuillez retourner dans la zone\nVotre position est révélée en continue`;
case GameState.FINISHED:
return `Vous avez ${captured ? (messages?.loser || "perdu...") : (messages?.winner || "gagné !")}`;
default:
return "Inconnue";
}
}, [gameState, messages, outOfZone, hasHandicap, timeLeftOutOfZone, captured]);
// Router
useEffect(() => {
if (!loading) {
@@ -61,45 +79,35 @@ const Interface = () => {
<Text style={{fontSize: 36, fontWeight: "bold", textAlign: "center"}}>{(name ?? "Indisponible")}</Text>
</View>
<View style={styles.logContainer}>
<TouchableOpacity style={{width:"100%"}}>
{ gameState == GameState.SETUP && <Text style={styles.gameState}>{messages?.waiting || "Préparation de la partie"}</Text>}
{ gameState == GameState.PLACEMENT && <Text style={styles.gameState}>Phase de placement</Text>}
{ gameState == GameState.PLAYING && !outOfZone && <Text style={styles.gameState}>La partie est en cours</Text>}
{ gameState == GameState.PLAYING && outOfZone && !hasHandicap && <Text style={styles.gameStateOutOfZone}>{`Veuillez retourner dans la zone\nHandicap dans ${secondsToMMSS(-timeLeftOutOfZone)}`}</Text>}
{ gameState == GameState.PLAYING && hasHandicap && <Text style={styles.gameStateOutOfZone}>{`Veuillez retourner dans la zone\nVotre position est révélée en continue`}</Text>}
{ gameState == GameState.FINISHED && <Text style={styles.gameState}>La partie est terminée</Text>}
<TouchableOpacity style={styles.gameState}>
<Text style={{fontSize: 18}}>{statusMessage}</Text>
</TouchableOpacity>
</View>
<View style={styles.infoContainer}>
{ gameState == GameState.PLACEMENT &&
<View style={styles.timersContainer}>
<View style={[styles.readyIndicator, {backgroundColor: ready ? "#3C3" : "#C33"}]}>
<Text style={{color: '#fff', fontSize: 16}}>{ready ? "Placé" : "Non placé"}</Text>
</View>
</View>
}
{ gameState == GameState.PLAYING && !captured && <Fragment>
<View style={styles.timersContainer}>
<TimerMMSS style={{width: "50%"}} title={isShrinking ? "Réduction de la zone" : "Durée de la zone"} seconds={-timeLeftNextZone} />
<TimerMMSS style={{width: "50%"}} title={"Position envoyée dans"} seconds={!hasHandicap ? -timeLeftSendLocation: 0} />
</Fragment>}
</View>
{ enemyHasHandicap &&
<Text style={{fontSize: 18, marginTop: 6, fontWeight: "bold"}}>Position ennemie révélée en continue !</Text>
}
</Fragment>}
{ gameState == GameState.PLAYING && captured &&
<View style={[styles.timersContainer, {height: 61}]}>
<Text style={{fontSize: 20}}>{messages?.captured || "Vous avez été éliminé..."}</Text>
</View>
}
{ gameState == GameState.FINISHED &&
<View style={[styles.timersContainer, {height: 61}]}>
{captured && <Text style={{fontSize: 20}}>{captured ? (messages?.loser || "Vous avez perdu...") : (messages?.winner || "Vous avez gagné !")}</Text>}
</View>
}
</View>
<View style={styles.bottomContainer} onLayout={(event) => setBottomContainerHeight(event.nativeEvent.layout.height)}>
<CustomMap/>
{ gameState == GameState.PLAYING && !captured && !hasHandicap &&
<TouchableOpacity style={styles.updatePosition} onPress={sendCurrentPosition}>
<Image source={require("../assets/images/update_position.png")} style={{width: 40, height: 40}} resizeMode="contain"></Image>
</TouchableOpacity>
}
{ gameState == GameState.PLAYING && !captured &&
<Drawer height={bottomContainerHeight}/>
}
</View>
</View>
);
@@ -138,19 +146,9 @@ const styles = StyleSheet.create({
borderRadius: 10,
width: "100%",
backgroundColor: 'white',
fontSize: 18,
padding: 10,
},
gameStateOutOfZone: {
borderWidth: 2,
borderRadius: 10,
width: "100%",
backgroundColor: 'white',
fontSize: 18,
padding: 10,
borderColor: 'red'
},
timersContainer: {
infoContainer: {
width: '100%',
alignItems: 'center',
justifyContent: 'center',
@@ -168,5 +166,18 @@ const styles = StyleSheet.create({
},
bottomContainer: {
flex: 1,
}
},
updatePosition: {
position: 'absolute',
right: 30,
bottom: 80,
width: 60,
height: 60,
borderRadius: 30,
backgroundColor: 'white',
borderWidth: 4,
borderColor: 'black',
alignItems: 'center',
justifyContent: 'center',
},
});

View File

@@ -1,5 +1,5 @@
// React
import { useState, useEffect, Fragment } from 'react';
import { useState, useEffect, useMemo, Fragment } from 'react';
import { ScrollView, View, Text, Image, StyleSheet, TouchableOpacity, TouchableHighlight, Alert } from 'react-native';
import Collapsible from 'react-native-collapsible';
import LinearGradient from 'react-native-linear-gradient';
@@ -16,7 +16,7 @@ import { useGame } from '../hook/useGame';
// Util
import { GameState } from '../util/gameState';
import { Colors } from '../util/colors';
import { secondsToHHMMSS } from '../util/format';
import { secondsToHHMMSS } from '../util/functions';
export const Drawer = ({ height }) => {
const [collapsibleState, setCollapsibleState] = useState(true);
@@ -25,11 +25,19 @@ export const Drawer = ({ height }) => {
const {gameState, startDate} = useTeamContext();
const {capture, enemyName, captureCode, name, teamId, distance, finishDate, nCaptures, nSentLocation, hasHandicap} = useGame();
const [timeSinceStart] = useTimeDifference(startDate, 1000);
const [avgSpeed, setAvgSpeed] = useState(0); // Speed in m/s
const [enemyImageURI, setEnemyImageURI] = useState("../assets/images/missing_image.jpg");
const [captureStatus, setCaptureStatus] = useState(0); // 0 : no capture | 1 : waiting for response from server | 2 : capture failed | 3 : capture succesful
const captureStatusColor = {0: "#777", 1: "#FFA500", 2: "#FF6B6B", 3: "#81C784"};
const avgSpeed = useMemo(() => {
const hours = (finishDate ? (finishDate - startDate) : timeSinceStart*1000) / 1000 / 3600;
if (hours <= 0 || distance <= 0) return 0;
const km = distance / 1000;
const speed = km / hours;
return parseFloat(speed.toFixed(1));
}, [finishDate, startDate, timeSinceStart, distance]);
// Capture state update
useEffect(() => {
if (captureStatus == 2 || captureStatus == 3) {
@@ -45,16 +53,7 @@ export const Drawer = ({ height }) => {
setEnemyImageURI(`${SERVER_URL}/photo/enemy?team=${teamId}&t=${new Date().getTime()}`);
}, [SERVER_URL, enemyName, teamId]);
// Update the average speed
useEffect(() => {
const hours = (finishDate ? (finishDate - startDate) : timeSinceStart*1000) / 1000 / 3600;
const km = distance / 1000;
setAvgSpeed(Math.floor(km / hours * 10) / 10);
}, [distance, finishDate, startDate, timeSinceStart]);
if (gameState != GameState.PLAYING) return;
function handleCapture() {
const handleCapture = () => {
if (captureStatus != 1) {
setCaptureStatus(1);
capture(enemyCaptureCode)
@@ -71,7 +70,7 @@ export const Drawer = ({ height }) => {
});
setEnemyCaptureCode("");
}
}
};
return (
<View style={styles.outerDrawerContainer}>

View File

@@ -0,0 +1,70 @@
import { Fragment } from 'react';
import { Polygon } from 'react-native-maps';
import { circleToPolygon } from '../util/functions';
export const InvertedPolygon = ({id, coordinates, fillColor}) => {
// We create 3 rectangles covering earth, with the first rectangle centered on the hole
const shift = Math.floor(coordinates[0].longitude);
const lat = 85;
const lon = 60;
const worldOuterBounds1 = [
{ latitude: -lat, longitude: -lon + shift },
{ latitude: -lat, longitude: lon + shift },
{ latitude: lat, longitude: lon + shift },
{ latitude: lat, longitude: -lon + shift },
];
const worldOuterBounds2 = [
{ latitude: -lat, longitude: -lon + 120 + shift },
{ latitude: -lat, longitude: lon + 120 + shift },
{ latitude: lat, longitude: lon + 120 + shift },
{ latitude: lat, longitude: -lon + 120 + shift },
];
const worldOuterBounds3 = [
{ latitude: -lat, longitude: -lon + 240 + shift },
{ latitude: -lat, longitude: lon + 240 + shift },
{ latitude: lat, longitude: lon + 240 + shift },
{ latitude: lat, longitude: -lon + 240 + shift },
];
return <Fragment>
<Polygon
key={`${id}-mask-1`}
geodesic={true}
holes={[coordinates]}
coordinates={worldOuterBounds1}
fillColor={fillColor}
strokeColor="rgba(0, 0, 0, 0)"
/>
<Polygon
key={`${id}-mask-2`}
geodesic={true}
coordinates={worldOuterBounds2}
fillColor={fillColor}
strokeColor="rgba(0, 0, 0, 0)"
/>
<Polygon
key={`${id}-mask-3`}
geodesic={true}
coordinates={worldOuterBounds3}
fillColor={fillColor}
strokeColor="rgba(0, 0, 0, 0)"
/>
</Fragment>;
};
export const InvertedCircle = ({id, center, radius, fillColor}) => {
return <InvertedPolygon id={id} coordinates={circleToPolygon({center: center, radius: radius})} fillColor={fillColor} />;
};
export const DashedCircle = ({id, center, radius, fillColor, strokeColor, strokeWidth, lineDashPattern}) => {
return (
<Polygon
key={id}
coordinates={circleToPolygon({center: center, radius: radius})}
fillColor={fillColor}
strokeColor={strokeColor}
strokeWidth={strokeWidth}
lineDashPattern={lineDashPattern}
/>
);
};

View File

@@ -1,8 +1,10 @@
// React
import { useState, useEffect, useRef, Fragment } from 'react';
import { useState, useEffect, useMemo, useRef } from 'react';
import { View, Image, Alert, StyleSheet, TouchableOpacity } from 'react-native';
import MapView, { Marker, Circle, Polygon } from 'react-native-maps';
import LinearGradient from 'react-native-linear-gradient';
// Components
import { DashedCircle, InvertedCircle, InvertedPolygon } from './layer';
// Contexts
import { useTeamContext } from '../context/teamContext';
// Hooks
@@ -13,77 +15,143 @@ import { ZoneTypes, InitialRegions } from '../util/constants';
export const CustomMap = () => {
const {zoneType, zoneExtremities, location, gameState} = useTeamContext();
const {sendCurrentPosition, enemyLocation, startingArea, captured, lastSentLocation, hasHandicap} = useGame();
const mapRef = useRef(null);
const {enemyLocation, startingArea, lastSentLocation, hasHandicap} = useGame();
const [centerMap, setCenterMap] = useState(true);
const mapRef = useRef(null);
// Center the map on user position
useEffect(() => {
if (centerMap && mapRef.current && location) {
mapRef.current.animateToRegion({latitude: location[0], longitude: location[1], latitudeDelta: 0, longitudeDelta: 0.02}, 1000);
}
}, [centerMap, mapRef, location]);
}, [centerMap, location]);
// Map layers
const latToLatitude = (pos) => ({latitude: pos.lat, longitude: pos.lng});
const startZone = useMemo(() => {
if (gameState != GameState.PLACEMENT || !startingArea) return null;
return (
<View style={styles.mapContainer}>
<Circle key="start-zone" center={{ latitude: startingArea.center.lat, longitude: startingArea.center.lng }} radius={startingArea.radius} strokeWidth={2} strokeColor={`rgba(0, 0, 255, 1)`} fillColor={`rgba(0, 0, 255, 0.2)`}/>
);
}, [gameState, startingArea]);
const gameZone = useMemo(() => {
if (gameState !== GameState.PLAYING || !zoneExtremities) return null;
const items = [];
const nextZoneStrokeColor = "rgb(90, 90, 90)";
const zoneColor = "rgba(25, 83, 169, 0.4)";
const strokeWidth = 3;
const lineDashPattern = [30, 10];
if (zoneType === ZoneTypes.circle) {
if (zoneExtremities.begin) items.push(
<InvertedCircle
key="game-zone-begin-circle"
id="game-zone-begin-circle"
center={latToLatitude(zoneExtremities.begin.center)}
radius={zoneExtremities.begin.radius}
fillColor={zoneColor}
/>
);
if (zoneExtremities.end) items.push(
<DashedCircle
key="game-zone-end-circle"
id="game-zone-end-circle"
center={latToLatitude(zoneExtremities.end.center)}
radius={zoneExtremities.end.radius}
strokeColor={nextZoneStrokeColor}
strokeWidth={strokeWidth}
lineDashPattern={lineDashPattern}
/>
);
} else if (zoneType === ZoneTypes.polygon) {
if (zoneExtremities.begin) items.push(
<InvertedPolygon
key="game-zone-begin-poly"
id="game-zone-begin-poly"
coordinates={zoneExtremities.begin.polygon.map(pos => latToLatitude(pos))}
fillColor={zoneColor}
/>
);
if (zoneExtremities.end) items.push(
<Polygon
key="game-zone-end-poly"
coordinates={zoneExtremities.end.polygon.map(pos => latToLatitude(pos))}
strokeColor={nextZoneStrokeColor}
strokeWidth={strokeWidth}
lineDashPattern={lineDashPattern}
/>
);
}
return items.length ? items : null;
}, [gameState, zoneType, zoneExtremities]);
const currentPositionMarker = useMemo(() => {
if (!location) return null;
return (
<Marker key={"current-position-marker"} coordinate={{ latitude: location[0], longitude: location[1] }} anchor={{ x: 0.33, y: 0.33 }} onPress={() => Alert.alert("Position actuelle", "Ceci est votre position")}>
<Image source={require("../assets/images/marker/blue.png")} style={styles.markerImage} resizeMode="contain"/>
</Marker>
);
}, [location]);
const lastPositionMarker = useMemo(() => {
if (gameState != GameState.PLAYING || !lastSentLocation || hasHandicap) return null;
return (
<Marker key={"last-position-marker"} coordinate={{ latitude: lastSentLocation[0], longitude: lastSentLocation[1] }} anchor={{ x: 0.33, y: 0.33 }} onPress={() => Alert.alert("Position envoyée", "Ceci est votre dernière position connue par le serveur")}>
<Image source={require("../assets/images/marker/grey.png")} style={styles.markerImage} resizeMode="contain"/>
</Marker>
);
}, [gameState, hasHandicap, lastSentLocation]);
const enemyPositionMarker = useMemo(() => {
if (gameState != GameState.PLAYING || !enemyLocation || hasHandicap) return null;
return (
<Marker key={"enemy-position-marker"} coordinate={{ latitude: enemyLocation[0], longitude: enemyLocation[1] }} anchor={{ x: 0.33, y: 0.33 }} onPress={() => Alert.alert("Position ennemie", "Ceci est la dernière position de vos ennemis connue")}>
<Image source={require("../assets/images/marker/red.png")} style={styles.markerImage} resizeMode="contain"/>
</Marker>
);
}, [gameState, hasHandicap, enemyLocation]);
return (
<View style={styles.container}>
<MapView ref={mapRef} style={{flex: 1}} initialRegion={InitialRegions.paris} mapType="standard" onTouchMove={() => setCenterMap(false)} toolbarEnabled={false}>
{ gameState == GameState.PLACEMENT && startingArea &&
<Circle center={{ latitude: startingArea.center.lat, longitude: startingArea.center.lng }} radius={startingArea.radius} strokeWidth={2} strokeColor={`rgba(0, 0, 255, 1)`} fillColor={`rgba(0, 0, 255, 0.2)`}/>
}
{ gameState == GameState.PLAYING && zoneExtremities &&
<Fragment>
{ zoneType == ZoneTypes.circle && zoneExtremities.begin && <Circle center={latToLatitude(zoneExtremities.begin.center)} radius={zoneExtremities.begin.radius} strokeColor="red" fillColor="rgba(255,0,0,0.1)" strokeWidth={2} />}
{ zoneType == ZoneTypes.circle && zoneExtremities.end && <Circle center={latToLatitude(zoneExtremities.end.center)} radius={zoneExtremities.end.radius} strokeColor="green" fillColor="rgba(0,255,0,0.1)" strokeWidth={2} />}
{ zoneType == ZoneTypes.polygon && zoneExtremities.begin && <Polygon coordinates={zoneExtremities.begin.polygon.map(pos => latToLatitude(pos))} strokeColor="red" fillColor="rgba(255,0,0,0.1)" strokeWidth={2} /> }
{ zoneType == ZoneTypes.polygon && zoneExtremities.end && <Polygon coordinates={zoneExtremities.end.polygon.map(pos => latToLatitude(pos))} strokeColor="green" fillColor="rgba(0,255,0,0.1)" strokeWidth={2} /> }
</Fragment>
}
{ location &&
<Marker coordinate={{ latitude: location[0], longitude: location[1] }} anchor={{ x: 0.33, y: 0.33 }} onPress={() => Alert.alert("Position actuelle", "Ceci est votre position")}>
<Image source={require("../assets/images/marker/blue.png")} style={{width: 24, height: 24}} resizeMode="contain"/>
</Marker>
}
{ gameState == GameState.PLAYING && lastSentLocation && !hasHandicap &&
<Marker coordinate={{ latitude: lastSentLocation[0], longitude: lastSentLocation[1] }} anchor={{ x: 0.33, y: 0.33 }} onPress={() => Alert.alert("Position envoyée", "Ceci est votre dernière position connue par le serveur")}>
<Image source={require("../assets/images/marker/grey.png")} style={{width: 24, height: 24}} resizeMode="contain"/>
</Marker>
}
{ gameState == GameState.PLAYING && enemyLocation && !hasHandicap &&
<Marker coordinate={{ latitude: enemyLocation[0], longitude: enemyLocation[1] }} anchor={{ x: 0.33, y: 0.33 }}>
<Image source={require("../assets/images/marker/red.png")} style={{width: 24, height: 24}} resizeMode="contain" onPress={() => Alert.alert("Position ennemie", "Ceci est la dernière position de vos ennemis connue")}/>
</Marker>
}
{startZone}
{gameZone}
{currentPositionMarker}
{lastPositionMarker}
{enemyPositionMarker}
</MapView>
<LinearGradient colors={['rgba(0,0,0,0.3)', 'rgba(0,0,0,0)']} style={{height: 40, width: "100%", position: "absolute"}}/>
{ !centerMap &&
<TouchableOpacity style={styles.centerMapContainer} onPress={() => setCenterMap(true)}>
<TouchableOpacity style={styles.centerMap} onPress={() => setCenterMap(true)}>
<Image source={require("../assets/images/centerMap.png")} style={{width: 30, height: 30}} resizeMode="contain"></Image>
</TouchableOpacity>
}
{ gameState == GameState.PLAYING && !captured &&
<View style={styles.toolBarRight}>
{ !hasHandicap &&
<TouchableOpacity style={styles.updatePositionContainer} onPress={sendCurrentPosition}>
<Image source={require("../assets/images/update_position.png")} style={{width: 40, height: 40}} resizeMode="contain"></Image>
</TouchableOpacity>
}
</View>
}
</View>
);
};
const styles = StyleSheet.create({
mapContainer: {
container: {
flex: 1,
width: '100%',
borderTopLeftRadius: 30,
borderTopRightRadius: 30,
overflow: 'hidden',
},
centerMapContainer: {
centerMap: {
position: 'absolute',
right: 20,
top: 20,
@@ -96,19 +164,8 @@ const styles = StyleSheet.create({
alignItems: 'center',
justifyContent: 'center',
},
toolBarRight: {
position: 'absolute',
right: 30,
bottom: 80
},
updatePositionContainer: {
width: 60,
height: 60,
borderRadius: 30,
backgroundColor: 'white',
borderWidth: 4,
borderColor: 'black',
alignItems: 'center',
justifyContent: 'center',
},
markerImage: {
width: 24,
height: 24
}
});

View File

@@ -1,7 +1,7 @@
// React
import { View, Text, StyleSheet } from 'react-native';
// Util
import { secondsToMMSS } from '../util/format';
import { secondsToMMSS } from '../util/functions';
export const TimerMMSS = ({ title, seconds, style }) => {
return (

View File

@@ -1,3 +1,21 @@
export const circleToPolygon = (circle) => {
// circle : {center: {latitude: ..., longitude: ...}, radius: ...}
// polygon : [{latitude: ..., longitude: ...}, ...]
const polygon = [];
const center = circle.center;
const radiusInDegrees = circle.radius / 111320; // Approximation m -> deg
for (let i = 0; i < 360; i += 5) {
const rad = (i * Math.PI) / 180;
polygon.push({
latitude: center.latitude + radiusInDegrees * Math.sin(rad),
longitude: center.longitude + radiusInDegrees * Math.cos(rad) / Math.cos(center.latitude * Math.PI / 180),
});
}
return polygon;
};
export const secondsToMMSS = (seconds) => {
if (!Number.isInteger(seconds)) return "Inconnue";
if (seconds < 0) seconds = 0;