feat: implement geofence using redis pub-sub

This commit is contained in:
Hamza-Ayed
2026-07-18 18:48:25 +03:00
parent cc5bc8c867
commit a15e65c42e
5 changed files with 135 additions and 3 deletions
+4
View File
@@ -12,3 +12,7 @@ DB_PORT=5432
DB_USER=transit_user DB_USER=transit_user
DB_PASSWORD=26ab87247a55f86032c943b7e361c475cd3ca8b3b772d82e2fe6a9b5ee0839a3 DB_PASSWORD=26ab87247a55f86032c943b7e361c475cd3ca8b3b772d82e2fe6a9b5ee0839a3
DB_NAME=tripz_transit DB_NAME=tripz_transit
# --- Redis (Shared with Main Tripz Backend) ---
REDIS_HOST=tripz-redis
REDIS_PORT=6379
+7
View File
@@ -22,6 +22,13 @@ services:
- .env - .env
depends_on: depends_on:
- transit-db - transit-db
networks:
- default
- tripz-net
volumes: volumes:
transit_pgdata: transit_pgdata:
networks:
tripz-net:
external: true
+3 -2
View File
@@ -13,8 +13,9 @@
"@nestjs/config": "^3.0.0", "@nestjs/config": "^3.0.0",
"@nestjs/core": "^10.0.0", "@nestjs/core": "^10.0.0",
"@nestjs/platform-express": "^10.0.0", "@nestjs/platform-express": "^10.0.0",
"@nestjs/typeorm": "^10.0.0", "@nestjs/typeorm": "^11.0.0",
"pg": "^8.11.0", "ioredis": "^5.3.2",
"pg": "^8.13.1",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"typeorm": "^0.3.17" "typeorm": "^0.3.17"
@@ -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<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;
}
}
+17 -1
View File
@@ -7,6 +7,9 @@ import { TransitBusTrip } from './entities/transit-bus-trip.entity';
import { TransitTicket } from './entities/transit-ticket.entity'; import { TransitTicket } from './entities/transit-ticket.entity';
import { TransitController } from './transit.controller'; import { TransitController } from './transit.controller';
import { TransitService } from './transit.service'; import { TransitService } from './transit.service';
import { TransitGeofenceService } from './transit-geofence.service';
import Redis from 'ioredis';
import { ConfigService } from '@nestjs/config';
@Module({ @Module({
imports: [ imports: [
@@ -19,7 +22,20 @@ import { TransitService } from './transit.service';
]), ]),
], ],
controllers: [TransitController], controllers: [TransitController],
providers: [TransitService], providers: [
TransitService,
TransitGeofenceService,
{
provide: 'REDIS_CLIENT',
inject: [ConfigService],
useFactory: (cfg: ConfigService) => {
return new Redis({
host: cfg.get<string>('REDIS_HOST', 'tripz-redis'),
port: cfg.get<number>('REDIS_PORT', 6379),
});
},
},
],
exports: [TypeOrmModule, TransitService], exports: [TypeOrmModule, TransitService],
}) })
export class TransitModule {} export class TransitModule {}