Skill Motifs de Store Locator
Motifs complets pour construire des localisateurs de magasins, des chercheurs de restaurants et des applications de recherche basée sur la localisation avec Mapbox GL JS. Couvre l'affichage des marqueurs, le filtrage, le calcul des distances, les listes interactives et l'intégration des directions.
Quand utiliser ce Skill
Utilisez ce skill lors de la construction d'applications qui :
- Affichent plusieurs emplacements sur une carte (magasins, restaurants, bureaux, etc.)
- Permettent aux utilisateurs de filtrer ou rechercher des emplacements
- Calculent les distances à partir de la localisation de l'utilisateur
- Fournissent des listes interactives synchronisées avec les marqueurs de la carte
- Affichent les détails des emplacements dans des popups ou des panneaux latéraux
- Intègrent les directions vers les emplacements sélectionnés
Dépendances
Obligatoires :
- Mapbox GL JS v3.x
- @turf/turf - Pour les calculs spatiaux (distance, surface, etc.)
Installation :
npm install mapbox-gl @turf/turf
Architecture centrale
Aperçu des motifs
Un localisateur de magasins typique se compose de :
- Affichage de la carte - Affiche tous les emplacements sous forme de marqueurs
- Données de localisation - GeoJSON contenant les informations de magasin/emplacement
- Liste interactive - Panneau latéral énumérant tous les emplacements
- Filtrage - Recherche textuelle, filtres de catégorie, filtres de distance
- Vue détaillée - Popup ou panneau avec les détails de l'emplacement
- Localisation de l'utilisateur - Géolocalisation pour le calcul des distances. Pour l'indicateur de localisation du point bleu, utilisez le
mapboxgl.GeolocateControlintégré — plus simple que les marqueurs personnalisés. - Directions - Itinéraire vers l'emplacement sélectionné (optionnel)
Structure des données
Format GeoJSON pour les emplacements :
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [-77.034084, 38.909671]
},
"properties": {
"id": "store-001",
"name": "Downtown Store",
"address": "123 Main St, Washington, DC 20001",
"phone": "(202) 555-0123",
"hours": "Mon-Sat: 9am-9pm, Sun: 10am-6pm",
"category": "retail",
"website": "https://example.com/downtown"
}
}
]
}
Propriétés clés :
id- Identifiant unique pour chaque emplacementname- Nom d'affichageaddress- Adresse complète pour l'affichage et le géocodagecoordinates- Format[longitude, latitude]category- Pour le filtrage (retail, restaurant, office, etc.)- Propriétés personnalisées selon les besoins (hours, phone, website, etc.)
Implémentation de base du Store Locator
Étape 1 : Initialiser la carte et les données
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
mapboxgl.accessToken = 'YOUR_MAPBOX_ACCESS_TOKEN';
// Store locations data
const stores = {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [-77.034084, 38.909671]
},
properties: {
id: 'store-001',
name: 'Downtown Store',
address: '123 Main St, Washington, DC 20001',
phone: '(202) 555-0123',
category: 'retail'
}
}
// ... more stores
]
};
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/standard',
center: [-77.034084, 38.909671],
zoom: 11
});
Étape 2 : Ajouter des marqueurs à la carte
Stratégie de marqueur selon le nombre d'emplacements :
| Nombre | Stratégie | Raison |
|---|---|---|
| Moins de 100 | Marqueurs HTML | Contrôle total du DOM/CSS ; le nombre de nœuds DOM est gérable |
| 100–1 000 | Symbol Layer (défaut) | Rendu sur le GPU via WebGL — un seul <canvas>, zéro éléments DOM par point |
| Plus de 1 000 | Clustering | Réduit le désordre visuel à grande échelle |
Les marqueurs HTML créent un élément DOM par point. Au-delà d'environ 100 emplacements, le navigateur dépense trop de temps sur la mise en page/peinture. Les couches de symboles contournent entièrement le DOM — le GPU dessine tous les points en un seul appel WebGL.
Implémentation de Symbol Layer (optimal pour 100–1 000 emplacements). Pour les marqueurs HTML (moins de 100) ou le Clustering (plus de 1 000), voir references/markers.md.
map.on('load', () => {
// Add store data as source
map.addSource('stores', {
type: 'geojson',
data: stores
});
// Add custom marker image
map.loadImage('/marker-icon.png', (error, image) => {
if (error) throw error;
map.addImage('custom-marker', image);
// Add symbol layer
map.addLayer({
id: 'stores-layer',
type: 'symbol',
source: 'stores',
layout: {
'icon-image': 'custom-marker',
'icon-size': 0.8,
'icon-allow-overlap': true,
'text-field': ['get', 'name'],
'text-font': ['Open Sans Bold', 'Arial Unicode MS Bold'],
'text-offset': [0, 1.5],
'text-anchor': 'top',
'text-size': 12
}
});
});
// Handle marker clicks using Interactions API (recommended)
map.addInteraction('store-click', {
type: 'click',
target: { layerId: 'stores-layer' },
handler: (e) => {
const store = e.feature;
flyToStore(store);
createPopup(store);
}
});
// Or using traditional event listener:
// map.on('click', 'stores-layer', (e) => {
// const store = e.features[0];
// flyToStore(store);
// createPopup(store);
// });
// Change cursor on hover
map.on('mouseenter', 'stores-layer', () => {
map.getCanvas().style.cursor = 'pointer';
});
map.on('mouseleave', 'stores-layer', () => {
map.getCanvas().style.cursor = '';
});
});
Étape 3 : Construire une liste interactive de localisations
function buildLocationList(stores) {
const listingContainer = document.getElementById('listings');
stores.features.forEach((store, index) => {
const listing = listingContainer.appendChild(document.createElement('div'));
listing.id = `listing-${store.properties.id}`;
listing.className = 'listing';
const link = listing.appendChild(document.createElement('a'));
link.href = '#';
link.className = 'title';
link.id = `link-${store.properties.id}`;
link.innerHTML = store.properties.name;
const details = listing.appendChild(document.createElement('div'));
details.innerHTML = `
<p>${store.properties.address}</p>
<p>${store.properties.phone || ''}</p>
`;
// Handle listing click
link.addEventListener('click', (e) => {
e.preventDefault();
flyToStore(store);
createPopup(store);
highlightListing(store.properties.id);
});
});
}
function flyToStore(store) {
map.flyTo({
center: store.geometry.coordinates,
zoom: 15,
duration: 1000
});
}
function createPopup(store) {
const popups = document.getElementsByClassName('mapboxgl-popup');
// Remove existing popups
if (popups[0]) popups[0].remove();
new mapboxgl.Popup({ closeOnClick: true })
.setLngLat(store.geometry.coordinates)
.setHTML(
`<h3>${store.properties.name}</h3>
<p>${store.properties.address}</p>
<p>${store.properties.phone}</p>
${store.properties.website ? `<a href="${store.properties.website}" target="_blank">Visit Website</a>` : ''}`
)
.addTo(map);
}
// IMPORTANT: highlightListing MUST include scrollIntoView — without it,
// selecting a marker on the map won't scroll the sidebar to the listing.
function highlightListing(id) {
// Remove existing highlights
const activeItem = document.getElementsByClassName('active');
if (activeItem[0]) {
activeItem[0].classList.remove('active');
}
// Add highlight to selected listing
const listing = document.getElementById(`listing-${id}`);
listing.classList.add('active');
// Scroll the selected listing into view (critical UX requirement)
listing.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
// Build the list on load
map.on('load', () => {
buildLocationList(stores);
});
Fichiers de référence
Chargez ces références pour des motifs supplémentaires selon les besoins :
| Référence | Fichier | Contenu |
|---|---|---|
| Marqueurs HTML et Clustering | references/markers.md |
Marqueurs HTML (< 100 emplacements), Clustering (> 1 000 emplacements) |
| Recherche et Filtrage | references/search-filter.md |
Recherche textuelle, filtrage par catégorie |
| Géolocalisation et Directions | references/geolocation-directions.md |
Localisation de l'utilisateur, calcul des distances, directions |
| Style et Disposition | references/styling-layout.md |
Disposition HTML/CSS complète, CSS de marqueur personnalisé |
| Performance et A11y | references/optimization-a11y.md |
Recherche avec debounce, gestion des données, gestion d'erreurs, accessibilité |
| Variations et React | references/variations-react.md |
Mobile-first, plein écran, carte seule, implémentation React |
Ressources
- Turf.js - Bibliothèque d'analyse spatiale (recommandée pour les calculs de distance)
- Mapbox GL JS API
- Interactions API Guide
- GeoJSON Specification
- Directions API
- Store Locator Tutorial