Theme Framework How the LeaseLeads theme frontend works

Maps — the engine adapter

A documentation-only Contract declares the interface every map engine must satisfy; each engine (Google Maps, MapLibre) is an adapter behind it, selected at runtime.

On this page 4

The theme can render a location map on either Google Maps or MapLibre, and a module that draws markers, clusters, and popups shouldn't know or care which. So Maps is built as a swappable subsystem: a single Contract describes the interface, each engine is an adapter that satisfies it, and Maps.load() picks one at runtime and dynamic-imports only that one.

Note

This adapter-behind-a-Contract pattern is Maps-specific. It is warranted only by the genuine complexity of swapping map engines — not a template to reach for by default. Wrapping a library in a Vendor is the normal boundary; the full Contract treatment is flagged in code review when a new subsystem's complexity actually merits it, and nowhere else.

The Contract is documentation, not code

utilities/Maps/Contract.js runs nothing — it exports an empty object. Its whole job is to declare, in one place, the shapes and interfaces every engine must implement, so the two adapters can be read against a single spec:

js
/**
 * The map engine contract. This file is documentation only — it declares the
 * shapes and interfaces every engine (GoogleMaps, and later Leaflet/MapLibre)
 * must satisfy. Nothing here runs.
 */

Four interfaces make up the surface — Engine mints the objects, and each minted object (EngineMap, EngineMarker, EngineCluster, EnginePopup) wraps one native thing:

js
/**
 * @interface Engine
 *
 * @function map - async (HTMLElement, MapOptions) => EngineMap (await .ready() for first paint)
 * @function marker - async (MarkerSpec) => EngineMarker
 * @function cluster - async (EngineMap, ClusterOptions) => EngineCluster
 * @function popup - async (PopupSpec) => EnginePopup
 */

The conventions every engine obeys

The Contract's header spells out the rules an adapter must hold to. These are the load-bearing ones:

  • Async methods only, plus a sync get controller(). Every wrapper method returns a promise; the one synchronous accessor is controller, which hands back the native object beneath the wrapper for the rare case a caller needs it.
  • Coordinates cross the boundary as plain {lat, lng}. Each engine converts at its own edge — MapLibre to LngLat, Google to its LatLng — so nothing upstream speaks an engine's native geometry.
  • Camera moves fold their own settle. A method like map.spotlight(pos) resolves only once the movement has come to rest. There is no event surface on the Contract; you await the move and it's done.
  • Boolean predicates come in affirmative + inverse pairsisAttached / isDetached, isOpened / isClosed.
js
/**
 * @interface EngineMap
 *
 * @property {Object} controller - The native map (getter).
 * @function ready - async () => void (resolves once tiles have first loaded)
 * @function zoom - async (undefined|string|number|LatLngLiteral[]) => number|void
 * @function pan - async (LatLngLiteral) => void
 * @function spotlight - async (LatLngLiteral) => void (recenter + zoom in on a position in one move)
 */

Selecting an engine

You never name a concrete engine in module code. You call Maps.load(config) with an engine key, and get back an object that satisfies the Engine contract:

js
// a module drawing a map — engine chosen by config, not hard-coded
const engine = await Maps.load({ engine: 'google', /* …map options */ })
const map = await engine.map(this.element, mapOptions)
await map.ready()

Maps.load reads config.engine (defaulting to google), dynamic-imports only that engine's module, and hands back its ready Engine. The other engine's chunk — and its whole vendor stack — never ships. Each engine caches a single instance and pulls in whatever it needs (Google fetches its libraries through the JS API loader and lazy-imports SnazzyInfoWindow; MapLibre takes a Protomaps key and wraps maplibregl), but that's behind the boundary: from Maps.load onward you speak only Contract methods.