Venues and Maps: Building a Personal Location Catalog
Every event needs a place. But anyone who organizes events regularly accumulates dozens of venues: restaurants, banquet halls, friends' homes, parks, nightclubs. In Play The Event, the venue module lets organizers build a personal catalog of locations, complete with addresses, GPS coordinates, contacts, ratings, and tags, ready to be reused from one event to the next.
What You Will Learn in This Article
- The Luogo entity and its fields: address, coordinates, contacts
- TipologiaLuogo for categorizing venues
- Personal ratings and favorites
- Linking venues to relationships (my friend's house)
- EventoLuogo: connecting venues to events with specific roles
- Tags for quick search and filtering
- Interactive maps with MapLibre GL JS
- Geocoding with Nominatim (OpenStreetMap)
- Polls for group venue selection
The Luogo Entity
Luogo (Italian for "place") is the Aggregate Root of the venue module. Each user manages their own venues independently: there is no shared database of locations. This design choice reflects the fact that the information an organizer saves (private notes, personal rating, average price) is subjective and user-specific.
@Entity
@Table(name = "luoghi")
public class Luogo {
private Long id;
private Long utenteId; // Owner user ID
// Category (restaurant, event hall, private home...)
private TipologiaLuogo tipologia;
// Basic information
private String nome; // "Ristorante Da Mario"
private String descrizione; // Public description
private String notePrivate; // Notes visible only to the organizer
// Full address
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
// Contacts
private String telefono;
private String email;
private String sitoWeb;
// Details
private Integer capienzaMax; // 150 people
private BigDecimal prezzoMedio; // 35.00
private String valuta; // "EUR"
// Rating and favorites
private Integer valutazione; // 1-5 stars
private Boolean preferito; // Quick access flag
// Link to a relationship
private Long collegamentoId; // "Marco's house"
// Metadata
private String immagineUrl;
private String tags; // "wedding,elegant,sea-view"
}
The factory method Luogo.crea(utenteId, tipologia, nome)
sets sensible defaults: country to "Italia", currency to "EUR", and
favorite to false. All other fields are updated through
dedicated business methods that enforce validation rules.
TipologiaLuogo: Venue Categories
Every venue belongs to a TipologiaLuogo (venue type): restaurant, event hall, private home, park, nightclub. Categories are persistent entities with built-in multilingual support (Italian and English), an icon, a color, and a customizable sort order.
@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; // Italian description
private String descrizioneEn; // English description
private String icona; // "MapPin" (Lucide icon)
private String colore; // "#6366f1"
private Integer ordine; // Sort order in menus
private Boolean attiva; // Whether the category is enabled
// Returns the name in the correct language
public String getNome(String lingua) {
return "en".equalsIgnoreCase(lingua) ? nomeEn : nomeIt;
}
}
Multilingual support is built directly into the entity. The
getNome(lingua) method returns the name in the requested
language without requiring separate translation tables. Categories can
be deactivated without deletion, preserving referential integrity for
venues already classified under them.
Example Venue Categories
- RISTORANTE: Restaurants, trattorias, pizzerias
- SALA_EVENTI: Banquet halls, conference spaces
- CASA_PRIVATA: Private homes of friends or family
- LOCALE_NOTTURNO: Pubs, nightclubs, cocktail bars
- SPAZIO_APERTO: Parks, gardens, beaches
- HOTEL: Hotels and accommodation facilities
- TEATRO: Theaters, cinemas, auditoriums
- ALTRO: Any category not covered above
Address and Geocoding
Every venue has a structured address composed of
street, city, province, postal code, and country. Additionally, the
system stores GPS coordinates as BigDecimal with high
precision (latitude 10,8 and longitude 11,8 digits), ensuring
accurate marker placement on the map.
// Update full address with GPS coordinates
luogo.aggiornaIndirizzo(
"Via Roma 42", // street address
"Bari", // city
"BA", // province
"70121", // postal code
"Italia", // country
new BigDecimal("41.12560000"), // latitude
new BigDecimal("16.86990000") // longitude
);
// Helper method for formatted address
luogo.getIndirizzoCompleto();
// Result: "Via Roma 42, Bari (BA) - 70121"
// Useful query methods
luogo.hasIndirizzo(); // true if address is present
luogo.hasCoordinate(); // true if lat/lng are present
Coordinates are never entered manually by the user. When the organizer types an address, the frontend sends a request to the backend, which acts as a proxy to Nominatim (the OpenStreetMap geocoding service). The returned coordinates are saved automatically alongside the address fields.
Contacts and Details
A venue can store contact information (phone, email, website) and operational details such as maximum capacity and average price. All these fields are optional because not every venue needs them: a friend's house has no website, and a park has no defined capacity.
// Contacts
luogo.aggiornaContatti(
"+39 080 1234567", // phone
"info@ristorantedamario.it", // email
"https://www.ristorantedamario.it" // website
);
// Details with validation
luogo.aggiornaDettagli(
150, // capienzaMax (must be non-negative)
new BigDecimal("35.00"), // prezzoMedio (must be non-negative)
"EUR" // valuta (defaults to EUR)
);
// The system automatically validates:
// - capienzaMax >= 0
// - prezzoMedio >= 0
// - valuta not null (defaults to "EUR")
Personal Rating: 1-5 Stars
Every venue can receive a personal rating from 1 to 5 stars. This is not a public rating like TripAdvisor: it is the organizer's subjective judgment based on their own experience. A restaurant might have 5 stars on Google but receive 2 stars from the organizer because the group service was slow.
// Rate the venue (1-5 stars)
luogo.valuta(4); // 4 stars
// Remove the rating
luogo.valuta(null);
// Query
luogo.hasValutazione(); // true/false
// Automatic validation:
// - stars must be between 1 and 5
// - null is allowed (no rating)
luogo.valuta(0); // IllegalArgumentException
luogo.valuta(6); // IllegalArgumentException
// DB index for rating-based searches
@Index(name = "idx_luoghi_valutazione",
columnList = "utente_id, valutazione")
The database index idx_luoghi_valutazione enables fast
filtering by rating: "show me all my restaurants rated 4 or 5 stars."
Favorites: Quick Access
Frequently used venues can be marked as favorites. The favorite flag enables quick filtering to surface the most-used locations during event creation.
// Mark as favorite
luogo.segnaPreferito();
// Remove from favorites
luogo.rimuoviPreferito();
// Toggle (invert the state)
luogo.togglePreferito();
// Query
luogo.isPreferito(); // true/false
// DB index for quick access
@Index(name = "idx_luoghi_preferito",
columnList = "utente_id, preferito")
Linking Venues to Relationships
A unique feature of Play The Event is the ability to link a venue to a relationship. If the venue is a friend's house, the organizer can associate that location with the corresponding contact. When searching for a venue for an event, the system then displays the relationship name alongside the address: "Marco's House (Via delle Rose 15, Lecce)."
// Link the venue to a relationship
luogo.associaCollegamento(collegamentoId);
// Example: the venue "Country House" is linked
// to the relationship "Marco Rossi"
// Remove the association
luogo.rimuoviCollegamento();
// Query
luogo.isAssociatoACollegamento(); // true/false
// In the frontend, when collegamentoId is present,
// the system also loads the relationship name
// to display it on the venue card
EventoLuogo: Connecting Venues to Events
The relationship between events and venues is many-to-many:
an event can have multiple venues (ceremony at a church, reception at a
restaurant, overnight stay at a hotel), and the same venue can be used
across different events. The bridge entity EventoLuogo
manages this association with an additional field: the usage
type.
public enum TipoUtilizzoLuogo {
PRINCIPALE("Main"), // Primary event location
SECONDARIO("Secondary"), // Additional location
CERIMONIA("Ceremony"), // For the ceremony (weddings)
RICEVIMENTO("Reception"), // For the reception
PERNOTTAMENTO("Overnight"), // Guest accommodation
PARCHEGGIO("Parking"), // Parking area
PUNTO_RITROVO("Meeting Point") // Where to gather
}
A wedding, for example, might have four associated venues: the church as CERIMONIA, the restaurant as RICEVIMENTO, the hotel as PERNOTTAMENTO, and a parking lot as PARCHEGGIO. Each association can also carry event-specific notes.
// Associate a venue as the main location
EventoLuogo principale = EventoLuogo.creaPrincipale(evento, ristorante);
// Associate with a specific type and notes
EventoLuogo cerimonia = EventoLuogo.crea(
evento,
chiesa,
TipoUtilizzoLuogo.CERIMONIA,
"Side entrance, parking at Piazza Garibaldi"
);
// Query methods
cerimonia.isPrincipale(); // false
cerimonia.getEventoId(); // Event ID
cerimonia.getLuogoId(); // Venue ID
// Update the usage type
cerimonia.aggiornaTipoUtilizzo(TipoUtilizzoLuogo.PRINCIPALE);
cerimonia.aggiornaNote("New main entrance");
Uniqueness Constraint
The combination (evento_id, luogo_id) is unique: the same
venue cannot be associated twice with the same event. If the usage type
needs to change, the existing association is updated rather than
creating a new one.
Tags: Search and Filtering
Every venue can have tags stored as a comma-separated string. Tags enable flexible searching beyond the predefined categories: "sea-view", "garden", "kid-friendly", "wifi", "private-parking."
// Set tags
luogo.impostaTags("wedding,elegant,sea-view,garden");
// Tags are stored as a comma-separated string
// The frontend splits them and renders them as chips/badges
// Tag search (repository level):
// SELECT * FROM luoghi
// WHERE utente_id = :utenteId
// AND tags LIKE '%sea-view%'
Interactive Maps with MapLibre GL JS
In the Angular frontend of Play The Event, venues come alive through MapLibre GL JS, an open-source vector map rendering library. Every venue with coordinates is displayed as a marker on the map, with interactive popups showing the name, category, and rating.
Map Features for Venues
- Color-coded markers by category (restaurants in red, event halls in blue, etc.)
- Automatic clustering when many venues are close together
- Interactive popups with venue details on click
- Filterable layers by category, rating, or tags
- Route lines between venues belonging to the same event
- Geolocation to show nearby venues from the current position
- Custom dark-mode style consistent with the app design
Choosing MapLibre GL JS over Google Maps is a strategic decision: no per-request costs, full control over map styling, and compatibility with free tile servers like OpenStreetMap. Vector rendering ensures high performance even with hundreds of markers on screen.
ANGULAR FRONTEND
│
├── MapComponent
│ ├── MapLibre GL JS initialization
│ ├── Style: Custom Dark Mode (Positron Dark / OSM Liberty)
│ └── Controls: Zoom, Rotation, Geolocation
│
├── Markers Layer
│ ├── Icons per TipologiaLuogo (color + shape)
│ ├── Automatic clustering (zoom < 12)
│ └── Popup with name, type, rating (stars)
│
├── Route Layer (for multi-venue events)
│ ├── Lines between venues of the same event
│ └── Order: CERIMONIA → RICEVIMENTO → PERNOTTAMENTO
│
└── Interaction
├── Click marker → Open venue detail
├── Click map → Reverse geocoding (new venue)
└── Drag marker → Update coordinates
Geocoding with Nominatim
Geocoding (converting addresses to coordinates) is handled through a backend proxy to Nominatim, the OpenStreetMap geocoding service. The backend proxy is necessary for two reasons: managing the rate limit (maximum 1 request per second) and setting an identifying User-Agent header as required by Nominatim's usage policy.
// REST Endpoints
GET /api/v1/geocoding/search?q=Via Roma 42, Bari&limit=5
GET /api/v1/geocoding/reverse?lat=41.1256&lng=16.8699
// NominatimClient: thread-safe rate limiting
private static final long MIN_INTERVALLO_MS = 1100; // 1.1 seconds
private final ReentrantLock rateLimitLock = new ReentrantLock();
private void rispettaRateLimit() {
rateLimitLock.lock();
try {
long elapsed = System.currentTimeMillis()
- ultimaRichiestaTimestamp;
if (elapsed < MIN_INTERVALLO_MS) {
Thread.sleep(MIN_INTERVALLO_MS - elapsed);
}
ultimaRichiestaTimestamp = System.currentTimeMillis();
} finally {
rateLimitLock.unlock();
}
}
// Formatted response
{
"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 and User-Agent
Nominatim is a free service with one fundamental rule: a maximum of
1 request per second. The NominatimClient
uses a ReentrantLock to enforce this limit even in
multi-threaded scenarios. The User-Agent header is mandatory and
identifies the application:
"PlayTheEvent/1.0 (info@playtheevent.com)".
Polls for Group Venue Selection
When a group needs to decide where to hold an event, the
poll system comes into play. The entity type
TipoEntitaSondaggio.LUOGO enables creating a poll
where the options are venues from the organizer's catalog.
// Create a poll to choose the venue
Sondaggio sondaggio = Sondaggio.creaPerLuoghi(
evento,
organizzatore,
"Where should we have the birthday dinner?"
);
// Add venues as poll options
sondaggio.aggiungiOpzioneLuogo(
"Ristorante Da Mario", // text
luogoMarioId, // venue ID
"Sea view, Apulian cuisine, 35 EUR/person",
"https://..." // image
);
sondaggio.aggiungiOpzioneLuogo(
"Trattoria La Nonna",
luogoNonnaId,
"Home cooking, outdoor garden, 25 EUR/person",
"https://..."
);
// Publish the poll
sondaggio.pubblica();
// After voting, close and select the winner
sondaggio.chiudi();
sondaggio.selezionaVincitore(winningOptionId, organizzatore, null);
// The winning venue can be automatically
// associated with the event as the main location
Polls of type LUOGO have an exclusive feature: winner selection. After the poll is closed, the organizer can select the winning option (typically the one with the most votes) and the system automatically associates that venue with the event.
Venue Poll Features
- Options linked to real venues with photos, descriptions, and coordinates
- Public sharing via a unique link with UUID code
- Winner selection with change history and motivations
- Reusable templates for recurring polls
- Anonymous or named voting, vote editing, and real-time results
Database Indexes and Performance
The luoghi table has strategic indexes to ensure high
performance on the most common queries: filtering by user, category,
city, favorites, and rating.
@Table(name = "luoghi", indexes = {
// All venues for a user
@Index(name = "idx_luoghi_utente",
columnList = "utente_id"),
// Filter by category
@Index(name = "idx_luoghi_tipologia",
columnList = "tipologia_id"),
// Search by city
@Index(name = "idx_luoghi_citta",
columnList = "citta"),
// User's favorites
@Index(name = "idx_luoghi_preferito",
columnList = "utente_id, preferito"),
// Sort by rating
@Index(name = "idx_luoghi_valutazione",
columnList = "utente_id, valutazione")
})
Key Takeaways
- Luogo is a personal catalog per organizer, not a shared venue database
- TipologiaLuogo provides multilingual categorization with icons and colors
- GPS coordinates are stored as BigDecimal for high precision
- The venue-relationship link enriches context ("Marco's House")
- EventoLuogo handles the many-to-many relationship with specific usage types
- MapLibre GL JS renders venues with markers, clustering, and route layers
- Nominatim provides free geocoding with backend-managed rate limiting
- LUOGO polls let the group vote on the venue with winner selection
The source code is available on GitHub. To explore the venue module in action, visit www.playtheevent.com.







