From 1e405bb87f71d01574596b281d48fc5c5c41a86f Mon Sep 17 00:00:00 2001 From: mudabbir-ahmad Date: Tue, 7 Apr 2026 17:19:25 +0100 Subject: [PATCH] Add FOV cone visualization to map screens; refactor geoMath utility for heading and cone calculations Have FOV config as it's own thing so it's easier to change the size of the FOV triangle without having to change multiple parts of the maths for it. Fov triangle needed because for some reason maps doesn't let us have our own fov ?? --- server/server.js | 4 +-- src/components/gameplay/PlayerMapView.js | 40 +++++++-------------- src/components/screens/ExpandedMapScreen.js | 40 ++++++++------------- src/components/screens/MapScreen.js | 33 +++++------------ src/utils/geoMath.js | 31 ++++++++++++++-- 5 files changed, 64 insertions(+), 84 deletions(-) diff --git a/server/server.js b/server/server.js index fe73fdc..1653266 100644 --- a/server/server.js +++ b/server/server.js @@ -4,7 +4,7 @@ const path = require('path'); // --- Configuration --- -const PORT = process.env.PORT || 80; +const PORT = process.env.PORT || 3000; const DATA_DIR = path.join(__dirname, 'data'); // --- Data Helpers --- @@ -560,7 +560,6 @@ addRoute('GET', '/game-data/:gid', (req, res, params) => { // Helper: convert storage cache to API response format const toCacheApi = (cache, gid) => ({ id: cache.CacheId, - name: cache.Name || '', coordinates: { latitude: cache.Latitude, longitude: cache.Longitude }, radius: cache.TriggerMeters, clue: cache.Title, @@ -573,7 +572,6 @@ const toCacheApi = (cache, gid) => ({ // Helper: convert API payload to storage cache format const toCacheStorage = (data, cacheId) => ({ CacheId: cacheId, - Name: data.name || data.Name || '', Title: data.clue || data.Title || '', Latitude: data.latitude != null ? data.latitude : (data.Latitude || 0), Longitude: data.longitude != null ? data.longitude : (data.Longitude || 0), diff --git a/src/components/gameplay/PlayerMapView.js b/src/components/gameplay/PlayerMapView.js index b4f3dfb..7ac9e0c 100644 --- a/src/components/gameplay/PlayerMapView.js +++ b/src/components/gameplay/PlayerMapView.js @@ -1,6 +1,6 @@ import React from 'react'; -import {StyleSheet, View} from 'react-native'; -import MapView, {Circle, Marker} from 'react-native-maps'; +import MapView, {Circle, Polygon} from 'react-native-maps'; +import {getFovCone} from '../../utils/geoMath'; const PlayerMapView = ({ userLocation, visibleCache, heading }) => { // Initialisation ------------- @@ -10,6 +10,10 @@ const PlayerMapView = ({ userLocation, visibleCache, heading }) => { if (!userLocation) return null; + const coneCoords = (heading !== null && heading !== undefined) + ? getFovCone(userLocation, heading) + : null; + return ( { strokeColor="rgba(250, 204, 21, 0.90)" /> ) : null} - {heading !== null && heading !== undefined && ( - - - - - + {coneCoords && ( + )} ); }; -const styles = StyleSheet.create({ - coneWrap: {width: 22, height: 24, alignItems: 'center'}, - cone: { - width: 0, - height: 0, - borderTopWidth: 24, - borderLeftWidth: 11, - borderRightWidth: 11, - borderTopColor: 'rgba(37,99,235,0.70)', - borderLeftColor: 'transparent', - borderRightColor: 'transparent', - }, -}); export default PlayerMapView; diff --git a/src/components/screens/ExpandedMapScreen.js b/src/components/screens/ExpandedMapScreen.js index 148e829..4f3d41e 100644 --- a/src/components/screens/ExpandedMapScreen.js +++ b/src/components/screens/ExpandedMapScreen.js @@ -1,10 +1,10 @@ import React, {useEffect, useState} from 'react'; import {StyleSheet, View} from 'react-native'; -import MapView, {Circle, Marker} from 'react-native-maps'; +import MapView, {Circle, Marker, Polygon} from 'react-native-maps'; import * as Location from 'expo-location'; import {Magnetometer} from 'expo-sensors'; import Screen from '../layout/Screen'; -import {toHeading} from '../../utils/geoMath'; +import {getFovCone, toHeading} from '../../utils/geoMath'; const DEFAULT_REGION = {latitude: 51.5074, longitude: -0.1278, latitudeDelta: 0.01, longitudeDelta: 0.01}; @@ -54,6 +54,10 @@ const ExpandedMapScreen = ({route}) => { // View ----------------------- + const coneCoords = (userLocation && heading !== null) + ? getFovCone(userLocation, heading) + : null; + return ( @@ -78,19 +82,14 @@ const ExpandedMapScreen = ({route}) => { /> ))} - {/* Heading cone for both admin and player */} - {userLocation && heading !== null && ( - - - - - + {/* Heading FOV cone */} + {coneCoords && ( + )} @@ -101,17 +100,6 @@ const ExpandedMapScreen = ({route}) => { const styles = StyleSheet.create({ container: {padding: 0}, mapWrap: {flex: 1}, - coneWrap: {width: 22, height: 24, alignItems: 'center'}, - cone: { - width: 0, - height: 0, - borderTopWidth: 24, - borderLeftWidth: 11, - borderRightWidth: 11, - borderTopColor: 'rgba(37,99,235,0.70)', - borderLeftColor: 'transparent', - borderRightColor: 'transparent', - }, }); export default ExpandedMapScreen; diff --git a/src/components/screens/MapScreen.js b/src/components/screens/MapScreen.js index bd8f710..5fe5878 100644 --- a/src/components/screens/MapScreen.js +++ b/src/components/screens/MapScreen.js @@ -1,6 +1,6 @@ import React, {useCallback, useEffect, useState} from 'react'; import {ActivityIndicator, Pressable, ScrollView, StyleSheet, Text, TextInput, View} from 'react-native'; -import MapView, {Circle, Marker} from 'react-native-maps'; +import MapView, {Circle, Marker, Polygon} from 'react-native-maps'; import * as Location from 'expo-location'; import {Magnetometer} from 'expo-sensors'; import Screen from '../layout/Screen'; @@ -11,7 +11,7 @@ import PlayerMapView from '../gameplay/PlayerMapView'; import useGameHook from '../../hooks/useGameHook'; import usePlayerGame from '../../hooks/usePlayerGame'; import {getSession, setSessionGroup, setSessionUser} from '../../hooks/SessionStore'; -import {toHeading} from '../../utils/geoMath'; +import {getFovCone, toHeading} from '../../utils/geoMath'; const DEFAULT_REGION = {latitude: 51.5074, longitude: -0.1278, latitudeDelta: 0.05, longitudeDelta: 0.05}; @@ -378,17 +378,12 @@ const MapScreen = ({navigation}) => { ))} {userLocation && heading !== null && ( - - - - - + )} @@ -563,18 +558,6 @@ const styles = StyleSheet.create({ cacheSection: {flex: 1, backgroundColor: '#f3f4f6', paddingHorizontal: 12, paddingTop: 10}, emptyText: {color: '#9ca3af', textAlign: 'center', marginTop: 20, fontSize: 14}, mapWrap: {flex: 1}, - // Heading direction cone (like Google Maps) - headingConeWrap: {width: 22, height: 24, alignItems: 'center'}, - headingCone: { - width: 0, - height: 0, - borderTopWidth: 24, - borderLeftWidth: 11, - borderRightWidth: 11, - borderTopColor: 'rgba(37,99,235,0.70)', - borderLeftColor: 'transparent', - borderRightColor: 'transparent', - }, }); export default MapScreen; diff --git a/src/utils/geoMath.js b/src/utils/geoMath.js index b0c11ad..748d5a6 100644 --- a/src/utils/geoMath.js +++ b/src/utils/geoMath.js @@ -1,9 +1,34 @@ import * as geolib from 'geolib'; -// Convert raw Magnetometer {x, y} reading to a 0-360° heading +// --- FOV Configuration --- +const FOV_DISTANCE = 20; // metres — radius of arc + length of straight edges +const FOV_ANGLE = 45; // degrees — total field of view (halved for each side) + +// Convert raw Magnetometer {x, y} reading to a 0-360° heading (0° = north) const toHeading = ({x, y}) => { const angle = Math.atan2(y, x) * (180 / Math.PI); - return angle < 0 ? angle + 360 : angle; + const heading = 90 - angle; + return heading < 0 ? heading + 360 : heading % 360; +}; + +// Build a pie-shaped FOV sector as polygon coordinates +const getFovCone = (location, headingDeg, radiusMeters = FOV_DISTANCE, halfAngle = FOV_ANGLE / 2, arcSegments = 20) => { + const origin = {latitude: location.latitude, longitude: location.longitude}; + const startBearing = headingDeg - halfAngle; + const endBearing = headingDeg + halfAngle; + const step = (endBearing - startBearing) / arcSegments; + + // Tip of the sector + const points = [location]; + + // Arc points at radiusMeters distance from origin + for (let i = 0; i <= arcSegments; i++) { + const bearing = startBearing + step * i; + const dest = geolib.computeDestinationPoint(origin, radiusMeters, bearing); + points.push({latitude: dest.latitude, longitude: dest.longitude}); + } + + return points; }; const isWithinRadius = (playerCoords, cacheCoords, radius) => { @@ -20,5 +45,5 @@ const isLookingAtCache = (playerHeading, playerCoords, cacheCoords, tolerance = return diff <= tolerance || diff >= 360 - tolerance; }; -export { toHeading, isWithinRadius, isLookingAtCache }; +export { toHeading, getFovCone, isWithinRadius, isLookingAtCache };