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.
@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.
@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).
// 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.
// 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.
// 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.
// 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)".
// 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.
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.
// 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".
// 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.
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.
// 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.
// 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.
@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.







