Skip to content

GeoLeaf.Core — Core module documentation ​

Applies to: @geoleaf/core v3.x (MapLibre GL JS ^6.0.0) Source (monorepo): packages/core/src/modules/built-in/map/


The GeoLeaf.Core module is the kernel of the GeoLeaf library.

It handles:

  • initialisation of the MapLibre GL JS map;
  • keeping a single map instance behind an IMapAdapter (engine abstraction);
  • managing and synchronising the UI theme (light / dark) with the other modules.

The other modules (Baselayers, UI, POI, GeoJSON, Route, Legend, Config, and so on) all rely on the map created by GeoLeaf.Core.

Architecture note: GeoLeaf.Core never exposes maplibregl.Map directly. It returns an IMapAdapter (implemented by MaplibreAdapter) that abstracts every map operation. This keeps the library independent of the rendering engine.


1. Functional role of the Core ​

GeoLeaf.Core has three main responsibilities:

  1. Create and initialise a MapLibre GL JS map in a DOM container through MaplibreAdapter.
  2. Expose the map instance to the other modules through GeoLeaf.Core.getMap() (returns an IMapAdapter).
  3. Centralise the current UI theme ("light" / "dark") and offer a simple API to read and change it.

Important: GeoLeaf.Core does not handle:

  • base layers (basemaps);
  • POIs / GeoJSON / routes;
  • advanced UI controls.

Those responsibilities belong to the other GeoLeaf modules.


2. Public API of GeoLeaf.Core ​

The Core object exports the following methods:

MethodRole
Core.init(options)Initialises the MapLibre GL JS map
Core.getMap()Returns the active IMapAdapter instance
Core.getAdapter()Alias of getMap() (internal use by modules)
Core.setTheme(theme)Changes the UI theme after initialisation
Core.getTheme()Returns the current UI theme

2.1 GeoLeaf.Core.init(options) ​

Main initialisation function. It resolves the DOM container, creates a MaplibreAdapter and applies the UI theme (first map only). The legend is not mounted here: it mounts on geoleaf:app:ready. The engine is read from globalThis.maplibregl — an npm build must put it there (Object.assign(globalThis, { maplibregl })), or the call returns null.

js
const adapter = GeoLeaf.Core.init(options);

Parameters:

  • options.mapId — required — id of the DOM element hosting the map.
  • options.center — initial centre, as a [lat, lng] array.
  • options.zoom — initial zoom level.
  • options.theme — UI theme ("light" by default).
  • options.mapOptions — additional engine options (see §3.5).

Returns:

  • The IMapAdapter instance when initialisation succeeds.
  • null on error (container not found, engine exception, and so on).

In automatic boot mode (GeoLeaf.boot()), Core.init() is called internally by app/init.ts through GeoLeaf.init(). Calling it manually is not required.

Direct example (standalone mode) ​

js
// Direct usage — bypasses the boot system
const adapter = GeoLeaf.Core.init({
    mapId: "geoleaf-map",
    center: [45.76, 4.84], // Lyon, FR — [lat, lng]
    zoom: 13,
    theme: "light",
});

if (!adapter) {
    console.error("[App] Map initialization failed — check mapId and DOM.");
}

2.2 GeoLeaf.Core.getMap() ​

Returns the IMapAdapter instance already initialised, or null when no map exists.

js
const adapter = GeoLeaf.Core.getMap();

if (adapter) {
    // Navigate to a new position
    adapter.setView({ lat: 48.85, lng: 2.35 }, 14); // Paris
    console.log("Current zoom:", adapter.getZoom());
}

Recommended usage:

  • inside the other GeoLeaf modules (POI, GeoJSON, Route, Legend, and so on);
  • in external code that needs to drive the map without reinitialising it.

2.3 GeoLeaf.Core.getAdapter() ​

Strict alias of getMap(). Used internally by the POI, Route and GeoJSON modules to state explicitly that they consume the adapter.

js
const adapter = GeoLeaf.Core.getAdapter();

2.4 GeoLeaf.Core.setTheme(theme) ​

Changes the UI theme after initialisation.

js
GeoLeaf.Core.setTheme("dark");

Parameter:

  • theme: "light" or "dark".

Behaviour:

  • When GeoLeaf.UI is present, the call is delegated to the canonical theme engine: CSS classes gl-theme-light / gl-theme-dark on document.body and on the #geoleaf-map container (full-screen support), persistence in localStorage (geoleaf_theme), synchronisation of the theme button aria-pressed state, and emission of the geoleaf:ui-theme-changed event.
  • Otherwise (UI not loaded), the gl-theme-* classes are applied to document.body.

On an invalid value:

  • GeoLeaf.Core logs a warning: [GeoLeaf.Core] setTheme() ignored an invalid theme: {value}.
  • The current theme is left unchanged.

GeoLeaf.Core.setTheme(), GeoLeaf.setTheme() and GeoLeaf.UI.applyTheme() therefore reach the same engine: they are interchangeable.


2.5 GeoLeaf.Core.getTheme() ​

Returns the current UI theme.

js
const currentTheme = GeoLeaf.Core.getTheme(); // "light" | "dark"

It reads through the canonical theme engine when GeoLeaf.UI is present, so the value also reflects changes made by the theme button, by GeoLeaf.setTheme() or by the boot sequence. Without GeoLeaf.UI, the value is derived from the class present on document.body (default "light").

Typical usage: synchronising an external component with the visual state of GeoLeaf.


3. Core options in detail ​

3.1 mapId (required) ​

  • Type: string
  • Required: yes
  • Description: id of the DOM element in which the map must be created.

HTML example:

html
<div id="geoleaf-map"></div>

Validation:

  • mapId must be a non-empty string.
  • A DOM element carrying that id must exist when the call is made.

If either fails, Core.init() does not throw — it logs an error and returns null:

  • mapId missing: [GeoLeaf.Core] init() requires options.mapId.
  • DOM element not found: [GeoLeaf.Core] init failed for "<mapId>": No DOM element found for mapId='<mapId>'. — this failure is also passed to GeoLeaf.Core.onError() when one is registered (§7.2).

  • Type: [number, number] — [latitude, longitude] array
  • Convention: latitude first (GeoLeaf order), longitude second.
  • Description: initial centre of the map.

Example:

js
center: [45.76, 4.84]; // Lyon, FR

In practice, during automatic boot the centre is computed from the map.bounds of the active profile. Specifying it manually is not required in that mode.


  • Type: number
  • Default value: CONSTANTS.DEFAULT_ZOOM (defined in modules/utils/constants/)
  • Description: initial zoom level of the map.

Recommended ranges:

  • practical range: 2 to 18 for most base maps;
  • some base maps go up to 19 or 20 depending on the provider.

3.4 theme (optional) ​

  • Type: "light" | "dark"
  • Default value: "light"
  • Description: current UI theme. It applies to the interface (header, buttons, panels, legend) and never to the tiles.
js
theme: "dark";

Behaviour:

  • When theme is "light" or "dark": the theme is stored and applied.
  • When theme is absent: "light" is used.
  • When theme holds an unknown value: a warning is logged and the current theme is left unchanged.

Reminder: the tile choice (Street / Topo / Satellite) is handled by GeoLeaf.Baselayers and does not depend on the UI theme.


3.5 mapOptions (optional) ​

Additional options forwarded to the MapLibre GL JS engine through the adapter.

KeyTypeDescription
minZoomnumberMinimum allowed zoom
maxZoomnumberMaximum allowed zoom
maxBoundsGeoLeafBoundsRestricts panning to a given area

GeoLeafBounds is an object { north, south, east, west } (all in decimal degrees).

Example with a position constraint:

js
GeoLeaf.Core.init({
    mapId: "geoleaf-map",
    center: [45.76, 4.84],
    zoom: 12,
    mapOptions: {
        minZoom: 8,
        maxZoom: 18,
        maxBounds: { north: 46.5, south: 45.0, east: 5.5, west: 4.0 },
    },
});

4. The IMapAdapter interface ​

GeoLeaf.Core.getMap() returns an IMapAdapter object, not a direct maplibregl.Map instance. That abstraction isolates the modules from the rendering engine.

4.1 Navigation and view ​

js
const adapter = GeoLeaf.Core.getMap();

// Set view (center + zoom)
adapter.setView({ lat: 48.85, lng: 2.35 }, 14);

// Animate to position
adapter.flyTo({ lat: 45.76, lng: 4.84 }, 13);

// Pan without zoom change
adapter.panTo({ lat: 43.3, lng: 5.37 });

// Fit bounds
adapter.fitBounds(
    { north: 46.5, south: 45.0, east: 5.5, west: 4.0 },
    { padding: { x: 50, y: 50 }, animate: false }
);

// Read state
const center = adapter.getCenter(); // { lat, lng }
const zoom = adapter.getZoom(); // number
const bounds = adapter.getBounds(); // { north, south, east, west }

4.2 Events ​

js
const adapter = GeoLeaf.Core.getMap();

const onMoveEnd = (e) => {
    console.log("Map moved, new center:", adapter.getCenter());
};

adapter.on("moveend", onMoveEnd);
adapter.once("load", () => console.log("Map loaded"));
adapter.off("moveend", onMoveEnd);

Events available through IMapAdapter: "click", "dblclick", "contextmenu", "moveend", "movestart", "zoomend", "zoomstart", "load", "unload", "resize".

4.3 Coordinates — ordering convention ​

GeoLeaf always uses { lat, lng } (latitude first). MapLibre GL JS uses [lng, lat] (GeoJSON order, longitude first). The conversion is handled internally by MaplibreAdapter — consumers never have to deal with it.

js
// GeoLeaf convention throughout the public API
const center: GeoLeafLatLng = { lat: 45.764, lng: 4.835 };

5. Boot System v2 ​

5.1 Overview ​

Since v2.0.0, GeoLeaf initialisation is fully orchestrated by the boot system located in packages/core/src/app/. That system handles sequential loading through a topologically sorted ModuleRegistry.

Boot system files:

FileRole
src/app/app-namespace.tsLogging, path detection, plugin checks, notification helpers
src/app/boot.tsExposes GeoLeaf.boot(), loads the config, registers the modules
src/app/init.tsOrchestrates map, UI, POI, Route, GeoJSON and legend creation
src/app/module-registry.tsModuleRegistry — module lifecycle management with topological sorting
src/app/init-features.tsFine-grained initialisation of the secondary modules

5.2 Full startup sequence ​

1. Bundle loading
   <script type="module" src="geoleaf.esm.js">  → ESM bundle (CDN or bundler)
   <script type="module" src="geoleaf-offline-ui.plugin.js"> → Optional plugin (before GeoLeaf.boot())
   <script type="module" src="geoleaf-offline-ui.plugin.js">  → Optional plugin (before GeoLeaf.boot())

2. GeoLeaf.boot() call
   └─ Checks document.readyState
   └─ Calls _app.startApp()

3. _app.startApp()
   └─ Registers the modules in ModuleRegistry (B1 → B8 core)
   └─ Loads geoleaf.config.json through GeoLeaf.loadConfig()
   └─ Registers the optional modules according to the profile (Route, Labels, Legend, Table, Search)
   └─ Loads the resources of the active profile
   └─ Runs ModuleRegistry.init() (topological order) — see 4.

4. ModuleRegistry.init(cfg) — CoreMapModule → SharedModule → UIModule (among others)
   └─ CoreMapModule: reads the profile extent (map.bounds, or map.center + map.zoom), calls
      GeoLeaf.init() → GeoLeaf.Core.init() → MaplibreAdapter
   └─ SharedModule: i18n, plugin check, app-wide lifecycles (pwa, offline)
   └─ UIModule: starts preloading the secondary modules (ESM code splitting),
      initialises UI, Storage, POI, Route, GeoJSON, Legend, LayerManager
   └─ Reveals the application after the geoleaf:theme:applied event
   └─ Emits geoleaf:map:ready then geoleaf:app:ready

5.3 GeoLeaf.boot() ​

The single recommended entry point to start GeoLeaf.

js
// Minimal usage — auto DOMContentLoaded guard
GeoLeaf.boot();

// With performance metrics callback
GeoLeaf.boot({
    onPerformanceMetrics: (metrics) => {
        console.log("Time to map ready:", metrics.timeToMapReadyMs, "ms");
        console.log("Time to app ready:", metrics.timeToAppReadyMs, "ms");
        console.log("Total startup:", metrics.startupTotalMs, "ms");
    },
});

GeoLeaf.boot() handles DOMContentLoaded on its own: calling it before or after the DOM is ready makes no difference.

Full signature:

ts
GeoLeaf.boot(options?: {
    /** A configuration object handed over in memory — no request is issued for it. Wins over `configUrl`. */
    config?: Record<string, unknown>;
    /** An explicit URL to fetch the configuration from, used when `config` is absent. */
    configUrl?: string;
    /** Runs after the configuration loads, before the map. Throwing aborts the boot. */
    beforeBoot?: (context: { config: Readonly<Record<string, unknown>> }) => Promise<void> | void;
    onPerformanceMetrics?: (metrics: {
        timeToMapReadyMs: number | null;
        timeToAppReadyMs: number | null;
        startupTotalMs: number | null;
        capturedAt: string;
    }) => void;
    /** Milliseconds without a reveal before the boot is reported as stalled. Default 45 000; `0` disables it. */
    watchdogMs?: number;
}): void

⚠️ This block listed onPerformanceMetrics alone until 11/09/2026, while config, configUrl and beforeBoot already existed.

A boot that cannot complete is never silent: geoleaf:boot:failed is dispatched and, when the page declares the #gl-loader veil, the veil turns into a failure screen offering « Reload » and a diagnostic to copy or download. Subscribe before calling boot(), and call preventDefault() on the event to draw your own interface instead — see EVENTS_API.md.

5.4 Lifecycle events ​

The boot system emits the following events on document:

EventEmitted whendetail payload
geoleaf:profile:failedResources the profile declares could not be obtained — cancelable{ profileId, fatal, failures }
geoleaf:boot:failedThe boot cannot complete — cancelable{ reason, phase, message, module?, provisional, timestamp }
geoleaf:boot:abortedThe host ended the boot (beforeBoot threw, or Core.destroy() removed the map mid-boot) — the veil is hidden{ reason } — "destroyed" in the second case
geoleaf:theme:applyingA theme starts loading (layers still being added)—
geoleaf:theme:appliedA theme has finished loading (all layers visible){ themeName, layerCount }
geoleaf:profile:loadedThe JSON profile has been loaded and parsed{ profileId, data }
geoleaf:map:readyMap visible, loader removed, fitBounds done—
geoleaf:app:readyApplication fully initialised{ version, timestamp }
geoleaf:map:moveEnd of a map movement{ center, zoom }
geoleaf:map:zoomEnd of a zoom change{ zoom }
js
// Listen for app ready
document.addEventListener("geoleaf:app:ready", (event) => {
    console.log("GeoLeaf v" + event.detail.version + " is ready.");
    const adapter = GeoLeaf.Core.getMap();
    // Use the adapter here...
});

// Listen for theme loaded
document.addEventListener("geoleaf:theme:applied", (event) => {
    console.log(event.detail.themeName + " loaded — layers:", event.detail.layerCount);
});

5.5 ModuleRegistry and initialisation sequence ​

The ModuleRegistry orchestrates module initialisation by resolving dependencies through a topological sort (Kahn's algorithm).

Core modules registered by default:

Module IDClassRole
core-mapCoreMapModuleCreation of the MapLibre GL JS map
configConfigModuleLoading and management of JSON profiles
sharedSharedModuleState shared between modules
geojsonGeoJSONModuleGeoJSON layers and styles
uiUIModuleInterface, controls, filters
theme-engineThemeEngineModuleApplies the default theme of the profile

INFO

There are six kernel modules, not eight. SecurityModule and APIModule are not registered modules: their subsystems are facades installed at import time, so there is nothing to sequence. POIModule no longer exists either — a POI is a generic GeoJSON point layer. The six identifiers above are declared in app/boot-modules/*.module.ts.

Optional modules (registered according to the profile):

Profile conditionRegistered module
route.enabled !== falseRouteModule
labels.enabled !== falseLabelsModule
modules.legend.enabled !== falseLegendModule

INFO

The full-text search engine (flexsearch) has been removed from the core: it was dormant, with no consumer. SearchModule is no longer registered and the ui.showSearch flag no longer exists; UI text search is provided by the in-core filter capability (text field of the Filter panel, now insensitive to accents and to word order).

INFO

The data table has been moved out of the core into the MIT plugin @geoleaf-plugins/table. TableModule is no longer registered by the core, and the ui.showTable flag has moved to modules.table.showButton. See the plugin README for the configuration (modules.table.*) and the migration.

INFO

The legend is an in-core capability unified under modules.legend. LegendModule is still registered, but through the CapabilityRegistry (gate modules.legend.enabled, opt-out); the ui.showLegend flag and the legendConfig block have moved to modules.legend.enabled / modules.legend.{title,position,collapsedByDefault} (file config/plugins/legend.json). See the Legend README.

Registering a third-party module:

js
// Third-party module self-registration (public API)
GeoLeaf.registry.register(new MyCustomModule());

5.6 Guard system (checkPlugins) ​

At startup, helpers.ts checks that the loaded plugins are consistent with the profile configuration:

  • Logs a warning when ui.showAddPoi=true without the AddPOI plugin loaded.
  • Logs a warning when storage is defined without the Storage plugin loaded.
  • Logs a warning when SyncHandler is loaded without the Storage plugin.

These warnings are advisory — the application still starts, in degraded mode.


6. Integration with JSON configuration ​

6.1 Minimal JSON profile (geoleaf.config.json) ​

json
{
    "map": {
        "target": "geoleaf-map",
        "bounds": [
            [45.5, 4.5],
            [46.0, 5.2]
        ],
        "initialMaxZoom": 13,
        "padding": [50, 50],
        "positionFixed": false
    },
    "ui": {
        "theme": "light",
        "showLegend": true,
        "showLayerManager": true,
        "showFilterPanel": true,
        "showCoordinates": true
    }
}

Important: the profile must position the map — map.bounds, or map.center with map.zoom. With neither, the application refuses to start and logs an explicit error. There is no default world map.

6.2 Mapping between JSON fields and Core options ​

JSON fieldEquivalent Core optionRequired
map.target / map.idoptions.mapId (default geoleaf-map)no
map.boundscentre computation + fitBoundsor center + zoom
map.initialMaxZoomceiling of the initial framingno
map.minZoomoptions.mapOptions.minZoomno
map.maxZoomoptions.mapOptions.maxZoomno
map.boundsMargin (default 0.3)maxBounds paddingno
map.positionFixedoptions.mapOptions.maxBounds (computed)no
ui.themeoptions.themeno

6.3 Loading the active profile ​

In multi-profile mode, GeoLeaf reads sessionStorage.getItem("gl-selected-profile") at startup to load the right profile. Only alphanumeric identifiers (/^[a-zA-Z0-9_-]{1,50}$/) are accepted.

js
// Select a profile before boot
sessionStorage.setItem("gl-selected-profile", "mon-profil");
GeoLeaf.boot();

7. Error handling and fallback behaviour ​

GeoLeaf.Core favours explicit behaviour (clear logs) over silent failure.

7.1 Summary of the main cases ​

SituationLog emittedReturn value
mapId missing[GeoLeaf.Core] init() requires options.mapIdnull
DOM element not found for mapId[GeoLeaf.Core] init failed for "<mapId>": No DOM element found for mapId='…'.null
No engine on globalThis.maplibregl[GeoLeaf.Core] init failed for "<mapId>": … (the ReferenceError's message)null
map.bounds absent from the profile[GeoLeaf] Active profile does not define valid map.bounds—
Unknown theme value[GeoLeaf.Core] setTheme() ignored an invalid theme: {value}theme unchanged
Core.init() already called for mapId[GeoLeaf.Core] Map "<mapId>" already initialized. Returning existing instance.existing instance
MapLibre engine exception[GeoLeaf.Core] init failed for "<mapId>": {message}null

7.2 Optional error callback ​

js
// Register an error callback before boot
window.GeoLeaf = window.GeoLeaf || {};
window.GeoLeaf.Core = window.GeoLeaf.Core || {};
window.GeoLeaf.Core.onError = function (err) {
    // Custom error handling (analytics, UI error banner, etc.)
    console.error("[App] Core error:", err.message);
};

GeoLeaf.boot();

7.3 Post-init best practice ​

js
document.addEventListener("geoleaf:app:ready", () => {
    const adapter = GeoLeaf.Core.getMap();

    if (!adapter) {
        console.error("[App] Map not initialized — check configuration and DOM.");
        return;
    }

    // Adapter is ready — safe to use
    console.log("Map ready, zoom:", adapter.getZoom());
});

8. Quick summary of Core.init() options ​

OptionTypeRequiredDefault valueRole
mapIdstringyes—ID of the map DOM container
center[number, number]recommendedcomputed from boundsInitial centre [lat, lng]
zoomnumberrecommendedCONSTANTS.DEFAULT_ZOOMInitial zoom
theme"light" | "dark"no"light"UI theme (interface only)
mapOptions.minZoomnumberno—Minimum allowed zoom
mapOptions.maxZoomnumberno—Maximum allowed zoom
mapOptions.maxBoundsGeoLeafBoundsno—Restricts panning to an area

9. Usage best practices ​

  1. Always use GeoLeaf.boot() in production:

    • it is the only entry point that loads the configuration, orchestrates the modules and emits geoleaf:app:ready.
    • calling GeoLeaf.Core.init() directly is reserved for tests and advanced integration.
  2. Listen for geoleaf:app:ready rather than DOMContentLoaded before acting on the map:

    • by then every module is initialised and the map is visible.
  3. Use GeoLeaf.Core.getMap() to retrieve the adapter in application code:

    • never store a direct reference to maplibregl.Map — the IMapAdapter abstraction is what keeps the code portable.
  4. Load the plugins before GeoLeaf.boot():

    • geoleaf-offline-ui.plugin.js extends the GeoLeaf.* namespace before startup; lazy plugins (editor, table, print, measure) do so on first use.
  5. Centralise the configuration in geoleaf.config.json:

    • every option (map, ui, poi, storage, route) in a single profile file.
    • this avoids divergence between modules and deployments.
  6. Watch the [GeoLeaf.Core] logs during development:

    • enable verbose logs with ?debug=true in the URL.
    • they state precisely which option is missing or invalid.
  7. Popup security: HTML content passed to adapter.createPopup() must be sanitised by the caller through GeoLeaf.Security.sanitize() beforehand. The adapter does not sanitise content.


10. See also ​

  • General architecture: docs/ARCHITECTURE_GUIDE.md
  • Developer guide: docs/DEVELOPER_GUIDE.md
  • Initialisation flow: docs/architecture/INITIALIZATION_FLOW.md
  • IMapAdapter contract: packages/core/src/contracts/map-adapter.contract.ts
  • MaplibreAdapter: packages/core/src/adapters/maplibre/maplibre-adapter.ts
  • ModuleRegistry: packages/core/src/app/module-registry.ts
  • Plugins: each @geoleaf-plugins/* ships its documentation inside its npm package

Released under the MIT License.