From 80ea0797c00284e0f31c518c81f3dab461ef62a9 Mon Sep 17 00:00:00 2001 From: Anthony Navarro Date: Fri, 24 Apr 2026 16:15:05 -0700 Subject: [PATCH 1/2] Various updates to the map page --- app/Livewire/FossilMap.php | 18 ++- app/Livewire/OccurrenceFilters.php | 52 ++++++-- resources/js/map.js | 38 ------ .../components/form/mode-toggle.blade.php | 28 ++++ resources/views/layouts/app.blade.php | 2 +- resources/views/livewire/fossil-map.blade.php | 31 ++++- .../livewire/occurrence-filters.blade.php | 122 ++++++++---------- tests/Feature/Livewire/FossilMapTest.php | 40 +++++- .../Livewire/OccurrenceFiltersPresetTest.php | 44 ++++++- .../OccurrenceFiltersValidationTest.php | 91 +++++++++++++ 10 files changed, 346 insertions(+), 120 deletions(-) create mode 100644 resources/views/components/form/mode-toggle.blade.php create mode 100644 tests/Feature/Livewire/OccurrenceFiltersValidationTest.php diff --git a/app/Livewire/FossilMap.php b/app/Livewire/FossilMap.php index 91fb460..48594f0 100644 --- a/app/Livewire/FossilMap.php +++ b/app/Livewire/FossilMap.php @@ -95,11 +95,25 @@ public function onFiltersApplied( $this->dispatch('occurrences-loaded', occurrences: $occurrences); } catch (ApiException $e) { - $this->loadError = $e->getMessage(); - $this->dispatch('occurrences-error', message: $e->getMessage()); + $this->loadError = 'The search could not be completed. Please try again or adjust your filters.'; + $this->dispatch('occurrences-error', message: $this->loadError); } } + /** + * Triggered when OccurrenceFilters resets all filter state. + * Clears map markers and restores the initial empty-state overlay. + */ + #[On('filters-reset')] + public function onFiltersReset(): void + { + $this->filtersApplied = false; + $this->resultCount = 0; + $this->resultTotal = 0; + $this->loadError = null; + $this->dispatch('occurrences-loaded', occurrences: []); + } + public function render(): View { return view('livewire.fossil-map', [ diff --git a/app/Livewire/OccurrenceFilters.php b/app/Livewire/OccurrenceFilters.php index 0a75f9a..f77725e 100644 --- a/app/Livewire/OccurrenceFilters.php +++ b/app/Livewire/OccurrenceFilters.php @@ -40,6 +40,14 @@ class OccurrenceFilters extends Component */ public bool $customTaxon = false; + /** + * When true, shows min/max Ma inputs instead of the named interval select. + */ + public bool $customInterval = false; + + /** Validation message shown near the Apply button when filters are incomplete. */ + public ?string $validationError = null; + /** * All PBDB envtype values available for filtering. * @@ -152,6 +160,7 @@ public static function countryOptions(): array */ public function updated(string $name): void { + $this->validationError = null; $this->dispatch('filtersChanged'); } @@ -176,6 +185,7 @@ public function loadPreset(string $id): void $this->envTypes = $preset->envTypes; $this->countryCodes = $preset->countryCodes ?? ''; $this->customTaxon = false; + $this->customInterval = $preset->minMa !== null || $preset->maxMa !== null; $this->applyFilters(); } @@ -198,6 +208,25 @@ public function disableCustomTaxon(): void $this->baseName = ''; } + /** + * Switch the time period field to custom age range inputs and clear the interval. + */ + public function enableCustomInterval(): void + { + $this->customInterval = true; + $this->interval = ''; + } + + /** + * Switch the time period field back to the named interval select and reset age range. + */ + public function disableCustomInterval(): void + { + $this->customInterval = false; + $this->minMa = 0; + $this->maxMa = 540; + } + /** * Construct an OccurrenceQuery from the current filter state. */ @@ -224,6 +253,14 @@ public function buildQuery(): OccurrenceQuery */ public function applyFilters(): void { + if (! $this->hasFilters()) { + $this->validationError = 'Please select an organism or time period to search.'; + + return; + } + + $this->validationError = null; + $this->dispatch( 'apply-filters', baseName: $this->baseName !== '' ? $this->baseName : null, @@ -257,6 +294,8 @@ public function resetFilters(): void $this->latMin = null; $this->latMax = null; $this->customTaxon = false; + $this->customInterval = false; + $this->validationError = null; $this->dispatch('filters-reset'); } @@ -284,21 +323,16 @@ public function setBoundingBox(float $lngMin, float $lngMax, float $latMin, floa } /** - * Returns true when at least one filter differs from its default. + * Returns true when a primary filter (organism or time period) is set. + * Environment, country, and ID quality are refiners and cannot stand alone — + * PBDB requires at least one taxon or temporal parameter. */ public function hasFilters(): bool { return $this->baseName !== '' || $this->interval !== '' || $this->minMa > 0 - || $this->maxMa < 540 - || $this->envTypes !== [] - || $this->countryCodes !== '' - || $this->idQual !== 'any' - || $this->lngMin !== null - || $this->lngMax !== null - || $this->latMin !== null - || $this->latMax !== null; + || $this->maxMa < 540; } public function render(): View diff --git a/resources/js/map.js b/resources/js/map.js index 67bc954..1b3ddcf 100644 --- a/resources/js/map.js +++ b/resources/js/map.js @@ -3,8 +3,6 @@ import 'leaflet/dist/leaflet.css'; import 'leaflet.markercluster/dist/MarkerCluster.css'; import 'leaflet.markercluster/dist/MarkerCluster.Default.css'; import 'leaflet.markercluster'; -import 'leaflet-draw/dist/leaflet.draw.css'; -import 'leaflet-draw'; import 'leaflet.heat'; // Vite bundles assets differently to webpack — provide explicit URLs for // Leaflet's default marker icons so they resolve correctly at runtime. @@ -50,7 +48,6 @@ function fossilMap() { map: null, markerClusterGroup: null, heatLayer: null, - drawControl: null, tileLayers: {}, currentOccurrences: [], heatmapMode: false, @@ -72,41 +69,6 @@ function fossilMap() { // Marker cluster group this.markerClusterGroup = L.markerClusterGroup(); this.map.addLayer(this.markerClusterGroup); - - // Feature group to hold drawn shapes - const drawnItems = new L.FeatureGroup(); - this.map.addLayer(drawnItems); - - // Draw control — rectangle only - this.drawControl = new L.Control.Draw({ - draw: { - rectangle: true, - polyline: false, - polygon: false, - circle: false, - marker: false, - circlemarker: false, - }, - edit: { featureGroup: drawnItems }, - }); - this.map.addControl(this.drawControl); - - // Push drawn rectangle bounds into the OccurrenceFilters Livewire - // component via a global Livewire event — OccurrenceFilters listens - // for 'bbox-set' with #[On('bbox-set')]. - this.map.on(L.Draw.Event.CREATED, (event) => { - drawnItems.clearLayers(); - drawnItems.addLayer(event.layer); - - const bounds = event.layer.getBounds(); - window.Livewire.dispatch('bbox-set', { - lngMin: bounds.getWest(), - lngMax: bounds.getEast(), - latMin: bounds.getSouth(), - latMax: bounds.getNorth(), - }); - }); - }, /** diff --git a/resources/views/components/form/mode-toggle.blade.php b/resources/views/components/form/mode-toggle.blade.php new file mode 100644 index 0000000..367d410 --- /dev/null +++ b/resources/views/components/form/mode-toggle.blade.php @@ -0,0 +1,28 @@ +@props([ + 'isCustom' => false, + 'defaultLabel' => 'Select', + 'customLabel' => 'Custom', + 'onDefault' => '', + 'onCustom' => '', +]) + +
+ + +
diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index c3e7dca..8112974 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -25,7 +25,7 @@ @vite(['resources/css/app.css', 'resources/js/app.js']) -