Implement LocationDisplayComponent and LocationMapComponent for interactive map features; add event handling and state management

This commit is contained in:
pacnpal
2025-02-23 21:17:27 -05:00
parent af4271b0a4
commit 27e584f427
12 changed files with 2255 additions and 184 deletions

View File

@@ -0,0 +1,118 @@
<?php
namespace App\Livewire\Location;
use Livewire\Component;
use App\Models\Location;
use Illuminate\Support\Collection;
class LocationDisplayComponent extends Component
{
// Map Configuration
public array $markers = [];
public bool $showClusters = true;
public int $clusterRadius = 50;
public int $defaultZoom = 13;
public ?array $bounds = null;
// Display Settings
public bool $showInfoWindow = false;
public ?array $activeMarker = null;
public array $customMarkers = [];
public array $markerCategories = [];
// State Management
public bool $isLoading = true;
public ?string $error = null;
public array $visibleMarkers = [];
// Lifecycle Hooks
public function mount(array $markers = [], ?array $bounds = null, array $categories = [])
{
$this->markers = $markers;
$this->bounds = $bounds;
$this->markerCategories = $categories;
$this->visibleMarkers = $markers;
}
// Event Handlers
public function markerClicked($markerId)
{
$this->activeMarker = collect($this->markers)->firstWhere('id', $markerId);
$this->showInfoWindow = true;
}
public function clusterClicked($clusterMarkers)
{
if (count($clusterMarkers) === 1) {
$this->markerClicked($clusterMarkers[0]['id']);
return;
}
$this->bounds = $this->calculateBounds($clusterMarkers);
}
public function closeInfoWindow()
{
$this->showInfoWindow = false;
$this->activeMarker = null;
}
public function boundsChanged($bounds)
{
$this->bounds = $bounds;
$this->visibleMarkers = $this->getVisibleMarkers($bounds);
}
public function updateMarkersVisibility($visible)
{
$this->visibleMarkers = collect($this->markers)
->filter(fn ($marker) => in_array($marker['id'], $visible))
->toArray();
}
// Helper Methods
protected function calculateBounds($markers): array
{
if (empty($markers)) {
return [
'north' => 0,
'south' => 0,
'east' => 0,
'west' => 0,
];
}
$lats = array_column($markers, 'lat');
$lons = array_column($markers, 'lng');
return [
'north' => max($lats),
'south' => min($lats),
'east' => max($lons),
'west' => min($lons),
];
}
protected function getVisibleMarkers($bounds): array
{
if (!$bounds) {
return $this->markers;
}
return collect($this->markers)
->filter(function ($marker) use ($bounds) {
return $marker['lat'] >= $bounds['south'] &&
$marker['lat'] <= $bounds['north'] &&
$marker['lng'] >= $bounds['west'] &&
$marker['lng'] <= $bounds['east'];
})
->values()
->toArray();
}
public function render()
{
return view('livewire.location.location-display');
}
}

View File

@@ -0,0 +1,162 @@
<?php
namespace App\Livewire\Location;
use Livewire\Component;
use Livewire\Attributes\Reactive;
use App\Services\GeocodeService;
class LocationMapComponent extends Component
{
/**
* The initial latitude for the map center
*/
#[Reactive]
public ?float $latitude = null;
/**
* The initial longitude for the map center
*/
#[Reactive]
public ?float $longitude = null;
/**
* The map zoom level (1-18)
*/
#[Reactive]
public int $zoom = 13;
/**
* Array of markers to display on the map
*/
#[Reactive]
public array $markers = [];
/**
* Currently selected location details
*/
#[Reactive]
public ?array $selectedLocation = null;
/**
* Whether the map is in interactive mode
*/
#[Reactive]
public bool $interactive = true;
/**
* Whether to show the map controls
*/
#[Reactive]
public bool $showControls = true;
/**
* Event listeners for the component
*/
protected $listeners = [
'locationSelected' => 'handleLocationSelected',
'markerClicked' => 'handleMarkerClicked',
'mapMoved' => 'handleMapMoved',
'zoomChanged' => 'handleZoomChanged'
];
/**
* Mount the component
*/
public function mount(
?float $latitude = null,
?float $longitude = null,
?int $zoom = null,
array $markers = [],
bool $interactive = true,
bool $showControls = true
) {
$this->latitude = $latitude;
$this->longitude = $longitude;
$this->zoom = $zoom ?? $this->zoom;
$this->markers = $markers;
$this->interactive = $interactive;
$this->showControls = $showControls;
}
/**
* Handle when a location is selected
*/
public function handleLocationSelected(array $location)
{
$this->selectedLocation = $location;
$this->latitude = $location['latitude'];
$this->longitude = $location['longitude'];
$this->dispatch('location-updated', [
'latitude' => $this->latitude,
'longitude' => $this->longitude,
'location' => $location
]);
}
/**
* Handle when a marker is clicked
*/
public function handleMarkerClicked(array $marker)
{
$this->selectedLocation = $marker;
$this->dispatch('marker-selected', [
'marker' => $marker
]);
}
/**
* Handle when the map is moved
*/
public function handleMapMoved(float $latitude, float $longitude)
{
$this->latitude = $latitude;
$this->longitude = $longitude;
$this->dispatch('map-moved', [
'latitude' => $latitude,
'longitude' => $longitude
]);
}
/**
* Handle when the zoom level changes
*/
public function handleZoomChanged(int $zoom)
{
$this->zoom = $zoom;
$this->dispatch('zoom-changed', [
'zoom' => $zoom
]);
}
/**
* Get the map configuration
*/
protected function getMapConfig(): array
{
return [
'center' => [
'lat' => $this->latitude,
'lng' => $this->longitude,
],
'zoom' => $this->zoom,
'markers' => $this->markers,
'interactive' => $this->interactive,
'showControls' => $this->showControls,
];
}
/**
* Render the component
*/
public function render()
{
return view('livewire.location.location-map', [
'mapConfig' => $this->getMapConfig()
]);
}
}

View File

@@ -0,0 +1,167 @@
<?php
namespace App\Livewire\Location;
use Livewire\Component;
use App\Services\GeocodeService;
use App\Exceptions\GeocodingException;
use App\Exceptions\ValidationException;
class LocationSelectorComponent extends Component
{
// Search State
public string $searchQuery = '';
public array $searchResults = [];
public bool $isSearching = false;
public ?string $validationError = null;
// Location State
public ?float $latitude = null;
public ?float $longitude = null;
public ?string $formattedAddress = null;
public bool $isValidLocation = false;
// UI State
public bool $showSearchResults = false;
public bool $isLoadingLocation = false;
public string $mode = 'search'; // search|coordinates|current
// Component Configuration
protected $geocodeService;
protected $listeners = [
'mapLocationSelected' => 'handleMapLocation',
'clearLocation' => 'clearSelection',
];
public function mount(GeocodeService $geocodeService)
{
$this->geocodeService = $geocodeService;
}
public function render()
{
return view('livewire.location.location-selector');
}
public function updatedSearchQuery()
{
if (strlen($this->searchQuery) < 3) {
$this->searchResults = [];
$this->showSearchResults = false;
return;
}
try {
$this->isSearching = true;
$this->searchResults = $this->geocodeService->geocode($this->searchQuery);
$this->showSearchResults = true;
$this->validationError = null;
} catch (GeocodingException $e) {
$this->validationError = $e->getMessage();
} finally {
$this->isSearching = false;
}
}
public function selectLocation(array $location)
{
try {
if (!isset($location['lat'], $location['lon'])) {
throw new ValidationException('Invalid location data');
}
$this->setCoordinates($location['lat'], $location['lon']);
$this->formattedAddress = $location['display_name'] ?? null;
$this->showSearchResults = false;
$this->searchQuery = '';
$this->isValidLocation = true;
$this->validationError = null;
$this->dispatch('locationSelected', [
'latitude' => $this->latitude,
'longitude' => $this->longitude,
'address' => $this->formattedAddress,
]);
} catch (ValidationException $e) {
$this->validationError = $e->getMessage();
$this->isValidLocation = false;
}
}
public function setCoordinates(float $lat, float $lon)
{
try {
if (!$this->geocodeService->validateCoordinates($lat, $lon)) {
throw new ValidationException('Invalid coordinates');
}
$this->latitude = $lat;
$this->longitude = $lon;
$this->isValidLocation = true;
$this->validationError = null;
$this->dispatch('coordinatesChanged', [
'latitude' => $this->latitude,
'longitude' => $this->longitude,
]);
} catch (ValidationException $e) {
$this->validationError = $e->getMessage();
$this->isValidLocation = false;
}
}
public function handleMapLocation($lat, $lon)
{
$this->setCoordinates($lat, $lon);
try {
$address = $this->geocodeService->reverseGeocode($lat, $lon);
$this->formattedAddress = $address['display_name'] ?? null;
} catch (GeocodingException $e) {
$this->validationError = $e->getMessage();
}
}
public function detectCurrentLocation()
{
$this->mode = 'current';
$this->isLoadingLocation = true;
$this->dispatch('requestCurrentLocation');
}
public function clearSelection()
{
$this->reset([
'latitude',
'longitude',
'formattedAddress',
'isValidLocation',
'searchQuery',
'searchResults',
'showSearchResults',
'validationError',
]);
$this->mode = 'search';
}
public function switchMode(string $mode)
{
if (!in_array($mode, ['search', 'coordinates', 'current'])) {
return;
}
$this->mode = $mode;
$this->clearSelection();
$this->dispatch('modeChanged', ['mode' => $mode]);
}
protected function rules()
{
return [
'latitude' => 'required|numeric|between:-90,90',
'longitude' => 'required|numeric|between:-180,180',
'searchQuery' => 'nullable|string|min:3|max:200',
];
}
}