mapbox-store-locator-patterns

Par mapbox · mapbox-agent-skills

Modèles courants pour créer des localisateurs de magasins, des guides de restaurants et des applications de recherche géolocalisée avec Mapbox. Couvre l'affichage des marqueurs, le filtrage, le calcul de distances et les listes interactives.

npx skills add https://github.com/mapbox/mapbox-agent-skills --skill mapbox-store-locator-patterns

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 :

  1. Affichage de la carte - Affiche tous les emplacements sous forme de marqueurs
  2. Données de localisation - GeoJSON contenant les informations de magasin/emplacement
  3. Liste interactive - Panneau latéral énumérant tous les emplacements
  4. Filtrage - Recherche textuelle, filtres de catégorie, filtres de distance
  5. Vue détaillée - Popup ou panneau avec les détails de l'emplacement
  6. Localisation de l'utilisateur - Géolocalisation pour le calcul des distances. Pour l'indicateur de localisation du point bleu, utilisez le mapboxgl.GeolocateControl intégré — plus simple que les marqueurs personnalisés.
  7. 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 emplacement
  • name - Nom d'affichage
  • address - Adresse complète pour l'affichage et le géocodage
  • coordinates - 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

Skills similaires