Skip to content

Guide: Multi-Source GeoJSON Layer System

Applies to: @geoleaf/core v3.x


Overview

The GeoLeaf.GeoJSON module supports several independent GeoJSON layers, with:

  • Per-layer show/hide
  • Automatic integration into the legend
  • Unified popups compatible with the existing POI system
  • Smart clustering for points
  • Configuration per business profile (tourism, etc.)

Configuration in profile.json

Basic structure

Add a geojsonLayers section to data/profiles/[profile]/profile.json:

json
{
    "id": "tourism",
    "label": "Profil tourisme",

    "geojsonLayers": [
        {
            "id": "tourism-routes",
            "label": "Itinéraires touristiques",
            "url": "../data/profiles/tourism/geojson/itineraries.geojson",
            "visible": true,
            "fitBoundsOnLoad": false,
            "maxZoomOnFit": 12,
            "clustering": false,
            "style": {
                "color": "#FF9800",
                "weight": 3,
                "opacity": 0.9
            },
            "popupTemplate": "default",
            "detailProfileId": "route_default"
        },
        {
            "id": "tourism-zones",
            "label": "Zones touristiques",
            "url": "../data/profiles/tourism/geojson/zone-test.geojson",
            "visible": true,
            "clustering": false,
            "style": {
                "color": "#0066cc",
                "weight": 2,
                "fillColor": "#66ccff",
                "fillOpacity": 0.35
            },
            "popupTemplate": "default"
        },
        {
            "id": "tourism-poi-nature",
            "label": "POI Nature",
            "url": "../data/profiles/tourism/geojson/poi-naturels.geojson",
            "visible": true,
            "clustering": true,
            "pointStyle": {
                "radius": 8,
                "color": "#ffffff",
                "weight": 2,
                "fillColor": "#10b981",
                "fillOpacity": 0.9
            }
        }
    ]
}

Layer properties

PropertyTypeRequiredDescription
idstringYesUnique layer identifier
labelstringYesLabel shown in the legend
urlstringYesPath to the GeoJSON file
visiblebooleanNo (default true)Initial visibility
fitBoundsOnLoadbooleanNo (default false)Fit the view to the layer on load
maxZoomOnFitnumberNo (default 16)Maximum zoom applied by fitBounds
clusteringbooleanNo (default auto)Enable clustering (points only)
styleobjectMapLibre paint style for polygons/lines
pointStyleobjectMapLibre paint style for points (circle layer)
popupTemplatestringNoPopup template (not implemented yet)
detailProfileIdstringNoDetail panel profile (not implemented yet)

Clustering configuration — per capability, no longer global

WARNING

poiConfig and applyToAllSources no longer exist. They set clustering for "all POIs" at once, back when a POI was a separate subsystem. A POI is now a GeoJSON point layer, and clustering is configured like any other capability: a default in the profile, an override per layer.

Profile defaultconfig/plugins/cluster.json, referenced by Files.modules.cluster:

jsonc
{
    "clustering": true,
    "clusterStrategy": "unified",
    "clusterRadius": 80,
    "disableClusteringAtZoom": 12,
}

Per layer — in the layer's *_config.json, the clustering key takes precedence over the default:

jsonc
{
    "id": "commerces",
    "clustering": false, // this layer never clusters, whatever the default is
}

The proximity search radius is configured separately, under profile.search (radiusMin, radiusMax, radiusStep, radiusDefault).


Use from the application

WARNING

GeoLeaf.GeoJSON is not a public namespace. Layers declared in geojsonLayers are loaded automatically at init. Visibility is handled through GeoLeaf.Legend.

Automatic loading

GeoJSON layers defined in geojsonLayers are loaded automatically when GeoLeaf.loadConfig() is called — no extra call is required:

javascript
GeoLeaf.loadConfig({ url: "./geoleaf.config.json", profileId: "tourism" }).then(() => {
    // geojsonLayers layers are already loaded and visible
    console.log("Map ready with the profile GeoJSON layers.");
});

Visibility management

javascript
// Hide a layer
GeoLeaf.Legend.setLayerVisibility("tourism-routes", false);

// Show a layer
GeoLeaf.Legend.setLayerVisibility("tourism-routes", true);

// List all active layers
const allLayers = GeoLeaf.Legend.getAllLayers();
console.log(allLayers);

Legend integration

The system integrates automatically with the GeoLeaf.Legend module:

  1. Section created automatically: "GeoJSON layers"
  2. Clickable items: one checkbox/switch per layer
  3. Two-way synchronisation: legend ↔ map

Events

javascript
// Listen for visibility changes
map.on("geoleaf:geojson:visibility-changed", (e) => {
    console.log(`Layer ${e.layerId}: ${e.visible ? "visible" : "hidden"}`);
});

// Listen for layer loading
map.on("geoleaf:geojson:layers-loaded", (e) => {
    console.log(`${e.count} layer(s) loaded`, e.layers);
});

Popups and detail panel

Automatic popups

Each feature shows a popup with:

  • Title: properties.name, properties.label or properties.title
  • Description: properties.description or properties.desc
  • "View details" button: rendered by the feature-info capability, configured on the layer (layers.<id>.capabilities.feature-info)
json
{
    "type": "FeatureCollection",
    "features": [
        {
            "type": "Feature",
            "properties": {
                "id": "poi-001",
                "name": "Parc National",
                "description": "Magnifique parc naturel avec vue panoramique.",
                "category": "nature",
                "subcategory": "parc"
            },
            "geometry": {
                "type": "Point",
                "coordinates": [-60.68, -32.95]
            }
        }
    ]
}

Side detail panel

Clicking a feature opens the existing POI detail panel automatically, with the adapted data.


POI clustering

Automatic activation

Clustering is enabled when:

  1. The layer contains Point geometries
  2. clustering: true in config/plugins/cluster.json (profile default)
  3. clustering !== false in the layer's *_config.json (no explicit opt-out)

Per-layer override

jsonc
{
    "id": "poi-sans-cluster",
    "label": "POI sans clustering",
    "url": "poi.geojson",
    "clustering": false, // ← disables clustering for this layer only
}

Supported geometry types

Geometry typeMapLibre renderingStyle configClustering
PointCircle layer or SymbolpointStyleYes
LineStringLine layerstyleNo
PolygonFill + Line layerstyleNo
MultiPointCircle layer or SymbolpointStyleYes
MultiLineStringLine layerstyleNo
MultiPolygonFill + Line layerstyleNo

Limits and performance

Layer limit

  • Warning above 10 layers in geojsonLayers[]
  • Recommendation: 3-5 layers maximum for optimal performance

Feature limit

  • No technical limit, but watch performance above 5000 features per layer
  • Use clustering for dense POI layers

MapLibre optimisations

  • Each layer uses an independent GeoJSON source (map.addSource())
  • Clustering through the supercluster implementation built into MapLibre (cluster: true on the source)
  • Efficient add/remove through map.addLayer() / map.removeLayer()
  • WebGL rendering for optimal performance on dense layers

Complete examples

Example 1: three layers (POI, routes, zones)

jsonc
"geojsonLayers": [
  {
    "id": "poi-restaurants",
    "label": "Restaurants",
    "url": "../data/profiles/tourism/geojson/restaurants.geojson",
    "visible": true,
    "clustering": true,
    "pointStyle": {
      "radius": 8,
      "fillColor": "#f97316",
      "color": "#fff",
      "weight": 2,
      "fillOpacity": 0.9
    }
  },
  {
    "id": "routes-velo",
    "label": "Pistes cyclables",
    "url": "../data/profiles/tourism/geojson/routes-velo.geojson",
    "visible": false,
    "clustering": false,
    "style": {
      "color": "#10b981",
      "weight": 4,
      "opacity": 0.8
    }
  },
  {
    "id": "zones-protection",
    "label": "Zones protégées",
    "url": "../data/profiles/tourism/geojson/zones-protection.geojson",
    "visible": true,
    "style": {
      "color": "#3b82f6",
      "weight": 2,
      "fillColor": "#93c5fd",
      "fillOpacity": 0.3
    }
  }
]

Example 2: programmatic visibility control

javascript
// Layers are loaded automatically through loadConfig()
// Handle visibility through GeoLeaf.Legend:

// Hide every layer except one
const allLayers = GeoLeaf.Legend.getAllLayers();
allLayers.forEach((info, layerId) => {
    if (layerId !== "poi-restaurants") {
        GeoLeaf.Legend.setLayerVisibility(layerId, false);
    }
});

// Count visible layers
const visibleCount = [...allLayers.entries()].filter(([, info]) => info.visible).length;
console.log("Visible layers:", visibleCount);

Declaring layers in the profile

GeoJSON layers are declared in profile.json and nowhere else. There is no public GeoLeaf.GeoJSON API — loading is driven entirely by configuration.

json
{
    "geojsonLayers": [
        {
            "id": "zones",
            "label": "Zones",
            "url": "zones.geojson",
            "visible": true
        }
    ]
}

See Configuration in profile.json for the full reference.


Troubleshooting

Problem: layers do not appear

Check:

  1. The GeoJSON file URLs are correct (relative to the profile)
  2. visible: true in the config
  3. The browser console for loading errors (404, invalid JSON)
  4. GeoLeaf.loadConfig() is called before the map is used

Problem: clustering does not work

Check:

  1. clustering: true in config/plugins/cluster.json
  2. Files.modules.cluster points to that file from profile.json
  3. Geometries are of type Point (not Polygon or LineString)
  4. No clustering: false in the layer's *_config.json

Clustering is built in natively through supercluster (MapLibre source clustering). No external dependency is required.

Problem: the legend does not appear

Check:

  1. The GeoLeaf.LayerManager module is initialised
  2. modules.legend.enabled is not disabled (opt-out: the legend is active unless modules.legend.enabled: false is set in config/plugins/legend.json)
  3. At least one layer loaded successfully

MVT / PBF vector tiles

For high-volume layers (more than 5,000 lines or polygons), GeoLeaf supports vector tiles (MVT/PBF) through the native vector sources of MapLibre GL JS. Tiles are pre-generated locally and loaded on demand according to zoom level and visible extent.

See the dedicated guide: MVT_GUIDE.md


References

  • Internal module: packages/core/src/modules/geojson/
  • Legend facade: packages/core/src/geoleaf.legend.ts
  • Tourism profile: profiles/tourism/profile.json
  • MVT guide: geojson/MVT_GUIDE.md

Changelog

v2.0.0

  • Rendering engine migration: Leaflet → MapLibre GL JS v5 (WebGL)
  • GeoJSON sources through the native map.addSource() / map.addLayer() of MapLibre
  • Native GPU clustering through cluster: true on sources
  • Data-driven styles through MapLibre expressions (match, interpolate, case)
  • [lng, lat] coordinates (GeoJSON standard) — the [lat, lng] inversion is gone

v1.1.0–v1.2.0

  • Multi-layer architecture through geojsonLayers[] in profile.json
  • Automatic integration with GeoLeaf.Legend (visibility, sections)
  • Unified popups compatible with the POI system
  • Clustering configured per capability: profile default, per-layer override
  • Configuration per business profile

Released under the MIT License.