SGERP Map

Interactive Mapbox GL map component with state-based configuration

Live Example

Operations Location Map

Interactive map showing operations locations with hover tooltips.

Features

  • Hover over locations to see details
  • Change map style using the dropdown (top-left)
  • Zoom and pan using mouse or touch
  • Data automatically updates when collection changes

Overview

The SGERPMap component is a flexible Mapbox GL wrapper that uses a state-based configuration approach. Instead of imperatively managing map sources and layers, you define a MapStateConfig object, and the component handles initialization and updates automatically.

Prerequisites

  1. Install required dependencies:
npm install mapbox-gl
  1. Set your Mapbox token in .env.local:
NEXT_PUBLIC_MAPBOX_TOKEN=your_mapbox_token_here

Basic Usage

import { SGERPMap } from '@/components/sgerp/map';
import type { MapStateConfig } from '@/components/sgerp/map';

const mapState: MapStateConfig = {
  mapLayers: {
    locations: {
      source: {
        type: 'geojson',
        data: {
          type: 'FeatureCollection',
          features: []
        }
      },
      layers: [{
        id: 'locations-layer',
        type: 'circle',
        source: 'locations',
        paint: {
          'circle-radius': 8,
          'circle-color': '#3b82f6'
        }
      }],
      interactions: [{
        layer_id: 'locations-layer',
        interaction_type: 'hover',
        properties: ['name', 'code']
      }]
    }
  }
};

export function MyMap() {
  return <SGERPMap state={mapState} />;
}

Props

PropTypeDefaultDescription
stateMapStateConfigRequiredMap configuration with sources, layers, and interactions
mapKeystring'sgerp-map'Unique key for localStorage (style preference)
initialCenter[number, number][103.8198, 1.3521]Initial map center [lng, lat]
initialZoomnumber12Initial zoom level
classNamestring''Additional CSS classes
styleReact.CSSProperties{}Inline styles for container

MapStateConfig Structure

interface MapStateConfig {
  mapBoundingBox?: [[number, number], [number, number]];
  mapLayers: {
    [key: string]: {
      source: mapboxgl.AnySourceData;
      layers: mapboxgl.AnyLayer[];
      interactions?: Array<{
        layer_id: string;
        interaction_type: 'hover' | 'click';
        properties: string | string[];
      }>;
    };
  };
}

Using with Collections

OperationsLocation Example

'use client'

import { useEffect, useState, useMemo } from 'react';
import { useSGERP } from 'sgerp-frontend-lib';
import { SGERPMap } from '@/components/sgerp/map';
import { OperationsLocationMapState } from 'sgerp-frontend-lib/lib/sgerp/map-states';

export function LocationsMap() {
  const api = useSGERP();
  const [loaded, setLoaded] = useState(false);

  // Fetch locations
  useEffect(() => {
    if (!api) return;

    async function loadLocations() {
      await api.collections.operationslocation.fetch({ limit: 1000 });
      setLoaded(true);
    }

    loadLocations();
  }, [api]);

  // Create map state with GeoJSON data
  const mapState = useMemo(() => {
    if (!api || !loaded) return OperationsLocationMapState;

    const geojson = api.collections.operationslocation.toGeoJSON();

    return {
      ...OperationsLocationMapState,
      mapLayers: {
        locations: {
          ...OperationsLocationMapState.mapLayers.locations,
          source: {
            type: 'geojson',
            data: geojson,
          },
        },
      },
    };
  }, [api, loaded]);

  return <SGERPMap state={mapState} />;
}

Predefined Map States

The library includes predefined map states for common use cases:

OperationsLocationMapState

Basic blue circle markers for locations:

import { OperationsLocationMapState } from 'sgerp-frontend-lib/lib/sgerp/map-states';

const mapState = {
  ...OperationsLocationMapState,
  mapLayers: {
    locations: {
      ...OperationsLocationMapState.mapLayers.locations,
      source: {
        type: 'geojson',
        data: myGeoJSON,
      },
    },
  },
};

OperationsLocationByProjectMapState

Color-coded by project ID:

import { OperationsLocationByProjectMapState } from 'sgerp-frontend-lib/lib/sgerp/map-states';

Custom Map States

Custom Colors

const customMapState: MapStateConfig = {
  mapLayers: {
    data: {
      source: {
        type: 'geojson',
        data: geojson,
      },
      layers: [{
        id: 'custom-layer',
        type: 'circle',
        source: 'data',
        paint: {
          'circle-radius': 10,
          'circle-color': ['get', 'color'], // Use color from properties
          'circle-stroke-width': 2,
          'circle-stroke-color': '#ffffff',
        },
      }],
      interactions: [{
        layer_id: 'custom-layer',
        interaction_type: 'hover',
        properties: '*', // Show all properties
      }],
    },
  },
};

Multiple Layers

const multiLayerState: MapStateConfig = {
  mapLayers: {
    locations: {
      source: {
        type: 'geojson',
        data: locationsGeoJSON,
      },
      layers: [{
        id: 'locations-circles',
        type: 'circle',
        source: 'locations',
        paint: {
          'circle-radius': 8,
          'circle-color': '#3b82f6',
        },
      }],
    },
    routes: {
      source: {
        type: 'geojson',
        data: routesGeoJSON,
      },
      layers: [{
        id: 'routes-lines',
        type: 'line',
        source: 'routes',
        paint: {
          'line-color': '#ef4444',
          'line-width': 3,
        },
      }],
    },
  },
};

Dynamic Updates

The map automatically updates when the state changes:

const [filter, setFilter] = useState<number | null>(null);

const mapState = useMemo(() => {
  const geojson = api?.collections.operationslocation.toGeoJSON();

  // Filter features based on project
  if (filter && geojson) {
    geojson.features = geojson.features.filter(
      f => f.properties?.project_id === filter
    );
  }

  return {
    ...OperationsLocationMapState,
    mapLayers: {
      locations: {
        ...OperationsLocationMapState.mapLayers.locations,
        source: { type: 'geojson', data: geojson },
      },
    },
  };
}, [api, filter]);

return (
  <>
    <select onChange={(e) => setFilter(Number(e.target.value))}>
      <option value="">All Projects</option>
      <option value="759">Project 759</option>
      <option value="760">Project 760</option>
    </select>
    <SGERPMap state={mapState} />
  </>
);

Interactions

Hover Tooltips

interactions: [{
  layer_id: 'my-layer',
  interaction_type: 'hover',
  properties: ['name', 'code', 'address'], // Specific properties
}]

Show All Properties

interactions: [{
  layer_id: 'my-layer',
  interaction_type: 'hover',
  properties: '*', // All properties
}]

Click Events

interactions: [{
  layer_id: 'my-layer',
  interaction_type: 'click',
  properties: ['name'],
}]

Map Styles

The component includes 5 built-in map styles:

  • Light - Clean light theme (default)
  • Dark - Dark theme
  • Satellite - Satellite imagery with labels
  • Street - Detailed street map
  • Outdoors - Terrain and outdoor features

Users can switch styles using the dropdown in the top-left corner. The selected style is saved to localStorage.

Bounding Box

Automatically fit map to data:

const mapState: MapStateConfig = {
  mapBoundingBox: [
    [minLng, minLat],
    [maxLng, maxLat]
  ],
  mapLayers: { ... }
};

Real-time Updates

Combine with live updates for dynamic maps:

import { useLiveUpdates } from 'sgerp-frontend-lib';

useLiveUpdates(api);

useEffect(() => {
  if (!api) return;

  return api.collections.operationslocation.onChange(() => {
    // Map will automatically re-render with new data
    forceUpdate();
  });
}, [api]);

Styling

Custom Height

<SGERPMap
  state={mapState}
  style={{ height: '600px' }}
/>

Fullscreen

<SGERPMap
  state={mapState}
  className="h-screen"
/>