@@ -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