Skip to main content

15 - Venues and Maps: Geocoding and Venue Management

15 - Venues and Maps: Geocoding and Venue Management: Venues and Maps: Your Personal Venue Catalog Every event needs a location. Managing multiple venues, capturing their coordinates, displaying them on interactive maps, and organizing them by event type is essential for a scalable event management system.

What do you think about this article?

At a Glance

15 - Venues and Maps: Geocoding and Venue Management: Venues and Maps: Your Personal Venue Catalog Every event needs a location. Managing multiple venues, capturing their coordinates, displaying them on interactive maps, and organizing them by event type is essential for a scalable event management system.

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.

Luogo Entity Structure
@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.

TipologiaLuogo Structure
@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.

Updating Address with Coordinates
// 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.

Updating Contacts and Details
// 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.

Rating System
// 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.

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

Venue-Relationship Association
// 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.

TipoUtilizzoLuogo: The Venue's Role
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.

Creating Event-Venue Associations
// 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."

Tag System
// 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.

Map Architecture in the Angular Frontend
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.

Geocoding Proxy in the Backend
// 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.

Venue Selection Poll
// 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.

Indexes on the Luoghi Table
@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.

Frequently asked questions

What is the Luogo Entity in the context of Play The Event?

The Luogo Entity, or "place" in Italian, is the Aggregate Root of the venue module and represents a user's personal catalog of locations.

How does the system handle geocoding for venues?

Geocoding is handled through a backend proxy to Nominatim, the OpenStreetMap geocoding service, which converts addresses to coordinates with high precision.

What is the purpose of linking a venue to a relationship in Play The Event?

Linking a venue to a relationship allows organizers to associate a location with a specific contact or person, making it easier to manage events and venues.

How does the system handle polls for group venue selection in Play The Event?

The poll system enables creating a poll where options are venues from the organizer's catalog, and after the poll is closed, the organizer can select the winning option and associate that venue with the event.

Commenti

Caricamento commenti...

Sign in to comment

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects