Files
tripz-llc/backend-transit/src/transit/transit-geofence.service.ts
T

105 lines
3.6 KiB
TypeScript

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<TransitBusTrip>,
@InjectRepository(TransitStation) private readonly stationsRepo: Repository<TransitStation>,
) {}
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;
}
}