Source: bemap-leaflet/bemap-map-leaflet-marker.js

/**
 * True when `native` is a Leaflet icon (L.Icon / L.DivIcon) built by this
 * backend — duck-typed on createIcon(). A foreign native (e.g. an ol.style.Icon
 * left on a bemap.Icon shared with an OpenLayers panel) lacks createIcon(), so
 * L.marker would throw "icon.createIcon is not a function" — rebuild it (PMT-29).
 * @private
 * @param {object} native
 * @return {boolean}
 */
bemap.LeafletMap._isOwnIcon = function (native) {
  return !!(native && typeof native.createIcon === 'function');
};

/**
 * Add a marker to the layer
 * @public
 * @param {bemap.Marker} marker
 * @param {object} options
 * @return {bemap.LeafletMap} this
 */

bemap.LeafletMap.prototype.addMarker = function(marker, options) {
  if (marker !== null && bemap.inheritsof(marker, bemap.Marker)) {
    var opts = options || {};
    var l = null;

    if (opts.layer !== undefined && (bemap.inheritsof(opts.layer, bemap.VectorLayer) || bemap.inheritsof(opts.layer, bemap.ClusterLayer))) {
      l = opts.layer;
    } else {
      l = this.getLayerByName(bemap.Map.DEFAULT_LAYER.MARKER);
      if (l === null) {
        l = new bemap.VectorLayer({
          name: bemap.Map.DEFAULT_LAYER.MARKER
        });
        this.addLayer(l);
      }
    }

    var icon = marker.getIcon();
    var nativeIcon = null;

    // Build the Leaflet L.icon. Two paths:
    //   - Explicit width+height: use L.icon with computed pixel iconAnchor.
    //   - Missing width or height: use L.divIcon + an <img> with a CSS
    //     translate(-X%, -Y%) so the natural image size + fractional anchor
    //     work without needing to know W/H upfront. The previous code path
    //     emitted iconAnchor = [NaN, NaN] in this case → Leaflet treated it
    //     as 0,0 (top-left) and the pin tip floated ~half-icon-height
    //     south of the actual lat/lng — invisible at high zoom, ~50 km off
    //     at zoom 5.
    var _buildIcon = function (ic) {
      var _sc = ic.scale || 1;
      if (ic.width && ic.height) {
        return L.icon({
          iconUrl:    ic.getSrc(),
          iconSize:   [ic.width * _sc, ic.height * _sc],
          iconAnchor: [ic.width * _sc * ic.getAnchorX(), ic.height * _sc * ic.getAnchorY()]
        });
      }
      // No explicit size — let the browser use the image's natural dimensions
      // and anchor it via CSS translate (-fracX*100%, -fracY*100%).
      var fracX = (typeof ic.getAnchorX === 'function') ? ic.getAnchorX() : 0.5;
      var fracY = (typeof ic.getAnchorY === 'function') ? ic.getAnchorY() : 1;
      var tx = (-100 * fracX).toFixed(2) + '%';
      var ty = (-100 * fracY).toFixed(2) + '%';
      var img = '<img src="' + ic.getSrc() + '" draggable="false"' +
                ' style="display:block;transform:translate(' + tx + ',' + ty + ');' +
                '-webkit-user-drag:none;pointer-events:none;">';
      return L.divIcon({
        className: 'bemap-marker',
        html:      img,
        // Zero-size wrapper sits exactly at the lat/lng; the <img> spills
        // outside via the CSS translate, positioning by fractional anchor.
        iconSize:  [0, 0]
      });
    };

    if (icon) {
       // Rebuild if the cached native isn't a Leaflet icon. A bemap.Icon shared
       // across engines may already carry another engine's native (e.g. an
       // ol.style.Icon from an OpenLayers panel) — see _isOwnIcon (PMT-29).
       if (!bemap.LeafletMap._isOwnIcon(icon.native)) {
         nativeIcon = _buildIcon(icon);
         icon.native = nativeIcon;
       }

     } else if (bemap.inheritsof(l, bemap.ClusterLayer)) {

       icon = l.style.icon;
       if (icon && !bemap.LeafletMap._isOwnIcon(icon.native)) {
         nativeIcon = _buildIcon(icon);
         icon.native = nativeIcon;

       }
     }

    // Use a default SVG icon if none provided (no PNG assets needed)
    var markerOpts = {};
    if (icon && icon.native) {
      markerOpts.icon = icon.native;
    } else {
      markerOpts.icon = L.divIcon({
        className: 'bemap-default-marker',
        html: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="36" viewBox="0 0 24 36"><path d="M12 0C5.4 0 0 5.4 0 12c0 9 12 24 12 24s12-15 12-24c0-6.6-5.4-12-12-12z" fill="#0066cc" stroke="#fff" stroke-width="2"/><circle cx="12" cy="12" r="5" fill="#fff"/></svg>',
        iconSize: [24, 36],
        iconAnchor: [12, 36]
      });
    }
    // Always create with `draggable: true` so Leaflet attaches the
    // Marker.Drag handler (otherwise `marker.dragging` is undefined and
    // calling draggableMarker() later does nothing). We immediately
    // disable it — markers are non-draggable until the customer calls
    // `map.draggableMarker(marker, fn)`, which flips it back on.
    markerOpts.draggable = true;
    // If the target layer has its own custom pane (set up by addLayer for
    // VectorLayer), put the marker INTO that pane so zIndexLayer can
    // restack the whole layer by mutating the pane's CSS z-index.
    // User-named VectorLayers have a dedicated marker pane (z=600 default,
    // shifted by zIndexLayer). Default layers (MARKER / POLYLINE / ROUTE)
    // skip this and inherit Leaflet's native markerPane.
    if (l && l._paneNameMarker) markerOpts.pane = l._paneNameMarker;
    marker.native = L.marker(marker.getCoordinate().getLatLonArray(), markerOpts);
    if (marker.native.dragging) marker.native.dragging.disable();

    if (marker.map === null) {
      marker.map = this;
    }

    if (marker.layer === null) {
      marker.layer = l;
    }

    marker.native.addTo(l.native);
  }

  return this;
};

/**
 * Set the coordinates of the marker.
 * @protected
 * @param {bemap.Marker} marker the marker object to remove.
 * @return {bemap.LeafletMap} this
 */
bemap.LeafletMap.prototype.setCoordinateMarker = function(marker) {
  var c = marker.getCoordinate();
  marker.native.setLatLng(L.latLng(c.getLat(), c.getLon()));

  return this;
};

/**
 * Remove a marker from his layer.
 * @param {bemap.Marker} marker the marker object to remove.
 * @return {bemap.LeafletMap} this
 */
bemap.LeafletMap.prototype.removeMarker = function(marker) {
  if (marker !== null && bemap.inheritsof(marker, bemap.Marker) && marker.layer && marker.layer.native) {
    marker.map.native.removeLayer(marker.native);
    marker.layer = null;
    marker.map = null;
  }
  return this;
};

/**
 * Set the listner when an specified eventType occur on bemap.Marker.
 * @public
 * @param {bemap.Marker} marker
 * @param {bemap.Map.EventType} eventType Event type.
 * @param {function} callback Function will be called when the specified eventType is occur.
 * @param {object} options options.
 * @return {bemap.Listener} this.
 */
bemap.LeafletMap.prototype.onMarker = function(marker, eventType, callback, options) {
  var opts = options ? options : {};

  var nativeListener = marker.native.on(eventType, function(evt) {
    var mapEvent = new bemap.MapEvent({
      native: evt,
      bemapObject: marker,
      x: evt.containerPoint ? evt.containerPoint.x : undefined,
      y: evt.containerPoint ? evt.containerPoint.y : undefined,
      coordinate: evt.latlng ? new bemap.Coordinate(evt.latlng.lng, evt.latlng.lat) : undefined,
      properties: options,
      map: this
    });
    callback(mapEvent);
  });
  // Give the marker a real hit area — the no-size (divIcon) path renders the
  // <img> at `pointer-events:none` inside a 0×0 wrapper, so a click passes
  // straight through and this listener would never fire (PMT-47). draggableMarker
  // applies the same fix for drag; here it's for click/hover events.
  this._enableMarkerHitArea(marker);
  var listener = new bemap.Listener({
    native: nativeListener,
    callback: callback,
    key: eventType,
    bemapObject: marker
  });
  return listener;
};

/**
 * Give a divIcon marker a real pointer HIT AREA. addMarker renders the no-size
 * (divIcon) marker with the <img> at `pointer-events:none` inside a 0×0 wrapper,
 * so pointer events pass straight through and neither onMarker's click nor a
 * hover ever fires (PMT-47). Re-enable pointer events on the marker's icon
 * image. No-op for the explicit-size L.icon path (already interactive) and
 * before the marker is on the map (no `_icon` yet). Cursor is intentionally
 * left untouched — a hand cursor is reserved for draggable markers.
 * @private
 * @param {bemap.Marker} marker
 * @return {bemap.LeafletMap} this
 */
bemap.LeafletMap.prototype._enableMarkerHitArea = function (marker) {
  var el = marker && marker.native && marker.native._icon;
  if (el) {
    var imgs = el.getElementsByTagName('img');
    for (var i = 0; i < imgs.length; i++) imgs[i].style.pointerEvents = 'auto';
  }
  return this;
};

/**
 * Add a multimarker to the map. Leaflet has no native multi-point marker, so —
 * exactly like the MapLibre backend — each point is rendered as its own child
 * bemap.Marker (id "<multimarkerId>_<i>"). Previously LeafletMap had NO
 * addMultiMarker at all, so `map.addMultiMarker(mm)` hit the base no-op and the
 * multimarker was silently dropped — the engines rendered different marker
 * counts for the same code (PMT-28). Now all three engines draw the same points.
 * @public
 * @param {bemap.MultiMarker} multimarker
 * @param {object} options
 * @return {bemap.LeafletMap} this
 */
bemap.LeafletMap.prototype.addMultiMarker = function(multimarker, options) {
  if (!multimarker || !bemap.inheritsof(multimarker, bemap.MultiMarker)) return this;
  multimarker._childMarkers = [];
  var coords = multimarker.getCoordinates();
  for (var i = 0; i < coords.length; i++) {
    // icon + id only (no name/textStyle) — matches the MapLibre backend's child
    // markers, so a MultiMarker renders identically on both (no per-child label).
    var m = new bemap.Marker(coords[i], {
      icon: multimarker.icon,
      id: (multimarker.id !== undefined && multimarker.id !== null) ? multimarker.id + '_' + i : null
    });
    this.addMarker(m, options);
    multimarker._childMarkers.push(m);
  }
  if (multimarker.map === null) multimarker.map = this;
  if (!this._multiMarkers) this._multiMarkers = [];
  this._multiMarkers.push(multimarker);
  return this;
};

/**
 * Remove a multimarker (and its child markers) from the map.
 * @public
 * @param {bemap.MultiMarker} multimarker
 * @return {bemap.LeafletMap} this
 */
bemap.LeafletMap.prototype.removeMultimarker = function(multimarker) {
  if (multimarker && multimarker._childMarkers) {
    for (var i = 0; i < multimarker._childMarkers.length; i++) {
      this.removeMarker(multimarker._childMarkers[i]);
    }
    multimarker._childMarkers = [];
  }
  if (multimarker) multimarker.map = null;
  if (this._multiMarkers) {
    var idx = this._multiMarkers.indexOf(multimarker);
    if (idx > -1) this._multiMarkers.splice(idx, 1);
  }
  return this;
};

/**
 * Attach a listener to every multimarker's child markers (they are ordinary
 * L.markers) so a click on any point fires with that child as bemapObject —
 * matching the MapLibre backend's onMultiMarkers behaviour.
 * @public
 * @param {bemap.Map.EventType} eventType
 * @param {function} callback
 * @param {object} options
 * @return {bemap.Listener}
 */
bemap.LeafletMap.prototype.onMultiMarkers = function(eventType, callback, options) {
  var mm = this._multiMarkers || [];
  var last = new bemap.Listener();
  for (var i = 0; i < mm.length; i++) {
    var children = mm[i]._childMarkers || [];
    for (var j = 0; j < children.length; j++) {
      last = this.onMarker(children[j], eventType, callback, options);
    }
  }
  return last;
};

/**
 * Define the draggable capability for bemap.Marker.
 * @protected
 * @param {bemap.Marker} marker bemap object.
 * @param {function} callback Function will be called when the specified eventType is occur.
 * @param {object} options Options.
 * @param {bemap.Layer} options.layerFilter set the bemap layer used as filter.
 * @return {bemap.Listener} bemap.listener.
 */
bemap.LeafletMap.prototype.draggableMarker = function(marker, callback, options) {
  var _this = this;
  // Use Leaflet's built-in drag handler instead of the manual
  // mousedown/mousemove/mouseup tracking that snapped the marker
  // under the cursor on every click. The native L.Marker drag
  // honours a click-vs-drag threshold and only moves the marker
  // when the cursor actually moves.
  if (marker.native && marker.native.dragging && typeof marker.native.dragging.enable === 'function') {
    marker.native.dragging.enable();
  } else if (marker.native && typeof marker.native.options === 'object') {
    // Older Leaflet versions: set the option directly.
    marker.native.options.draggable = true;
  }

  // The no-size (divIcon) icon path in addMarker renders the <img> with
  // `pointer-events:none` inside a 0×0 wrapper — so there is nothing for the
  // user to grab and the marker never actually drags (dragstart/dragend never
  // fire). Re-enable pointer events on the icon image now that the marker is
  // draggable, and show a move cursor. (The explicit width/height L.icon path
  // is already grabbable, so this is a no-op there.)
  var _iconEl = marker.native._icon;
  if (_iconEl) {
    _iconEl.style.cursor = 'pointer';
    var _imgs = _iconEl.getElementsByTagName('img');
    for (var _ii = 0; _ii < _imgs.length; _ii++) {
      _imgs[_ii].style.pointerEvents = 'auto';
      _imgs[_ii].style.cursor = 'pointer';
    }
  }

  // Capture the container-pixel position where the drag started.
  // Consumers (e.g. bfleet's gps-marker drop) gate their drag handling on
  // the MapEvent's `startX`/`startY` and compare them against `x`/`y` —
  // just like the OpenLayers backend's drag MapEvent. Without these pixel
  // fields the drop is silently ignored and no recalculation runs.
  var startPoint = null;
  var startLatLng = null;
  marker.native.on('dragstart', function () {
    startLatLng = marker.native.getLatLng();
    startPoint = _this.native.latLngToContainerPoint(startLatLng);
  });

  marker.native.on('dragend', function (evt) {
    var ll = marker.native.getLatLng();
    // Keep the bemap.Coordinate on the marker in sync.
    if (marker.coordinate && typeof marker.coordinate.setLat === 'function') {
      marker.coordinate.setLon(ll.lng).setLat(ll.lat);
    }
    var endPoint = _this.native.latLngToContainerPoint(ll);
    var sPt = startPoint || endPoint;
    var sLL = startLatLng || ll;
    var mapEvent = new bemap.MapEvent({
      native: evt,
      bemapObject: marker,
      coordinate: new bemap.Coordinate(ll.lng, ll.lat),
      startCoordinate: new bemap.Coordinate(sLL.lng, sLL.lat),
      x: endPoint.x,
      y: endPoint.y,
      startX: sPt.x,
      startY: sPt.y,
      properties: options,
      map: _this
    });
    startPoint = null;
    startLatLng = null;
    if (typeof callback === 'function') callback(mapEvent);
  });

  this.events.dragFeature = new bemap.Listener({
    native: null,
    key: "dragFeature",
    bemapObject: marker
  });

  return this.events.dragFeature;
};