GeoJSON Support

Convert collections to GeoJSON for mapping with Mapbox GL and other mapping libraries

Overview

SGERP collections support converting models to GeoJSON format, making it easy to visualize spatial data with mapping libraries like Mapbox GL, Leaflet, or deck.gl.

Basic Usage

Using Predefined toGeoJSON

Collections with geospatial data (like OperationsLocation) have built-in toGeoJSON() methods:

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

const api = useSGERP();

// Fetch locations
await api?.collections.operationslocation.fetch({ limit: 100 });

// Convert to GeoJSON
const geojson = api?.collections.operationslocation.toGeoJSON();

console.log(geojson);
// {
//   type: 'FeatureCollection',
//   features: [
//     {
//       type: 'Feature',
//       geometry: { type: 'Point', coordinates: [longitude, latitude] },
//       properties: { name: '...', code: '...', ... },
//       id: 123
//     },
//     ...
//   ]
// }

Using Custom Conversion Function

For collections without predefined toGeoJSON, or to customize the output, pass a conversion function:

// Custom GeoJSON conversion
const geojson = api?.collections.vehicle.toGeoJSON((vehicle) => {
  if (!vehicle.latitude || !vehicle.longitude) return null;

  return {
    type: 'Feature',
    geometry: {
      type: 'Point',
      coordinates: [vehicle.longitude, vehicle.latitude]
    },
    properties: {
      service_number: vehicle.service_number,
      color: vehicle.color,
      status: vehicle.status
    },
    id: vehicle.id
  };
});

Integration with Mapbox GL

Basic Map Example

'use client'

import { useEffect, useRef, useState } from 'react';
import { useSGERP } from 'sgerp-frontend-lib';
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';

export function LocationsMap() {
  const api = useSGERP();
  const mapContainer = useRef<HTMLDivElement>(null);
  const map = useRef<mapboxgl.Map | null>(null);
  const [mapLoaded, setMapLoaded] = useState(false);

  // Initialize map
  useEffect(() => {
    if (!mapContainer.current) return;

    mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN!;

    map.current = new mapboxgl.Map({
      container: mapContainer.current,
      style: 'mapbox://styles/mapbox/streets-v12',
      center: [0, 0],
      zoom: 2
    });

    map.current.on('load', () => {
      setMapLoaded(true);
    });

    return () => map.current?.remove();
  }, []);

  // Load locations and add to map
  useEffect(() => {
    if (!api || !mapLoaded || !map.current) return;

    async function loadLocations() {
      await api.collections.operationslocation.fetch({ limit: 1000 });
      const geojson = api.collections.operationslocation.toGeoJSON();

      if (!geojson || !map.current) return;

      // Add source
      map.current.addSource('locations', {
        type: 'geojson',
        data: geojson
      });

      // Add layer
      map.current.addLayer({
        id: 'locations',
        type: 'circle',
        source: 'locations',
        paint: {
          'circle-radius': 6,
          'circle-color': '#3b82f6',
          'circle-stroke-width': 2,
          'circle-stroke-color': '#ffffff'
        }
      });

      // Fit bounds to show all points
      if (geojson.features.length > 0) {
        const bounds = new mapboxgl.LngLatBounds();
        geojson.features.forEach(feature => {
          if (feature.geometry?.type === 'Point') {
            bounds.extend(feature.geometry.coordinates as [number, number]);
          }
        });
        map.current.fitBounds(bounds, { padding: 50 });
      }
    }

    loadLocations();
  }, [api, mapLoaded]);

  return <div ref={mapContainer} style={{ width: '100%', height: '500px' }} />;
}

Interactive Popups

// Add click handler for popups
map.current.on('click', 'locations', (e) => {
  if (!e.features || e.features.length === 0) return;

  const feature = e.features[0];
  const coordinates = (feature.geometry as any).coordinates.slice();
  const properties = feature.properties;

  new mapboxgl.Popup()
    .setLngLat(coordinates)
    .setHTML(`
      <h3>${properties.name}</h3>
      <p><strong>Code:</strong> ${properties.code}</p>
      <p><strong>Address:</strong> ${properties.address}</p>
    `)
    .addTo(map.current!);
});

// Change cursor on hover
map.current.on('mouseenter', 'locations', () => {
  map.current!.getCanvas().style.cursor = 'pointer';
});

map.current.on('mouseleave', 'locations', () => {
  map.current!.getCanvas().style.cursor = '';
});

Filtering with React State

const [projectId, setProjectId] = useState<number | null>(null);

useEffect(() => {
  if (!api || !mapLoaded || !map.current) return;

  async function updateLocations() {
    // Fetch with filter
    await api.collections.operationslocation.fetch({
      limit: 1000,
      ...(projectId && { project_id: projectId })
    });

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

    if (!geojson || !map.current) return;

    // Update source data
    const source = map.current.getSource('locations') as mapboxgl.GeoJSONSource;
    if (source) {
      source.setData(geojson);
    }
  }

  updateLocations();
}, [api, mapLoaded, projectId]);

GeoJSON Types

The library exports standard GeoJSON types:

import type {
  GeoJSONFeatureCollection,
  GeoJSONFeature,
  GeoJSONGeometry,
  GeoJSONPoint,
  GeoJSONPolygon,
  ToGeoJSONFeature
} from 'sgerp-frontend-lib';

Type Definitions

// Feature
interface GeoJSONFeature<P = Record<string, any>> {
  type: 'Feature';
  geometry: GeoJSONGeometry | null;
  properties: P;
  id?: string | number;
}

// FeatureCollection
interface GeoJSONFeatureCollection<P = Record<string, any>> {
  type: 'FeatureCollection';
  features: GeoJSONFeature<P>[];
}

// Conversion function type
type ToGeoJSONFeature<T> = (model: T) => GeoJSONFeature | null;

Collections with Built-in GeoJSON Support

OperationsLocationCollection

const geojson = api.collections.operationslocation.toGeoJSON();
// Returns Point features with location properties

Properties included:

  • id, name, code, external_id
  • address, postal_code
  • group_id, project_id
  • h3, created_at, modified_at
  • calculation_params, data

Custom Implementations

You can implement toGeoJSON in custom collection classes:

import { Collection } from 'sgerp-frontend-lib';
import type { GeoJSONFeatureCollection } from 'sgerp-frontend-lib';

class MyCustomCollection extends Collection<MyModel> {
  toGeoJSON(): GeoJSONFeatureCollection {
    return {
      type: 'FeatureCollection',
      features: this.models
        .filter(model => model.geometry)
        .map(model => ({
          type: 'Feature',
          geometry: model.geometry,
          properties: {
            name: model.name,
            type: model.type
          },
          id: model.id
        }))
    };
  }
}

Real-time Updates

Combine with live updates for dynamic maps:

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

// Enable live updates
useLiveUpdates(api);

// Listen for collection changes
useEffect(() => {
  if (!api || !map.current) return;

  const unsubscribe = api.collections.operationslocation.onChange(() => {
    const geojson = api.collections.operationslocation.toGeoJSON();
    const source = map.current!.getSource('locations') as mapboxgl.GeoJSONSource;
    if (source && geojson) {
      source.setData(geojson);
    }
  });

  return unsubscribe;
}, [api]);