mirror of
https://github.com/pacnpal/thrillwiki_laravel.git
synced 2025-12-20 06:11:09 -05:00
Implement LocationDisplayComponent and LocationMapComponent for interactive map features; add event handling and state management
This commit is contained in:
118
app/Livewire/Location/LocationDisplayComponent.php
Normal file
118
app/Livewire/Location/LocationDisplayComponent.php
Normal 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');
|
||||
}
|
||||
}
|
||||
162
app/Livewire/Location/LocationMapComponent.php
Normal file
162
app/Livewire/Location/LocationMapComponent.php
Normal 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()
|
||||
]);
|
||||
}
|
||||
}
|
||||
167
app/Livewire/Location/LocationSelectorComponent.php
Normal file
167
app/Livewire/Location/LocationSelectorComponent.php
Normal 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',
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user