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_idaddress,postal_codegroup_id,project_idh3,created_at,modified_atcalculation_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]);