gps_system/app/Livewire/LiveTracking.php
sackey 6b878bb0a0
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
Initial commit
2025-09-12 16:19:56 +00:00

406 lines
14 KiB
PHP

<?php
namespace App\Livewire;
use Livewire\Component;
use App\Models\Device;
use App\Models\Position;
use App\Services\TraccarService;
use App\Services\MapService;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Carbon\Carbon;
class LiveTracking extends Component
{
public $selectedDevices = [];
public $allDevicesSelected = true;
public $mapCenter = ['lat' => 40.7128, 'lng' => -74.0060]; // Default to NYC
public $zoomLevel = 10;
public $refreshInterval = 15; // seconds - automatic background refresh
public $showTrails = false;
public $trailDuration = 24; // hours
public $selectedDevice = null;
public $deviceDetails = [];
public $mapProvider = 'openstreetmap';
public $mapStyle = 'standard';
public $followDevice = null;
public $showOfflineDevices = true;
public $lastUpdate = null;
public $sidebarCollapsed = false;
public $availableProviders = [];
public $availableStyles = [];
public function mount()
{
$mapService = app(MapService::class);
$this->mapProvider = $mapService->getDefaultProvider();
$this->availableProviders = $mapService->getAvailableProviders();
$this->availableStyles = $mapService->getMapStyles($this->mapProvider);
$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 = app(TraccarService::class)->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 refreshData()
{
$this->loadRealTimePositions();
$this->dispatch('map-updated', [
'devices' => $this->deviceDetails,
'center' => $this->mapCenter,
'zoom' => $this->zoomLevel
]);
}
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 toggleSidebar()
{
$this->sidebarCollapsed = !$this->sidebarCollapsed;
$this->dispatch('sidebar-toggled', $this->sidebarCollapsed);
}
public function toggleShowTrails()
{
$this->showTrails = !$this->showTrails;
$this->dispatch('trails-toggled', $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;
$this->dispatch('map-style-changed', $style);
}
public function changeMapProvider($provider)
{
$mapService = app(MapService::class);
if ($mapService->isProviderEnabled($provider)) {
$this->mapProvider = $provider;
$this->availableStyles = $mapService->getMapStyles($provider);
$this->mapStyle = array_key_first($this->availableStyles);
$mapConfig = $mapService->getMapConfig($provider);
$this->dispatch('map-provider-changed', [
'provider' => $provider,
'styles' => $mapConfig['styles'],
'config' => $mapConfig['config'],
'enabled' => $mapConfig['enabled']
]);
}
}
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()
{
return view('livewire.live-tracking', [
'devices' => Device::where('user_id', Auth::id())->get(),
'onlineCount' => $this->getOnlineDevicesCount(),
'offlineCount' => $this->getOfflineDevicesCount(),
'totalCount' => $this->getTotalDevicesCount(),
]);
}
}