Source: bemap-maplibre/bemap-maplibre-v5/bemap-map-maplibre-polyline.js

/**
 * BeNomad BeMap JavaScript API - MapLibre v5 - Polyline
 */

bemap.MapLibreMap.prototype.addPolyline = function(polyline, options) {
  if (!polyline || !bemap.inheritsof(polyline, bemap.Polyline)) return this;

  var opts = options || {};
  this._addOwnToProperties(polyline);

  var id = polyline._bemapId;
  polyline._maplibreSourceId = id + '-src';
  polyline._maplibreLayerId = id + '-layer';

  var coords = [];
  var bemapCoords = polyline.getCoordinates();
  for (var i = 0; i < bemapCoords.length; i++) {
    coords.push([bemapCoords[i].getLon(), bemapCoords[i].getLat()]);
  }

  var lineColor = 'rgba(0,0,0,1)', lineWidth = 3, lineDash = [];
  if (polyline.style) {
    if (polyline.style.color) lineColor = this._colorToRgba(polyline.style.color);
    if (polyline.style.width) lineWidth = polyline.style.width;
    if (polyline.style.type === bemap.LineStyle.TYPE.DASH) lineDash = [10, 5];
    else if (polyline.style.type === bemap.LineStyle.TYPE.DOT) lineDash = [2, 5];
    else if (polyline.style.type === bemap.LineStyle.TYPE.DOT_DASH) lineDash = [2, 5, 10, 5];
  }

  var props = { _bemapId: polyline._bemapId };
  if (polyline.properties) { for (var k in polyline.properties) { props[k] = polyline.properties[k]; } }

  var geojson = {
    type: 'Feature',
    geometry: { type: 'LineString', coordinates: coords },
    properties: props
  };

  this._geoJsonData[polyline._maplibreSourceId] = geojson;

  // Resolve the target layer FIRST and record the claim SYNCHRONOUSLY (push
  // to geoJsonIds + features). The cancellation check inside addToMap
  // reads geoJsonIds — if we pushed AFTER scheduling, a sync-fired
  // addToMap would see an empty array and abort.
  polyline.native = { sourceId: polyline._maplibreSourceId, layerId: polyline._maplibreLayerId };
  if (polyline.map === null) polyline.map = this;
  var l = opts.layer && bemap.inheritsof(opts.layer, bemap.Layer) ? opts.layer : this.getLayerByName(bemap.Map.DEFAULT_LAYER.POLYLINE);
  if (polyline.layer === null) polyline.layer = l;
  if (l && l.native && l.native.geoJsonIds) l.native.geoJsonIds.push(polyline._maplibreLayerId);
  if (l && l.native && l.native.features) l.native.features.push(polyline);

  var _this = this;
  var _added = false;
  var addToMap = function() {
    if (_added) return;
    // CANCELLATION CHECK: if the polyline was added during a style-load
    // gap (e.g. just after a 401-triggered setStyle), `addToMap` was
    // queued on _whenStyleReady. If `clearLayer` ran between the
    // schedule and the actual fire — which is what happens when evmove5
    // re-renders routes — the layer's geoJsonIds array was reset and the
    // polyline was logically cleared. Without this guard, the queued
    // addToMap still fires later, adds source+layer to MapLibre's style,
    // and produces an ORPHAN that no future clearLayer can find (the
    // layer's tracking doesn't claim it). Symptom: "first 5 polylines
    // stay on top in true colour after route swap".
    if (l && l.native && l.native.geoJsonIds &&
        l.native.geoJsonIds.indexOf(polyline._maplibreLayerId) === -1) {
      return;
    }
    try {
      if (!_this.native.getSource(polyline._maplibreSourceId)) {
        _this.native.addSource(polyline._maplibreSourceId, { type: 'geojson', data: geojson });
      }
      if (!_this.native.getLayer(polyline._maplibreLayerId)) {
        var layerDef = {
          id: polyline._maplibreLayerId,
          type: 'line',
          source: polyline._maplibreSourceId,
          paint: { 'line-color': lineColor, 'line-width': lineWidth, 'line-opacity': 1 },
          layout: { 'line-cap': 'round', 'line-join': 'round' }
        };
        if (lineDash.length > 0) layerDef.paint['line-dasharray'] = lineDash;
        _this.native.addLayer(layerDef);
        if (_this._geoJsonLayerIds.indexOf(polyline._maplibreLayerId) === -1) {
          _this._geoJsonLayerIds.push(polyline._maplibreLayerId);
        }
      }
      _added = true;
    } catch(e) {
      if (typeof console !== 'undefined' && console.warn) {
        console.warn('[bemap.MapLibreMap.addPolyline] add failed for ' +
                     polyline._maplibreLayerId + ': ' + (e && e.message ? e.message : e));
      }
    }
  };

  // Schedule via _enqueueStyleOp so a burst of addPolyline calls (e.g. an
  // EV-routing re-render after route swap, 17+ polylines per click) coalesces
  // into ONE MapLibre render pass per rAF tick instead of 17. Eliminates the
  // "blank flicker" between clearLayer and the first new polyline.
  this._enqueueStyleOp(polyline._maplibreLayerId, addToMap);

  return this;
};

bemap.MapLibreMap.prototype.removePolyline = function(polyline) {
  if (polyline && polyline.native) {
    try {
      if (this.native.getLayer(polyline._maplibreLayerId)) this.native.removeLayer(polyline._maplibreLayerId);
      if (this.native.getSource(polyline._maplibreSourceId)) this.native.removeSource(polyline._maplibreSourceId);
      var idx = this._geoJsonLayerIds.indexOf(polyline._maplibreLayerId);
      if (idx > -1) this._geoJsonLayerIds.splice(idx, 1);
    } catch(e) {}
    if (polyline._bemapId) delete this._featureRegistry[polyline._bemapId];
    if (polyline._maplibreSourceId) delete this._geoJsonData[polyline._maplibreSourceId];
    polyline.map = null;
    polyline.layer = null;
  }
  return this;
};

bemap.MapLibreMap.prototype.draggablePolyline = function(polyline, callback, options) {
  return this._draggableFeature(polyline, callback, options, { singleFeature: true });
};

bemap.MapLibreMap.prototype.draggablePolylines = function(callback, options) {
  return this._draggableFeature(null, callback, options, { polylines: true });
};