/**
* 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;
};
}
});
})();