# Service Locations Map

This example demonstrates an interactive map widget using Leaflet.js to display service locations with popups, navigation, and real-time updates.

## Key Features

* **Interactive Leaflet.js map** with zoom and pan controls
* **Custom markers** for each service location
* **Information popups** with service details and images
* **Navigation integration** to service detail screens
* **Automatic map bounds** fitting all service locations
* **Real-time updates** when service data changes
* **Responsive design** that works on all devices
* **Error handling** with user-friendly messages

## Use Cases

Perfect for applications requiring:

* Service location directories and finders
* Store locators and branch finders
* Event venue mapping
* Real estate property maps
* Delivery service coverage areas
* Field service management
* Tourism and travel guides

## Implementation Overview

This widget uses the **Other Tables Data** pattern, fetching data from a "Service Locations" or "Services" datatable. Key implementation features:

* Leaflet.js integration for professional mapping
* GeoJSON coordinate handling for location data
* Popup content generation with images and details
* Automatic map bounds calculation
* Real-time data synchronization

## Code Example

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Service Locations Map</title>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <script
      src="https://cdn.jsdelivr.net/npm/react@18.2.0/umd/react.production.min.js"
      crossorigin
    ></script>
    <script
      src="https://cdn.jsdelivr.net/npm/react-dom@18.2.0/umd/react-dom.production.min.js"
      crossorigin
    ></script>
    <script
      src="https://cdn.jsdelivr.net/npm/@babel/standalone/babel.min.js"
      crossorigin
    ></script>
    <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
    <link
      href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
      rel="stylesheet"
    />
    <style>
      html,
      body,
      #root {
        height: 100%;
        width: 100%;
        margin: 0;
        padding: 0;
      }
      .map-container {
        height: 100%;
        width: 100%;
        position: relative;
      }
      .map-loading {
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        background: rgba(255, 255, 255, 0.9);
        padding: 1rem;
        border-radius: 8px;
        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
        z-index: 1000;
      }
      .map-error {
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        background: #fee;
        color: #c33;
        padding: 1rem;
        border-radius: 8px;
        border: 1px solid #fcc;
        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
        z-index: 1000;
        text-align: center;
        max-width: 300px;
      }
      .popup-content {
        display: flex;
        gap: 10px;
        align-items: flex-start;
      }
      .popup-content img {
        width: 80px;
        height: 60px;
        object-fit: cover;
        border-radius: 4px;
      }
      .popup-text {
        flex: 1;
        min-width: 0;
      }
      .popup-text h4 {
        margin: 0 0 0.5rem 0;
        font-size: 1rem;
        font-weight: 600;
      }
      .popup-text p {
        margin: 0.25rem 0;
        font-size: 0.9rem;
        color: #666;
      }
      .popup-button {
        margin-top: 0.5rem;
        padding: 0.25rem 0.5rem;
        background: #3273dc;
        color: white;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        font-size: 0.8rem;
      }
      .popup-button:hover {
        background: #2366d1;
      }
      @media (max-width: 768px) {
        .popup-content {
          flex-direction: column;
        }
        .popup-content img {
          width: 100%;
          height: 120px;
        }
      }
    </style>
  </head>
  <body>
    <div id="root"></div>
    <script type="text/babel">
      const buzzyFrameAPI = new BuzzyFrameAPI();

      const MAP_CONFIG = {
        tileLayer: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
        maxZoom: 19,
        defaultZoom: 10,
        defaultCenter: [40.7128, -74.006],
      };

      function ServiceLocationsMap() {
        const { useState, useEffect, useRef } = React;

        const [services, setServices] = useState([]);
        const [loading, setLoading] = useState(true);
        const [error, setError] = useState('');
        const [map, setMap] = useState(null);
        const [markersLayer, setMarkersLayer] = useState(null);
        const [serviceDetailScreenID, setServiceDetailScreenID] =
          useState(null);
        const [serviceDataTableID, setServiceDataTableID] = useState(null);

        const mapRef = useRef(null);

        useEffect(() => {
          async function initialiseAll() {
            try {
              console.log('[init] Calling buzzyFrameAPI.initialise()...');
              const init = await buzzyFrameAPI.initialise();

              const { resourceJSON } = init || {};
              const serviceDataTable = resourceJSON?.children?.find(
                child =>
                  child.title === 'Service Locations' ||
                  child.title === 'Services',
              );

              if (serviceDataTable) {
                setServiceDataTableID(serviceDataTable._id);

                buzzyFrameAPI.addMicroappListener({
                  microAppID: serviceDataTable._id,
                  listener: async () => {
                    console.log(
                      '[listener] Service data changed, refreshing...',
                    );
                    await fetchServices();
                  },
                });

                await fetchServices();
              } else {
                setError(
                  'Service data table not found. Please ensure there is a table named "Service Locations" or "Services".',
                );
                setLoading(false);
              }

              try {
                const detailScreenID = await buzzyFrameAPI.getScreenID({
                  screenName: 'Service Detail',
                });
                setServiceDetailScreenID(detailScreenID);
              } catch (err) {
                console.warn('Service detail screen not found:', err);
              }
            } catch (err) {
              setError('Error during initialisation: ' + (err.message || err));
              console.error('[error]', err);
              setLoading(false);
            }
          }

          initialiseAll();
        }, []);

        useEffect(() => {
          if (mapRef.current && !map) {
            try {
              const newMap = L.map(mapRef.current, {
                scrollWheelZoom: false,
                zoomControl: true,
              });

              L.tileLayer(MAP_CONFIG.tileLayer, {
                maxZoom: MAP_CONFIG.maxZoom,
                attribution: '© OpenStreetMap contributors',
              }).addTo(newMap);

              const newMarkersLayer = L.layerGroup().addTo(newMap);

              setMap(newMap);
              setMarkersLayer(newMarkersLayer);
            } catch (err) {
              setError('Error initializing map: ' + (err.message || err));
              console.error('[error]', err);
            }
          }
        }, [mapRef.current]);

        async function fetchServices() {
          if (!serviceDataTableID) return;

          setLoading(true);
          setError('');
          console.log('[fetchServices] Fetching services...');

          try {
            const serviceData = await buzzyFrameAPI.fetchDataTableRows({
              microAppID: serviceDataTableID,
              subLimit: 50,
            });

            const validServices = serviceData.filter(
              service => service.location,
            );
            console.log(
              '[fetchServices] Got',
              validServices.length,
              'valid services',
            );
            setServices(validServices);
          } catch (err) {
            setError('Error fetching services: ' + (err.message || err));
            console.error('[error]', err);
          } finally {
            setLoading(false);
          }
        }

        useEffect(() => {
          if (map && markersLayer && services.length > 0) {
            markersLayer.clearLayers();

            const bounds = L.latLngBounds();
            let hasValidMarkers = false;

            services.forEach(service => {
              try {
                const location =
                  typeof service.location === 'string' && service.location
                    ? JSON.parse(service.location)
                    : service.location || {};

                const coordinates = location?.coordinates;
                if (
                  coordinates &&
                  Array.isArray(coordinates) &&
                  coordinates.length >= 2
                ) {
                  const [lng, lat] = coordinates;

                  if (
                    typeof lat === 'number' &&
                    typeof lng === 'number' &&
                    lat >= -90 &&
                    lat <= 90 &&
                    lng >= -180 &&
                    lng <= 180
                  ) {
                    const marker = L.marker([lat, lng]);

                    const popupContent = createPopupContent(service);
                    marker.bindPopup(popupContent);

                    marker.bindTooltip(service.name || 'Service Location', {
                      permanent: false,
                      direction: 'top',
                    });

                    marker.addTo(markersLayer);
                    bounds.extend([lat, lng]);
                    hasValidMarkers = true;
                  }
                }
              } catch (err) {
                console.warn(
                  'Error processing service location:',
                  service.name,
                  err,
                );
              }
            });

            if (hasValidMarkers) {
              map.fitBounds(bounds, { padding: [20, 20] });
            } else {
              map.setView(MAP_CONFIG.defaultCenter, MAP_CONFIG.defaultZoom);
            }
          }
        }, [map, markersLayer, services]);

        function createPopupContent(service) {
          const div = document.createElement('div');
          div.className = 'popup-content';

          const image =
            service.image && service.image.length > 0
              ? `<img src="${service.image[0]}" alt="${
                  service.name || 'Service'
                }" />`
              : '';

          const description = service.description || service.notes || '';
          const address = service.address || '';

          div.innerHTML = `
            ${image}
            <div class="popup-text">
              <h4>${service.name || 'Unnamed Service'}</h4>
              ${description ? `<p>${description}</p>` : ''}
              ${address ? `<p><strong>Address:</strong> ${address}</p>` : ''}
              ${
                serviceDetailScreenID
                  ? '<button class="popup-button" onclick="openServiceDetails(\'' +
                    service._id +
                    '\')">View Details</button>'
                  : ''
              }
            </div>
          `;

          return div;
        }

        function openServiceDetails(serviceID) {
          if (serviceDetailScreenID) {
            buzzyFrameAPI.navigate({
              screenID: serviceDetailScreenID,
              rowID: serviceID,
            });
          }
        }

        window.openServiceDetails = openServiceDetails;

        return (
          <div className="map-container">
            {loading && (
              <div className="map-loading">
                <span
                  className="material-symbols-outlined"
                  style={{ marginRight: '0.5rem' }}>
                  map
                </span>
                Loading service locations...
              </div>
            )}
            {error && (
              <div className="map-error">
                <span
                  className="material-symbols-outlined"
                  style={{
                    display: 'block',
                    fontSize: '2rem',
                    marginBottom: '0.5rem',
                  }}>
                  error
                </span>
                {error}
              </div>
            )}
            <div
              ref={mapRef}
              style={{ height: '100%', width: '100%' }}
              role="application"
              aria-label="Interactive map showing service locations"
            />
          </div>
        );
      }

      ReactDOM.render(<ServiceLocationsMap />, document.getElementById('root'));
    </script>
  </body>
</html>
```

## Key Concepts

### Leaflet.js Map Integration

* **Professional Mapping**: Uses industry-standard Leaflet.js for reliable map functionality
* **Custom Markers**: Places markers at service locations with tooltips
* **Interactive Popups**: Rich popups with images, descriptions, and action buttons
* **Map Controls**: Zoom controls and pan functionality built-in

### Location Data Handling

* **GeoJSON Support**: Handles standard GeoJSON coordinate format \[longitude, latitude]
* **Data Validation**: Validates coordinates to ensure they're within valid ranges
* **Error Handling**: Graceful handling of invalid or missing location data
* **Flexible Data Sources**: Works with "Service Locations" or "Services" tables

### Advanced Map Features

* **Auto-fitting Bounds**: Automatically zooms to show all service locations
* **Responsive Popups**: Popups adapt to mobile and desktop layouts
* **Navigation Integration**: Popup buttons navigate to service detail screens
* **Real-time Updates**: Map updates immediately when service data changes

### User Experience

* **Loading States**: Clear loading indicators while data is fetched
* **Error Messages**: User-friendly error messages for various failure scenarios
* **Mobile Optimization**: Touch-friendly controls and responsive design
* **Accessibility**: ARIA labels and keyboard navigation support

This map widget demonstrates how code widgets can integrate sophisticated mapping libraries to create professional location-based applications with rich interactivity and real-time data updates.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.buzzy.buzz/the-building-blocks/code-widget-custom-code/examples/service-locations-map.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
