# 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.
