src/tracking/tracking.service.ts
Methods |
|
constructor(prisma: PrismaService, geofenceService: GeofenceService)
|
|||||||||
|
Defined in src/tracking/tracking.service.ts:7
|
|||||||||
|
Parameters :
|
| Async getTripTracking | ||||||
getTripTracking(tripId: string)
|
||||||
|
Defined in src/tracking/tracking.service.ts:215
|
||||||
|
Get trip tracking data with stops, current vehicle position, and ETA
Parameters :
Returns :
unknown
|
| Async getVehiclePosition | ||||||
getVehiclePosition(vehicleId: string)
|
||||||
|
Defined in src/tracking/tracking.service.ts:53
|
||||||
|
Get single vehicle position with recent trail
Parameters :
Returns :
unknown
|
| Async getVehiclePositions | ||||||
getVehiclePositions(orgId: string)
|
||||||
|
Defined in src/tracking/tracking.service.ts:17
|
||||||
|
Get current positions for all vehicles in an organization
Parameters :
Returns :
unknown
|
| Async getVehicleTrail |
getVehicleTrail(vehicleId: string, from: Date, to: Date)
|
|
Defined in src/tracking/tracking.service.ts:193
|
|
Get GPS event trail for a vehicle in a time range
Returns :
unknown
|
| Async updateVehiclePosition | ||||||||||||||||||||||||||||
updateVehiclePosition(vehicleId: string, lat: number, lng: number, speed: number, heading: number, source: string)
|
||||||||||||||||||||||||||||
|
Defined in src/tracking/tracking.service.ts:109
|
||||||||||||||||||||||||||||
|
Update a vehicle's position and create a GpsEvent record
Parameters :
Returns :
unknown
|
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { GeofenceService } from './geofence.service';
@Injectable()
export class TrackingService {
private readonly logger = new Logger(TrackingService.name);
constructor(
private readonly prisma: PrismaService,
private readonly geofenceService: GeofenceService,
) {}
/**
* Get current positions for all vehicles in an organization
*/
async getVehiclePositions(orgId: string) {
const vehicles = await this.prisma.vehicle.findMany({
where: { organizationId: orgId },
select: {
id: true,
unitNumber: true,
type: true,
status: true,
licensePlate: true,
currentLat: true,
currentLng: true,
currentSpeed: true,
lastGpsAt: true,
driverId: true,
driver: {
select: {
id: true,
firstName: true,
lastName: true,
phone: true,
},
},
},
});
return vehicles.map((v) => ({
...v,
isOnline: v.lastGpsAt
? Date.now() - new Date(v.lastGpsAt).getTime() < 5 * 60 * 1000 // online if GPS update within 5 min
: false,
}));
}
/**
* Get single vehicle position with recent trail
*/
async getVehiclePosition(vehicleId: string) {
const vehicle = await this.prisma.vehicle.findUnique({
where: { id: vehicleId },
include: {
driver: {
select: {
id: true,
firstName: true,
lastName: true,
phone: true,
},
},
trips: {
where: { status: { in: ['IN_TRANSIT', 'AT_STOP'] } },
take: 1,
orderBy: { startDate: 'desc' },
include: {
stops: { orderBy: { sequence: 'asc' } },
},
},
},
});
if (!vehicle) {
throw new NotFoundException(`Vehicle ${vehicleId} not found`);
}
// Get recent trail (last 2 hours)
const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000);
const trail = await this.prisma.gpsEvent.findMany({
where: {
vehicleId,
timestamp: { gte: twoHoursAgo },
},
orderBy: { timestamp: 'asc' },
select: {
lat: true,
lng: true,
speed: true,
heading: true,
timestamp: true,
},
});
return {
...vehicle,
trail,
isOnline: vehicle.lastGpsAt
? Date.now() - new Date(vehicle.lastGpsAt).getTime() < 5 * 60 * 1000
: false,
};
}
/**
* Update a vehicle's position and create a GpsEvent record
*/
async updateVehiclePosition(
vehicleId: string,
lat: number,
lng: number,
speed: number,
heading: number,
source = 'manual',
) {
const now = new Date();
// Update vehicle current position
const vehicle = await this.prisma.vehicle.update({
where: { id: vehicleId },
data: {
currentLat: lat,
currentLng: lng,
currentSpeed: speed,
lastGpsAt: now,
},
});
// Create GPS event record
const gpsEvent = await this.prisma.gpsEvent.create({
data: {
vehicleId,
lat,
lng,
speed,
heading,
timestamp: now,
source,
},
});
// Check geofences and create events for any transitions
try {
const geofenceEvents = await this.geofenceService.checkGeofences(
vehicleId,
lat,
lng,
);
for (const gfEvent of geofenceEvents) {
await this.prisma.gpsEvent.create({
data: {
vehicleId,
lat,
lng,
speed: 0,
heading: 0,
timestamp: now,
source: `geofence_${gfEvent.event}`,
},
});
this.logger.log(
`Geofence ${gfEvent.event}: vehicle ${vehicleId} ${gfEvent.event === 'enter' ? 'entered' : 'exited'} "${gfEvent.name}" (${gfEvent.geofenceId})`,
);
}
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
this.logger.warn(
`Geofence check failed for vehicle ${vehicleId}: ${message}`,
);
}
this.logger.debug(
`Updated position for vehicle ${vehicleId}: ${lat}, ${lng} @ ${speed} km/h`,
);
return {
vehicleId,
lat,
lng,
speed,
heading,
timestamp: now,
unitNumber: vehicle.unitNumber,
};
}
/**
* Get GPS event trail for a vehicle in a time range
*/
async getVehicleTrail(vehicleId: string, from: Date, to: Date) {
return this.prisma.gpsEvent.findMany({
where: {
vehicleId,
timestamp: { gte: from, lte: to },
},
orderBy: { timestamp: 'asc' },
select: {
id: true,
lat: true,
lng: true,
speed: true,
heading: true,
timestamp: true,
source: true,
},
});
}
/**
* Get trip tracking data with stops, current vehicle position, and ETA
*/
async getTripTracking(tripId: string) {
const trip = await this.prisma.trip.findUnique({
where: { id: tripId },
include: {
stops: { orderBy: { sequence: 'asc' } },
vehicle: {
select: {
id: true,
unitNumber: true,
currentLat: true,
currentLng: true,
currentSpeed: true,
lastGpsAt: true,
driver: {
select: {
id: true,
firstName: true,
lastName: true,
phone: true,
},
},
},
},
orders: {
select: {
id: true,
orderNumber: true,
status: true,
destName: true,
destCity: true,
},
},
},
});
if (!trip) {
throw new NotFoundException(`Trip ${tripId} not found`);
}
// Compute ETA to next stop
let nextStop = null;
let etaMinutes: number | null = null;
if (trip.vehicle?.currentLat && trip.vehicle?.currentLng) {
// Find the next unvisited stop
nextStop =
trip.stops.find((s) => !s.actualArrival) || null;
if (nextStop?.lat && nextStop?.lng) {
const distanceKm = this.haversineDistance(
trip.vehicle.currentLat,
trip.vehicle.currentLng,
nextStop.lat,
nextStop.lng,
);
// Use current speed or default 60 km/h
const speedKmh = trip.vehicle.currentSpeed && trip.vehicle.currentSpeed > 5
? trip.vehicle.currentSpeed
: 60;
etaMinutes = Math.round((distanceKm / speedKmh) * 60);
}
}
return {
...trip,
nextStop,
etaMinutes,
etaTime: etaMinutes ? new Date(Date.now() + etaMinutes * 60 * 1000) : null,
};
}
/**
* Haversine formula to calculate distance between two GPS coordinates
*/
private haversineDistance(
lat1: number,
lng1: number,
lat2: number,
lng2: number,
): number {
const R = 6371; // Earth radius in km
const dLat = this.toRad(lat2 - lat1);
const dLng = this.toRad(lng2 - lng1);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(this.toRad(lat1)) *
Math.cos(this.toRad(lat2)) *
Math.sin(dLng / 2) *
Math.sin(dLng / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
private toRad(deg: number): number {
return deg * (Math.PI / 180);
}
}