All skills
mapbox avatar

/mapbox-store-locator-patterns

@9077a82 official
by mapboxmapbox/mapbox-agent-skills80 stars
17

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

Use this Skill: https://skilld.dev/gh/mapbox/mapbox-agent-skills/mapbox-store-locator-patterns

This session only. Nothing lands on disk.

referencesgeolocation-directions.md

≈1.3k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Geolocation, Distance & Directions

Geolocation and Distance Calculation

Two separate APIs for two separate jobs:

  1. mapboxgl.GeolocateControl — Adds the blue dot on the map showing the user's live position. This is a UI control only; use it for the visual indicator.
  2. navigator.geolocation.getCurrentPosition() — Returns the raw coordinates you need for distance calculation and sorting. Call this separately because GeolocateControl does not expose coordinates in a convenient way for data processing.
let userLocation = null;

// 1. Add GeolocateControl for the blue dot on-map indicator
map.addControl(
  new mapboxgl.GeolocateControl({
    positionOptions: {
      enableHighAccuracy: true
    },
    trackUserLocation: true,
    showUserHeading: true
  })
);

// 2. Use navigator.geolocation.getCurrentPosition() separately to get
//    coordinates for distance calculation and sorting
navigator.geolocation.getCurrentPosition(
  (position) => {
    userLocation = [position.coords.longitude, position.coords.latitude];

    // Calculate distances and sort
    const storesWithDistance = stores.features.map((store) => {
      const distance = calculateDistance(userLocation, store.geometry.coordinates);
      return {
        ...store,
        properties: {
          ...store.properties,
          distance: distance
        }
      };
    });

    // Sort by distance
    storesWithDistance.sort((a, b) => a.properties.distance - b.properties.distance);

    // Update data
    stores.features = storesWithDistance;

    // Rebuild list with distances
    document.getElementById('listings').innerHTML = '';
    buildLocationList(stores);
  },
  (error) => {
    console.error('Error getting location:', error);
  }
);

// Calculate distance using Turf.js (recommended)
import * as turf from '@turf/turf';

function calculateDistance(from, to) {
  const fromPoint = turf.point(from);
  const toPoint = turf.point(to);
  const distance = turf.distance(fromPoint, toPoint, { units: 'miles' });
  return distance.toFixed(1); // Distance in miles
}

// Update listing to show distance
function buildLocationList(stores) {
  const listingContainer = document.getElementById('listings');

  stores.features.forEach((store) => {
    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.innerHTML = store.properties.name;

    const details = listing.appendChild(document.createElement('div'));
    details.innerHTML = `
      ${store.properties.distance ? `<p class="distance">${store.properties.distance} mi</p>` : ''}
      <p>${store.properties.address}</p>
      <p>${store.properties.phone || ''}</p>
    `;

    link.addEventListener('click', (e) => {
      e.preventDefault();
      flyToStore(store);
      createPopup(store);
      highlightListing(store.properties.id);
    });
  });
}

Directions Integration

async function getDirections(from, to) {
  const query = await fetch(
    `https://api.mapbox.com/directions/v5/mapbox/driving/${from[0]},${from[1]};${to[0]},${to[1]}?` +
      `steps=true&geometries=geojson&access_token=${mapboxgl.accessToken}`
  );

  const data = await query.json();
  const route = data.routes[0];

  // Display route on map
  if (map.getSource('route')) {
    map.getSource('route').setData({
      type: 'Feature',
      geometry: route.geometry
    });
  } else {
    map.addSource('route', {
      type: 'geojson',
      data: {
        type: 'Feature',
        geometry: route.geometry
      }
    });

    map.addLayer({
      id: 'route',
      type: 'line',
      source: 'route',
      paint: {
        'line-color': '#3b9ddd',
        'line-width': 5,
        'line-opacity': 0.75
      }
    });
  }

  // Display directions info
  const duration = Math.floor(route.duration / 60);
  const distance = (route.distance * 0.000621371).toFixed(1); // Convert to miles

  return { duration, distance, steps: route.legs[0].steps };
}

// Add "Get Directions" button to popup
function createPopup(store) {
  const popups = document.getElementsByClassName('mapboxgl-popup');
  if (popups[0]) popups[0].remove();

  const popup = 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>
       ${userLocation ? '<button id="get-directions">Get Directions</button>' : ''}`
    )
    .addTo(map);

  // Handle directions button
  if (userLocation) {
    document.getElementById('get-directions').addEventListener('click', async () => {
      const directions = await getDirections(userLocation, store.geometry.coordinates);

      // Update popup with directions
      popup.setHTML(
        `<h3>${store.properties.name}</h3>
         <p><strong>${directions.distance} mi • ${directions.duration} min</strong></p>
         <p>${store.properties.address}</p>
         <div class="directions-steps">
           ${directions.steps.map((step) => `<p>${step.maneuver.instruction}</p>`).join('')}
         </div>`
      );
    });
  }
}

Source: SKILL.md on GitHub

1 warning14d5 checks · Risk SAFE
  • Gen Agent Trust Hub14d

    The skill provides patterns for building map-based applications using Mapbox. It includes minor security risks related to how external location data is rendered in the user interface, which could allow for script execution if the data source is untrusted.

  • Socket14d

    No alerts

  • Snyk14d

    Risk: LOW · No issues

  • Runlayer7mo

    2/2 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 9077a82. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 hours ago.

Activeupdated 2 weeks ago
  • mapbox
  • store-locator
  • geojson
  • markers
  • filtering
  • geolocation
  • turf
  • javascript
  • interactive-map

README badge

README badge for mapbox/mapbox-agent-skills/mapbox-store-locator-patterns

Provides patterns for building store locators, restaurant finders, and location-based search apps with Mapbox GL JS, covering marker display (symbol layers for 100–1000 locations, HTML markers for fewer), filtering, distance calculation, and interactive location lists. Includes data structure templates, popup and directions integration, and references for geolocation, search, clustering, and React implementations.

Generated from the current SKILL.md.

Does this skill work with HTML markers or only symbol layers?
The main implementation uses symbol layers for 100–1,000 locations. HTML markers and clustering patterns are covered in the `references/markers.md` reference file for smaller or larger datasets.
What libraries are required besides Mapbox GL JS?
Turf.js (@turf/turf) is required for spatial calculations like distance. It's listed as a dependency and installed via npm.
Does this skill handle user geolocation and directions?
Yes. The skill references geolocation and directions patterns in `references/geolocation-directions.md`, including distance calculation and route integration.
Is there a React implementation included?
React patterns and variations are documented in `references/variations-react.md`, along with mobile-first and fullscreen layouts.
What's the expected performance threshold for marker count?
Symbol layers scale to 100–1,000 locations efficiently on the GPU. Below 100, HTML markers work fine; above 1,000, clustering is recommended.

Generated from the current SKILL.md. These answers refresh after source changes.