import { Injectable, OnModuleInit, Inject, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { TransitBusTrip } from './entities/transit-bus-trip.entity'; import { TransitStation } from './entities/transit-station.entity'; import Redis from 'ioredis'; const GEOFENCE_RADIUS_METERS = 150; @Injectable() export class TransitGeofenceService implements OnModuleInit { private readonly logger = new Logger(TransitGeofenceService.name); constructor( @Inject('REDIS_CLIENT') private readonly redis: Redis, @InjectRepository(TransitBusTrip) private readonly tripsRepo: Repository, @InjectRepository(TransitStation) private readonly stationsRepo: Repository, ) {} onModuleInit() { this.redis.subscribe('channel:bus-locations', (err, count) => { if (err) { this.logger.error('Failed to subscribe to bus-locations channel', err); } else { this.logger.log(`Subscribed to ${count} channels.`); } }); this.redis.on('message', async (channel, message) => { if (channel === 'channel:bus-locations') { try { const data = JSON.parse(message); await this.processBusLocation(data.driverId, data.lat, data.lng); } catch (error) { this.logger.error('Error processing bus location', error); } } }); } private async processBusLocation(driverId: string, lat: number, lng: number) { // 1. Find active bus trip for this driver const trip = await this.tripsRepo.findOne({ where: { driver_id: driverId, status: 'active' }, }); if (!trip) return; // No active trip, ignore // 2. Fetch stations for the route // Note: To optimize, this could be cached in memory const stations = await this.stationsRepo.find({ where: { route_id: trip.route_id }, order: { order_index: 'ASC' }, }); // 3. Find if bus is within any station's radius for (const station of stations) { const distance = this.haversineDistance(lat, lng, Number(station.latitude), Number(station.longitude)); if (distance <= GEOFENCE_RADIUS_METERS) { // Bus is inside the geofence of this station! if (trip.current_station_id !== station.id) { this.logger.log(`🚌 Bus for driver ${driverId} arrived at station: ${station.name}`); // Update trip status trip.current_station_id = station.id; await this.tripsRepo.save(trip); // Publish event so main Tripz backend can notify passengers via WebSockets this.redis.publish( 'channel:bus-arrived', JSON.stringify({ tripId: trip.id, driverId, routeId: trip.route_id, stationId: station.id, stationName: station.name, }), ); } break; // Found the station, no need to check others } } } /** * Calculates the great-circle distance between two points on the Earth's surface. * Returns distance in meters. */ private haversineDistance(lat1: number, lon1: number, lat2: number, lon2: number): number { const toRad = (x: number) => (x * Math.PI) / 180; const R = 6371e3; // Earth's radius in meters const dLat = toRad(lat2 - lat1); const dLon = toRad(lon2 - lon1); const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; } }