mapbox-store-locator-patterns

bởi mapbox

Các mẫu phổ biến để xây dựng công cụ tìm cửa hàng, tìm nhà hàng và ứng dụng tìm kiếm dựa trên vị trí với Mapbox. Bao gồm hiển thị điểm đánh dấu, lọc, khoảng cách…

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

Store Locator Patterns Skill

Comprehensive patterns for building store locators, restaurant finders, and location-based search applications with Mapbox GL JS. Covers marker display, filtering, distance calculation, interactive lists, and directions integration.

When to Use This Skill

Use this skill when building applications that:

  • Display multiple locations on a map (stores, restaurants, offices, etc.)
  • Allow users to filter or search locations
  • Calculate distances from user location
  • Provide interactive lists synced with map markers
  • Show location details in popups or side panels
  • Integrate directions to selected locations

Dependencies

Required:

  • Mapbox GL JS v3.x
  • @turf/turf - For spatial calculations (distance, area, etc.)

Installation:

npm install mapbox-gl @turf/turf

Core Architecture

Pattern Overview

A typical store locator consists of:

  1. Map Display - Shows all locations as markers
  2. Location Data - GeoJSON with store/location information
  3. Interactive List - Side panel listing all locations
  4. Filtering - Search, category filters, distance filters
  5. Detail View - Popup or panel with location details
  6. User Location - Geolocation for distance calculation. For the blue dot location indicator, use the built-in mapboxgl.GeolocateControl — simpler than custom markers.
  7. Directions - Route to selected location (optional)

Data Structure

GeoJSON format for locations:

{
  "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"
      }
    }
  ]
}

Key properties:

  • id - Unique identifier for each location
  • name - Display name
  • address - Full address for display and geocoding
  • coordinates - [longitude, latitude] format
  • category - For filtering (retail, restaurant, office, etc.)
  • Custom properties as needed (hours, phone, website, etc.)

Basic Store Locator Implementation

Step 1: Initialize Map and Data

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
});

Step 2: Add Markers to Map

Marker strategy by location count:

CountStrategyReason
Fewer than 100HTML MarkersFull DOM/CSS control; DOM node count is manageable
100–1,000Symbol Layer (default)Renders on the GPU via WebGL — one <canvas>, zero per-point DOM elements
More than 1,000ClusteringReduces visual clutter at large scale

HTML Markers create one DOM element per point. Beyond ~100 locations the browser spends too much time on layout/paint. Symbol layers bypass the DOM entirely — the GPU draws all points in a single WebGL draw call.

Symbol Layer implementation (best for 100–1,000 locations). For HTML Markers (fewer than 100) or Clustering (more than 1,000), see 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 = '';
  });
});

Step 3: Build Interactive Location List

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);
});

Reference Files

Load these references for additional patterns as needed:

ReferenceFileContents
HTML Markers & Clusteringreferences/markers.mdHTML Markers (< 100 locations), Clustering (> 1000 locations)
Search & Filterreferences/search-filter.mdText search, category filter
Geolocation & Directionsreferences/geolocation-directions.mdUser location, distance calculation, route directions
Styling & Layoutreferences/styling-layout.mdFull HTML/CSS layout, custom marker CSS
Performance & A11yreferences/optimization-a11y.mdDebounced search, data management, error handling, accessibility
Variations & Reactreferences/variations-react.mdMobile-first, fullscreen, map-only, React implementation

Resources

Thêm skills từ mapbox

mapbox-android-patterns
mapbox
Các mẫu tích hợp chính thức cho Mapbox Maps SDK trên Android. Bao gồm cài đặt, thêm điểm đánh dấu, vị trí người dùng, dữ liệu tùy chỉnh, kiểu dáng, điều khiển camera, và…
official
mapbox-cartography
mapbox
Hướng dẫn chuyên sâu về các nguyên tắc thiết kế bản đồ, lý thuyết màu sắc, phân cấp trực quan, kiểu chữ và các phương pháp bản đồ tốt nhất để tạo ra những bản đồ hiệu quả và đẹp mắt…
official
mapbox-data-visualization-patterns
mapbox
Các mẫu trực quan hóa dữ liệu trên bản đồ bao gồm bản đồ choropleth, bản đồ nhiệt, trực quan hóa 3D, tạo kiểu dựa trên dữ liệu và dữ liệu động. Bao gồm các loại lớp,…
official
mapbox-geospatial-operations
mapbox
Hướng dẫn chuyên sâu về cách chọn công cụ không gian địa lý phù hợp dựa trên loại vấn đề, yêu cầu độ chính xác và nhu cầu hiệu suất
official
mapbox-google-maps-migration
mapbox
Hướng dẫn di chuyển dành cho nhà phát triển chuyển từ Google Maps Platform sang Mapbox GL JS, bao gồm các API tương đương, cách chuyển đổi mẫu mã và những khác biệt chính.
official
mapbox-ios-patterns
mapbox
Các mẫu tích hợp chính thức cho Mapbox Maps SDK trên iOS. Bao gồm cài đặt, thêm điểm đánh dấu, vị trí người dùng, dữ liệu tùy chỉnh, kiểu, điều khiển camera, và…
official
mapbox-location-grounding
mapbox
Kết hợp các công cụ Mapbox MCP để tạo ra các phản hồi có căn cứ, trích dẫn và nhận biết vị trí từ dữ liệu trực tiếp thay vì dữ liệu huấn luyện.
official
mapbox-maplibre-migration
mapbox
Hướng dẫn di chuyển từ MapLibre GL JS sang Mapbox GL JS, bao gồm khả năng tương thích API, thiết lập token, cấu hình style và lợi ích của bản chính thức từ Mapbox…
official