Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/MapboxView.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 (
<InfoBubbleOverlay
key={entry.id}
positionOffset={screenOffset}
iconSize={{ width: iconPixelSize, height: iconPixelSize }}
iconSize={iconPixelSize}
iconOffset={icon ? icon.anchor : { x: 0.5, y: 0.5 }}
infoAnchorOffset={icon ? icon.infoAnchor : { x: 0.5, y: 0.5 }}
tailOffset={entry.tailOffset}
Expand Down
108 changes: 63 additions & 45 deletions src/marker/MapboxMarkerController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ import {
type MarkerFingerPrint,
type MarkerState,
type RasterLayerState,
markerSearchBounds,
pickMarkerHit,
preferMarkerHit,
type MarkerHit,
type GeoPointInterface,
} from '@mapconductor/js-sdk-core';
import mapboxgl from 'mapbox-gl';
import { MapboxMapViewHolder } from '../MapboxMapViewHolder';
Expand Down Expand Up @@ -266,14 +271,18 @@ export class MapboxMarkerController extends AbstractMarkerController<MapboxActua
zoom: number,
pointerType: 'touch' | 'mouse',
): MarkerEntity<MapboxActualMarker> | null {
const tolerance =
pointerType === 'touch' ? MARKER_HIT_RADIUS_TOUCH_PX : MARKER_HIT_RADIUS_MOUSE_PX;
let regular: MarkerHit<MarkerEntity<MapboxActualMarker>> | 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)) {
Expand All @@ -296,55 +305,64 @@ export class MapboxMarkerController extends AbstractMarkerController<MapboxActua
layers: [layerId],
}),
);
if (!hit) {
const t = Settings.Default.tapTolerance;
hit = topMost(
map.queryRenderedFeatures(
[
[clickScreen.x - t, clickScreen.y - t],
[clickScreen.x + t, clickScreen.y + t],
] as unknown as [[number, number], [number, number]],
{ layers: [layerId] },
),
// 指の下に描かれている通常マーカー。タイル化マーカーより手前に描かれるので、そのまま勝つ。
if (hit) return hit;
const t = Settings.Default.tapTolerance;
hit = topMost(
map.queryRenderedFeatures(
[
[clickScreen.x - t, clickScreen.y - t],
[clickScreen.x + t, clickScreen.y + t],
] as unknown as [[number, number], [number, number]],
{ layers: [layerId] },
),
);
if (hit) {
const markerScreen = this.holder.toScreenOffset(hit.state.position);
regular = {
item: hit,
exact: false,
anchorY: markerScreen.y,
distance: Math.hypot(clickScreen.x - markerScreen.x, clickScreen.y - markerScreen.y),
};
} else {
// 最後の手段。タップ点の周りの通常マーカーを候補にし、コアの pickMarkerHit で選ぶ
// (指の下にアイコンかラベルがあるものを優先し、なければ許容範囲内でアンカーの近いもの)。
// android / iOS の find と同じ規則。以前は最近傍の 1 つだけを見ていたので、
// 隣のピンの頭や横に置いたラベルを押すと外れていた。
const project = (p: GeoPointInterface) => 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<MarkerEntity<MapboxActualMarker>>(
regular,
tiledHit && tiledEntity ? { ...tiledHit, item: tiledEntity } : null,
);
return best?.item ?? null;
}


// ── other existing methods ────────────────────────────────────────────────

override async update(state: MarkerState): Promise<void> {
Expand Down