/** * Geofence Engine - Core logic for geofence calculations * Handles distance calculations and geofence state tracking */ import type { Location, Geofence, GeofenceEvent, CreateGeofenceEventInput } from './types'; /** * Calculate distance between two geographic points using Haversine formula * Returns distance in meters */ export function calculateDistance( lat1: number, lon1: number, lat2: number, lon2: number ): number { const R = 6371e3; // Earth radius in meters const φ1 = (lat1 * Math.PI) / 180; const φ2 = (lat2 * Math.PI) / 180; const Δφ = ((lat2 - lat1) * Math.PI) / 180; const Δλ = ((lon2 - lon1) * Math.PI) / 180; const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) + Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; // Distance in meters } /** * Check if a point is inside a circular geofence */ export function isInsideGeofence( latitude: number, longitude: number, geofence: Geofence ): boolean { if (geofence.shape_type === 'circle') { const distance = calculateDistance( latitude, longitude, geofence.center_latitude, geofence.center_longitude ); return distance <= geofence.radius_meters; } return false; } /** * Check location against all active geofences for a device * Returns array of events (enter/exit) that should be generated */ export async function checkGeofences( location: Location, deviceId: string, geofenceDb: any ): Promise { const events: CreateGeofenceEventInput[] = []; // Convert latitude/longitude to numbers if they're strings const lat = typeof location.latitude === 'string' ? parseFloat(location.latitude) : location.latitude; const lon = typeof location.longitude === 'string' ? parseFloat(location.longitude) : location.longitude; // Get all active geofences for this device const geofences = await geofenceDb.findActiveForDevice(deviceId); for (const geofence of geofences) { // Check if currently inside const isInside = isInsideGeofence(lat, lon, geofence); // Get previous status const status = await geofenceDb.getStatus(deviceId, geofence.id); const wasInside = status ? status.is_inside === 1 : false; // Calculate distance from center const distanceFromCenter = calculateDistance( lat, lon, geofence.center_latitude, geofence.center_longitude ); // Generate events based on state change if (isInside && !wasInside) { // ENTER event events.push({ geofence_id: geofence.id, device_id: deviceId, location_id: location.id!, event_type: 'enter', latitude: location.latitude, longitude: location.longitude, timestamp: location.timestamp, distance_from_center: distanceFromCenter, }); } else if (!isInside && wasInside) { // EXIT event events.push({ geofence_id: geofence.id, device_id: deviceId, location_id: location.id!, event_type: 'exit', latitude: location.latitude, longitude: location.longitude, timestamp: location.timestamp, distance_from_center: distanceFromCenter, }); } // Update status for next check await geofenceDb.updateStatus(deviceId, geofence.id, isInside); } return events; }