Skip to content

Commit cff0600

Browse files
authored
Merge pull request #147 from parkdu7/dev
merge : Dev to main(POI 충돌로직 수정)
2 parents 44779a3 + b6f3e83 commit cff0600

File tree

4 files changed

+291
-200
lines changed

4 files changed

+291
-200
lines changed

android/campung/app/src/main/java/com/shinhan/campung/presentation/ui/map/ClusterManagerInitializer.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ class ClusterManagerInitializer(
1717
fun createClusterManager(
1818
naverMap: NaverMap,
1919
mapContainer: ViewGroup? = null,
20-
onHighlightedContentChanged: (MapContent?) -> Unit
20+
onHighlightedContentChanged: (MapContent?) -> Unit,
21+
poiMarkerManager: POIMarkerManager? = null
2122
): MapClusterManager {
2223
return MapClusterManager(context, naverMap, mapContainer).also { manager ->
2324
manager.setupClustering()
@@ -85,6 +86,12 @@ class ClusterManagerInitializer(
8586
Log.d("ClusterManagerInitializer", "🎯 통합 클러스터 클릭: ${mixedClusterItems.size}개 아이템")
8687
mapViewModel.selectMixedCluster(mixedClusterItems)
8788
}
89+
90+
// POI 매니저와 연결 (충돌 감지용)
91+
if (poiMarkerManager != null) {
92+
manager.poiMarkerManager = poiMarkerManager
93+
Log.d("ClusterManagerInitializer", "🏪 POI 매니저와 마커 매니저 연결 완료")
94+
}
8895
}
8996
}
9097
}

android/campung/app/src/main/java/com/shinhan/campung/presentation/ui/map/MapClusterManager.kt

Lines changed: 254 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ class MapClusterManager(
5757

5858
// POI 충돌 방지를 위한 마커 위치 업데이트 콜백
5959
var onMarkerPositionsUpdated: ((List<LatLng>, Double) -> Unit)? = null
60+
61+
// POI 매니저 참조 (POI 위치 정보 가져오기 위함)
62+
var poiMarkerManager: POIMarkerManager? = null
63+
64+
// 카메라 변경 시 마커 재배치 콜백
65+
var onCameraChangeForMarkerRepositioning: (() -> Unit)? = null
6066

6167
// 선택된 마커 상태 관리
6268
var selectedMarker: Marker? = null
@@ -93,6 +99,10 @@ class MapClusterManager(
9399
private val MARKER_SIZE get() = MarkerConfig.BASE_MARKER_SIZE
94100
private val SELECTED_MARKER_SCALE get() = MarkerConfig.SELECTED_SCALE
95101
private val HIGHLIGHTED_MARKER_SCALE get() = MarkerConfig.HIGHLIGHTED_SCALE
102+
103+
// 마커 충돌 감지 설정
104+
private const val COLLISION_DETECTION_MIN_ZOOM = 19.0 // 줌 19 이상에서만 충돌 감지
105+
private const val MARKER_COLLISION_RADIUS_PX = 60 // 마커 충돌 반경 (픽셀)
96106
}
97107

98108
// 아이콘 캐싱 시스템
@@ -438,6 +448,12 @@ class MapClusterManager(
438448

439449
// POI 매니저에 현재 마커 위치들 전달 (충돌 방지용)
440450
notifyMarkerPositions()
451+
452+
// 줌 레벨이 높고 POI가 있으면 마커 재배치 (POI 변경 시 대응)
453+
if (currentZoom >= COLLISION_DETECTION_MIN_ZOOM && poiMarkerManager != null) {
454+
Log.d("MapClusterManager", "🔄 POI 업데이트 감지 - 마커 재배치 검토")
455+
redistributeMarkersAvoidingPOI()
456+
}
441457

442458
// 클러스터링 완료 콜백 호출
443459
onComplete?.invoke()
@@ -530,11 +546,22 @@ class MapClusterManager(
530546
// 단일 마커
531547
val content = cluster[0]
532548
Log.d("MapClusterManager", "📍 [MARKER] 단일 마커 생성: ${content.title} (ID: ${content.contentId})")
533-
Log.d("MapClusterManager", "📍 [MARKER] 위치: (${content.location.latitude}, ${content.location.longitude})")
549+
Log.d("MapClusterManager", "📍 [MARKER] 원래 위치: (${content.location.latitude}, ${content.location.longitude})")
534550
Log.d("MapClusterManager", "🔗 [MARKER] 마커 생성 시점 onMarkerClick: ${onMarkerClick}")
535551

552+
// POI와의 충돌을 피한 최적 위치 계산
553+
val originalPosition = LatLng(content.location.latitude, content.location.longitude)
554+
val optimalPosition = calculateOptimalMarkerPosition(originalPosition, content)
555+
556+
if (optimalPosition != originalPosition) {
557+
Log.e("MapClusterManager", "🕷️ [MARKER] Spider 적용됨: ${content.title}")
558+
Log.e("MapClusterManager", "🕷️ [MARKER] 최종 위치: (${optimalPosition.latitude}, ${optimalPosition.longitude})")
559+
} else {
560+
Log.d("MapClusterManager", "📍 [MARKER] 충돌 없음 - 원위치 사용: ${content.title}")
561+
}
562+
536563
val marker = Marker().apply {
537-
position = LatLng(content.location.latitude, content.location.longitude)
564+
position = optimalPosition
538565
icon = getNormalMarkerIcon(content.postType)
539566
map = naverMap
540567
tag = content // MapContent 저장
@@ -551,7 +578,7 @@ class MapClusterManager(
551578
onMarkerClick?.invoke(content)
552579
} else {
553580
Log.d("MapClusterManager", "🎯 [CLICK] 새 마커 선택 - selectMarker 호출")
554-
// 새로운 마커 선택 및 카메라 이동 (줌레벨 유지)
581+
// 새로운 마커 선택 및 카메라 이동 (원래 위치로 이동)
555582
selectMarker(content)
556583

557584
// 마커 클릭 이동 플래그 설정
@@ -876,7 +903,7 @@ class MapClusterManager(
876903

877904
when (item) {
878905
is MapContentItem -> {
879-
val marker = createContentMarker(item.content)
906+
val marker = createContentMarkerWithCollisionDetection(item.content)
880907
markers.add(marker)
881908
Log.d("MapClusterManager", "✅ [MIXED] Content 마커 추가 완료")
882909
}
@@ -966,7 +993,52 @@ class MapClusterManager(
966993
}
967994

968995
/**
969-
* Content 마커를 생성하는 헬퍼 함수
996+
* Content 마커를 생성하는 헬퍼 함수 (충돌 감지 적용)
997+
*/
998+
private fun createContentMarkerWithCollisionDetection(content: MapContent): Marker {
999+
// POI와의 충돌을 피한 최적 위치 계산
1000+
val originalPosition = LatLng(content.location.latitude, content.location.longitude)
1001+
val optimalPosition = calculateOptimalMarkerPosition(originalPosition, content)
1002+
1003+
if (optimalPosition != originalPosition) {
1004+
Log.e("MapClusterManager", "🕷️ [MIXED CONTENT] Spider 적용됨: ${content.title}")
1005+
}
1006+
1007+
return Marker().apply {
1008+
position = optimalPosition
1009+
icon = getNormalMarkerIcon(content.postType)
1010+
map = naverMap
1011+
tag = content
1012+
1013+
setOnClickListener {
1014+
Log.e("MapClusterManager", "🎯🎯🎯 [MIXED CONTENT] 개별 Content 마커 클릭!!!")
1015+
1016+
if (selectedContent?.contentId == content.contentId) {
1017+
clearSelection()
1018+
onMarkerClick?.invoke(content)
1019+
} else {
1020+
selectMarker(content)
1021+
1022+
isClusterMoving = true
1023+
1024+
naverMap.moveCamera(
1025+
CameraUpdate.scrollTo(LatLng(content.location.latitude, content.location.longitude))
1026+
.animate(CameraAnimation.Easing)
1027+
)
1028+
1029+
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
1030+
isClusterMoving = false
1031+
}, 1000)
1032+
1033+
onMarkerClick?.invoke(content)
1034+
}
1035+
true
1036+
}
1037+
}
1038+
}
1039+
1040+
/**
1041+
* Content 마커를 생성하는 헬퍼 함수 (기존 방식 - 호환성 유지)
9701042
*/
9711043
private fun createContentMarker(content: MapContent): Marker {
9721044
return Marker().apply {
@@ -1705,4 +1777,181 @@ class MapClusterManager(
17051777
// POI 매니저에 콜백으로 전달
17061778
onMarkerPositionsUpdated?.invoke(allPositions, currentZoom)
17071779
}
1780+
1781+
// ===== 🆕 마커 충돌 감지 및 Spider 알고리즘 시스템 =====
1782+
1783+
/**
1784+
* POI와의 충돌을 피해 마커 최적 위치 계산 (화면 좌표 기반)
1785+
*/
1786+
private fun calculateOptimalMarkerPosition(originalPosition: LatLng, content: MapContent): LatLng {
1787+
val currentZoom = naverMap.cameraPosition.zoom
1788+
1789+
// 줌 레벨이 충돌 감지 임계값보다 낮으면 원위치 사용
1790+
if (currentZoom < COLLISION_DETECTION_MIN_ZOOM) {
1791+
return originalPosition
1792+
}
1793+
1794+
// POI 위치들 가져오기
1795+
val poiPositions = poiMarkerManager?.getPOIPositions() ?: emptyList()
1796+
1797+
if (poiPositions.isEmpty()) {
1798+
return originalPosition
1799+
}
1800+
1801+
Log.d("MapClusterManager", "🎯 마커 충돌 감지: ${content.title} - POI ${poiPositions.size}개와 비교")
1802+
1803+
// 1. 마커 원래 위치를 화면 좌표로 변환
1804+
val markerScreenPoint = naverMap.projection.toScreenLocation(originalPosition)
1805+
1806+
// 2. POI들의 화면 좌표 계산
1807+
val poiScreenPoints = poiPositions.map { position ->
1808+
naverMap.projection.toScreenLocation(position)
1809+
}
1810+
1811+
// 3. 화면상 충돌 감지
1812+
var hasCollision = false
1813+
poiScreenPoints.forEachIndexed { index, screenPoint ->
1814+
val pixelDistance = sqrt(
1815+
(markerScreenPoint.x - screenPoint.x).toDouble().pow(2) +
1816+
(markerScreenPoint.y - screenPoint.y).toDouble().pow(2)
1817+
)
1818+
1819+
if (pixelDistance <= MARKER_COLLISION_RADIUS_PX) {
1820+
hasCollision = true
1821+
Log.e("MapClusterManager", "🚨 마커-POI 충돌 감지! POI[$index]: ${pixelDistance.toInt()}px ≤ ${MARKER_COLLISION_RADIUS_PX}px")
1822+
}
1823+
}
1824+
1825+
if (!hasCollision) {
1826+
Log.d("MapClusterManager", "✅ 마커 충돌 없음 - 원위치 사용: ${content.title}")
1827+
return originalPosition
1828+
}
1829+
1830+
// 4. 충돌 시 Spider 알고리즘으로 8방향 오프셋 적용
1831+
Log.e("MapClusterManager", "🕷️ Spider 알고리즘 시작: ${content.title}")
1832+
1833+
val offsetDistance = MARKER_COLLISION_RADIUS_PX + 20 // 여유 공간
1834+
val angles = listOf(0, 45, 90, 135, 180, 225, 270, 315) // 8방향
1835+
1836+
for (angle in angles) {
1837+
val radians = Math.toRadians(angle.toDouble())
1838+
val offsetX = cos(radians) * offsetDistance
1839+
val offsetY = sin(radians) * offsetDistance
1840+
1841+
val offsetScreenPoint = android.graphics.PointF(
1842+
(markerScreenPoint.x + offsetX).toFloat(),
1843+
(markerScreenPoint.y + offsetY).toFloat()
1844+
)
1845+
1846+
// 오프셋된 화면 좌표를 지리 좌표로 변환
1847+
val offsetPosition = naverMap.projection.fromScreenLocation(offsetScreenPoint)
1848+
1849+
// 오프셋 위치에서도 충돌 체크
1850+
var offsetHasCollision = false
1851+
poiScreenPoints.forEach { screenPoint ->
1852+
val pixelDistance = sqrt(
1853+
(offsetScreenPoint.x - screenPoint.x).toDouble().pow(2) +
1854+
(offsetScreenPoint.y - screenPoint.y).toDouble().pow(2)
1855+
)
1856+
if (pixelDistance <= MARKER_COLLISION_RADIUS_PX) {
1857+
offsetHasCollision = true
1858+
}
1859+
}
1860+
1861+
if (!offsetHasCollision) {
1862+
Log.e("MapClusterManager", "✅ Spider: 최적 위치 찾음 ${angle}도 - ${content.title}")
1863+
Log.e("MapClusterManager", "✅ 원래: (${originalPosition.latitude}, ${originalPosition.longitude})")
1864+
Log.e("MapClusterManager", "✅ 최종: (${offsetPosition.latitude}, ${offsetPosition.longitude})")
1865+
return offsetPosition
1866+
}
1867+
}
1868+
1869+
// 모든 방향에서 충돌하면 원위치 강제 사용
1870+
Log.e("MapClusterManager", "❌ Spider: 모든 방향 충돌 - 원위치 강제 사용: ${content.title}")
1871+
return originalPosition
1872+
}
1873+
1874+
/**
1875+
* 마커들을 POI 충돌을 피해 재배치
1876+
*/
1877+
fun redistributeMarkersAvoidingPOI() {
1878+
val currentZoom = naverMap.cameraPosition.zoom
1879+
1880+
if (currentZoom < COLLISION_DETECTION_MIN_ZOOM) {
1881+
Log.d("MapClusterManager", "줌 레벨 낮음 ($currentZoom) - 마커 재배치 스킵")
1882+
return
1883+
}
1884+
1885+
Log.e("MapClusterManager", "🔄 === 마커 재배치 시작 ===")
1886+
Log.e("MapClusterManager", "🔄 대상 마커: ${markers.size}")
1887+
1888+
var repositionedCount = 0
1889+
1890+
markers.forEach { marker ->
1891+
val content = marker.tag as? MapContent ?: return@forEach
1892+
val originalPosition = LatLng(content.location.latitude, content.location.longitude)
1893+
val newPosition = calculateOptimalMarkerPosition(originalPosition, content)
1894+
1895+
if (newPosition != originalPosition) {
1896+
marker.position = newPosition
1897+
repositionedCount++
1898+
Log.e("MapClusterManager", "🔄 마커 재배치됨: ${content.title}")
1899+
}
1900+
}
1901+
1902+
Log.e("MapClusterManager", "🔄 마커 재배치 완료: ${repositionedCount}/${markers.size}")
1903+
}
1904+
1905+
/**
1906+
* 마커들을 원래 위치로 복원 (줌 아웃 시)
1907+
*/
1908+
fun restoreMarkersToOriginalPositions() {
1909+
Log.e("MapClusterManager", "🔄 === 마커 원위치 복원 ===")
1910+
1911+
var restoredCount = 0
1912+
1913+
markers.forEach { marker ->
1914+
val content = marker.tag as? MapContent ?: return@forEach
1915+
val originalPosition = LatLng(content.location.latitude, content.location.longitude)
1916+
1917+
if (marker.position != originalPosition) {
1918+
marker.position = originalPosition
1919+
restoredCount++
1920+
Log.e("MapClusterManager", "🔄 마커 원위치 복원: ${content.title}")
1921+
}
1922+
}
1923+
1924+
Log.e("MapClusterManager", "🔄 원위치 복원 완료: ${restoredCount}/${markers.size}")
1925+
}
1926+
1927+
/**
1928+
* 카메라 변경 시 호출되는 마커 재배치 처리
1929+
*/
1930+
fun onCameraChanged(zoom: Double) {
1931+
val wasCollisionActive = zoom >= COLLISION_DETECTION_MIN_ZOOM
1932+
val prevZoom = naverMap.cameraPosition.zoom
1933+
val wasPrevCollisionActive = prevZoom >= COLLISION_DETECTION_MIN_ZOOM
1934+
1935+
Log.d("MapClusterManager", "📷 카메라 변경: $prevZoom$zoom")
1936+
1937+
// 충돌 감지 임계값을 넘나들 때만 재배치
1938+
when {
1939+
!wasPrevCollisionActive && wasCollisionActive -> {
1940+
// 줌 인: 마커 재배치 시작
1941+
Log.e("MapClusterManager", "🔍 줌 인: 마커 재배치 시작 ($zoom$COLLISION_DETECTION_MIN_ZOOM)")
1942+
redistributeMarkersAvoidingPOI()
1943+
}
1944+
wasPrevCollisionActive && !wasCollisionActive -> {
1945+
// 줌 아웃: 마커 원위치 복원
1946+
Log.e("MapClusterManager", "🔍 줌 아웃: 마커 원위치 복원 ($zoom < $COLLISION_DETECTION_MIN_ZOOM)")
1947+
restoreMarkersToOriginalPositions()
1948+
}
1949+
wasCollisionActive && wasPrevCollisionActive -> {
1950+
// 고줌에서 이동/줌: 실시간 재배치
1951+
Log.d("MapClusterManager", "🔍 고줌 상태 변경: 실시간 재배치")
1952+
redistributeMarkersAvoidingPOI()
1953+
}
1954+
// else: 저줌 상태 유지 - 아무 작업 안함
1955+
}
1956+
}
17081957
}

0 commit comments

Comments
 (0)