Overview
S-Parking uses the Google Maps JavaScript API with Advanced Markers for high-performance rendering of parking spots. The integration includes custom map styling, dynamic clustering, and interactive overlays.
Map Initialization
The map/core.js module handles Maps API loading and initialization with robust fallback support.
API Loading Strategy
export function loadGoogleMapsAPI () {
return new Promise (( resolve , reject ) => {
// Check if modern API is already loaded
if ( window . google && window . google . maps &&
typeof google . maps . importLibrary === 'function' ) {
logger . debug ( "✅ API de Maps moderna detectada en caché." );
resolve ();
return ;
}
// Dynamic callback for async loading
const callbackName = `initMap_ ${ Date . now () } ` ;
window [ callbackName ] = () => {
logger . debug ( "✅ API de Maps cargada exitosamente vía Callback." );
delete window [ callbackName ];
resolve ();
};
// Inject script tag
const script = document . createElement ( "script" );
script . src = `https://maps.googleapis.com/maps/api/js?key= ${ CONFIG . GOOGLE_MAPS_API_KEY } &map_ids= ${ CONFIG . GOOGLE_MAPS_ID } &loading=async&v=weekly&callback= ${ callbackName } ` ;
script . async = true ;
script . defer = true ;
script . onerror = reject ;
document . head . appendChild ( script );
});
}
The v=weekly parameter ensures you always get the latest stable Google Maps features.
Map Instance Creation
export async function initMap ( containerId ) {
await loadGoogleMapsAPI ();
const mapElement = document . getElementById ( containerId );
if ( ! mapElement ) throw new Error ( `Element ${ containerId } not found` );
// Modern API with importLibrary
const { Map } = await google . maps . importLibrary ( "maps" );
const { AdvancedMarkerElement , PinElement } =
await google . maps . importLibrary ( "marker" );
await google . maps . importLibrary ( "geometry" );
// Store in module state
mapState . AdvancedMarkerElement = AdvancedMarkerElement ;
mapState . PinElement = PinElement ;
mapState . geometry = google . maps . geometry ;
mapState . map = new Map ( mapElement , {
center: { lat: - 33.43306733282499 , lng: - 70.61471532552095 },
zoom: 19 ,
mapId: CONFIG . GOOGLE_MAPS_ID ,
tilt: 0 ,
disableDefaultUI: true ,
zoomControl: false ,
rotateControl: true ,
gestureHandling: 'greedy'
});
mapState . infoWindow = new google . maps . InfoWindow ();
return mapState ;
}
Map State Object
export const mapState = {
map: null , // Google Maps instance
AdvancedMarkerElement: null , // Advanced Marker class
PinElement: null , // Pin Element class
geometry: null , // Geometry library
infoWindow: null // Shared InfoWindow
};
Why Advanced Markers? Advanced Markers provide better performance than legacy markers:
Hardware-accelerated rendering
Custom HTML content support
Built-in collision detection
Smoother animations
Custom Marker System
The map/markers.js module implements a sophisticated marker management system with caching and dynamic scaling.
Marker Caching
const markersCache = {}; // { spotId: AdvancedMarkerElement }
const zoneMarkersCache = {}; // { zoneId: AdvancedMarkerElement }
function renderSpotMarkers ( spots , onMarkerClick ) {
// Track active spot IDs
const activeIds = new Set ( spots . map ( s => s . id ));
// Remove deleted markers
Object . keys ( markersCache ). forEach ( id => {
if ( ! activeIds . has ( id )) {
markersCache [ id ]. map = null ;
delete markersCache [ id ];
}
});
// Create or update markers
spots . forEach ( spot => {
const pinContent = document . createElement ( 'div' );
pinContent . className = getPinClass ( spot . status );
if ( markersCache [ spot . id ]) {
// Update existing
const marker = markersCache [ spot . id ];
marker . content . className = pinContent . className ;
marker . position = { lat: spot . lat , lng: spot . lng };
} else {
// Create new
const marker = new mapState . AdvancedMarkerElement ({
map: mapState . map ,
position: { lat: spot . lat , lng: spot . lng },
content: pinContent ,
title: spot . id
});
marker . addListener ( 'click' , () => {
showMiniInfoWindow ( marker , spot );
if ( onMarkerClick ) onMarkerClick ( spot );
});
markersCache [ spot . id ] = marker ;
}
});
}
Pin Styling
function getPinClass ( status ) {
const base = 'parking-pin' ;
if ( status === 1 ) return ` ${ base } pin-free` ; // Green
if ( status === 0 ) return ` ${ base } pin-occupied` ; // Red
if ( status === 2 ) return ` ${ base } pin-reserved` ; // Amber
return ` ${ base } pin-unknown` ;
}
Corresponding CSS:
.parking-pin {
width : 24 px ;
height : 24 px ;
border-radius : 50 % 50 % 50 % 0 ;
transform : rotate ( -45 deg );
border : 2 px solid white ;
box-shadow : 0 2 px 8 px rgba ( 0 , 0 , 0 , 0.3 );
transition : all 0.3 s cubic-bezier ( 0.4 , 0 , 0.2 , 1 );
}
.pin-free {
background : linear-gradient ( 135 deg , #10b981 0 % , #059669 100 % );
}
.pin-occupied {
background : linear-gradient ( 135 deg , #f43f5e 0 % , #e11d48 100 % );
}
.pin-reserved {
background : linear-gradient ( 135 deg , #f59e0b 0 % , #d97706 100 % );
}
.pin-highlight {
animation : pulse 1.5 s ease-in-out infinite ;
}
@keyframes pulse {
0% , 100% { transform : rotate ( -45 deg ) scale ( 1 ); }
50% { transform : rotate ( -45 deg ) scale ( 1.3 ); }
}
Dynamic Zoom Clustering
To prevent visual clutter at low zoom levels, the system switches between individual spots and zone clusters:
const CLUSTER_ZOOM_THRESHOLD = 17 ;
export function updateClusterView ( spots , zones , onMarkerClick ) {
if ( ! mapState . map ) return ;
const zoom = mapState . map . getZoom ();
if ( zoom >= CLUSTER_ZOOM_THRESHOLD ) {
// High zoom: show individual spots
renderSpotMarkers ( spots , onMarkerClick );
Object . values ( zoneMarkersCache ). forEach ( m => { m . map = null ; });
updateMarkerScale ();
} else {
// Low zoom: show zone clusters
Object . values ( markersCache ). forEach ( m => { m . map = null ; });
renderZoneMarkers ( spots , zones );
}
}
Zone Cluster Markers
function renderZoneMarkers ( spots , zones ) {
zones . forEach ( zone => {
const zoneSpots = spots . filter ( s => s . zone_id === zone . id );
if ( ! zoneSpots . length ) return ;
const free = zoneSpots . filter ( s => s . status === 1 ). length ;
const occupied = zoneSpots . filter ( s => s . status === 0 || s . status === 2 ). length ;
// Calculate center point
const avgLat = zoneSpots . reduce (( acc , s ) => acc + s . lat , 0 ) / zoneSpots . length ;
const avgLng = zoneSpots . reduce (( acc , s ) => acc + s . lng , 0 ) / zoneSpots . length ;
const content = document . createElement ( 'div' );
content . className = 'zone-marker' ;
content . innerHTML = `
<div class="zone-title"> ${ zone . name || zone . id } </div>
<div class="zone-counts">
<span class="zone-free">Libre: ${ free } </span> •
<span class="zone-occupied">Ocupado: ${ occupied } </span>
</div>
` ;
if ( zoneMarkersCache [ zone . id ]) {
const marker = zoneMarkersCache [ zone . id ];
marker . position = { lat: avgLat , lng: avgLng };
marker . content . innerHTML = content . innerHTML ;
if ( ! marker . map ) marker . map = mapState . map ;
} else {
const marker = new mapState . AdvancedMarkerElement ({
map: mapState . map ,
position: { lat: avgLat , lng: avgLng },
content ,
title: zone . name
});
zoneMarkersCache [ zone . id ] = marker ;
}
});
}
High Zoom (19+)
Low Zoom (16 or less)
Individual parking spots visible with color-coded pins:
Green: Available
Red: Occupied
Amber: Reserved
Zone clusters showing aggregate counts:
Zone name badge
Free count in green
Occupied + Reserved in red
Zoom-Based Marker Scaling
Markers dynamically scale to remain visible at different zoom levels:
function updateMarkerScale () {
if ( ! mapState . map ) return ;
const zoom = mapState . map . getZoom ();
let scale = 1 ;
if ( zoom >= 19 ) scale = 1.0 ;
else if ( zoom >= 18 ) scale = 0.8 ;
else if ( zoom >= 17 ) scale = 0.6 ;
else if ( zoom >= 16 ) scale = 0.45 ;
else if ( zoom >= 15 ) scale = 0.3 ;
else scale = 0.2 ;
Object . values ( markersCache ). forEach ( marker => {
if ( marker . content ) {
marker . content . style . transform = `scale( ${ scale } )` ;
}
});
}
function attachZoomListener () {
if ( isZoomListenerAttached || ! mapState . map ) return ;
mapState . map . addListener ( 'zoom_changed' , () => {
updateMarkerScale ();
});
isZoomListenerAttached = true ;
updateMarkerScale ();
}
Interactive InfoWindows
function showMiniInfoWindow ( marker , spot ) {
if ( ! mapState . infoWindow ) return ;
let statusText = spot . status === 1 ? 'Libre' :
spot . status === 0 ? 'Ocupado' : 'Reservado' ;
const content = `
<div class="px-2 py-1 text-center">
<h3 class="font-bold text-slate-800"> ${ spot . id } </h3>
<p class="text-xs text-slate-500"> ${ statusText } </p>
</div>
` ;
mapState . infoWindow . setContent ( content );
mapState . infoWindow . open ( mapState . map , marker );
}
Map Event Listeners
The main application sets up map click handlers for admin features:
const mapObj = MapCore . mapState . map ;
mapObj . addListener ( 'click' , ( e ) => {
const latLng = e . latLng ;
if ( state . isAdminMode && state . isBuilderMode ) {
// Builder mode: multi-spot line tool
const result = MapBuilder . handleMapClick ( latLng );
if ( result && result . start && result . end ) {
showLineBuilderConfig ( result . start , result . end );
}
} else if ( state . isAdminMode ) {
// Admin mode: single spot creation
createSingleSpot ( latLng );
}
});
mapObj . addListener ( 'zoom_changed' , () => {
MapMarkers . updateClusterView ( state . spots , state . zones , handleSpotClick );
});
Map Styling
Custom map styles are configured via the Maps ID in Google Cloud Console. Key features:
Minimal POI labels
High contrast roads
Satellite hybrid view for parking lot context
Marker Limit Problem : Rendering 1000+ markers causes lagSolution : Cluster view below zoom 17 reduces visible markers by ~90%
Update Efficiency Problem : Re-rendering all markers on every data updateSolution : Cache-based diffing only updates changed markers
Memory Management Problem : Marker instances accumulate in memorySolution : Setting marker.map = null properly disposes markers
Builder Mode Learn about the visual spot builder tool
Dashboard Understand the overall application architecture