From a15e65c42e7d6a2706402e1338b4dd840c794def Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Sat, 18 Jul 2026 18:48:25 +0300 Subject: [PATCH] feat: implement geofence using redis pub-sub --- backend-transit/.env.example | 4 + backend-transit/docker-compose.yml | 7 ++ backend-transit/package.json | 5 +- .../src/transit/transit-geofence.service.ts | 104 ++++++++++++++++++ backend-transit/src/transit/transit.module.ts | 18 ++- 5 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 backend-transit/src/transit/transit-geofence.service.ts diff --git a/backend-transit/.env.example b/backend-transit/.env.example index 1e06154..2adad36 100644 --- a/backend-transit/.env.example +++ b/backend-transit/.env.example @@ -12,3 +12,7 @@ DB_PORT=5432 DB_USER=transit_user DB_PASSWORD=26ab87247a55f86032c943b7e361c475cd3ca8b3b772d82e2fe6a9b5ee0839a3 DB_NAME=tripz_transit + +# --- Redis (Shared with Main Tripz Backend) --- +REDIS_HOST=tripz-redis +REDIS_PORT=6379 diff --git a/backend-transit/docker-compose.yml b/backend-transit/docker-compose.yml index c11865d..d3e0a51 100644 --- a/backend-transit/docker-compose.yml +++ b/backend-transit/docker-compose.yml @@ -22,6 +22,13 @@ services: - .env depends_on: - transit-db + networks: + - default + - tripz-net volumes: transit_pgdata: + +networks: + tripz-net: + external: true diff --git a/backend-transit/package.json b/backend-transit/package.json index 0098605..ce15d55 100644 --- a/backend-transit/package.json +++ b/backend-transit/package.json @@ -13,8 +13,9 @@ "@nestjs/config": "^3.0.0", "@nestjs/core": "^10.0.0", "@nestjs/platform-express": "^10.0.0", - "@nestjs/typeorm": "^10.0.0", - "pg": "^8.11.0", + "@nestjs/typeorm": "^11.0.0", + "ioredis": "^5.3.2", + "pg": "^8.13.1", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", "typeorm": "^0.3.17" diff --git a/backend-transit/src/transit/transit-geofence.service.ts b/backend-transit/src/transit/transit-geofence.service.ts new file mode 100644 index 0000000..eb84e67 --- /dev/null +++ b/backend-transit/src/transit/transit-geofence.service.ts @@ -0,0 +1,104 @@ +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; + } +} diff --git a/backend-transit/src/transit/transit.module.ts b/backend-transit/src/transit/transit.module.ts index 24c7568..12a6ec1 100644 --- a/backend-transit/src/transit/transit.module.ts +++ b/backend-transit/src/transit/transit.module.ts @@ -7,6 +7,9 @@ import { TransitBusTrip } from './entities/transit-bus-trip.entity'; import { TransitTicket } from './entities/transit-ticket.entity'; import { TransitController } from './transit.controller'; import { TransitService } from './transit.service'; +import { TransitGeofenceService } from './transit-geofence.service'; +import Redis from 'ioredis'; +import { ConfigService } from '@nestjs/config'; @Module({ imports: [ @@ -19,7 +22,20 @@ import { TransitService } from './transit.service'; ]), ], controllers: [TransitController], - providers: [TransitService], + providers: [ + TransitService, + TransitGeofenceService, + { + provide: 'REDIS_CLIENT', + inject: [ConfigService], + useFactory: (cfg: ConfigService) => { + return new Redis({ + host: cfg.get('REDIS_HOST', 'tripz-redis'), + port: cfg.get('REDIS_PORT', 6379), + }); + }, + }, + ], exports: [TypeOrmModule, TransitService], }) export class TransitModule {}