105 lines
3.6 KiB
TypeScript
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;
|
|
}
|
|
}
|