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