import { CommonModule } from '@angular/common'; import { Component, ViewChild } from '@angular/core'; import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators, } from '@angular/forms'; import { GoogleMap, GoogleMapsModule, MapInfoWindow, MapMarker, } from '@angular/google-maps'; import { LuogoRes, StrutturePubblicheResDto, TipoStruttura } from '@api/models'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { faCity, faGlobeEurope, faHospital, faMapLocationDot, } from '@fortawesome/free-solid-svg-icons'; import { RxState } from '@rx-angular/state'; import { MessageService, ToastMessageOptions } from 'primeng/api'; import { AutoCompleteModule } from 'primeng/autocomplete'; import { ButtonModule } from 'primeng/button'; import { DividerModule } from 'primeng/divider'; import { DropdownModule } from 'primeng/dropdown'; import { IconFieldModule } from 'primeng/iconfield'; import { InputIconModule } from 'primeng/inputicon'; import { InputTextModule } from 'primeng/inputtext'; import { MultiSelectModule } from 'primeng/multiselect'; import { RatingModule } from 'primeng/rating'; import { Table, TableModule } from 'primeng/table'; import { ToastModule } from 'primeng/toast'; import { ToggleButtonModule } from 'primeng/togglebutton'; import { catchError, EMPTY, endWith, map, Observable, of, startWith, switchMap, tap, } from 'rxjs'; import { StrutturePubblicheControllerFindManyStrutture$Params } from '../../../../api/fn/strutture-pubbliche/strutture-pubbliche-controller-find-many-strutture'; import { TableColumn } from '../../shared'; import { StrutturePubblicheService } from './strutture-pubbliche.service'; const cercaStruttureFormGroupFunc = () => { return new FormGroup({ tipologiaStruttura: new FormControl(null), luogo: new FormControl(null, { validators: [Validators.required], }), indirizzo: new FormControl(null), }); }; export type cercaStruttureForm = ReturnType; export type cercaStruttureFormValue = cercaStruttureForm['value']; export interface CercaStruttureComponentState { strutture: StrutturePubblicheResDto[]; tipologieStrutture: TipoStruttura[]; filteredLuoghiEsteso: Array; cercaStruttureFormSubmitted: boolean; tipologieStruttureAreLoading: boolean; struttureAreLoading: boolean; luoghiAreLoading: boolean; isSearching: boolean; } type State = CercaStruttureComponentState; @Component({ selector: 'app-strutture-pubbliche', imports: [ CommonModule, FormsModule, ReactiveFormsModule, ButtonModule, DividerModule, MultiSelectModule, ToggleButtonModule, GoogleMapsModule, AutoCompleteModule, FontAwesomeModule, InputTextModule, DropdownModule, ToastModule, TableModule, IconFieldModule, InputIconModule, RatingModule, ], providers: [RxState], templateUrl: './strutture-pubbliche.component.html', styleUrl: './strutture-pubbliche.component.scss', }) export class StrutturePubblicheComponent { model$: Observable; messages: ToastMessageOptions[] = []; @ViewChild('gmap', { static: false }) map!: GoogleMap; @ViewChild(MapInfoWindow) infoWindow!: MapInfoWindow; options: google.maps.MapOptions; markerOptions: google.maps.MarkerOptions[] = []; indirizzoGmaps: string = 'indirizoooo'; struttureTable!: Table; calcolaIndirizzoDa: string | null | undefined; @ViewChild('struttureTable') set table(table: Table) { if (!table || this.struttureTable === table) return; this.struttureTable = table; } cercaStruttureForm = cercaStruttureFormGroupFunc(); faCity = faCity; faGlobeEurope = faGlobeEurope; faMapLocationDot = faMapLocationDot; faHospital = faHospital; struttureConCoords: StrutturePubblicheResDto[] = []; cols: TableColumn[] = [ { header: 'Tipologia', field: 'struttura.struttureTipiStrutture', }, { header: 'Strutture', field: 'struttura.nome', sortField: 'struttura.nome', }, { header: 'Indirizzo', field: 'indirizzo', }, { header: 'Posizione', field: 'posizione', }, ]; constructor( private strutturePubblicheService: StrutturePubblicheService, public state: RxState, private messageService: MessageService, ) { this.model$ = this.state.select(); this.state.set({ strutture: undefined, tipologieStrutture: [], filteredLuoghiEsteso: [], cercaStruttureFormSubmitted: false, tipologieStruttureAreLoading: false, struttureAreLoading: false, luoghiAreLoading: false, isSearching: false, }); const fetchTipologieStrutture$ = of(EMPTY).pipe( switchMap(() => this.strutturePubblicheService.getTipologieStrutture().pipe( map((res) => ({ tipologieStrutture: res, })), catchError(() => of({ tipologieStrutture: [] })), startWith({ tipologieStruttureAreLoading: true }), endWith({ tipologieStruttureAreLoading: false }), ), ), ); this.state.connect(fetchTipologieStrutture$); this.options = { center: { lat: 45.4627123, lng: 9.1075213 }, zoom: 8, // mapId: 'c18657fa9d1be788abd38482', }; } fetchLuogo( event: { originalEvent: Event; query: string }, flagAttivo?: number, ) { if (!event.query || event.query.length < 3) { this.state.set({ filteredLuoghiEsteso: [] }); return; } const fetchCities$ = this.strutturePubblicheService .getLuoghiEsteso(event.query, flagAttivo) .pipe( catchError((err) => { return of(null); }), map((res) => { const mapped = res?.rows.map((r) => ({ ...r, dataKey: r.tipo + ' → ' + r.luogo, })) ?? []; // ⭐ ORDINAMENTO EURISTICO LOCALE const sorted = mapped.sort((a, b) => { const rankA = this.rankLuogo(a, event.query); const rankB = this.rankLuogo(b, event.query); if (rankA !== rankB) return rankA - rankB; return a.luogo!.localeCompare(b.luogo!); }); return { filteredLuoghiEsteso: sorted }; }), ); this.state.connect(fetchCities$); } getStrutture() { this.state.set({ cercaStruttureFormSubmitted: true, }); if (this.cercaStruttureForm.invalid) { return; } const _form = this.cercaStruttureForm.value; this.cercaStruttureForm.disable(); const params: StrutturePubblicheControllerFindManyStrutture$Params = { indirizzo: _form.indirizzo, tipoStruttura: _form.tipologiaStruttura, luogo: _form.luogo!.luogo!, 'luogo.tipo': _form.luogo!.tipo!, }; this.calcolaIndirizzoDa = _form.indirizzo; const strutture$ = this.strutturePubblicheService.getStrutture(params).pipe( catchError((err) => { this.messageService.add({ severity: 'error', life: 5000, summary: 'Attenzione!', detail: err.status === 400 ? 'Il luogo è obbligatorio' : 'Impossibile recuperare la lista delle strutture in questo momento, riprova più tardi', }); return of([] as StrutturePubblicheResDto[]); }), tap((res) => { this.cercaStruttureForm.enable(); if (!res) return; this.gMapMarkers(res, false); }), map((res) => ({ strutture: res })), startWith({ struttureAreLoading: true, isSearching: true }), endWith({ struttureAreLoading: false, isSearching: false }), ); this.state.connect(strutture$); } openInfoWindow(marker: MapMarker, index: number) { if (this.struttureConCoords.length === index) { // se è home icon (il push è stato fatto alla fine, quindi sarà l'ultimo nella lista) const contentStringHome = `

${this.indirizzoGmaps}

`; this.infoWindow.infoWindow?.setContent(contentStringHome); this.infoWindow.open(marker); } else { const info = this.struttureConCoords[index]; const contentString = `
${''.repeat( info.struttura.stelline.length, )}

${ info.struttura.nome }

${info.struttura.indirizzo}${ info.struttura.numeroCivico ? ', ' + info.struttura.numeroCivico : '' }

${info.struttura.cap} ${info.struttura.codiceLuogo.comune} ${ info.struttura.codiceLuogo.provincia ? '(' + info.struttura.codiceLuogo.siglaProvincia + ')' : '' }

`; this.infoWindow.infoWindow?.setContent(contentString); this.infoWindow.open(marker); this.infoWindow.infoWindow?.addListener('domready', () => { const el = document.getElementById(`facility_${info?.struttura.id}`); if (el) { el.addEventListener('click', () => { // this.next(info); }); } }); } } gMapMarkers(items: StrutturePubblicheResDto[], filter: boolean) { // Filtra solo strutture con coordinate valide const itemsWithCoords = items.filter((x) => { const lat = this.parseCoord(x.struttura.latitudine); const lng = this.parseCoord(x.struttura.longitudine); return lat !== null && lng !== null; }); this.struttureConCoords = itemsWithCoords; if (!itemsWithCoords.length) { this.markerOptions = []; return; } // Calcola centro mappa const firstItem = itemsWithCoords[0]; const centerLat = this.parseCoord(firstItem.struttura.latitudine) ?? 45.472520078847595; const centerLng = this.parseCoord(firstItem.struttura.longitudine) ?? 9.194890732834459; this.options = { center: { lat: centerLat, lng: centerLng }, zoom: itemsWithCoords.length ? 13 : 11, // mapId: 'c18657fa9d1be788abd38482', }; // Marker strutture this.markerOptions = itemsWithCoords.map((x, i) => ({ position: { lat: this.parseCoord(x.struttura.latitudine)!, lng: this.parseCoord(x.struttura.longitudine)!, }, title: x.struttura.nome, draggable: false, icon: !filter && i < 6 ? 'https://maps.google.com/mapfiles/kml/paddle/blu-stars.png' : undefined, })); // Marker "home" if (firstItem.startingLocation) { this.markerOptions.push({ position: firstItem.startingLocation, title: this.indirizzoGmaps, icon: { url: 'https://maps.google.com/mapfiles/kml/pal3/icon56.png', scaledSize: new google.maps.Size(50, 50), }, }); } // Crea i marker cluster dopo che la mappa è pronta // setTimeout(() => { // if (!this.map?.googleMap) return; // const markers = this.markerOptions // .filter((opt) => !!opt.position) // .map((opt) => { // return new google.maps.marker.AdvancedMarkerElement({ // position: opt.position!, // title: opt.title, // map: this.map.googleMap!, // }); // }); // new MarkerClusterer({ // map: this.map.googleMap, // markers, // }); // }); } private parseCoord(value?: string | null): number | null { if (!value) return null; const n = Number(value.replace(',', '.')); return Number.isFinite(n) ? n : null; } externalLink(url: string) { window.open('//' + url, '_blank'); } applyFilterGlobal($event: Event, stringVal: string) { console.log(this.struttureTable); this.struttureTable.filterGlobal( ($event.target as HTMLInputElement).value, stringVal, ); } calculateMapsLink(struttura: StrutturePubblicheResDto, open = true): string { // https://www.google.com/maps/dir/?api=1&origin=45.476333943968626,9.17169124076061&destination=45.4692422,9.16562&travelmode=driving // https://www.google.com/maps/dir/?api=1&destination=45.4692422,9.16562&travelmode=driving const baseUrl = 'https://www.google.com/maps/dir/?api=1'; const travelMode = 'driving'; const destinationMode: 'latlng' | 'nameAddress' = 'nameAddress'; let url = ((destinationMode: 'latlng' | 'nameAddress') => { switch (destinationMode) { case 'nameAddress': { let address = `${struttura.struttura.indirizzo}${ struttura.struttura.numeroCivico ? ', ' + struttura.struttura.numeroCivico : '' }`; address += `${struttura.struttura.cap} ${struttura.struttura.codiceLuogo.comune}(${struttura.struttura.codiceLuogo.siglaProvincia})`; // address += `${struttura.struttura.codiceLuogo.provincia ? '(' + struttura.struttura.codiceLuogo.siglaProvincia + ')' : ''}`; const destination = encodeURIComponent( `${struttura.struttura.nome}, ${address}`, ); return `${baseUrl}&destination=${destination}`; } case 'latlng': return `${baseUrl}&destination=${struttura.struttura.latitudine},${struttura.struttura.longitudine}`; } })(destinationMode); if (this.calcolaIndirizzoDa) { url = `${url}&origin=${this.calcolaIndirizzoDa}`; } const luogo = this.cercaStruttureForm.controls.luogo.value; if (luogo) { url = `${url}, ${luogo.luogo}`; } url = `${url}&travelmode=${travelMode}`; if (open) window.open(url, '_blank'); return url; } checkLuogo() { //Hack: rimosso dall'autocomplete forceSelection perchè non funzionava come previsto, selezionando ad esempio "Milano" (comune), selezionava "Milano" (comune). const luogo = this.cercaStruttureForm.controls['luogo'].value; if (typeof luogo === 'string') { this.cercaStruttureForm.controls['luogo'].setValue(null); } } private rankLuogo(luogo: any, query: string): number { const q = query.toLowerCase(); const nome = luogo.luogo.toLowerCase(); // 1. Match perfetto (inizia per query) if (nome.startsWith(q)) return 0; // 2. Match contenuto ma non all'inizio if (nome.includes(q)) return 1; // 3. Nessun match diretto → meno rilevante return 2; } }