import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { DataSource } from 'typeorm'; /** * Traffic Grid Service - In-Memory GeoHash Grid * نظام شبكة التنبؤ المروري الذكي: يحول بيانات قاعدة البيانات إلى خارطة سرعة في الذاكرة */ @Injectable() export class TrafficGridService implements OnModuleInit { private readonly logger = new Logger(TrafficGridService.name); // Key: "geohash:hr:dow", Value: trafficFactor private grid: Map = new Map(); private readonly GEOHASH_PRECISION = 6; // ~100m accuracy constructor(private dataSource: DataSource) {} async onModuleInit() { await this.refreshGrid(); } /** * Rebuilds the in-memory lookup table from PostGIS * إعادة بناء الجدول المرجعي في الذاكرة من قاعدة البيانات */ async refreshGrid() { this.logger.log('📡 Rebuilding Traffic Grid v3 (In-Memory Lookup)...'); try { const startTime = Date.now(); const data = await this.dataSource.query(` SELECT ST_X(ST_Centroid(geometry::geometry)) as lng, ST_Y(ST_Centroid(geometry::geometry)) as lat, p."hourOfDay", p."dayOfWeek", p."averageSpeed", s."averageSpeed" as base_speed FROM road_speed_profiles p JOIN road_segment_stats s ON p."segmentId" = s."segmentId" WHERE s.geometry IS NOT NULL `); this.grid.clear(); let count = 0; for (const row of data) { const gh = this.encodeGeohash(row.lat, row.lng, this.GEOHASH_PRECISION); const factor = row.base_speed / Math.max(row.averageSpeed, 1); // Key includes time buckets to handle day/night variations const key = `${gh}:${row.hourOfDay}:${row.dayOfWeek}`; this.grid.set(key, factor); count++; } this.logger.log(`✅ Grid rebuilt in ${Date.now() - startTime}ms. Cached ${count} cells (${this.grid.size} unique keys).`); } catch (e) { this.logger.error('❌ Failed to refresh traffic grid:', e.message); } } /** * Fetches the average traffic factor for a decoded polyline * جلب معامل الازدحام المتوسط لمخطط الرحلة من الذاكرة */ getTrafficFactor(coords: [number, number][], hr: number, dow: number): number { if (this.grid.size === 0) return 1.0; let totalFactor = 0; let matches = 0; // We sample points every ~100m to speed up (every 2nd or 3rd coord) for (let i = 0; i < coords.length; i += 2) { const [lng, lat] = coords[i]; const gh = this.encodeGeohash(lat, lng, this.GEOHASH_PRECISION); const key = `${gh}:${hr}:${dow}`; const factor = this.grid.get(key); if (factor) { totalFactor += factor; matches++; } } if (matches === 0) return 1.0; // Average factor clipped to sane ranges (0.8 - 3.0) let finalFactor = totalFactor / matches; return Math.min(Math.max(finalFactor, 0.8), 3.0); } /** * Pure Math GeoHash implementation (No outside dependencies) */ private encodeGeohash(lat: number, lng: number, precision: number): string { const BASE32 = "0123456789bcdefghjkmnpqrstuvwxyz"; let minLat = -90, maxLat = 90; let minLng = -180, maxLng = 180; let result = ""; let isEven = true; let bit = 0; let ch = 0; while (result.length < precision) { if (isEven) { let mid = (minLng + maxLng) / 2; if (lng > mid) { ch |= (1 << (4 - bit)); minLng = mid; } else { maxLng = mid; } } else { let mid = (minLat + maxLat) / 2; if (lat > mid) { ch |= (1 << (4 - bit)); minLat = mid; } else { maxLat = mid; } } isEven = !isEven; if (bit < 4) { bit++; } else { result += BASE32[ch]; bit = 0; ch = 0; } } return result; } }