false, 'message' => 'Unauthorized']); exit; } // ── Parameters ──────────────────────────────────────────────── $driverLat = (float)filterRequest('lat'); $driverLng = (float)filterRequest('lng'); $radiusKm = (float)(filterRequest('radius') ?? 10); // تحويل km إلى درجات تقريباً (1° ≈ 111km) $radiusDeg = $radiusKm / 111.0; // ── Redis Cache ─────────────────────────────────────────────── try { $redis = getRedisConnection(); } catch (Exception $e) { $redis = null; } $cacheRaw = $redis ? $redis->get('siro:cache:predictive_demand') : null; if ($cacheRaw) { $cacheData = json_decode($cacheRaw, true); $allZones = $cacheData['zones'] ?? []; // فلتر: فقط المناطق ضمن نطاق السائق $nearbyZones = array_filter($allZones, function ($z) use ($driverLat, $driverLng, $radiusDeg) { if ($driverLat == 0 || $driverLng == 0) return true; // لو ما أرسل موقعه، أرجع الكل return abs($z['lat'] - $driverLat) <= $radiusDeg && abs($z['lng'] - $driverLng) <= $radiusDeg; }); // ترتيب: الأقرب للسائق أولاً if ($driverLat != 0) { usort($nearbyZones, function ($a, $b) use ($driverLat, $driverLng) { $da = abs($a['lat'] - $driverLat) + abs($a['lng'] - $driverLng); $db = abs($b['lat'] - $driverLat) + abs($b['lng'] - $driverLng); return $da <=> $db; }); } echo json_encode([ 'success' => true, 'source' => 'cache', 'predicted_hour' => $cacheData['predicted_hour'] ?? null, 'last_updated' => $cacheData['last_updated'] ?? null, 'zones' => array_values($nearbyZones), ], JSON_UNESCAPED_UNICODE); exit; } // ── Fallback: حساب مباشر من DB إذا لم يتوفر cache ──────────── try { $con = Database::get('main'); } catch (Exception $e) { echo json_encode(['success' => false, 'message' => 'DB error']); exit; } $nextHour = ((int)date('H') + 1) % 24; $dow = (int)date('N'); $gridSize = 0.01; $sql = " SELECT ROUND(pickup_lat / :g) * :g AS lat, ROUND(pickup_lng / :g) * :g AS lng, COUNT(*) AS demand_score, country_code FROM rides WHERE status IN ('completed', 'cancelled_by_driver', 'timeout') AND HOUR(created_at) = :hour AND DAYOFWEEK(created_at) = :dow AND created_at >= DATE_SUB(NOW(), INTERVAL 4 WEEK) AND pickup_lat BETWEEN (:dlat - :r) AND (:dlat + :r) AND pickup_lng BETWEEN (:dlng - :r) AND (:dlng + :r) GROUP BY lat, lng, country_code HAVING demand_score >= 2 ORDER BY demand_score DESC LIMIT 10 "; $stmt = $con->prepare($sql); $stmt->execute([ ':g' => $gridSize, ':hour' => $nextHour, ':dow' => $dow, ':dlat' => $driverLat ?: 31.95, // fallback عمّان ':dlng' => $driverLng ?: 35.93, ':r' => $radiusDeg, ]); $zones = $stmt->fetchAll(PDO::FETCH_ASSOC); // تسمية المناطق foreach ($zones as &$z) { $stmtN = $con->prepare(" SELECT zone_name FROM geofence_zones WHERE is_active = 1 AND ABS(latitude - :lat) < 0.05 AND ABS(longitude - :lng) < 0.05 ORDER BY ABS(latitude - :lat) + ABS(longitude - :lng) ASC LIMIT 1 "); $stmtN->execute([':lat' => $z['lat'], ':lng' => $z['lng']]); $nameRow = $stmtN->fetch(PDO::FETCH_ASSOC); $z['zone_name'] = $nameRow['zone_name'] ?? 'منطقة قريبة'; $z['lat'] = (float)$z['lat']; $z['lng'] = (float)$z['lng']; $z['demand_score'] = (int)$z['demand_score']; } unset($z); echo json_encode([ 'success' => true, 'source' => 'realtime', 'predicted_hour' => $nextHour, 'last_updated' => date('Y-m-d H:i:s'), 'zones' => $zones, ], JSON_UNESCAPED_UNICODE); ?>