From 309ca9c8127735f787b4681209b7f9b659a37bca Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Thu, 22 Oct 2026 18:53:57 -0700 Subject: [PATCH] Pick the marker drawn under the tap, label included, and size the info bubble by the icon's real extent --- src/MapboxView.web.tsx | 5 +- src/marker/MapboxMarkerController.ts | 108 ++++++++++++++++----------- 2 files changed, 66 insertions(+), 47 deletions(-) diff --git a/src/MapboxView.web.tsx b/src/MapboxView.web.tsx index 9a6dc1f..21c8d42 100644 --- a/src/MapboxView.web.tsx +++ b/src/MapboxView.web.tsx @@ -26,6 +26,7 @@ import { type MarkerAnimationOverlayEntry, type MapViewControllerInterface, mapViewStateInternal, + markerIconExtentPx, } from '@mapconductor/js-sdk-core'; import { MapboxProvider, MapboxConfig } from './MapboxProvider'; import type { MapboxViewStateInterface } from './MapboxViewState'; @@ -287,12 +288,12 @@ function InternalMapBoxMapView({ const pos = entry.positionProvider(); const screenOffset = holder.toScreenOffset(pos); const icon = entry.icon; - const iconPixelSize = icon ? icon.iconSize * icon.scale : 0; + const iconPixelSize = markerIconExtentPx(icon); return ( | null { + const tolerance = + pointerType === 'touch' ? MARKER_HIT_RADIUS_TOUCH_PX : MARKER_HIT_RADIUS_MOUSE_PX; + let regular: MarkerHit> | null = null; // Defers to Mapbox's own rendered-geometry query (`queryRenderedFeatures`), // which returns the icons actually painted under the point in render order — // topmost first. That respects the real stacking order AND the true on-screen // icon size/placement, so it stays correct under tilt, rotation and icon // scaling (a hand-rolled geo/pixel-box test does not: the icon's native bitmap // size and tilt projection make it match far-off markers). A `tapTolerance` - // box is queried as a fallback for near-misses on small icons. Falls back to - // the tiled-marker (raster) radius hit-test when nothing regular is hit. + // box is queried as a fallback for near-misses on small icons. Tiled markers + // are compared with the same rule as regular ones (`preferMarkerHit`): one + // drawn under the pointer beats a near miss. const map = this.holder.map; const layerId = this.renderer.markerLayer.layerId; if (map.getLayer(layerId)) { @@ -296,55 +305,64 @@ export class MapboxMarkerController extends AbstractMarkerController this.holder.toScreenOffset(p as GeoPoint); + const bounds = markerSearchBounds(position, clickScreen, project); + const candidates = bounds + ? this.markerManager.findMarkersInBounds(bounds).filter(entity => entity.marker !== null) + : []; + regular = pickMarkerHit( + clickScreen, + candidates, + entity => { + const bmp = (entity.state.icon ?? createDefaultIcon()).toBitmapIcon(); + return { + anchorScreen: project(entity.state.position), + width: bmp.size.width, + height: bmp.size.height, + anchor: bmp.anchor, + }; + }, + tolerance, ); } - if (!hit) { - // アイコン画像サイズを考慮したフォールバック。最近傍の通常マーカーを投影し、 - // クリックがそのアイコン画像の矩形(size + anchor + 入力別 tolerance)内なら採用する。 - // queryRenderedFeatures / 固定 tapTolerance box では取りこぼす、大きなアイコン - //(幅広ラベルや大きな画像)でも画像全体でヒットさせる(android の find と同じ判定)。 - const nearest = this.markerManager.findNearest(position); - if (nearest && nearest.marker !== null) { - const bmp = (nearest.state.icon ?? createDefaultIcon()).toBitmapIcon(); - const markerScreen = this.holder.toScreenOffset(nearest.state.position); - if (markerScreen) { - const tol = - pointerType === 'touch' ? MARKER_HIT_RADIUS_TOUCH_PX : MARKER_HIT_RADIUS_MOUSE_PX; - const dx = clickScreen.x - markerScreen.x; - const dy = clickScreen.y - markerScreen.y; - const left = -bmp.anchor.x * bmp.size.width - tol; - const right = (1 - bmp.anchor.x) * bmp.size.width + tol; - const top = -bmp.anchor.y * bmp.size.height - tol; - const bottom = (1 - bmp.anchor.y) * bmp.size.height + tol; - if (dx >= left && dx <= right && dy >= top && dy <= bottom) { - hit = nearest; - } - } - } - } - if (hit) return hit; } - const nearest = this.markerManager.findNearest(position); - if (nearest?.marker === null) { - const hitRadius = - pointerType === 'touch' ? MARKER_HIT_RADIUS_TOUCH_PX : MARKER_HIT_RADIUS_MOUSE_PX; - const found = this.tileRenderer?.findNearest(position, hitRadius, zoom); - return found ? (this.markerManager.getEntity(found.id) ?? null) : null; - } - return null; + // タイル化マーカーも同じ規則で比べる(指の下に描かれていれば、通常マーカーの近接より勝つ)。 + const tiledHit = this.tileRenderer?.findHit(position, tolerance, zoom) ?? null; + const tiledEntity = tiledHit ? this.markerManager.getEntity(tiledHit.item.id) : null; + const best = preferMarkerHit>( + regular, + tiledHit && tiledEntity ? { ...tiledHit, item: tiledEntity } : null, + ); + return best?.item ?? null; } + // ── other existing methods ──────────────────────────────────────────────── override async update(state: MarkerState): Promise {