40.7128, 'lng' => -74.0060]; // Default to NYC public $zoomLevel = 10; public $autoRefresh = true; public $refreshInterval = 15; // seconds public $showTrails = false; public $trailDuration = 24; // hours public $selectedDevice = null; public $deviceDetails = []; public $mapStyle = 'streets'; public $followDevice = null; public $showOfflineDevices = true; public $lastUpdate = null; protected $traccarService; public function boot(TraccarService $traccarService) { $this->traccarService = $traccarService; } public function mount() { $this->loadUserDevices(); $this->loadRealTimePositions(); $this->lastUpdate = now()->toTimeString(); } public function loadUserDevices() { $devices = Device::where('user_id', Auth::id()) ->whereNotNull('traccar_device_id') ->get(); $this->selectedDevices = $devices->pluck('id')->toArray(); $this->allDevicesSelected = true; } public function loadRealTimePositions() { try { // Get devices with their Traccar IDs $devices = Device::where('user_id', Auth::id()) ->whereIn('id', $this->selectedDevices) ->whereNotNull('traccar_device_id') ->get(); if ($devices->isEmpty()) { $this->deviceDetails = []; return; } $traccarDeviceIds = $devices->pluck('traccar_device_id')->toArray(); // Get real-time positions from Traccar $traccarPositions = $this->traccarService->getRealTimePositions($traccarDeviceIds); // Process positions $positions = []; foreach ($devices as $device) { $traccarPosition = collect($traccarPositions)->firstWhere('deviceId', $device->traccar_device_id); if ($traccarPosition) { $positions[] = [ 'device_id' => $device->id, 'traccar_device_id' => $device->traccar_device_id, 'device_name' => $device->name, 'unique_id' => $device->unique_id, 'latitude' => $traccarPosition['latitude'], 'longitude' => $traccarPosition['longitude'], 'speed' => $this->convertSpeed($traccarPosition['speed'] ?? 0), 'course' => $traccarPosition['course'] ?? 0, 'altitude' => $traccarPosition['altitude'] ?? 0, 'accuracy' => $traccarPosition['accuracy'] ?? 0, 'status' => $this->calculateDeviceStatus($traccarPosition), 'last_update' => $traccarPosition['deviceTime'] ?? $traccarPosition['serverTime'], 'address' => $traccarPosition['address'] ?? 'Unknown location', 'attributes' => $traccarPosition['attributes'] ?? [], 'valid' => $traccarPosition['valid'] ?? false, 'protocol' => $traccarPosition['protocol'] ?? '', ]; } elseif ($this->showOfflineDevices) { // Include offline devices $positions[] = [ 'device_id' => $device->id, 'traccar_device_id' => $device->traccar_device_id, 'device_name' => $device->name, 'unique_id' => $device->unique_id, 'latitude' => null, 'longitude' => null, 'speed' => 0, 'course' => 0, 'altitude' => 0, 'accuracy' => 0, 'status' => 'offline', 'last_update' => null, 'address' => 'No signal', 'attributes' => [], 'valid' => false, 'protocol' => '', ]; } } $this->deviceDetails = $positions; $this->lastUpdate = now()->toTimeString(); // Auto-center map if devices are found and no device is being followed if (!empty($positions) && !$this->followDevice) { $this->autoCenter(); } // If following a device, update map center if ($this->followDevice) { $this->updateFollowedDevice(); } // Sync positions to local database (optional for history) $this->syncPositionsToLocal($positions); } catch (\Exception $e) { Log::error('Failed to load real-time positions: ' . $e->getMessage()); session()->flash('error', 'Failed to load device positions: ' . $e->getMessage()); } } private function convertSpeed($speedKnots) { // Convert from knots to km/h return round($speedKnots * 1.852, 1); } private function calculateDeviceStatus($position) { if (!isset($position['deviceTime']) && !isset($position['serverTime'])) { return 'offline'; } $lastUpdate = Carbon::parse($position['deviceTime'] ?? $position['serverTime']); $minutesAgo = $lastUpdate->diffInMinutes(now()); // If position is not valid, consider offline if (!($position['valid'] ?? false)) { return 'offline'; } if ($minutesAgo <= 5) { return 'online'; } elseif ($minutesAgo <= 30) { return 'idle'; } else { return 'offline'; } } private function syncPositionsToLocal($positions) { try { foreach ($positions as $positionData) { if ($positionData['latitude'] && $positionData['longitude']) { Position::updateOrCreate( [ 'device_id' => $positionData['device_id'], 'device_time' => $positionData['last_update'] ], [ 'latitude' => $positionData['latitude'], 'longitude' => $positionData['longitude'], 'speed' => $positionData['speed'] / 1.852, // Convert back to knots for storage 'course' => $positionData['course'], 'altitude' => $positionData['altitude'], 'accuracy' => $positionData['accuracy'], 'address' => $positionData['address'], 'valid' => $positionData['valid'], 'protocol' => $positionData['protocol'], 'attributes' => $positionData['attributes'], 'server_time' => now(), 'fix_time' => $positionData['last_update'], ] ); } } } catch (\Exception $e) { Log::warning('Failed to sync positions to local database: ' . $e->getMessage()); } } public function refreshPositions() { $this->loadRealTimePositions(); $this->dispatch('positions-updated', $this->deviceDetails); } public function toggleDevice($deviceId) { if (in_array($deviceId, $this->selectedDevices)) { $this->selectedDevices = array_filter($this->selectedDevices, fn($id) => $id !== $deviceId); } else { $this->selectedDevices[] = $deviceId; } $this->allDevicesSelected = false; $this->loadRealTimePositions(); } public function toggleAllDevices() { if ($this->allDevicesSelected) { $this->selectedDevices = []; $this->allDevicesSelected = false; } else { $this->loadUserDevices(); $this->allDevicesSelected = true; } $this->loadRealTimePositions(); } public function selectDevice($deviceId) { $this->selectedDevice = $deviceId; $device = collect($this->deviceDetails)->firstWhere('device_id', $deviceId); if ($device && $device['latitude'] && $device['longitude']) { $this->mapCenter = [ 'lat' => $device['latitude'], 'lng' => $device['longitude'] ]; $this->zoomLevel = 15; } } public function followDevice($deviceId) { $this->followDevice = $this->followDevice === $deviceId ? null : $deviceId; if ($this->followDevice) { $this->updateFollowedDevice(); } } private function updateFollowedDevice() { $device = collect($this->deviceDetails)->firstWhere('device_id', $this->followDevice); if ($device && $device['latitude'] && $device['longitude']) { $this->mapCenter = [ 'lat' => $device['latitude'], 'lng' => $device['longitude'] ]; } } public function autoCenter() { $validPositions = collect($this->deviceDetails) ->filter(fn($device) => $device['latitude'] && $device['longitude']); if ($validPositions->isEmpty()) { return; } if ($validPositions->count() === 1) { $device = $validPositions->first(); $this->mapCenter = [ 'lat' => $device['latitude'], 'lng' => $device['longitude'] ]; $this->zoomLevel = 15; } else { // Calculate bounds for multiple devices $lats = $validPositions->pluck('latitude'); $lngs = $validPositions->pluck('longitude'); $this->mapCenter = [ 'lat' => ($lats->min() + $lats->max()) / 2, 'lng' => ($lngs->min() + $lngs->max()) / 2 ]; // Determine zoom level based on spread $latSpread = $lats->max() - $lats->min(); $lngSpread = $lngs->max() - $lngs->min(); $maxSpread = max($latSpread, $lngSpread); if ($maxSpread > 1) { $this->zoomLevel = 8; } elseif ($maxSpread > 0.1) { $this->zoomLevel = 10; } elseif ($maxSpread > 0.01) { $this->zoomLevel = 12; } else { $this->zoomLevel = 15; } } } public function toggleAutoRefresh() { $this->autoRefresh = !$this->autoRefresh; } public function toggleShowTrails() { $this->showTrails = !$this->showTrails; } public function toggleOfflineDevices() { $this->showOfflineDevices = !$this->showOfflineDevices; $this->loadRealTimePositions(); } public function updateRefreshInterval($interval) { $this->refreshInterval = max(5, min(300, $interval)); // Between 5 seconds and 5 minutes } public function changeMapStyle($style) { $this->mapStyle = $style; } public function getOnlineDevicesCount() { return collect($this->deviceDetails)->where('status', 'online')->count(); } public function getOfflineDevicesCount() { return collect($this->deviceDetails)->whereIn('status', ['offline', 'idle'])->count(); } public function getTotalDevicesCount() { return count($this->deviceDetails); } // Real-time updates using polling (can be enhanced with WebSockets later) public function getListeners() { return [ 'refresh-positions' => 'refreshPositions', ]; } public function render() { $query = Device::where('user_id', Auth::id()); // Check if device groups have users relationship (safely handle missing relationship) if (method_exists(\App\Models\DeviceGroup::class, 'users')) { $query->orWhereHas('group.users', function($subQuery) { $subQuery->where('user_id', Auth::id()); }); } $devices = $query->get(); return view('livewire.live-tracking', [ 'devices' => $devices, 'onlineCount' => $this->getOnlineDevicesCount(), 'offlineCount' => $this->getOfflineDevicesCount(), 'totalCount' => $this->getTotalDevicesCount(), ]); } public function toggleTrails() { $this->showTrails = !$this->showTrails; $this->dispatch('toggleTrails', $this->showTrails); } } $devices = $query->get(); return view('livewire.live-tracking', [ 'devices' => $devices ]); } }