Source: bemap-maplibre/bemap-tiles-overlay-catalog.js

/**
 * BeNomad BeMap JavaScript API — TilesOverlayCatalog (v2.0)
 *
 * Per-map catalogue of user overlays (markers, polylines, polygons, circles,
 * popups, cluster layers, raster layers, heatmaps, draw layer). Native
 * MapLibre `setStyle()` wipes user-added sources/layers; this catalogue
 * captures **factories** that recreate each overlay so the library can
 * replay them after a style swap — including the 401 → relogin → setStyle
 * path that would otherwise destroy customer overlays mid-session.
 *
 * Public API:
 *   - `register(type, id, factory)`  — `factory(map)` runs after `setStyle`
 *   - `unregister(type, id)`         — drop a factory from the catalogue
 *   - `replayOnto(map)`              — iterate insertion-order, invoke each
 *   - `clear()`                      — empty the catalogue
 *   - `size()`                       — number of registered overlays
 *
 * @since 2.0.0
 * @public
 * @constructor
 */
bemap.TilesOverlayCatalog = function () {
  this._entries = []; // { type, id, factory }
  this._index = {};   // type → { id → entry }
};

/**
 * Register an overlay factory. Calling with the same (type, id) replaces
 * the previous registration (move-to-end).
 * @public
 * @param {String} type   `'marker'` | `'polyline'` | etc.
 * @param {String} id     Unique within its type.
 * @param {Function} factory `function(map)` — receives the map instance, no return required.
 */
bemap.TilesOverlayCatalog.prototype.register = function (type, id, factory) {
  if (!type || !id || typeof factory !== 'function') return;
  this.unregister(type, id);
  var entry = { type: type, id: id, factory: factory };
  this._entries.push(entry);
  this._index[type] = this._index[type] || {};
  this._index[type][id] = entry;
};

/**
 * @public
 */
bemap.TilesOverlayCatalog.prototype.unregister = function (type, id) {
  if (!type || !id) return;
  var bucket = this._index[type];
  if (!bucket || !bucket[id]) return;
  var existing = bucket[id];
  delete bucket[id];
  for (var i = 0; i < this._entries.length; i++) {
    if (this._entries[i] === existing) {
      this._entries.splice(i, 1);
      break;
    }
  }
};

/**
 * Run every registered factory against the given map, in insertion order.
 * Factories that throw are caught and surfaced via the map's error channel
 * (when available); they do not abort the replay loop.
 * @public
 */
bemap.TilesOverlayCatalog.prototype.replayOnto = function (map) {
  if (!map) return;
  for (var i = 0; i < this._entries.length; i++) {
    var entry = this._entries[i];
    try {
      entry.factory(map);
    } catch (e) {
      if (typeof map._fireError === 'function') {
        map._fireError(new bemap.Error({
          code: bemap.Error.STYLE_LOAD_FAILED,
          message: 'TilesOverlayCatalog: factory threw for ' + entry.type + ':' + entry.id,
          context: { error: e && e.message ? e.message : String(e) }
        }));
      } else {
        console.warn('TilesOverlayCatalog: factory threw for ' + entry.type + ':' + entry.id, e);
      }
    }
  }
};

/**
 * @public
 */
bemap.TilesOverlayCatalog.prototype.clear = function () {
  this._entries.length = 0;
  this._index = {};
};

/**
 * @public
 * @return {Number}
 */
bemap.TilesOverlayCatalog.prototype.size = function () {
  return this._entries.length;
};

/**
 * Iterate over (type, id) entries in registration order. Useful for tests.
 * @public
 */
bemap.TilesOverlayCatalog.prototype.entries = function () {
  return this._entries.map(function (e) { return { type: e.type, id: e.id }; });
};

/* ============================================================
 * Hookup — wrap every public `add*` method on
 * `bemap.MapLibreMap.prototype` so it auto-registers an overlay
 * factory; wrap every matching `remove*` so it unregisters. The
 * catalogue is per-instance (`this._overlayCatalog`) — without
 * this hookup, only `add3DBuildings` would register and a
 * `setStyle()` reload after a 401 would silently drop every
 * marker, polyline, polygon, popup, raster overlay and heatmap.
 *
 * Re-entry guard: when `replayOnto(map)` invokes a stored
 * factory, that factory in turn calls e.g. `map.addMarker(obj)`
 * — without `_replaying`, the wrapped method would re-register
 * the same object endlessly. The flag short-circuits the
 * re-registration during replay.
 * ============================================================ */
(function () {
  if (typeof bemap === 'undefined' || typeof bemap.MapLibreMap !== 'function') return;

  // Map of public add method → (type label). Only overlays backed by a
  // MapLibre SOURCE/LAYER belong here — those are what `setStyle()` wipes and
  // must be replayed. Markers, multimarkers and popups are DOM overlays
  // (maplibregl.Marker / maplibregl.Popup) that live in the map's marker/popup
  // container, NOT in the style — `setStyle()` leaves them untouched. Replaying
  // them would create a SECOND element (the first orphaned on the map) and
  // repoint `marker.native` to a fresh instance that was never `setDraggable`'d
  // — the "duplicate markers that can't be dragged" bug (PMT-28). So they are
  // intentionally EXCLUDED from the replay set.
  var overlayMethods = [
    { add: 'addPolyline',    remove: 'removePolyline',    type: 'polyline' },
    { add: 'addPolygon',     remove: 'removePolygon',     type: 'polygon' },
    { add: 'addCircle',      remove: 'removeCircle',      type: 'circle' },
    { add: 'addRasterLayer', remove: 'removeLayer',       type: 'rasterlayer' },
    { add: 'addHeatmap',     remove: 'removeHeatmap',     type: 'heatmap' }
  ];

  // Generate a stable id per registered object. We use a counter on
  // the catalogue instance (not on the object — customer-supplied
  // bemap objects are frozen / shared references we shouldn't
  // monkey-patch with our own `_catalogId`).
  function nextCatalogId(catalog) {
    catalog._idCounter = (catalog._idCounter || 0) + 1;
    return catalog._idCounter;
  }

  overlayMethods.forEach(function (m) {
    var proto = bemap.MapLibreMap.prototype;
    if (typeof proto[m.add] !== 'function') return;

    // Wrap add — call original, then register a factory keyed by the
    // bemap object. The factory captures the same argument list and
    // replays it on demand.
    var originalAdd = proto[m.add];
    proto[m.add] = function () {
      var args = Array.prototype.slice.call(arguments);
      var result = originalAdd.apply(this, args);
      if (this._overlayCatalog && !this._replaying) {
        var obj = args[0]; // every add* method takes the bemap object first
        if (obj) {
          if (!obj.__catalogId) {
            try { obj.__catalogId = nextCatalogId(this._overlayCatalog); } catch (e) { /* frozen object */ }
          }
          var id = obj.__catalogId || nextCatalogId(this._overlayCatalog);
          var savedArgs = args;
          var self = this;
          this._overlayCatalog.register(m.type, id, function (replayMap) {
            // Don't replay against a destroyed map.
            if (replayMap !== self || !self.native) return;
            self._replaying = true;
            try { originalAdd.apply(self, savedArgs); }
            finally { self._replaying = false; }
          });
        }
      }
      return result;
    };

    // Wrap remove — unregister by the same object reference.
    if (typeof proto[m.remove] === 'function') {
      var originalRemove = proto[m.remove];
      proto[m.remove] = function () {
        var args = Array.prototype.slice.call(arguments);
        var result = originalRemove.apply(this, args);
        if (this._overlayCatalog && !this._replaying) {
          var obj = args[0];
          if (obj && obj.__catalogId) {
            this._overlayCatalog.unregister(m.type, obj.__catalogId);
          }
        }
        return result;
      };
    }
  });
})();