Add Geofence MVP feature implementation
Implemented complete MVP for geofencing functionality with database, backend logic, MQTT integration, and API endpoints. **Phase 1: Database & Core Logic** - scripts/init-geofence-db.js: Database initialization for Geofence tables - lib/types.ts: TypeScript types for Geofence, GeofenceEvent, GeofenceStatus - lib/geofence-engine.ts: Core geofencing logic (Haversine distance, state tracking) - lib/geofence-db.ts: Database layer with CRUD operations - package.json: Added db:init:geofence script **Phase 2: MQTT Integration & Email Notifications** - emails/geofence-enter.tsx: React Email template for enter events - emails/geofence-exit.tsx: React Email template for exit events - lib/email-renderer.ts: Added geofence email rendering functions - lib/geofence-notifications.ts: Notification service for geofence events - lib/mqtt-subscriber.ts: Integrated automatic geofence checking on location updates **Phase 3: Minimal API** - app/api/geofences/route.ts: GET (list) and POST (create) endpoints - app/api/geofences/[id]/route.ts: DELETE endpoint - All endpoints with authentication and ownership checks **MVP Simplifications:** - No zone limit enforcement (unlimited for all users) - No notification flags (always send Enter + Exit emails) - Device assignment required (no NULL device logic) - Circular geofences only **Features:** ✅ Automatic geofence detection on MQTT location updates ✅ Email notifications for enter/exit events ✅ State tracking to prevent duplicate events ✅ REST API for geofence management ✅ Non-blocking async processing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
123
lib/geofence-engine.ts
Normal file
123
lib/geofence-engine.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* 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<CreateGeofenceEventInput[]> {
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user