diff --git a/backend/models.py b/backend/models.py index 57e216d..6442b18 100644 --- a/backend/models.py +++ b/backend/models.py @@ -17,6 +17,8 @@ class StudySpot(db.Model): study_spot_name = db.Column(db.String(200), nullable=False) building_name = db.Column(db.String(200), nullable=True) address = db.Column(db.String(500), nullable=False) + latitude = db.Column(db.Float, nullable=True) + longitude = db.Column(db.Float, nullable=True) floor = db.Column(db.Integer, nullable=True) tags = db.Column(db.JSON, nullable=False, default=lambda: []) pictures = db.Column(db.JSON, nullable=False, default=lambda: []) @@ -37,6 +39,8 @@ def to_dict(self): 'study_spot_name': self.study_spot_name, 'building_name': self.building_name, 'address': self.address, + 'latitude': self.latitude, + 'longitude': self.longitude, 'floor': self.floor, 'tags': self.tags if self.tags is not None else [], 'pictures': self.pictures if self.pictures is not None else [], diff --git a/backend/routes.py b/backend/routes.py index 2299840..5a9f970 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -241,3 +241,67 @@ def delete_study_spot(spot_id): db.session.rollback() logger.error(f"Error deleting study spot {spot_id}: {str(e)}") return jsonify({'error': 'Failed to delete study spot'}), 500 + + +@api_bp.route('/study_spots/sort_by_distance', methods=['POST']) +def sort_by_distance(): + """Sort study spots by distance from user location.""" + try: + data = request.get_json() + if not data: + return jsonify({'error': 'No data provided'}), 400 + + user_lat = data.get('user_lat') + user_lng = data.get('user_lng') + + if user_lat is None or user_lng is None: + return jsonify({'error': 'user_lat and user_lng are required'}), 400 + + # Convert to float + try: + user_lat = float(user_lat) + user_lng = float(user_lng) + except (ValueError, TypeError): + return jsonify({'error': 'Invalid coordinates'}), 400 + + # Get all study spots + spots = StudySpot.query.all() + + # Calculate distances and sort + spots_with_distance = [] + for spot in spots: + # Skip spots without coordinates + if not hasattr(spot, 'latitude') or not hasattr(spot, 'longitude'): + continue + if spot.latitude is None or spot.longitude is None: + continue + + distance = _haversine_distance(user_lat, user_lng, spot.latitude, spot.longitude) + spot_dict = spot.to_dict() + spot_dict['distance_from_user'] = distance + spots_with_distance.append(spot_dict) + + # Sort by distance (closest first) + spots_with_distance.sort(key=lambda x: x['distance_from_user']) + + return jsonify(spots_with_distance), 200 + + except Exception as e: + logger.error(f"Error sorting by distance: {str(e)}") + return jsonify({'error': 'Failed to sort by distance'}), 500 + + +def _haversine_distance(lat1, lon1, lat2, lon2): + import math + + # Convert decimal degrees to radians + lat1, lon1, lat2, lon2 = map(math.radians, [lat1, lon1, lat2, lon2]) + + # Haversine formula + dlat = lat2 - lat1 + dlon = lon2 - lon1 + a = math.sin(dlat/2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon/2)**2 + c = 2 * math.asin(math.sqrt(a)) + + r = 3956 + return c * r diff --git a/frontend/app/(tabs)/index.tsx b/frontend/app/(tabs)/index.tsx index 35d759e..171ad3b 100644 --- a/frontend/app/(tabs)/index.tsx +++ b/frontend/app/(tabs)/index.tsx @@ -6,8 +6,56 @@ import ParallaxScrollView from '@/components/parallax-scroll-view'; import { ThemedText } from '@/components/themed-text'; import { ThemedView } from '@/components/themed-view'; import { Link } from 'expo-router'; +import * as Location from 'expo-location'; +import { useEffect, useState } from 'react'; export default function HomeScreen() { + + const [userLat, setUserLat] = useState(null); + const [userLng, setUserLng] = useState(null); + const [spots, setSpots] = useState([]); + + async function getUserLocation() { + const { status } = await Location.requestForegroundPermissionsAsync(); + if (status !== 'granted') { + return; + } + + const location = await Location.getCurrentPositionAsync({}); + setUserLat(location.coords.latitude); + setUserLng(location.coords.longitude); + } + + async function fetchSortedByDistance() { + if (!userLat || !userLng) return; + + try { + const response = await fetch('http://localhost:8000/api/study_spots/sort_by_distance', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + user_lat: userLat, + user_lng: userLng + }) + }); + + const sortedSpots = await response.json(); + setSpots(sortedSpots); + } catch (error) { + console.error('Error fetching sorted spots:', error); + } + } + + useEffect(() => { + getUserLocation(); + }, []); + + useEffect(() => { + if (userLat && userLng) { + fetchSortedByDistance(); + } + }, [userLat, userLng]); + return (