Salta al contenuto principale

15 - Luoghi e Mappe: Geocoding e Venue Management

15 - Luoghi e Mappe: Geocoding e Venue Management: Luoghi e Mappe: Il Catalogo Personale delle Venue Ogni evento ha bisogno di un luogo.

Cosa ne pensi di questo articolo?

15 - Luoghi e Mappe: Geocoding e Venue Management
Play the EventFoto:Christina MorilloPexels

In Breve

15 - Luoghi e Mappe: Geocoding e Venue Management: Luoghi e Mappe: Il Catalogo Personale delle Venue Ogni evento ha bisogno di un luogo.

Luoghi e Mappe: Il Catalogo Personale delle Venue

Ogni evento ha bisogno di un luogo. Ma chi organizza eventi regolarmente accumula decine di venue: ristoranti, sale ricevimenti, case di amici, parchi, locali notturni. In Play The Event il modulo luoghi permette di costruire un catalogo personale di venue, completo di indirizzo, coordinate GPS, contatti, valutazioni e tag, da riutilizzare evento dopo evento.

Cosa Troverai in Questo Articolo

  • L'entità Luogo e i suoi campi: indirizzo, coordinate, contatti
  • TipologiaLuogo per categorizzare le venue
  • Rating personale e preferiti
  • Associazione luogo-collegamento (la casa del mio amico)
  • EventoLuogo: collegare venue a eventi con ruoli specifici
  • Tag per ricerca e filtro rapido
  • Mappe interattive con MapLibre GL JS
  • Geocoding con Nominatim (OpenStreetMap)
  • Sondaggi per la scelta del luogo in gruppo

L'Entità Luogo

Il Luogo e l'Aggregate Root del modulo. Ogni utente gestisce i propri luoghi in modo indipendente: non esiste un database condiviso di venue. Questo perchè le informazioni che un organizzatore salva (note private, valutazione, prezzo medio) sono soggettive e personali.

Struttura dell'Entità Luogo
@Entity
@Table(name = "luoghi")
public class Luogo {

    private Long id;
    private Long utenteId;

    // Tipologia (ristorante, sala eventi, casa privata...)
    private TipologiaLuogo tipologia;

    // Informazioni base
    private String nome;            // "Ristorante Da Mario"
    private String descrizione;     // Descrizione pubblica
    private String notePrivate;     // Note solo per l'organizzatore

    // Indirizzo completo
    private String indirizzo;       // "Via Roma 42"
    private String citta;           // "Bari"
    private String provincia;       // "BA"
    private String cap;             // "70121"
    private String paese;           // "Italia"
    private BigDecimal latitudine;  // 41.12560000
    private BigDecimal longitudine; // 16.86990000

    // Contatti
    private String telefono;
    private String email;
    private String sitoWeb;

    // Dettagli
    private Integer capienzaMax;    // 150 persone
    private BigDecimal prezzoMedio; // 35.00
    private String valuta;          // "EUR"

    // Valutazione e preferiti
    private Integer valutazione;    // 1-5 stelle
    private Boolean preferito;      // Accesso rapido

    // Link a collegamento
    private Long collegamentoId;    // "Casa di Marco"

    // Metadati
    private String immagineUrl;
    private String tags;            // "matrimonio,elegante,vista-mare"
}

Il factory method Luogo.crea(utenteId, tipologia, nome) imposta i valori di default: paese "Italia", valuta "EUR" e preferito a false. Tutti gli altri campi si aggiornano tramite metodi di business dedicati.

TipologiaLuogo: Categorizzare le Venue

Ogni luogo appartiene a una TipologiaLuogo: ristorante, sala eventi, casa privata, parco, locale notturno. Le tipologie sono entità persistenti con supporto multilingua (italiano e inglese), un'icona, un colore e un ordinamento personalizzabile.

Struttura TipologiaLuogo
@Entity
@Table(name = "tipologie_luogo")
public class TipologiaLuogo {

    private Long id;
    private String codice;          // "RISTORANTE"
    private String nomeIt;          // "Ristorante"
    private String nomeEn;          // "Restaurant"
    private String descrizioneIt;   // Descrizione in italiano
    private String descrizioneEn;   // Descrizione in inglese
    private String icona;           // "MapPin" (icona Lucide)
    private String colore;          // "#6366f1"
    private Integer ordine;         // Per ordinamento nel menu
    private Boolean attiva;         // Tipologia abilitata

    public String getNome(String lingua) {
        return "en".equalsIgnoreCase(lingua) ? nomeEn : nomeIt;
    }
}

Il supporto multilingua e integrato direttamente nell'entità. Il metodo getNome(lingua) restituisce il nome nella lingua corretta senza bisogno di tabelle di traduzione separate. Le tipologie possono essere disattivate senza eliminarle, mantenendo la coerenza dei luoghi già classificati.

Esempi di Tipologie

  • RISTORANTE: Ristoranti, trattorie, pizzerie
  • SALA_EVENTI: Sale ricevimenti, spazi per conferenze
  • CASA_PRIVATA: Abitazioni private di amici o familiari
  • LOCALE_NOTTURNO: Pub, discoteche, cocktail bar
  • SPAZIO_APERTO: Parchi, giardini, spiagge
  • HOTEL: Alberghi e strutture ricettive
  • TEATRO: Teatri, cinema, auditorium
  • ALTRO: Qualsiasi tipologia non prevista

Indirizzo e Geocoding

Ogni luogo ha un indirizzo strutturato composto da via, citta, provincia, CAP e paese. In aggiunta, il sistema salva le coordinate GPS come BigDecimal con alta precisione (latitudine 10,8 e longitudine 11,8 cifre).

Aggiornamento Indirizzo con Coordinate
// Aggiorna indirizzo completo con coordinate GPS
luogo.aggiornaIndirizzo(
    "Via Roma 42",            // indirizzo
    "Bari",                   // citta
    "BA",                     // provincia
    "70121",                  // cap
    "Italia",                 // paese
    new BigDecimal("41.12560000"),  // latitudine
    new BigDecimal("16.86990000")   // longitudine
);

// Metodo helper per indirizzo formattato
luogo.getIndirizzoCompleto();
// Risultato: "Via Roma 42, Bari (BA) - 70121"

// Query utili
luogo.hasIndirizzo();    // true se indirizzo presente
luogo.hasCoordinate();   // true se lat/lng presenti

Le coordinate non vengono inserite manualmente dall'utente. Quando l'organizzatore digita un indirizzo, il frontend invia una richiesta al backend che funge da proxy verso Nominatim (il servizio di geocoding di OpenStreetMap). Le coordinate restituite vengono salvate automaticamente insieme all'indirizzo.

Contatti e Dettagli

Un luogo può avere contatti (telefono, email, sito web) e dettagli operativi come capienza massima e prezzo medio. Questi campi sono tutti opzionali perchè non tutti i luoghi li richiedono: la casa di un amico non ha un sito web, un parco non ha una capienza definita.

Aggiornamento Contatti e Dettagli
// Contatti
luogo.aggiornaContatti(
    "+39 080 1234567",                  // telefono
    "info@ristorantedamario.it",        // email
    "https://www.ristorantedamario.it"  // sito web
);

// Dettagli con validazione
luogo.aggiornaDettagli(
    150,                           // capienzaMax (non negativa)
    new BigDecimal("35.00"),       // prezzoMedio (non negativo)
    "EUR"                          // valuta (default EUR)
);

// Il sistema valida automaticamente:
// - capienzaMax >= 0
// - prezzoMedio >= 0
// - valuta non null (default "EUR")

Rating Personale: 1-5 Stelle

Ogni luogo può ricevere una valutazione personale da 1 a 5 stelle. Questo non e un rating pubblico come TripAdvisor: e il giudizio soggettivo dell'organizzatore basato sulla propria esperienza. Un ristorante potrebbe avere 5 stelle su Google ma ricevere 2 stelle dall'organizzatore perchè il servizio per gruppi era lento.

Sistema di Valutazione
// Valuta il luogo (1-5 stelle)
luogo.valuta(4);         // 4 stelle

// Rimuovi la valutazione
luogo.valuta(null);

// Query
luogo.hasValutazione();  // true/false

// Validazione automatica:
// - stelle deve essere tra 1 e 5
// - null e ammesso (nessuna valutazione)
luogo.valuta(0);   // IllegalArgumentException
luogo.valuta(6);   // IllegalArgumentException

// Indice DB per ricerca per valutazione
@Index(name = "idx_luoghi_valutazione",
       columnList = "utente_id, valutazione")

L'indice sul database idx_luoghi_valutazione permette di filtrare rapidamente i luoghi per valutazione: "mostrami tutti i miei ristoranti con 4 o 5 stelle".

Preferiti: Accesso Rapido

I luoghi usati frequentemente possono essere segnati come preferiti. Il flag preferito permette di filtrare rapidamente i luoghi più utilizzati durante la creazione di un evento.

Gestione Preferiti
// Segna come preferito
luogo.segnaPreferito();

// Rimuovi dai preferiti
luogo.rimuoviPreferito();

// Toggle (inverti lo stato)
luogo.togglePreferito();

// Query
luogo.isPreferito();  // true/false

// Indice DB per accesso rapido
@Index(name = "idx_luoghi_preferito",
       columnList = "utente_id, preferito")

Collegamento a Relazioni

Una funzionalità unica di Play The Event e la possibilità di collegare un luogo a una relazione. Se il luogo e la casa di un amico, l'organizzatore può associare quel luogo al collegamento corrispondente. Cosi quando si cerca un luogo per un evento, il sistema mostra anche il nome della relazione associata: "Casa di Marco (Via delle Rose 15, Lecce)".

Associazione Luogo-Collegamento
// Associa il luogo a un collegamento
luogo.associaCollegamento(collegamentoId);
// Esempio: il luogo "Casa in campagna" viene associato
// al collegamento "Marco Rossi"

// Rimuovi l'associazione
luogo.rimuoviCollegamento();

// Query
luogo.isAssociatoACollegamento();  // true/false

// Nel frontend, quando collegamentoId e presente,
// il sistema carica anche il nome del collegamento
// per mostrarlo nella card del luogo

EventoLuogo: Associare Venue agli Eventi

La relazione tra eventi e luoghi e many-to-many: un evento può avere più luoghi (cerimonia in chiesa, ricevimento al ristorante, pernottamento in hotel) e lo stesso luogo può essere usato per eventi diversi. La tabella ponte EventoLuogo gestisce questa associazione con un campo aggiuntivo: il tipo di utilizzo.

TipoUtilizzoLuogo: Il Ruolo della Venue
public enum TipoUtilizzoLuogo {

    PRINCIPALE("Principale"),      // Location principale
    SECONDARIO("Secondario"),      // Location aggiuntiva
    CERIMONIA("Cerimonia"),        // Per la cerimonia (matrimoni)
    RICEVIMENTO("Ricevimento"),    // Per il ricevimento
    PERNOTTAMENTO("Pernottamento"),// Per gli ospiti
    PARCHEGGIO("Parcheggio"),      // Area parcheggio
    PUNTO_RITROVO("Punto Ritrovo") // Dove trovarsi
}

Un matrimonio, ad esempio, potrebbe avere 4 luoghi associati: la chiesa come CERIMONIA, il ristorante come RICEVIMENTO, l'hotel come PERNOTTAMENTO e un parcheggio come PARCHEGGIO. Ogni associazione può anche avere delle note specifiche per l'evento.

Creazione dell'Associazione Evento-Luogo
// Associa un luogo come venue principale
EventoLuogo principale = EventoLuogo.creaPrincipale(evento, ristorante);

// Associa con tipo specifico e note
EventoLuogo cerimonia = EventoLuogo.crea(
    evento,
    chiesa,
    TipoUtilizzoLuogo.CERIMONIA,
    "Ingresso laterale, parcheggio in Piazza Garibaldi"
);

// Query
cerimonia.isPrincipale();  // false
cerimonia.getEventoId();   // ID dell'evento
cerimonia.getLuogoId();    // ID del luogo

// Cambio tipo di utilizzo
cerimonia.aggiornaTipoUtilizzo(TipoUtilizzoLuogo.PRINCIPALE);
cerimonia.aggiornaNote("Nuovo ingresso principale");

Vincolo di Unicita

La combinazione (evento_id, luogo_id) e unica: lo stesso luogo non può essere associato due volte allo stesso evento. Se serve cambiare il tipo di utilizzo, si aggiorna l'associazione esistente anziche crearne una nuova.

Tags: Ricerca e Filtro

Ogni luogo può avere dei tag separati da virgola. I tag permettono una ricerca flessibile che va oltre le tipologie predefinite: "vista-mare", "giardino", "adatto-bambini", "wifi", "parcheggio-privato".

Sistema Tag
// Imposta i tag
luogo.impostaTags("matrimonio,elegante,vista-mare,giardino");

// I tag sono salvati come stringa separata da virgola
// Il frontend li divide e li mostra come chip/badge

// Ricerca per tag (lato repository):
// SELECT * FROM luoghi
// WHERE utente_id = :utenteId
// AND tags LIKE '%vista-mare%'

Mappe Interattive con MapLibre GL JS

Nel frontend Angular di Play The Event, i luoghi prendono vita grazie a MapLibre GL JS, una libreria open-source per mappe vettoriali. Ogni luogo con coordinate viene visualizzato come un marker sulla mappa, con popup interattivi che mostrano nome, tipologia e valutazione.

Funzionalità della Mappa Luoghi

  • Marker colorati per tipologia (ristoranti in rosso, sale eventi in blu, etc.)
  • Clustering automatico quando ci sono molti luoghi vicini
  • Popup interattivi con dettagli del luogo al click
  • Layer filtrabili per tipologia, valutazione o tag
  • Linee di percorso tra i luoghi di un evento
  • Geolocalizzazione per mostrare i luoghi vicini alla posizione attuale
  • Stile personalizzato dark-mode coerente con il design dell'app

La scelta di MapLibre GL JS rispetto a Google Maps e strategica: nessun costo per richiesta, piena personalizzazione dello stile della mappa e compatibilità con tile server gratuiti come OpenStreetMap. Il rendering vettoriale garantisce performance elevate anche con centinaia di marker.

Architettura Mappa nel Frontend Angular
FRONTEND ANGULAR
│
├── MapComponent
│   ├── Inizializzazione MapLibre GL JS
│   ├── Stile: Dark Mode custom (Positron Dark / OSM Liberty)
│   └── Controlli: Zoom, Rotazione, Geolocalizzazione
│
├── Markers Layer
│   ├── Icone per TipologiaLuogo (colore + forma)
│   ├── Clustering automatico (zoom < 12)
│   └── Popup con nome, tipo, valutazione (stelle)
│
├── Route Layer (per eventi multi-luogo)
│   ├── Linee tra venue dello stesso evento
│   └── Ordine: CERIMONIA → RICEVIMENTO → PERNOTTAMENTO
│
└── Interazione
    ├── Click su marker → Apri dettaglio luogo
    ├── Click su mappa → Reverse geocoding (nuovo luogo)
    └── Drag marker → Aggiorna coordinate

Geocoding con Nominatim

Il geocoding (conversione indirizzo-to-coordinate) e gestito tramite un proxy backend verso Nominatim, il servizio di OpenStreetMap. Il backend e necessario per due motivi: gestire il rate limiting (massimo 1 richiesta al secondo) e impostare un User-Agent identificativo come richiesto dalle policy di Nominatim.

Proxy Geocoding nel Backend
// Endpoint REST
GET /api/v1/geocoding/search?q=Via Roma 42, Bari&limit=5
GET /api/v1/geocoding/reverse?lat=41.1256&lng=16.8699

// NominatimClient: gestione rate limiting thread-safe
private static final long MIN_INTERVALLO_MS = 1100; // 1.1 secondi
private final ReentrantLock rateLimitLock = new ReentrantLock();

private void rispettaRateLimit() {
    rateLimitLock.lock();
    try {
        long tempoTrascorso = System.currentTimeMillis()
                              - ultimaRichiestaTimestamp;
        if (tempoTrascorso < MIN_INTERVALLO_MS) {
            Thread.sleep(MIN_INTERVALLO_MS - tempoTrascorso);
        }
        ultimaRichiestaTimestamp = System.currentTimeMillis();
    } finally {
        rateLimitLock.unlock();
    }
}

// Risposta formattata
{
    "latitudine": "41.1256000",
    "longitudine": "16.8699000",
    "nomeVisualizzato": "Via Roma 42, Bari, BA, Italia",
    "indirizzo": {
        "via": "Via Roma",
        "numeroCivico": "42",
        "citta": "Bari",
        "provincia": "Bari",
        "cap": "70121",
        "paese": "Italia"
    }
}

Rate Limiting e User-Agent

Nominatim e un servizio gratuito con una regola fondamentale: massimo 1 richiesta al secondo. Il NominatimClient usa un ReentrantLock per garantire il rispetto del limite anche in scenari multi-thread. L'header User-Agent e obbligatorio e identifica l'applicazione: "PlayTheEvent/1.0 (info@playtheevent.com)".

Sondaggi per la Scelta del Luogo

Quando un gruppo deve decidere dove tenere un evento, il sistema dei sondaggi entra in gioco. Il tipo di entità TipoEntitaSondaggio.LUOGO permette di creare un sondaggio dove le opzioni sono luoghi dal catalogo dell'organizzatore.

Sondaggio per Scelta Luogo
// Crea un sondaggio per scegliere il luogo
Sondaggio sondaggio = Sondaggio.creaPerLuoghi(
    evento,
    organizzatore,
    "Dove facciamo la cena di compleanno?"
);

// Aggiungi luoghi come opzioni
sondaggio.aggiungiOpzioneLuogo(
    "Ristorante Da Mario",     // testo
    luogoMarioId,              // luogoId
    "Vista mare, cucina pugliese, 35 EUR/persona",
    "https://..."              // immagine
);

sondaggio.aggiungiOpzioneLuogo(
    "Trattoria La Nonna",
    luogoNonnaId,
    "Cucina casalinga, giardino esterno, 25 EUR/persona",
    "https://..."
);

// Pubblica il sondaggio
sondaggio.pubblica();

// Dopo le votazioni, chiudi e seleziona il vincitore
sondaggio.chiudi();
sondaggio.selezionaVincitore(opzioneVincitriceId, organizzatore, null);

// Il luogo vincitore può essere automaticamente
// associato all'evento come venue principale

I sondaggi di tipo LUOGO hanno una funzionalità esclusiva: la selezione del vincitore. Dopo che il sondaggio viene chiuso, l'organizzatore può selezionare l'opzione vincitrice (che tipicamente corrisponde alla più votata) e il sistema associa automaticamente quel luogo all'evento.

Caratteristiche del Sondaggio Luoghi

  • Opzioni collegate a luoghi reali con foto, descrizione e coordinate
  • Condivisione pubblica tramite link con codice univoco (UUID)
  • Selezione vincitore con storico delle modifiche e motivazioni
  • Template riutilizzabili per sondaggi ricorrenti
  • Voto anonimo o nominale, modifica voto e risultati in tempo reale

Indici e Performance

La tabella luoghi ha indici strategici per garantire performance elevate nelle query più comuni: ricerca per utente, filtro per tipologia, ricerca per citta, accesso ai preferiti e ordinamento per valutazione.

Indici sulla Tabella Luoghi
@Table(name = "luoghi", indexes = {
    // Tutti i luoghi di un utente
    @Index(name = "idx_luoghi_utente",
           columnList = "utente_id"),

    // Filtro per tipologia
    @Index(name = "idx_luoghi_tipologia",
           columnList = "tipologia_id"),

    // Ricerca per citta
    @Index(name = "idx_luoghi_citta",
           columnList = "citta"),

    // Preferiti dell'utente
    @Index(name = "idx_luoghi_preferito",
           columnList = "utente_id, preferito"),

    // Ordinamento per valutazione
    @Index(name = "idx_luoghi_valutazione",
           columnList = "utente_id, valutazione")
})

Punti Chiave

  • Il Luogo e un catalogo personale dell'organizzatore, non un database condiviso
  • TipologiaLuogo offre categorizzazione multilingua con icone e colori
  • Le coordinate GPS sono salvate come BigDecimal per alta precisione
  • Il collegamento luogo-relazione arricchisce il contesto ("Casa di Marco")
  • EventoLuogo gestisce la relazione many-to-many con tipi di utilizzo specifici
  • MapLibre GL JS visualizza i luoghi con marker, clustering e route layer
  • Nominatim fornisce geocoding gratuito con rate limiting gestito dal backend
  • I sondaggi LUOGO permettono al gruppo di votare la venue con selezione del vincitore

Il codice sorgente e disponibile su GitHub. Per esplorare il modulo luoghi in azione, visita www.playtheevent.com.

Domande frequenti

Come funziona la creazione del catalogo personale di venue?

Il modulo "Luoghi" consente di costruire un catalogo personale di venue, completo di indirizzo, coordinate GPS, contatti, valutazioni e tag, da riutilizzare evento dopo evento.

Perché le informazioni salvate sui luoghi sono soggettive e personali?

Le informazioni salvate sui luoghi sono soggettive e personali perché ogni utente gestisce i propri luoghi in modo indipendente, senza un database condiviso di venue.

Come funziona la categorizzazione delle venue?

Ogni luogo appartiene a una TipologiaLuogo (ad esempio ristorante, sala eventi, casa privata) che è un'entità persistente con supporto multilingua e personalizzabile.

Come vengono salvate le coordinate GPS dei luoghi?

Le coordinate GPS sono salvate automaticamente quando l'organizzatore digita un indirizzo, grazie al geocoding tramite Nominatim (il servizio di OpenStreetMap).

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti