Trip places: filter by location
This commit is contained in:
parent
df09c4c799
commit
69cde40fc1
@ -1,13 +1,8 @@
|
|||||||
<section>
|
<section>
|
||||||
<div class="max-w-full overflow-y-auto">
|
<div class="max-w-full overflow-y-auto">
|
||||||
<div class="mb-4">
|
|
||||||
<p-floatlabel variant="in">
|
|
||||||
<input id="search" pSize="small" [formControl]="searchInput" pInputText fluid />
|
|
||||||
<label for="search">Search...</label>
|
|
||||||
</p-floatlabel>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-8 flex justify-between items-center gap-4">
|
<div class="mt-8 flex justify-between items-center gap-4 cursor-pointer"
|
||||||
|
(click)="showSelectedPlaces = !showSelectedPlaces">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<h1 class="font-semibold tracking-tight text-xl">Selected</h1>
|
<h1 class="font-semibold tracking-tight text-xl">Selected</h1>
|
||||||
@ -18,9 +13,9 @@
|
|||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p-button [icon]="showSelectedPlaces ? 'pi pi-chevron-up' : 'pi pi-chevron-down'" text
|
<p-button [icon]="showSelectedPlaces ? 'pi pi-chevron-up' : 'pi pi-chevron-down'" text />
|
||||||
(click)="showSelectedPlaces = !showSelectedPlaces" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (showSelectedPlaces) {
|
@if (showSelectedPlaces) {
|
||||||
@for (p of selectedPlaces; track p.id) {
|
@for (p of selectedPlaces; track p.id) {
|
||||||
<div
|
<div
|
||||||
@ -58,9 +53,39 @@
|
|||||||
|
|
||||||
<div class="mt-8 mb-16">
|
<div class="mt-8 mb-16">
|
||||||
<h1 class="font-semibold tracking-tight text-xl">List</h1>
|
<h1 class="font-semibold tracking-tight text-xl">List</h1>
|
||||||
<span class="text-xs text-gray-500">Available points</span>
|
<span class="text-xs text-gray-500">Available Places</span>
|
||||||
@defer {
|
@defer {
|
||||||
|
|
||||||
|
<div class="grid md:grid-cols-3 gap-2 mt-2 p-2">
|
||||||
|
<p-floatlabel variant="in" class="md:col-span-2">
|
||||||
|
<input id="search" pSize="small" [formControl]="searchInput" pInputText fluid />
|
||||||
|
<label for="search">Search...</label>
|
||||||
|
</p-floatlabel>
|
||||||
|
|
||||||
|
<div class="relative">
|
||||||
|
<p-floatlabel variant="in">
|
||||||
|
<input id="geocodeInput" pSize="small" (keydown.enter)="gmapsGeocodeFilter()"
|
||||||
|
[formControl]="googleGeocodeInput" pInputText autofocus fluid />
|
||||||
|
<label for="geocodeInput">Filter by Country / City </label>
|
||||||
|
</p-floatlabel>
|
||||||
|
|
||||||
|
@if (boundariesFiltering) {<p-button icon="pi pi-times" variant="text" severity="danger"
|
||||||
|
class="absolute right-2 top-1/2 -translate-y-1/2" pTooltip="Clear and Cancel"
|
||||||
|
(click)="resetGeocodeFilters()" />}
|
||||||
|
@else {<p-button icon="pi pi-sparkles" variant="text" [disabled]="!googleGeocodeInput.value"
|
||||||
|
class="absolute right-2 top-1/2 -translate-y-1/2" pTooltip="Filter using location (GMaps API)"
|
||||||
|
(click)="gmapsGeocodeFilter()" />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (boundariesFiltering) {
|
||||||
|
<div class="mt-4 flex justify-center items-center gap-1">
|
||||||
|
<p class="text-gray-500 dark:text-gray-400">Filtering for location: {{ googleGeocodeInput.value }}</p>
|
||||||
|
<p-button icon="pi pi-times" variant="text" severity="danger" pTooltip="Cancel"
|
||||||
|
(click)="resetGeocodeFilters()" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
@for (p of displayedPlaces; track p.id) {
|
@for (p of displayedPlaces; track p.id) {
|
||||||
<div
|
<div
|
||||||
class="mt-4 flex items-center gap-4 hover:bg-gray-50 rounded-xl cursor-pointer py-2 px-4 dark:hover:bg-gray-800"
|
class="mt-4 flex items-center gap-4 hover:bg-gray-50 rounded-xl cursor-pointer py-2 px-4 dark:hover:bg-gray-800"
|
||||||
@ -91,8 +116,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
} @empty {
|
} @empty {
|
||||||
<div class="text-center">
|
<div class="mt-8 text-center">
|
||||||
<h1 class="tracking-tight">Nothing to see</h1>
|
<p class="text-base text-gray-500 dark:text-gray-400">Nothing to see</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,20 +4,25 @@ import { ButtonModule } from 'primeng/button';
|
|||||||
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
|
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
|
||||||
import { FloatLabelModule } from 'primeng/floatlabel';
|
import { FloatLabelModule } from 'primeng/floatlabel';
|
||||||
import { InputTextModule } from 'primeng/inputtext';
|
import { InputTextModule } from 'primeng/inputtext';
|
||||||
import { Place } from '../../types/poi';
|
import { GoogleBoundaries, Place } from '../../types/poi';
|
||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '../../services/api.service';
|
||||||
import { SkeletonModule } from 'primeng/skeleton';
|
import { SkeletonModule } from 'primeng/skeleton';
|
||||||
import { UtilsService } from '../../services/utils.service';
|
import { UtilsService } from '../../services/utils.service';
|
||||||
|
import { take } from 'rxjs';
|
||||||
|
import { isPointInBounds } from '../../shared/map';
|
||||||
|
import { TooltipModule } from 'primeng/tooltip';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-trip-place-select-modal',
|
selector: 'app-trip-place-select-modal',
|
||||||
imports: [FloatLabelModule, InputTextModule, ButtonModule, ReactiveFormsModule, SkeletonModule],
|
imports: [FloatLabelModule, InputTextModule, ButtonModule, ReactiveFormsModule, SkeletonModule, TooltipModule],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
templateUrl: './trip-place-select-modal.component.html',
|
templateUrl: './trip-place-select-modal.component.html',
|
||||||
styleUrl: './trip-place-select-modal.component.scss',
|
styleUrl: './trip-place-select-modal.component.scss',
|
||||||
})
|
})
|
||||||
export class TripPlaceSelectModalComponent {
|
export class TripPlaceSelectModalComponent {
|
||||||
searchInput = new FormControl('');
|
searchInput = new FormControl('');
|
||||||
|
googleGeocodeInput = new FormControl('');
|
||||||
|
boundariesFiltering?: GoogleBoundaries;
|
||||||
|
|
||||||
selectedPlaces: Place[] = [];
|
selectedPlaces: Place[] = [];
|
||||||
showSelectedPlaces: boolean = false;
|
showSelectedPlaces: boolean = false;
|
||||||
@ -48,17 +53,7 @@ export class TripPlaceSelectModalComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.searchInput.valueChanges.subscribe({
|
this.searchInput.valueChanges.subscribe({
|
||||||
next: (value) => {
|
next: () => this.filterPlaces(),
|
||||||
if (!value) {
|
|
||||||
this.displayedPlaces = this.places;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const v = value.toLowerCase();
|
|
||||||
this.displayedPlaces = this.places.filter(
|
|
||||||
(p) => p.name.toLowerCase().includes(v) || p.description?.toLowerCase().includes(v),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,7 +81,43 @@ export class TripPlaceSelectModalComponent {
|
|||||||
this.selectedPlaces.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
this.selectedPlaces.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filterPlaces() {
|
||||||
|
const searchInputValue = (this.searchInput.value || '').toLowerCase();
|
||||||
|
this.displayedPlaces = this.places.filter((place) => {
|
||||||
|
if (this.boundariesFiltering) {
|
||||||
|
if (!isPointInBounds(place.lat, place.lng, this.boundariesFiltering)) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!searchInputValue) return true;
|
||||||
|
return (
|
||||||
|
place.name.toLowerCase().includes(searchInputValue) ||
|
||||||
|
place.description?.toLowerCase().includes(searchInputValue)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resetGeocodeFilters() {
|
||||||
|
this.boundariesFiltering = undefined;
|
||||||
|
this.googleGeocodeInput.setValue('');
|
||||||
|
this.filterPlaces();
|
||||||
|
}
|
||||||
|
|
||||||
closeDialog() {
|
closeDialog() {
|
||||||
this.ref.close(this.selectedPlaces);
|
this.ref.close(this.selectedPlaces);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gmapsGeocodeFilter() {
|
||||||
|
const value = this.googleGeocodeInput.value;
|
||||||
|
if (!value) return;
|
||||||
|
|
||||||
|
this.apiService
|
||||||
|
.gmapsGeocodeBoundaries(value)
|
||||||
|
.pipe(take(1))
|
||||||
|
.subscribe({
|
||||||
|
next: (boundaries) => {
|
||||||
|
this.boundariesFiltering = boundaries;
|
||||||
|
this.filterPlaces();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import * as L from 'leaflet';
|
import * as L from 'leaflet';
|
||||||
import 'leaflet.markercluster';
|
import 'leaflet.markercluster';
|
||||||
import 'leaflet-contextmenu';
|
import 'leaflet-contextmenu';
|
||||||
import { Place } from '../types/poi';
|
import { GoogleBoundaries, Place } from '../types/poi';
|
||||||
|
|
||||||
export const DEFAULT_TILE_URL = 'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png';
|
export const DEFAULT_TILE_URL = 'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png';
|
||||||
export interface ContextMenuItem {
|
export interface ContextMenuItem {
|
||||||
@ -143,3 +143,14 @@ export function gpxToPolyline(gpx: string): L.Polyline {
|
|||||||
|
|
||||||
return L.polyline(latlngs, { color: 'blue' });
|
return L.polyline(latlngs, { color: 'blue' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isPointInBounds(lat: number, lng: number, bounds: GoogleBoundaries): boolean {
|
||||||
|
if (!bounds || !bounds.northeast || !bounds.southwest) return false;
|
||||||
|
|
||||||
|
const ne = bounds.northeast;
|
||||||
|
const sw = bounds.southwest;
|
||||||
|
|
||||||
|
if (lat < sw.lat || lat > ne.lat) return false;
|
||||||
|
|
||||||
|
return sw.lng <= ne.lng ? lng >= sw.lng && lng <= ne.lng : lng >= sw.lng || lng <= ne.lng;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user