How TREK Renders Interactive Maps with Leaflet and Mapbox GL (3D Buildings & Terrain)

The TREK application uses a dual-provider architecture that lets users choose between classic Leaflet raster tiles or modern Mapbox GL vector rendering with 3D terrain and buildings, switching dynamically via MapSettingsTab.tsx.

TREK implements a flexible mapping system that supports both lightweight raster tiles and GPU-accelerated vector graphics. The repository's interactive maps with Leaflet and Mapbox GL are implemented through separate React components that share a common settings interface. Users can toggle between providers at runtime without page reloads, enabling 3D building visualization when hardware and tokens permit.

Leaflet Rendering with react-leaflet

The Leaflet stack provides a lightweight, raster-based mapping experience ideal for low-end devices or offline scenarios.

MapView Component Architecture

The Leaflet implementation lives in client/src/components/Map/MapView.tsx. It leverages react-leaflet to wrap Leaflet primitives—such as MapContainer, TileLayer, Marker, and Polyline—in React-friendly components. The tile source defaults to Carto light basemaps but can be swapped via user settings.

A set of controller components manages map state: MapController, BoundsController, SelectionController, and ViewportController handle centering, bounds fitting, and viewport-change events for POI-explore pills.

Custom Markers and Clustering

Marker customization uses Leaflet's DivIcon via the createPlaceIcon function to render photo-circle or category-icon markers. For performance with large datasets, react-leaflet-cluster groups markers using the clusterIconCreateFunction helper, displaying cluster counts without overwhelming the DOM.

Location and Reservation Layers

The LeafletLocationLayer component draws the user's GPS dot and accuracy circle using the shared useGeolocation hook. For trip planning, the ReservationOverlay component renders flight- and train-style arcs on top of the basemap.

// Render a Leaflet map with custom markers
import { MapView } from '@/components/Map/MapView';

export default function TripMap() {
  return (
    <MapView
      places={myPlaces}
      center={[48.8566, 2.3522]}
      zoom={12}
      tileUrl='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
    />
  );
}

Mapbox GL Rendering with 3D Terrain and Buildings

The Mapbox GL stack delivers high-resolution vector tiles, GPU-accelerated rendering, and true 3D visualization.

Direct API Integration

The Mapbox implementation is found in client/src/components/Map/MapViewGL.tsx. Unlike the Leaflet wrapper, this component talks directly to the mapbox-gl API, creating a map instance inside a container div referenced by containerRef. The Mapbox style URL and access token are read from the useSettingsStore hook (settings.mapbox_access_token), and changing these values rebuilds the map via a useEffect watching [mapboxStyle, mapboxToken, mapbox3d].

Enabling 3D Terrain and Buildings

When the selected style supports terrain (indicated by wantsTerrain), the addTerrainAndSky helper from client/src/components/Map/mapboxSetup.ts injects a DEM source and sky layer. The addCustom3dBuildings function adds a fill-extrusion layer that renders vector-tile building data with realistic heights, adjusting colors based on the dark mode flag.

// Render a Mapbox GL map with 3D terrain and buildings
import { MapViewGL } from '@/components/Map/MapViewGL';

export default function TripMapGL() {
  return (
    <MapViewGL
      places={myPlaces}
      center={[48.8566, 2.3522]}
      zoom={12}
    />
  );
}

Altitude-Aware Markers

Because Mapbox GL places HTML markers at sea level by default, the syncMarkerAltitudes routine (lines 50-70 of MapViewGL.tsx) periodically queries map.queryTerrainElevation and updates each marker's LngLat to [lng, lat, elevation]. This prevents the "drift" bug where markers appear to float above or sink below the terrain when the camera pitches.

Custom marker elements are built by createMarkerElement (lines 60-134), mimicking the Leaflet photo-circle style with order-badge support, and attached via new mapboxgl.Marker({ element, anchor: 'center' }).

Route Layers and Event Handling

Two GeoJSON sources (trip-route and trip-gpx) are added on load and rendered with a "casing-plus-core" line style that reproduces the Apple Maps look—dark blue casing with a bright blue core. The ReservationMapboxOverlay (in client/src/components/Map/reservationsMapbox.ts) mirrors the Leaflet overlay, drawing arcs and endpoint badges.

Event handling wires directly to the Mapbox instance. The "add-place" action is explicitly bound to the middle mouse button to avoid conflicts with Mapbox's built-in rotate/pitch gestures.

Switching Between Map Providers

The MapSettingsTab component (in client/src/components/Settings/MapSettingsTab.tsx) stores the provider choice in settings.map_provider. A wrapper component conditionally renders either stack:

// Provider switch UI
import { useSettingsStore } from '@/store/settingsStore';
import { MapView } from '@/components/Map/MapView';
import { MapViewGL } from '@/components/Map/MapViewGL';

export default function MapWrapper(props) {
  const provider = useSettingsStore(s => s.settings.map_provider ?? 'leaflet');

  return provider === 'leaflet' ? (
    <MapView {...props} />
  ) : (
    <MapViewGL {...props} />
  );
}

This architecture allows the entire map experience to be toggled at runtime without reloading the page, falling back to Leaflet when Mapbox tokens are unavailable.

Summary

  • TREK abstracts its map layer behind a dual-provider architecture supporting both Leaflet and Mapbox GL.
  • Leaflet (MapView.tsx) provides lightweight raster tiles with clustering via react-leaflet and react-leaflet-cluster.
  • Mapbox GL (MapViewGL.tsx) enables 3D terrain and buildings using addTerrainAndSky and addCustom3dBuildings from mapboxSetup.ts.
  • The syncMarkerAltitudes function prevents marker drift by querying DEM elevation data and updating marker positions to [lng, lat, elevation].
  • Users switch providers at runtime through MapSettingsTab.tsx, which updates settings.map_provider and conditionally renders the appropriate component.

Frequently Asked Questions

How does TREK switch between Leaflet and Mapbox GL?

The MapSettingsTab.tsx component stores the provider selection in settings.map_provider. A wrapper component reads this value from the useSettingsStore hook and conditionally renders either MapView for Leaflet or MapViewGL for Mapbox GL, allowing runtime switching without page reloads.

Why do Mapbox markers need altitude synchronization?

Mapbox GL places HTML markers at sea level by default, causing them to float or sink when the camera tilts over 3D terrain. The syncMarkerAltitudes function queries map.queryTerrainElevation for each marker's coordinates and updates their LngLat to include the terrain height, keeping them anchored to the surface during pitch changes.

What enables 3D buildings and terrain in the Mapbox view?

The addTerrainAndSky helper adds a DEM source and sky layer from Mapbox's terrain service, while addCustom3dBuildings injects a fill-extrusion layer that renders building heights from vector tiles. These utilities are located in client/src/components/Map/mapboxSetup.ts and are called when the selected style requests terrain support.

Can I use custom markers with both map providers?

Yes. Leaflet uses DivIcon via createPlaceIcon for photo-circle markers, while Mapbox GL uses createMarkerElement to build HTML elements attached with new mapboxgl.Marker(). Both approaches support custom styling, order badges, and category colors, with Mapbox additionally requiring altitude synchronization to align with terrain.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →