"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; var TelemetryAnalyzerService_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.TelemetryAnalyzerService = void 0; const common_1 = require("@nestjs/common"); const typeorm_1 = require("@nestjs/typeorm"); const typeorm_2 = require("typeorm"); const schedule_1 = require("@nestjs/schedule"); const telemetry_entity_1 = require("./telemetry.entity"); const road_stat_entity_1 = require("../maps/road-stat.entity"); const candidate_road_entity_1 = require("../maps/candidate-road.entity"); const redis_service_1 = require("../common/redis.service"); const external_telemetry_service_1 = require("./external-telemetry.service"); let TelemetryAnalyzerService = TelemetryAnalyzerService_1 = class TelemetryAnalyzerService { telemetryRepo; roadStatRepo; candidateRoadRepo; dataSource; redisService; externalTelemetry; logger = new common_1.Logger(TelemetryAnalyzerService_1.name); TRAFFIC_CACHE_KEY = 'traffic_snapshot'; constructor(telemetryRepo, roadStatRepo, candidateRoadRepo, dataSource, redisService, externalTelemetry) { this.telemetryRepo = telemetryRepo; this.roadStatRepo = roadStatRepo; this.candidateRoadRepo = candidateRoadRepo; this.dataSource = dataSource; this.redisService = redisService; this.externalTelemetry = externalTelemetry; } async handleNightlyIntelligence() { this.logger.log('⏰ Starting automated 3 AM intelligence process...'); try { await this.syncExternalData(1); await this.analyzeRoadSpeeds(24); await this.discoverNewRoads(168); await this.refreshTrafficCache(); this.logger.log('✅ 3 AM intelligence process complete.'); } catch (error) { this.logger.error('❌ Nightly intelligence failed:', error.stack); } } async syncExternalData(days = 1) { this.logger.log(`📡 Starting batch sync (Window: ${days} days)...`); const tracks = await this.externalTelemetry.fetchCarTracks(days); if (!tracks || tracks.length === 0) { this.logger.warn(`⚠️ No tracks found on external server for the last ${days} days.`); return { imported: 0 }; } this.logger.log(`✅ Received ${tracks.length} tracks. Commencing batch insertion...`); this.logger.log(`📥 Saving ${tracks.length} points to database...`); const entities = tracks.map(t => ({ driverId: t.driver_id, latitude: t.latitude, longitude: t.longitude, speed: t.speed, heading: t.heading, timestamp: new Date(t.created_at || t.timestamp), location: { type: 'Point', coordinates: [t.longitude, t.latitude], }, })); const CHUNK_SIZE = 1000; for (let i = 0; i < entities.length; i += CHUNK_SIZE) { const chunk = entities.slice(i, i + CHUNK_SIZE); const logs = this.telemetryRepo.create(chunk); await this.telemetryRepo.save(logs); } return { imported: tracks.length }; } async refreshTrafficCache() { this.logger.log('🚀 Refreshing Redis traffic snapshot...'); const congested = await this.roadStatRepo.query(` SELECT "segmentId", "congestionFactor", ST_AsGeoJSON(geometry) as geojson FROM road_segment_stats WHERE "congestionFactor" > 1.1 `); if (congested.length === 0) { await this.redisService.del(this.TRAFFIC_CACHE_KEY); return { cachedCount: 0 }; } await this.redisService.set(this.TRAFFIC_CACHE_KEY, congested); this.logger.log(`✅ Cached ${congested.length} congested segments in Redis.`); return { cachedCount: congested.length }; } async analyzeRoadSpeeds(sinceHours = 24) { this.logger.log(`🔍 Starting road speed analysis for last ${sinceHours}h...`); const matchedData = await this.dataSource.query(` WITH matched_points AS ( SELECT t.id AS telemetry_id, t.speed, t."driverId", l.osm_id, l.name, l.highway, l.way_4326, ST_Distance(t.location::geography, l.way_4326::geography) AS distance_m FROM telemetry_logs t CROSS JOIN LATERAL ( SELECT osm_id, name, highway, ST_Transform(way, 4326) AS way_4326 FROM planet_osm_line WHERE highway IS NOT NULL ORDER BY way <-> ST_Transform(t.location::geometry, 3857) LIMIT 1 ) l WHERE t.timestamp >= NOW() - INTERVAL '${sinceHours} hours' AND t.speed > 2 -- Ignore stationary points / تجاهل النقاط الثابتة AND ST_Distance(t.location::geography, l.way_4326::geography) < 15 -- 15m snap threshold ) SELECT osm_id::text AS segment_id, name, highway, AVG(speed) AS avg_speed, PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY speed) AS median_speed, COUNT(*) AS sample_count, COUNT(DISTINCT "driverId") AS unique_drivers, ST_AsGeoJSON(MIN(way_4326)) AS geojson FROM matched_points GROUP BY osm_id, name, highway HAVING COUNT(*) >= 3 -- Minimum 3 samples for reliability / 3 عينات على الأقل ORDER BY sample_count DESC `); let segmentsUpdated = 0; let totalPoints = 0; for (const row of matchedData) { const medianSpeed = parseFloat(row.median_speed); const sampleCount = parseInt(row.sample_count); const geojson = JSON.parse(row.geojson); totalPoints += sampleCount; let congestionFactor = 1.0; const majorRoadTypes = ['primary', 'secondary', 'trunk', 'motorway']; if (majorRoadTypes.includes(row.highway) && medianSpeed < 30) { congestionFactor = 30 / Math.max(medianSpeed, 1); } await this.roadStatRepo.upsert({ segmentId: row.segment_id, averageSpeed: medianSpeed, congestionFactor, sampleCount, lastUpdated: new Date(), geometry: geojson, }, ['segmentId']); segmentsUpdated++; } return { segmentsUpdated, totalPointsProcessed: totalPoints, topAdjustments: matchedData.slice(0, 10).map(d => ({ segmentId: d.segment_id, name: d.name || 'Unnamed Road', speed: Math.round(parseFloat(d.median_speed)), samples: parseInt(d.sample_count) })) }; } async discoverNewRoads(sinceHours = 168) { this.logger.log(`🛣️ Starting road discovery for last ${sinceHours}h (${sinceHours / 24}d)...`); const candidates = await this.dataSource.query(` WITH off_road_points AS ( SELECT t.id, t."driverId", t.speed, t.heading, t.location, t.timestamp FROM telemetry_logs t WHERE t.timestamp >= NOW() - INTERVAL '${sinceHours} hours' AND t.speed > 5 -- Moving, not parked / متحرك وليس متوقف AND NOT EXISTS ( SELECT 1 FROM planet_osm_line l WHERE l.highway IS NOT NULL AND ST_DWithin(t.location::geography, ST_Transform(l.way, 4326)::geography, 15) ) ), -- Step 2: Cluster nearby off-road points using DBSCAN -- الخطوة 2: تجميع النقاط القريبة باستخدام DBSCAN clustered AS ( SELECT *, ST_ClusterDBSCAN(location::geometry, eps := 0.0003, minpoints := 5) OVER () AS cluster_id FROM off_road_points ) -- Step 3: Aggregate clusters into candidate road lines -- الخطوة 3: تحويل التجمعات إلى خطوط طرق مرشحة SELECT cluster_id, COUNT(*) AS total_points, COUNT(DISTINCT "driverId") AS unique_drivers, AVG(speed) AS avg_speed, ST_AsGeoJSON(ST_MakeLine(location::geometry ORDER BY timestamp)) AS geojson_line, ST_Length(ST_MakeLine(location::geometry ORDER BY timestamp)::geography) AS length_m FROM clustered WHERE cluster_id IS NOT NULL GROUP BY cluster_id HAVING COUNT(DISTINCT "driverId") >= 2 -- At least 2 drivers / سائقان على الأقل AND COUNT(*) >= 10 -- At least 10 points / 10 نقاط على الأقل ORDER BY unique_drivers DESC, total_points DESC `); const savedCandidates = []; for (const c of candidates) { const geojson = JSON.parse(c.geojson_line); const confidence = this.calculateConfidence(parseInt(c.unique_drivers), parseInt(c.total_points), parseFloat(c.length_m)); const candidate = this.candidateRoadRepo.create({ geometry: geojson, uniqueDriverCount: parseInt(c.unique_drivers), totalPoints: parseInt(c.total_points), averageSpeed: parseFloat(c.avg_speed), lengthMeters: parseFloat(c.length_m), confidence, status: 'pending', }); const saved = await this.candidateRoadRepo.save(candidate); savedCandidates.push({ id: saved.id, uniqueDrivers: saved.uniqueDriverCount, totalPoints: saved.totalPoints, averageSpeed: Math.round(saved.averageSpeed * 10) / 10, lengthMeters: Math.round(saved.lengthMeters), confidence: Math.round(saved.confidence * 100) / 100, geojson, }); } this.logger.log(`✅ Road discovery complete: ${savedCandidates.length} candidates found`); return { candidatesFound: savedCandidates.length, candidates: savedCandidates, }; } async getCongestionData(bounds) { return this.dataSource.query(` SELECT rs."segmentId" AS segment_id, rs."averageSpeed" AS avg_speed, rs."congestionFactor" AS congestion_factor, rs."sampleCount" AS sample_count, ST_AsGeoJSON(rs.geometry) AS geojson FROM road_segment_stats rs WHERE rs.geometry IS NOT NULL AND ST_Intersects( rs.geometry::geometry, ST_MakeEnvelope($1, $2, $3, $4, 4326) ) AND rs."sampleCount" >= 3 ORDER BY rs."congestionFactor" DESC `, [bounds.west, bounds.south, bounds.east, bounds.north]); } async getAnalysisSummary() { const [telemetryCount] = await this.dataSource.query(`SELECT COUNT(*) as count FROM telemetry_logs`); const [roadStatCount] = await this.dataSource.query(`SELECT COUNT(*) as count FROM road_segment_stats`); const [candidateCount] = await this.dataSource.query(`SELECT COUNT(*) as count, COUNT(*) FILTER (WHERE status = 'pending') as pending, COUNT(*) FILTER (WHERE status = 'approved') as approved, COUNT(*) FILTER (WHERE status = 'rejected') as rejected FROM candidate_roads`); const [recentActivity] = await this.dataSource.query(` SELECT COUNT(*) as points_last_24h, COUNT(DISTINCT "driverId") as active_drivers_24h FROM telemetry_logs WHERE timestamp >= NOW() - INTERVAL '24 hours' `); return { telemetry: { totalPoints: parseInt(telemetryCount?.count || '0'), last24h: parseInt(recentActivity?.points_last_24h || '0'), activeDrivers24h: parseInt(recentActivity?.active_drivers_24h || '0'), }, roadSegments: { analyzed: parseInt(roadStatCount?.count || '0'), }, candidateRoads: { total: parseInt(candidateCount?.count || '0'), pending: parseInt(candidateCount?.pending || '0'), approved: parseInt(candidateCount?.approved || '0'), rejected: parseInt(candidateCount?.rejected || '0'), }, }; } calculateConfidence(uniqueDrivers, totalPoints, lengthMeters) { const driverScore = Math.min(uniqueDrivers / 5, 1.0) * 0.4; const pointScore = Math.min(totalPoints / 50, 1.0) * 0.3; let lengthScore = 0; if (lengthMeters >= 50 && lengthMeters <= 2000) { lengthScore = 0.3; } else if (lengthMeters > 2000) { lengthScore = 0.2; } return Math.round((driverScore + pointScore + lengthScore) * 100) / 100; } }; exports.TelemetryAnalyzerService = TelemetryAnalyzerService; __decorate([ (0, schedule_1.Cron)('0 3 * * *'), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], TelemetryAnalyzerService.prototype, "handleNightlyIntelligence", null); exports.TelemetryAnalyzerService = TelemetryAnalyzerService = TelemetryAnalyzerService_1 = __decorate([ (0, common_1.Injectable)(), __param(0, (0, typeorm_1.InjectRepository)(telemetry_entity_1.TelemetryLog)), __param(1, (0, typeorm_1.InjectRepository)(road_stat_entity_1.RoadSegmentStat)), __param(2, (0, typeorm_1.InjectRepository)(candidate_road_entity_1.CandidateRoad)), __metadata("design:paramtypes", [typeorm_2.Repository, typeorm_2.Repository, typeorm_2.Repository, typeorm_2.DataSource, redis_service_1.RedisService, external_telemetry_service_1.ExternalTelemetryService]) ], TelemetryAnalyzerService); //# sourceMappingURL=telemetry-analyzer.service.js.map