Skip to content

GeoLeaf JS — User Guide ​

Applies to: @geoleaf/core v3.x

Audience: developers integrating GeoLeaf into their own applications

This guide covers every GeoLeaf JS feature, from basic use to advanced configuration.


Table of Contents ​

  1. Introduction & Overview
  2. Installation
  3. Quick Start
  4. Understanding Profiles
  5. Configuration Basics
  6. Working with Maps
  7. UI Components
  8. Advanced Topics
  9. Troubleshooting
  10. Next Steps

1. Introduction & Overview ​

What is GeoLeaf? ​

GeoLeaf JS is a TypeScript interactive mapping library built on MapLibre GL JS v6 (WebGL rendering, native vector tiles). It provides a high-level API for POIs (points of interest), GeoJSON layers, themes, filters and plugin integrations — entirely configurable through JSON profiles, with no application-side development.

Key Features ​

  • Multi-Profile System — preconfigured profiles for tourism and custom use cases
  • Layer data — read and write features through GeoLeaf.Layers, whatever the geometry
  • Theme System — light/dark themes with customisable layer-visibility presets
  • GeoJSON Support — display of polygons, lines and complex geographic data
  • WebGL Rendering — GPU rendering through MapLibre GL JS v6
  • Offline — engine built into the core (modules.offline, IndexedDB), loaded on demand
  • Label System — dynamic labels with zoom-based visibility
  • Advanced Filters — multi-criteria filtering with taxonomies and categories
  • Point symbol — per-category icons, tints and badges through the taxonomy capability
  • Security — XSS protection through the secure DOM helpers

The data table is provided by the MIT plugin @geoleaf-plugins/table.

When to Use GeoLeaf ​

Suitable use cases:

  • Tourism mapping applications (attractions, hotels, restaurants)
  • Real-estate applications
  • Venue and event management
  • Geographic portals driven by JSON configuration

Not recommended for:

  • Real-time GPS tracking with sub-second updates
  • Flight simulators or advanced 3D navigation
  • Applications requiring 10,000+ simultaneous markers

Browser Support ​

BrowserMinimum version
Chrome/Edge90+
Firefox88+
Safari14+
Mobile SafariiOS 14+
Chrome Android90+

JavaScript required: ES2022+ (async/await, Promises, ESM modules)

MapLibre GL JS v6 requires WebGL 2.0 support (available in every modern browser).


2. Installation ​

bash
npm install @geoleaf/core maplibre-gl
typescript
import * as maplibregl from "maplibre-gl";
import { Core } from "@geoleaf/core";
import "maplibre-gl/dist/maplibre-gl.css";
import "@geoleaf/core/style.css";

// GeoLeaf reads the engine from `globalThis.maplibregl`, which MapLibre 6 no longer sets
Object.assign(globalThis, { maplibregl });

Core.init({
    mapId: "map",
    center: [48.8566, 2.3522], // [lat, lng] — GeoLeaf; MapLibre expects [lng, lat], the conversion is internal
    zoom: 12,
});

Peer dependency: maplibre-gl ^6.0.0 must be installed separately.

Option B: CDN (quick start) ​

GeoLeaf is distributed exclusively as ESM. Use <script type="module">:

html
<!-- MapLibre GL JS (peer dependency — must be loaded BEFORE GeoLeaf) -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@6.12/dist/maplibre-gl.css" />
<script type="module">
    import * as maplibregl from "https://cdn.jsdelivr.net/npm/maplibre-gl@6.12/dist/maplibre-gl.mjs";
    globalThis.maplibregl = maplibregl;
</script>

<!-- GeoLeaf styles -->
<link
    rel="stylesheet"
    href="https://cdn.jsdelivr.net/npm/@geoleaf/core@3/dist/geoleaf-main.min.css"
/>

WARNING

The two shim lines are not decorative. MapLibre has been ESM-only since v6 and no longer exposes a global; geoleaf.esm.js declares it external and reaches it only through globalThis.maplibregl. Without the shim, new maplibregl.Map() throws and the map never boots. Because the block is inline, it requires 'unsafe-inline' (or a nonce/hash) in script-src — in production, prefer the self-hosted Option C, whose shim is a file.

Before </body>:

html
<!-- geoleaf:docs:fragment — continues the `<head>` block above, which carries the MapLibre shim -->
<script type="module">
    import { Core } from "https://cdn.jsdelivr.net/npm/@geoleaf/core@3/dist/geoleaf.esm.js";
    Core.init({ mapId: "map", center: [48.8566, 2.3522], zoom: 12 });
</script>

Or through jsDelivr:

html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@6.12/dist/maplibre-gl.css" />
<script type="module">
    import * as maplibregl from "https://cdn.jsdelivr.net/npm/maplibre-gl@6.12/dist/maplibre-gl.mjs";
    globalThis.maplibregl = maplibregl;
</script>
<link
    rel="stylesheet"
    href="https://cdn.jsdelivr.net/npm/@geoleaf/core@3/dist/geoleaf-main.min.css"
/>

<script type="module">
    import { Core } from "https://cdn.jsdelivr.net/npm/@geoleaf/core@3/dist/geoleaf.esm.js";
    Core.init({ mapId: "map", center: [48.8566, 2.3522], zoom: 12 });
</script>

Option C: Self-hosted ​

Download from the releases and host the files on your own server:

html
<!-- Self-hosted MapLibre: the shim is a FILE, so no CSP exception has to be granted -->
<link rel="stylesheet" href="/assets/maplibre-gl/maplibre-gl.css" />
<script type="module" src="/assets/maplibre-gl/global.mjs"></script>

<link rel="stylesheet" href="/assets/geoleaf/geoleaf-main.min.css" />
<script type="module">
    import { Core } from "/assets/geoleaf/geoleaf.esm.js";
    Core.init({ mapId: "map", center: [48.8566, 2.3522], zoom: 12 });
</script>

The contents of global.mjs are two lines — the same shim as above, moved out of the document so that it no longer requires 'unsafe-inline':

javascript
import * as maplibregl from "./maplibre-gl.mjs";
globalThis.maplibregl = maplibregl;

Verifying the installation ​

Open the browser console:

javascript
console.log(GeoLeaf.version);
// Outputs the version you installed — the one npm served you, not a number this page froze.

3. Quick Start ​

Minimal example ​

html
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />

        <link
            rel="stylesheet"
            href="https://cdn.jsdelivr.net/npm/maplibre-gl@6.12/dist/maplibre-gl.css"
        />
        <script type="module">
            import * as maplibregl from "https://cdn.jsdelivr.net/npm/maplibre-gl@6.12/dist/maplibre-gl.mjs";
            globalThis.maplibregl = maplibregl;
        </script>
        <link
            rel="stylesheet"
            href="https://cdn.jsdelivr.net/npm/@geoleaf/core@3/dist/geoleaf-main.min.css"
        />

        <style>
            #map {
                width: 100%;
                height: 600px;
            }
        </style>
    </head>

    <body>
        <div id="map"></div>

        <script type="module">
            import { Core } from "https://cdn.jsdelivr.net/npm/@geoleaf/core@3/dist/geoleaf.esm.js";

            GeoLeaf.Core.init({ mapId: "map", center: [46.2, 2.2], zoom: 6 });

            // A POI is a feature of an ordinary point layer.
            // `GeoLeaf.POI` no longer exists as of v3 — see CHANGELOG [3.0.0].
            document.addEventListener("geoleaf:app:ready", () => {
                GeoLeaf.Layers.addFeature("ma-couche", {
                    type: "Feature",
                    geometry: { type: "Point", coordinates: [2.3522, 48.8566] },
                    properties: {
                        id: "paris",
                        title: "Paris",
                        description: "Capital of France",
                    },
                });
            });
        </script>
    </body>
</html>

Coordinate order — both orders coexist, deliberately:Core.init({ center }) and a profile's map.center expect [lat, lng]; the coordinates of a GeoJSON feature stay [lng, lat] (the GeoJSON standard, which MapLibre follows). The conversion lives exclusively in the adapter — see ARCHITECTURE_GUIDE.md.

Step-by-step tutorial ​

See GETTING_STARTED.md for a detailed 5-minute tutorial.


4. Understanding Profiles ​

What Are Profiles? ​

Profiles are predefined configurations that define:

  • UI layout (layer manager, filter panel, cache controls)
  • Basemaps (available background maps — MapLibre GL JS styles)
  • POI configuration (categories, icons, search)
  • File paths (where to load the JSON data from)
  • Taxonomy (category hierarchy and icons)
  • Default settings (initial zoom, centre and theme)

Built-in profiles ​

4.1 Tourism profile ​

Purpose: tourist attractions, hotels, restaurants, events

Features:

  • 35+ preconfigured layers (climate, conservation zones, cities, routes)
  • Rich taxonomy with 50+ categories (museums, monuments, hotels, restaurants)
  • Icon sprites optimised for tourism
  • Search by attraction name, city or category

Configuration: profiles/tourism/geoleaf.config.json

4.2 Custom profiles ​

Profiles can be created for any business domain. See QUICKSTART_TUTORIAL.md.

Switching Profiles ​

At start-up ​

The profile is named by the configuration GeoLeaf.boot() loads — GeoLeaf.init() reads only map and ui, and ignores a data key without a word:

javascript
GeoLeaf.boot({
    config: {
        data: {
            activeProfile: "tourism",
            profilesBasePath: "/profiles/",
        },
    },
});

Loading a profile from a URL ​

javascript
await GeoLeaf.loadConfig("/profiles/tourism/geoleaf.config.json");

Note: loading a new profile reloads the whole configuration and clears the current POIs.


5. Configuration Basics ​

5.1 Main configuration file ​

The entry point is geoleaf.config.json:

json
{
    "debug": {
        "enabled": false,
        "modules": ["config", "poi", "storage"]
    },
    "data": {
        "activeProfile": "tourism",
        "profilesBasePath": "/profiles/"
    }
}

Key fields:

  • debug.enabled — enables verbose console logging
  • debug.modules — modules to debug (or ["*"] for all of them)
  • data.activeProfile — profile to load
  • data.profilesBasePath — base path to the profile directories

5.2 Profile configuration file ​

Every profile has its own profile.json:

json
{
    "name": "Tourism",
    "version": "1.0",
    "ui": {
        "layerManager": { "enabled": true, "position": "topright" },
        "filterPanel": { "enabled": true, "position": "topleft" },
        "searchBar": { "enabled": true, "position": "topleft" },
        "cacheControls": { "enabled": true, "position": "bottomleft" }
    },
    "basemaps": [
        {
            "id": "osm",
            "name": "Street Map",
            "style": "https://tiles.openfreemap.org/styles/liberty",
            "default": true
        }
    ],
    "Files": {
        "taxonomy": "taxonomy.json",
        "themes": "themes.json",
        "layers": "layers/",
        "poi": "data/poi.json"
    },
    "defaultSettings": {
        "map": {
            "center": [2.3522, 48.8566],
            "zoom": 12,
            "minZoom": 5,
            "maxZoom": 18
        },
        "theme": "light"
    }
}

Note (v2.0.0): basemaps now use the style field (a URL to a MapLibre GL JS style) rather than an XYZ tile URL template.

5.3 Taxonomy configuration ​

Defines the categories, subcategories and icons in taxonomy.json:

json
{
    "icons": {
        "sprite": "assets/icons/tourism-sprite.png",
        "iconSize": [32, 32],
        "iconAnchor": [16, 32]
    },
    "categories": [
        {
            "id": "accommodation",
            "name": "Hébergement",
            "icon": "bed",
            "subcategories": [
                { "id": "hotel", "name": "Hôtel", "icon": "hotel" },
                { "id": "hostel", "name": "Auberge", "icon": "hostel" },
                { "id": "camping", "name": "Camping", "icon": "camping" }
            ]
        },
        {
            "id": "food",
            "name": "Restauration",
            "icon": "restaurant",
            "subcategories": [
                { "id": "restaurant", "name": "Restaurant", "icon": "restaurant" },
                { "id": "cafe", "name": "Café", "icon": "cafe" },
                { "id": "bar", "name": "Bar", "icon": "bar" }
            ]
        }
    ]
}

5.4 Theme configuration ​

Defines the layer-visibility presets in themes.json:

json
{
    "config": {
        "defaultTheme": "default",
        "allowCustomThemes": true
    },
    "themes": [
        {
            "id": "default",
            "name": "Vue par Défaut",
            "type": "primary",
            "layers": {
                "climate": true,
                "cities": true,
                "poi": true,
                "conservation-zones": false
            }
        },
        {
            "id": "heritage",
            "name": "Sites Patrimoniaux",
            "type": "secondary",
            "layers": {
                "monuments": true,
                "conservation-zones": true,
                "museums": true,
                "cities": false
            }
        }
    ]
}

A theme may also say where it frames the map, with view — an extent to fit (bounds), or a point with its zoom (center and zoom):

json
{
    "id": "north-sector",
    "label": "North sector",
    "type": "primary",
    "layers": [{ "id": "cities", "visible": true }],
    "view": {
        "bounds": [
            [43.2, 1.2],
            [43.6, 1.6]
        ]
    }
}

Choosing such a theme moves the map to that view; a theme without view leaves the map where you left it. When the theme the profile starts on declares one, the map opens on it.

5.5 Layer style configuration ​

A layer can carry several styles in layers/<layer-name>/styles/<style-id>.json:

json
{
    "id": "default",
    "description": "Default style for climate layer",
    "label": {
        "enabled": true,
        "visibleByDefault": false
    },
    "scaleConfig": {
        "minScale": 9222148,
        "maxScale": 2252
    },
    "labelScale": {
        "minScale": 576384,
        "maxScale": 2252
    },
    "style": {
        "fillColor": "#3388ff",
        "fillOpacity": 0.2,
        "color": "#3388ff",
        "weight": 2
    },
    "legend": {
        "enabled": true,
        "items": [
            { "label": "Temperate", "color": "#3388ff" },
            { "label": "Mediterranean", "color": "#ff8833" }
        ]
    }
}

Key fields:

  • label.enabled — whether labels are supported for this layer
  • label.visibleByDefault — initial label visibility state
  • scaleConfig — layer visibility range, in scale denominators (the X of 1:X, the value the scale control displays) — not zoom levels. minScale is the larger of the two numbers: it bounds the widest view, and a denominator grows as the map zooms out. A value <= 24 is rejected (it would be a zoom level entered by mistake)
  • labelScale — the same for labels (same unit), usually a narrower range
  • style — styling options for the layer
  • legend — legend configuration

6. Working with Maps ​

6.1 POI Management ​

Adding POIs ​

BREAKING (v3.0.0) — GeoLeaf.POI no longer exists. A POI is a point feature of an ordinary GeoJSON layer: read and write its data through GeoLeaf.Layers, and its symbol is driven by the taxonomy capability. Coordinates follow the GeoJSON / MapLibre GL JS convention: [longitude, latitude]. Full reference: API_REFERENCE.md.

javascript
// Add a feature. `layerId` is the id declared in config/core/layers.json.
GeoLeaf.Layers.addFeature("monuments", {
    type: "Feature",
    geometry: { type: "Point", coordinates: [2.2945, 48.8584] }, // [lng, lat]
    properties: {
        id: "eiffel-tower",
        title: "Eiffel Tower",
        description: "Iconic iron tower",
        categoryId: "monument", // resolved by the taxonomy capability
        address: "Champ de Mars, Paris",
        website: "https://www.toureiffel.paris",
    },
});

// Adding several at once: setData replaces the base dataset in a single call
// (preferable to N calls to addFeature — one source re-render only).
GeoLeaf.Layers.setData("monuments", features);

Reloading ​

javascript
// Replaces every feature of the layer
GeoLeaf.Layers.setData("monuments", newFeatures);

Reading ​

javascript
// One feature by its stable id
const feature = GeoLeaf.Layers.getFeatureById("monuments", "eiffel-tower");

// Every feature of the layer
const all = GeoLeaf.Layers.getFeatures("monuments");

// How many, and which layers exist
const n = GeoLeaf.Layers.getFeatureCount("monuments");
const ids = GeoLeaf.Layers.listLayerIds();

BREAKING (v3.0.0) — GeoLeaf.Filters.filterPoiList is removed (no internal consumer). Use the GeoLeaf.Filter (singular) capability's active panel state instead: GeoLeaf.Filter.getActiveFilter() / GeoLeaf.Filter.applyFilter(state).

6.2 Basemaps ​

Changing basemap ​

javascript
// Programmatically change basemap
GeoLeaf.Baselayers.setBaseLayer("satellite");

// Get current basemap key
const currentKey = GeoLeaf.Baselayers.getActiveKey();
console.log(currentKey); // "satellite"

// Get current basemap object
const currentLayer = GeoLeaf.Baselayers.getActiveLayer();

Custom basemaps ​

Add them to profile.json:

json
{
    "basemaps": [
        {
            "id": "my-vector-map",
            "name": "Ma Carte Vectorielle",
            "style": "https://tiles.myserver.com/style.json",
            "default": false
        }
    ]
}

3D basemaps with terrain ​

Enable 3D relief by adding a terrain object to the basemap definition (basemaps.json):

json
{
    "basemaps": {
        "satellite-3d": {
            "id": "satellite-3d",
            "label": "Satellite 3D",
            "type": "tile",
            "url": "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
            "attribution": "Tiles &copy; Esri",
            "defaultBasemap": false,
            "terrain": {
                "enabled": true,
                "demUrl": "https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png",
                "demEncoding": "terrarium",
                "demMaxZoom": 15,
                "exaggeration": 1.5,
                "default3D": true,
                "pitch": 60,
                "bearing": 0
            }
        }
    }
}

default3D: true — terrain is enabled automatically as soon as this basemap is selected. There is no UI toggle: switching to a basemap without terrain turns 3D relief off automatically.

map.maxPitch — the MapLibre default is 60°. GeoLeaf raises the ceiling to 80° by default. Configurable through profile.json > map.maxPitch.

A relief that comes when the map is tilted ​

A basemap with default3D opens tilted, and its relief is on for as long as it is active. To open flat and still offer the relief, tell the terrain to follow the tilt:

json
{
    "basemaps": {
        "satellite": {
            "id": "satellite",
            "label": "Satellite",
            "url": "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
            "attribution": "Tiles &copy; Esri",
            "terrain": {
                "enabled": true,
                "demUrl": "https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png",
                "activation": "pitch"
            }
        }
    }
}

What a flat map costs — nothing. No relief, and no elevation tile requested: a visitor who never tilts the map downloads none. Do not write default3D: true with pitch: 0 for this — the relief would then be on at all times, and its tiles downloaded by everyone.

When the relief comes — once the camera comes to rest tilted to 12° or more, and it goes once the camera comes to rest under 6°. The decision is taken when a movement ends, so the relief appears a moment after the gesture is released. The two thresholds are terrain.activateFromPitch and terrain.deactivateBelowPitch.

The camera is the visitor's — it is never tilted, flattened or turned by this mode, neither when the relief comes nor when another basemap is selected. pitch, bearing and default3D: true have no role here, and the profile schema refuses them next to activation: "pitch".

Relief shading over a basemap ​

The terrain gives the map its shape, not its shadows: on a vector basemap the slopes of a 3D relief are flat colours, and only the ridge line reads. Add a hillshade block with overlay: true to shade them. The basemap stays what it is — vector, tiles, WMS or WMTS — and the shading is drawn over it:

json
{
    "basemaps": {
        "plan-relief": {
            "id": "plan-relief",
            "label": "Plan with relief",
            "type": "maplibre",
            "style": "https://tiles.openfreemap.org/styles/liberty",
            "attribution": "© OpenFreeMap © OpenMapTiles © OpenStreetMap",
            "terrain": {
                "enabled": true,
                "demUrl": "https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png",
                "default3D": true,
                "pitch": 60
            },
            "hillshade": {
                "overlay": true,
                "beforeLayer": "waterway_tunnel",
                "demUrl": "https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png",
                "exaggeration": 0.45
            }
        }
    }
}

Where the shading goes — on an image basemap, right above the basemap and under your data. On a vector basemap, before the style layer beforeLayer names: pick the first layer that must stay above the shading, usually the first water or road layer (waterway_tunnel in the style above). Without beforeLayer the shading goes before the style's first label layer — the labels stay readable, but water and roads are shaded too.

Without overlay a hillshade block is read on a basemap of type: "hillshade" only, where the shading is the basemap itself.

Two elevation sources — the overlay never shares the terrain's source, even with the same demUrl: the tiles are requested twice, and the shading is one zoom level finer for it.

Shading only with the relief — add "onlyWithTerrain": true to the hillshade block and the overlay is shown only while a relief is on the map; hidden, it requests no elevation tile. With a terrain that follows the tilt, a flat map then stays exactly what it is without either block.

3D layers — extruded polygons (fill-extrusion) ​

Available since v2.2.0

Layers of type fill-extrusion render GeoJSON polygons as 3D volumes (buildings, density, statistical data). Rendering is handled by the MapLibre GL JS WebGL engine.

Typical use cases:

  • Building models with real or relative height
  • Visualisation of area statistics (density, income, votes and so on)
  • Regulatory or risk zones with a 3D visual indicator

{layer}_config.json configuration:

json
{
    "id": "batiments",
    "label": "Bâtiments 3D",
    "geometry": "fill-extrusion",
    "interactiveShape": true,
    "data": { "dataUrl": "https://..." }
}

Associated style file:

json
{
    "id": "defaut",
    "label": "Vue 3D bâtiments",
    "style": {
        "fillExtrusionColor": "#a8dadc",
        "fillExtrusionOpacity": 0.7,
        "fillExtrusionHeight": "hauteur",
        "fillExtrusionBase": 0
    }
}

fillExtrusionHeight accepts either a fixed numeric value (metres) or a feature field name — GeoLeaf then generates the MapLibre expression ["get", "hauteur"] automatically.

GPU performance — fill-extrusion rendering is WebGL-accelerated. Performance stays optimal up to roughly 10,000 extruded polygons at zoom levels ≥ 14.

6.3 GeoJSON Layers ​

GeoJSON layers are configured through the profile (geojsonLayers in geoleaf.config.json). Layer visibility is handled by the LayerManager panel (UI).

javascript
// Initialize the layer manager control on the map (kernel — nothing to load first)
GeoLeaf.LayerManager.init({ map });

// Refresh the layer manager display
GeoLeaf.LayerManager.refresh();

6.4 Themes ​

UI theme (light/dark) ​

For the interface theme (light/dark), use Core.setTheme:

javascript
GeoLeaf.Core.setTheme("dark");
const current = GeoLeaf.Core.getTheme(); // "dark"

6.5 Labels ​

Labels are an in-core capability (modules.labels), configured through the layer style files (the label field). They ship in the bundle — nothing to load. Label visibility is handled automatically by the LayerManager panel (a per-layer toggle button), and the capability can be switched off in the profile (modules.labels.enabled: false).

Label configuration happens in the layer's style file (see section 5.5).


7. UI Components ​

7.1 Layer Manager ​

Purpose: control layer visibility (POI categories, GeoJSON layers)

Configuration in profile.json:

json
{
    "ui": {
        "layerManager": {
            "enabled": true,
            "position": "topright",
            "collapsed": false
        }
    }
}

Programmatic control:

javascript
GeoLeaf.LayerManager.init({ map });
GeoLeaf.LayerManager.refresh();

7.2 Filter Panel ​

Purpose: filter POIs by multiple criteria

Configuration:

json
{
    "ui": {
        "filterPanel": {
            "enabled": true,
            "position": "topleft",
            "collapsed": true
        }
    }
}

Programmatic filtering:

javascript
// Drive the map's own filter panel. A state is `{ fields: [...] }`, one entry per
// constrained field (`id` is the field's id in `modules.filter.fields`), and it replaces
// the panel's selection: a panel field left out is cleared. Start from the current state.
const active = GeoLeaf.Filter.getActiveFilter();
const others = active.fields.filter((f) => f.id !== "searchText");
GeoLeaf.Filter.applyFilter({
    fields: [...others, { id: "searchText", kind: "text", text: "easy" }],
});

BREAKING (v3.1.0) — the whole GeoLeaf.Filters namespace (plural) is removed, along with its last method filterRouteList, which had no caller. To filter an array of your own data, use Array.prototype.filter; to drive the map, use the GeoLeaf.Filter capability (singular) as shown above.

Earlier, in v3.0.0: GeoLeaf.Filters.filterPoiList was removed for the same reason.

Purpose: search POIs by name, category or address

Configuration:

json
{
    "ui": {
        "searchBar": {
            "enabled": true,
            "position": "topleft",
            "placeholder": "Rechercher...",
            "minChars": 2
        }
    }
}

BREAKING (v3.0.0) — GeoLeaf.Search no longer exists. The full-text engine (flexsearch) was dormant: no profile enabled it and its index was never built. It has been removed, together with its npm dependency. The interface's actual text search — the search field of the Filter panel — is provided by the in-core filter capability (kind: "text"), and it is now insensitive to accents and to word order. Nothing to change for the UI; an integrator calling GeoLeaf.Search.query() from a script must implement their own search or index server-side.

Programmatic use:

javascript
// Read the filter panel state (including the text field), serialisable
const state = GeoLeaf.Filter.getActiveFilter();

// Restore a state: it is written back onto the panel's controls, then applied to the map
GeoLeaf.Filter.applyFilter(state);

// Is a filter active?
if (GeoLeaf.Filter.hasActiveFilters()) {
    GeoLeaf.Filter.reset();
}

7.4 Cache Controls ​

Purpose: offline cache management (IndexedDB storage) through the Storage plugin.

Note: the offline cache features require the @geoleaf-plugins/offline-ui plugin (MIT, npmjs.org) — they are not part of the core bundle.

The cache API comes from the Storage plugin. See the plugin documentation for integration details.

javascript
// Verify the storage plugin is available
if (GeoLeaf.plugins.isLoaded("storage")) {
    // Storage plugin API is available via GeoLeaf.Storage
    console.log("Storage plugin ready");
}

7.5 Notifications ​

javascript
GeoLeaf.Notifications.success("POI added successfully");
GeoLeaf.Notifications.error("Failed to load the data");
GeoLeaf.Notifications.warning("Unstable connection");
GeoLeaf.Notifications.info("Loading...");
GeoLeaf.Notifications.success("Saved", { duration: 2000 });

GeoLeaf.Notifications.notify("Custom message", "info", 5000);

// Clear all visible notifications
GeoLeaf.Notifications.clearAll();

// Get current notification system status
const status = GeoLeaf.Notifications.getStatus();

7.6 Address Search (Geocoding) ​

WARNING

Address search (geocoding) has moved out of @geoleaf/core: it is now provided by the MIT plugin @geoleaf-plugins/geocoding (public on npmjs.org). The configuration moves from the root key geocodingConfig to modules.geocoding.* (declared in config/plugins/geocoding.json through Files.modules.geocoding) — a breaking migration, with no shim. The GeoLeaf.Geocoding API, the geoleaf:geocoding:result event and the search control all come from the plugin. See the plugin README (packages/plugins/geocoding/README.md).


7.7 Responsive & Mobile Interface ​

GeoLeaf ships a fully responsive interface that adapts to every screen size — from smartphone to large desktop — with no extra configuration.

Breakpoints ​

RangeDevicesBehaviour
≤ 768 pxSmartphone, 6" tabletMobile mode — pill toolbar (left side), sheet overlays
769 – 1 024 px10" PC / small laptopDesktop layout, 360 px side panel
≥ 1 025 px13"+ PCDesktop layout, 420 px side panel

The threshold values are exposed as CSS variables in geoleaf-theme.css (:root):

css
--gl-bp-sm: 480px; /* smartphone    */
--gl-bp-md: 640px; /* phablet       */
--gl-bp-lg: 768px; /* tablet 6" / mobile threshold */
--gl-bp-xl: 1024px; /* PC 10"        */

Mobile pill toolbar (≤ 768 px) ​

On narrow viewports, the standard desktop controls are replaced by a pill-shaped icon bar anchored to the left side of the map.

IconAction
FullscreenToggle fullscreen mode
LegendShow or hide the legend
Zoom + / −Map zoom
My locationToggle geolocation
SearchOpen the search sheet
ProximityEnable proximity search mode
FiltersOpen the filter sheet. Reset indicator when filters are active
ThemesOpen the theme selection sheet
LayersOpen the layer manager sheet
TableOpen the data table sheet

Sheet overlay ​

Every panel uses a bottom sheet overlay (role="dialog", aria-modal="true", aria-labelledby). Supported:

  • Tap on the dark backdrop to close
  • Escape key to close
  • Full focus trap: Tab/Shift-Tab stay inside the sheet
  • Focus returned to the triggering button on close

Viewport meta tag ​

Make sure the HTML page includes:

html
<meta name="viewport" content="width=device-width, initial-scale=1.0" />

Do not use user-scalable=no — it blocks pinch-zoom and harms accessibility.


8. Advanced Topics ​

8.1 Custom Profiles ​

Create a profile by copying the structure of an existing one:

profiles/
  my-custom-profile/
    geoleaf.config.json    # Optional, falls back to the root config
    profile.json           # Required
    taxonomy.json          # Required
    themes.json            # Required
    layers/                # Optional, for GeoJSON layers
    data/                  # POI data files

8.2 OGC API Features ​

GeoLeaf natively supports loading layers from an OGC API Features endpoint (the REST/JSON successor to classic WFS).

Minimal configuration:

json
{
    "id": "roads",
    "label": "Routes",
    "geometry": "polyline",
    "data": {
        "ogcApi": {
            "url": "https://api.example.com/collections/roads/items",
            "maxFeatures": 5000
        }
    }
}

With viewport auto-refresh:

json
{
    "data": {
        "ogcApi": {
            "url": "https://data.geopf.fr/ogcapi/collections/BDTOPO_V3:batiment/items",
            "maxFeatures": 2000,
            "limit": 500,
            "autoRefresh": true,
            "autoRefreshDebounce": 400
        }
    }
}

With autoRefresh: true, GeoLeaf reloads the data at the end of every map pan or zoom, passing the current viewport bbox. Only the visible data is displayed, without preloading the whole dataset.

With authentication:

json
{
    "data": {
        "ogcApi": {
            "url": "https://secure.api.com/collections/parcels/items",
            "headers": { "Authorization": "Bearer YOUR_TOKEN" },
            "maxFeatures": 10000
        }
    }
}

Through GeoLeaf.Utils.wktToGeoJSON():

If an OGC API Features endpoint returns geometries in WKT format (some non-conforming implementations do), GeoLeaf converts them automatically. The function is also available publicly:

javascript
const geom = GeoLeaf.Utils.wktToGeoJSON("POINT(2.3522 48.8566)");
// → { type: "Point", coordinates: [2.3522, 48.8566] }

const polygon = GeoLeaf.Utils.wktToGeoJSON("POLYGON((0 0, 4 0, 4 4, 0 4, 0 0))");
// → { type: "Polygon", coordinates: [[[0,0],[4,0],[4,4],[0,4],[0,0]]] }

8.3 Offline Mode ​

The offline engine is in @geoleaf/core. It loads when both modules.pwa.enabled and modules.offline.enabled are true, and holds the downloaded data and the queue of edits made offline — see the offline write cycle. The download window and the cache button come from the @geoleaf-plugins/offline-ui plugin.

javascript
// Verify the offline-ui plugin is loaded
const isLoaded = GeoLeaf.plugins.isLoaded("offline-ui");
console.log("offline-ui loaded:", isLoaded);

8.3 Custom Themes (CSS) ​

Override the default styles by loading a custom CSS file after geoleaf-main.min.css:

html
<link
    rel="stylesheet"
    href="https://cdn.jsdelivr.net/npm/@geoleaf/core@3/dist/geoleaf-main.min.css"
/>
<link rel="stylesheet" href="/my-custom-theme.css" />
css
/* Change primary color */
.geoleaf-button-primary {
    background-color: #e74c3c;
    border-color: #c0392b;
}

/* Change layer manager background */
.geoleaf-layer-manager {
    background-color: #2c3e50;
    color: #ecf0f1;
}

8.4 Events API ​

GeoLeaf uses the GeoLeaf.Events event bus (CustomEvent dispatched on document).

javascript
// POI events
GeoLeaf.Events.on("geoleaf:poi:click", (e) => {
    console.log("POI clicked:", e.detail.poiId);
});

// Layer events
GeoLeaf.Events.on("geoleaf:layer:toggle", (e) => {
    console.log("Layer:", e.detail.layerId, "visible:", e.detail.visible);
});

// Theme events
GeoLeaf.Events.on("geoleaf:theme:applied", (e) => {
    console.log("Theme applied:", e.detail.themeName);
});

// Filter events
GeoLeaf.Events.on("geoleaf:filter:apply", (e) => {
    console.log("Active filters:", e.detail.activeCount);
});

// One-time listener
GeoLeaf.Events.once("geoleaf:app:ready", () => {
    console.log("App is ready!");
});

// Remove listener
const handler = (e) => console.log(e.detail);
GeoLeaf.Events.on("geoleaf:poi:click", handler);
GeoLeaf.Events.off("geoleaf:poi:click", handler);

See EVENTS_API.md for the full list of events.

8.5 Data Import/Export ​

The table can export the current selection:

INFO

The data table has been moved out of the core into the MIT plugin @geoleaf-plugins/table. It loads through its own script (after @geoleaf/core); GeoLeaf.Table becomes available once the plugin is loaded, without _loadModule("table").

javascript
// Export the current table selection (CSV/GeoJSON)
GeoLeaf.Table.exportSelection();

To import points, write to the layer that carries them — since v3.0.0 a POI is a feature of an ordinary GeoJSON point layer:

javascript
// Import points from a JSON file into an existing point layer
const fileInput = document.querySelector("#file-input");
fileInput.addEventListener("change", async (event) => {
    const file = event.target.files[0];
    const text = await file.text();
    const features = JSON.parse(text); // FeatureCollection or array of Feature
    GeoLeaf.Layers.setData("mes-points", features);
});

8.6 Importing geographic files (plugin) ​

To import GPX, KML/KMZ, CSV or TopoJSON files straight into the map, use the @geoleaf-plugins/file-import plugin (MIT):

javascript
import "@geoleaf/core";
import "@geoleaf-plugins/file-import";

// Convert a file to GeoJSON
const geojson = await GeoLeaf.FileImport.convert(file); // format detected automatically

// Import and display it directly as a layer
await GeoLeaf.FileImport.importAsLayer(file, { layerId: "user-import" });

For FlatGeobuf data (streaming with spatial filtering):

javascript
import "@geoleaf-plugins/flatgeobuf";

// Bounding-box loading (HTTP Range — only the area is downloaded)
const geojson = await GeoLeaf.FlatGeobuf.loadBbox("https://example.com/data.fgb", {
    minX: 2.2,
    minY: 48.8,
    maxX: 2.5,
    maxY: 49.0,
});

See PLUGIN_CONFIGURATION_GUIDE.md → the "Plugin File Import" and "Plugin FlatGeobuf" sections for the full option list.


9. Troubleshooting ​

Common problems ​

9.1 The map does not appear ​

Symptoms: blank space where the map should be

Solutions:

  1. Check that the #map div has an explicit height in CSS:

    css
    #map {
        height: 600px;
    }
  2. Make sure both the MapLibre GL JS and the GeoLeaf stylesheets are loaded

  3. Check the browser console for errors (F12)

  4. Make sure the container id matches mapId in the configuration

  5. Check that WebGL is supported: console.log(!!window.WebGLRenderingContext)

9.2 POIs not displayed ​

Symptoms: the map is visible but there are no markers

Solutions:

  1. Check that the coordinates use the [longitude, latitude] order (MapLibre GL JS convention)
  2. Check that the POI category matches a taxonomy category
  3. Make sure the layer is enabled in the layer manager
  4. Check that the current scale falls inside the layer's scaleConfig range — and that its bounds really are scale denominators (1:X), not zoom levels: this is the most frequent confusion, and it makes the layer invisible at every zoom

9.3 Profile not loaded ​

Symptoms: a "Failed to load profile" error in the console

Solutions:

  1. Check profilesBasePath in geoleaf.config.json
  2. Make sure profile.json exists in the profile directory
  3. Check that the paths inside profile.json are correct
  4. Make sure the JSON files are valid (JSONLint.com)
  5. Check for 404 errors in the DevTools Network tab

9.4 Labels not displayed ​

Solutions:

  1. Check label.enabled: true in the layer style file
  2. Check that the zoom falls inside the labelScale range
  3. Enable the label button in the layer manager
  4. Make sure label.visibleByDefault is present in the style file

9.5 Cache not working ​

Solutions:

  1. Check that the @geoleaf-plugins/offline-ui plugin is loaded
  2. Check that the browser supports IndexedDB
  3. Check that the browser storage quota is not exceeded
  4. Make sure HTTPS is used
  5. Private/Incognito mode can disable the cache in some browsers

Debug mode ​

debug is a key of the configuration GeoLeaf.boot() loads (§5.1) — passed to GeoLeaf.init(), it is ignored without a word:

javascript
GeoLeaf.boot({
    config: {
        debug: {
            enabled: true,
            modules: ["*"], // Or specific: ['poi', 'config', 'storage']
        },
        data: { activeProfile: "tourism", profilesBasePath: "/profiles/" },
    },
});

10. Next Steps ​

Documentation ​

Examples ​

  • Demo application — run npm run build then npm run build:deploy to reach the demo
  • Tourism example — see profiles/tourism/ for the tourism profile showcase

Community ​

  • GitHub Repository — source code, issues, discussions
  • Contact: Mattieu Pottier — contact@geoleaf.dev

Need help?
See the Cookbook · Report Issues · Read the Contributing Guide

Released under the MIT License.