Source: bemap-maplibre/bemap-maplibre-v5/bemap-map-maplibre-3dbuildings.js

/**
 * BeNomad BeMap JavaScript API - MapLibre v5 - 3D Buildings
 *
 * Usage:
 *   map.add3DBuildings({
 *     sourceId: 'openmaptiles',
 *     sourceLayer: 'building',
 *     heightProperty: 'render_height',
 *     color: '#ddd',
 *     opacity: 0.8,
 *     minZoom: 14
 *   });
 */

bemap.MapLibreMap.prototype.add3DBuildings = function(options) {
  var opts = options || {};
  var _this = this;

  this._buildingsLayerId = this._uniqueId('buildings-3d');

  var config = {
    id: this._buildingsLayerId,
    type: 'fill-extrusion',
    paint: {
      'fill-extrusion-color': opts.color || '#aaa',
      'fill-extrusion-height': ['get', opts.heightProperty || 'height'],
      'fill-extrusion-base': ['get', opts.baseHeightProperty || 'min_height'],
      'fill-extrusion-opacity': opts.opacity !== undefined ? opts.opacity : 0.6
    }
  };

  if (opts.minZoom) config.minzoom = opts.minZoom;
  if (opts.sourceId) config.source = opts.sourceId;
  if (opts.sourceLayer) config['source-layer'] = opts.sourceLayer;

  // Cancellation token — flipped by remove3DBuildings so a queued
  // deferred add can't resurrect the layer (Bug 9).
  var token = { cancelled: false };
  this._3dBuildingsTokens = this._3dBuildingsTokens || {};
  this._3dBuildingsTokens[this._buildingsLayerId] = token;

  // If GeoJSON data is provided directly
  if (opts.data) {
    var sourceId = this._uniqueId('buildings-src');
    var addBuildings = function() {
      if (token.cancelled) return;
      if (!_this.native.getSource(sourceId)) {
        _this.native.addSource(sourceId, { type: 'geojson', data: opts.data });
        config.source = sourceId;
        _this.native.addLayer(config);
      }
    };
    if (this.native.isStyleLoaded()) { addBuildings(); } else { this.native.on('load', addBuildings); }
  } else {
    // Use existing source (e.g., from vector tiles in the style)
    var addFromSource = function() {
      if (token.cancelled) return;
      if (config.source && !_this.native.getLayer(_this._buildingsLayerId)) {
        _this.native.addLayer(config);
      }
    };
    if (this.native.isStyleLoaded()) { addFromSource(); } else { this.native.on('load', addFromSource); }
  }

  // Register in the active-effects bar so customers can tear it down
  // with one click. Bug 13 / Part 3.
  if (typeof this._registerEffect === 'function') {
    this._registerEffect('3DBuildings', function () { _this.remove3DBuildings(); });
  }

  return this;
};

bemap.MapLibreMap.prototype.remove3DBuildings = function() {
  // Cancel any pending deferred add (Bug 9 race).
  if (this._buildingsLayerId && this._3dBuildingsTokens && this._3dBuildingsTokens[this._buildingsLayerId]) {
    this._3dBuildingsTokens[this._buildingsLayerId].cancelled = true;
    delete this._3dBuildingsTokens[this._buildingsLayerId];
  }
  if (this._buildingsLayerId) {
    try {
      if (this.native.getLayer(this._buildingsLayerId)) {
        this.native.removeLayer(this._buildingsLayerId);
      }
    } catch(e) {}
    this._buildingsLayerId = null;
  }
  if (typeof this._unregisterEffect === 'function') this._unregisterEffect('3DBuildings');
  return this;
};