Skip to content
Merged
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
57 changes: 18 additions & 39 deletions Sources/MapConductorForHERE/marker/HereMarkerHitTest.swift
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
import CoreGraphics
import Foundation
import heresdk
import MapConductorCore
@_spi(MapConductorDriver) import MapConductorCore

/// Screen-space marker hit test, shared by the native-marker path
/// (`HereMarkerController.hitTest`) and the strategy-marker path
/// (`HereMapView`'s coordinator, used for clustering).
///
/// Mirrors `HereMarkerController.find` on Android: the icon's own box expanded by
/// `Settings.Default.tapTolerance` on every side, with overlapping markers broken
/// by distance to the anchor.
/// 選び方はコアの ``MarkerHitTest/pick`` と同じ(android-sdk の `AbstractMarkerController.find` とも
/// 同じ)。指の下にアイコン(ラベル込み)が描かれているマーカーを、アンカーの近い別のマーカーより
/// 優先し、どれにも当たらなければ `Settings.Default.tapTolerance` 以内でアンカーの近いものを選ぶ。
///
/// This replaced an iOS-only "44pt minimum touch target" floor
/// (`max(iconSize × scale, 44)`), which never actually applied to the default 48pt
/// icon and which refused taps below the marker tip entirely. Measured on device,
/// the box went from 48/0/24/23 pt (up/down/left/right) to 62/13/38/37 pt.
/// HERE のビュー座標は物理ピクセルなので、`pixelScale` で割ってポイントに直してから比べる。
enum HereMarkerHitTest {
/// - Parameters:
/// - screenPoint: tap location in HERE view coordinates (physical pixels).
Expand All @@ -26,36 +23,18 @@ enum HereMarkerHitTest {
states: [MarkerState],
defaultIcon: any MarkerIconProtocol
) -> MarkerState? {
let pixelScale = CGFloat(mapView.pixelScale)
let tolerancePx = Settings.Default.tapTolerance * pixelScale

var bestState: MarkerState?
var bestDistance = CGFloat.infinity

for state in states {
guard let p = mapView.geoToViewCoordinates(
geoCoordinates: state.position.toGeoCoordinates()
) else { continue }

let icon: any MarkerIconProtocol = state.icon ?? defaultIcon
// Rendered size in physical pixels: iconSize (pts) × scale × pixelScale.
// Icons are square canvases; anchor is normalized (0–1).
let iconPx = icon.iconSize * icon.scale * pixelScale
let renderedPx = iconPx + 2 * tolerancePx

let left = CGFloat(p.x) - icon.anchor.x * iconPx - tolerancePx
let top = CGFloat(p.y) - icon.anchor.y * iconPx - tolerancePx
let hitRect = CGRect(x: left, y: top, width: renderedPx, height: renderedPx)

guard hitRect.contains(screenPoint) else { continue }

// Among overlapping markers prefer the one whose anchor is closest to the tap.
let distance = hypot(screenPoint.x - CGFloat(p.x), screenPoint.y - CGFloat(p.y))
if distance < bestDistance {
bestDistance = distance
bestState = state
}
}
return bestState
let pixelScale = max(CGFloat(mapView.pixelScale), 1)
let touch = CGPoint(x: screenPoint.x / pixelScale, y: screenPoint.y / pixelScale)
return MarkerHitTest.pick(
touchScreen: touch,
candidates: states,
state: { $0 },
project: { position in
mapView.geoToViewCoordinates(geoCoordinates: position.toGeoCoordinates()).map {
CGPoint(x: CGFloat($0.x) / pixelScale, y: CGFloat($0.y) / pixelScale)
}
},
defaultIcon: defaultIcon
)
}
}