406 lines
14 KiB
PHP
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(),
|
|
]);
|
|
}
|
|
}
|